Skip to content

Commit adde61a

Browse files
committed
refactor(ui): remove dCloseOnClick in dropdown
BREAKING CHANGE: onItemClick support control close.
1 parent 291d28d commit adde61a

9 files changed

Lines changed: 73 additions & 19 deletions

File tree

packages/ui/src/components/cascader/Cascader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,10 +328,10 @@ function Cascader<V extends DId, T extends DCascaderItem<V>>(
328328
node,
329329
};
330330
})}
331-
dCloseOnClick={false}
332331
onItemClick={(id, item) => {
333332
const checkeds = (item.node as MultipleTreeNode<V, T>).changeStatus('UNCHECKED', select as Set<V>);
334333
changeSelect(Array.from(checkeds.keys()));
334+
return false;
335335
}}
336336
>
337337
<DTag className={`${dPrefix}cascader__multiple-count`} tabIndex={-1} dSize={size}>

packages/ui/src/components/dropdown/Dropdown.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { isNull, isNumber, isUndefined, nth } from 'lodash';
44
import React, { useImperativeHandle, useRef, useState } from 'react';
55
import ReactDOM from 'react-dom';
66

7-
import { useEvent, useEventCallback, useId, useRefExtra } from '@react-devui/hooks';
7+
import { useEvent, useEventCallback, useId, useImmer, useRefExtra } from '@react-devui/hooks';
88
import { getClassName, getVerticalSidePosition, scrollToView } from '@react-devui/utils';
99

1010
import { useMaxIndex, useDValue } from '../../hooks';
@@ -42,10 +42,9 @@ export interface DDropdownProps<ID extends DId, T extends DDropdownItem<ID>>
4242
dPlacement?: 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right';
4343
dTrigger?: 'hover' | 'click';
4444
dArrow?: boolean;
45-
dCloseOnClick?: boolean;
4645
dZIndex?: number | string;
4746
onVisibleChange?: (visible: boolean) => void;
48-
onItemClick?: (id: T['id'], item: T) => void;
47+
onItemClick?: (id: T['id'], item: T) => void | boolean | Promise<void>;
4948
afterVisibleChange?: (visible: boolean) => void;
5049
}
5150

@@ -61,7 +60,6 @@ function Dropdown<ID extends DId, T extends DDropdownItem<ID>>(
6160
dPlacement = 'bottom-right',
6261
dTrigger = 'hover',
6362
dArrow = false,
64-
dCloseOnClick = true,
6563
dZIndex,
6664
onVisibleChange,
6765
onItemClick,
@@ -103,6 +101,8 @@ function Dropdown<ID extends DId, T extends DDropdownItem<ID>>(
103101
const triggerId = children.props.id ?? `${dPrefix}dropdown-trigger-${uniqueId}`;
104102
const getItemId = (id: ID) => `${dPrefix}dropdown-item-${id}-${uniqueId}`;
105103

104+
const [loadingIds, setLoadingIds] = useImmer(new Set<ID>());
105+
106106
const { popupIds, setPopupIds, addPopupId, removePopupId } = useNestedPopup<ID>();
107107
const [focusIds, setFocusIds] = useState<ID[]>([]);
108108
const [isFocus, setIsFocus] = useState(false);
@@ -237,10 +237,19 @@ function Dropdown<ID extends DId, T extends DDropdownItem<ID>>(
237237
const popupState = popupIds.find((v) => v.id === itemId);
238238

239239
const handleItemClick = () => {
240-
onItemClick?.(itemId, item);
240+
const shouldClose = onItemClick?.(itemId, item);
241241

242242
setFocusIds(subParents.map((parentItem) => parentItem.id).concat([itemId]));
243-
if (dCloseOnClick) {
243+
if (shouldClose instanceof Promise) {
244+
setLoadingIds((draft) => {
245+
draft.add(itemId);
246+
});
247+
shouldClose.then(() => {
248+
setLoadingIds((draft) => {
249+
draft.delete(itemId);
250+
});
251+
});
252+
} else if (shouldClose !== false) {
244253
changeVisible(false);
245254
}
246255
};
@@ -350,6 +359,7 @@ function Dropdown<ID extends DId, T extends DDropdownItem<ID>>(
350359
dLevel={level}
351360
dIcon={itemIcon}
352361
dFocusVisible={focusVisible && isFocus}
362+
dLoading={loadingIds.has(itemId)}
353363
dDisabled={itemDisabled}
354364
onItemClick={handleItemClick}
355365
>

packages/ui/src/components/dropdown/Item.tsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { LoadingOutlined } from '@react-devui/icons';
12
import { checkNodeExist, getClassName } from '@react-devui/utils';
23

4+
import { TTANSITION_DURING_SLOW } from '../../utils';
5+
import { DCollapseTransition } from '../_transition';
36
import { usePrefixConfig } from '../root';
47

58
export interface DItemProps {
@@ -8,30 +11,63 @@ export interface DItemProps {
811
dLevel: number;
912
dIcon: React.ReactNode | undefined;
1013
dFocusVisible: boolean;
14+
dLoading: boolean;
1115
dDisabled: boolean;
1216
onItemClick: () => void;
1317
}
1418

1519
export function DItem(props: DItemProps): JSX.Element | null {
16-
const { children, dId, dLevel, dIcon, dFocusVisible, dDisabled, onItemClick } = props;
20+
const { children, dId, dLevel, dIcon, dFocusVisible, dLoading, dDisabled, onItemClick } = props;
1721

1822
//#region Context
1923
const dPrefix = usePrefixConfig();
2024
//#endregion
2125

26+
const itemIcon = (loading: boolean, iconRef?: React.RefObject<HTMLDivElement>, style?: React.CSSProperties) => (
27+
<div ref={iconRef} className={`${dPrefix}dropdown__item-icon`} style={style}>
28+
{loading ? <LoadingOutlined dSpin /> : dIcon}
29+
</div>
30+
);
31+
2232
return (
2333
<li
2434
id={dId}
2535
className={getClassName(`${dPrefix}dropdown__item`, `${dPrefix}dropdown__item--basic`, {
26-
'is-disabled': dDisabled,
36+
'is-disabled': dDisabled || dLoading,
2737
})}
2838
style={{ paddingLeft: 12 + dLevel * 16 }}
2939
role="menuitem"
30-
aria-disabled={dDisabled}
40+
aria-disabled={dDisabled || dLoading}
3141
onClick={onItemClick}
3242
>
3343
{dFocusVisible && <div className={`${dPrefix}focus-outline`}></div>}
34-
{checkNodeExist(dIcon) && <div className={`${dPrefix}dropdown__item-icon`}>{dIcon}</div>}
44+
{checkNodeExist(dIcon) ? (
45+
itemIcon(dLoading)
46+
) : (
47+
<DCollapseTransition
48+
dOriginalSize={{
49+
width: '',
50+
}}
51+
dCollapsedStyle={{
52+
width: 0,
53+
}}
54+
dIn={dLoading}
55+
dDuring={TTANSITION_DURING_SLOW}
56+
dHorizontal
57+
dStyles={{
58+
entering: {
59+
transition: ['width', 'padding', 'margin'].map((attr) => `${attr} ${TTANSITION_DURING_SLOW}ms linear`).join(', '),
60+
},
61+
leaving: {
62+
transition: ['width', 'padding', 'margin'].map((attr) => `${attr} ${TTANSITION_DURING_SLOW}ms linear`).join(', '),
63+
},
64+
leaved: { display: 'none' },
65+
}}
66+
dDestroyWhenLeaved
67+
>
68+
{(collapseRef, collapseStyle) => itemIcon(true, collapseRef, collapseStyle)}
69+
</DCollapseTransition>
70+
)}
3571
<div className={`${dPrefix}dropdown__item-content`}>{children}</div>
3672
</li>
3773
);

packages/ui/src/components/dropdown/README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ interface DDropdownProps<ID extends DId, T extends DDropdownItem<ID>> extends Om
2626
dPlacement?: 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right';
2727
dTrigger?: 'hover' | 'click';
2828
dArrow?: boolean;
29-
dCloseOnClick?: boolean;
3029
dZIndex?: number | string;
3130
onVisibleChange?: (visible: boolean) => void;
3231
onItemClick?: (id: T['id'], item: T) => void;
@@ -42,7 +41,6 @@ interface DDropdownProps<ID extends DId, T extends DDropdownItem<ID>> extends Om
4241
| dPlacement | Set dropdown position | `'bottom-right'` | |
4342
| dTrigger | Set trigger behavior | `'hover'` | |
4443
| dArrow | Whether to show arrows | `false` | |
45-
| dCloseOnClick | Close the dropdown when the menu item is clicked | `true` | |
4644
| dZIndex | Set the `z-index` of the dropdown | - | |
4745
| onVisibleChange | Callback for visible change | - | |
4846
| onItemClick | Callback for menu item click | - | |

packages/ui/src/components/dropdown/README.zh-CN.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ interface DDropdownProps<ID extends DId, T extends DDropdownItem<ID>> extends Om
2424
dPlacement?: 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right';
2525
dTrigger?: 'hover' | 'click';
2626
dArrow?: boolean;
27-
dCloseOnClick?: boolean;
2827
dZIndex?: number | string;
2928
onVisibleChange?: (visible: boolean) => void;
3029
onItemClick?: (id: T['id'], item: T) => void;
@@ -40,7 +39,6 @@ interface DDropdownProps<ID extends DId, T extends DDropdownItem<ID>> extends Om
4039
| dPlacement | 设置下拉菜单位置 | `'bottom-right'` | |
4140
| dTrigger | 设置触发行为 | `'hover'` | |
4241
| dArrow | 是否展示箭头 | `false` | |
43-
| dCloseOnClick | 当点击菜单项时关闭下拉菜单 | `true` | |
4442
| dZIndex | 设置下拉菜单的 `z-index` | - | |
4543
| onVisibleChange | 显/隐的回调 | - | |
4644
| onItemClick | 点击菜单项的回调 | - | |

packages/ui/src/components/dropdown/demos/1.Basic.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ The simplest usage.
1313
最简单的用法。
1414

1515
```tsx
16+
import { useAsync } from '@react-devui/hooks';
1617
import { DownOutlined, ExperimentOutlined } from '@react-devui/icons';
1718
import { DDropdown, DButton } from '@react-devui/ui';
1819

1920
export default function Demo() {
21+
const async = useAsync();
22+
2023
return (
2124
<>
2225
<DDropdown
@@ -33,7 +36,7 @@ export default function Demo() {
3336
<DDropdown
3437
dList={[
3538
{ id: 'Item1', label: 'Item 1', type: 'item' },
36-
{ id: 'Item2', label: 'Item 2', type: 'item' },
39+
{ id: 'Item2', label: 'Item 2', type: 'item', icon: <ExperimentOutlined /> },
3740
{
3841
id: 'Group3',
3942
label: 'Group 3',
@@ -46,6 +49,15 @@ export default function Demo() {
4649
{ id: 'Item4', label: 'Item 4', type: 'item' },
4750
]}
4851
dTrigger="click"
52+
onItemClick={(id) => {
53+
return ['Item1', 'Item2'].includes(id)
54+
? new Promise((r) => {
55+
async.setTimeout(() => {
56+
r(true);
57+
}, 3000);
58+
})
59+
: true;
60+
}}
4961
>
5062
<DButton dType="secondary" dIcon={<DownOutlined />} dIconRight>
5163
Click me

packages/ui/src/components/select/Select.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,9 @@ function Select<V extends DId, T extends DSelectItem<V>>(
353353
disabled: itemDisabled,
354354
};
355355
})}
356-
dCloseOnClick={false}
357356
onItemClick={(id: V) => {
358357
changeSelectByClick(id);
358+
return false;
359359
}}
360360
>
361361
<DTag className={`${dPrefix}select__multiple-count`} tabIndex={-1} dSize={size}>

packages/ui/src/components/tabs/Tabs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,9 +362,9 @@ function Tabs<ID extends DId, T extends DTabItem<ID>>(props: DTabsProps<ID, T>,
362362
};
363363
})}
364364
dPlacement={dPlacement === 'left' ? 'bottom-left' : 'bottom-right'}
365-
dCloseOnClick={false}
366365
onItemClick={(id: ID) => {
367366
changeActiveId(id);
367+
return false;
368368
}}
369369
>
370370
<div

packages/ui/src/components/tree-select/TreeSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,10 +355,10 @@ function TreeSelect<V extends DId, T extends DTreeItem<V>>(
355355
node,
356356
};
357357
})}
358-
dCloseOnClick={false}
359358
onItemClick={(id, item) => {
360359
const checkeds = (item.node as MultipleTreeNode<V, T>).changeStatus('UNCHECKED', select as Set<V>);
361360
changeSelect(Array.from(checkeds.keys()));
361+
return false;
362362
}}
363363
>
364364
<DTag className={`${dPrefix}tree-select__multiple-count`} tabIndex={-1} dSize={size}>

0 commit comments

Comments
 (0)