@mezzanine-ui/react 1.0.0-rc.6 → 1.0.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AutoComplete/AutoComplete.d.ts +25 -9
- package/AutoComplete/AutoComplete.js +84 -17
- package/AutoComplete/AutoCompleteInside.d.ts +54 -0
- package/AutoComplete/AutoCompleteInside.js +17 -0
- package/AutoComplete/useAutoCompleteKeyboard.d.ts +2 -1
- package/AutoComplete/useAutoCompleteKeyboard.js +4 -1
- package/Breadcrumb/BreadcrumbDropdown.d.ts +1 -1
- package/Breadcrumb/BreadcrumbOverflowMenuDropdown.d.ts +1 -1
- package/Breadcrumb/typings.d.ts +1 -1
- package/COMPONENTS.md +2 -2
- package/Checkbox/Checkbox.js +24 -3
- package/Cropper/Cropper.d.ts +1 -1
- package/Description/Description.d.ts +1 -1
- package/Description/Description.js +1 -1
- package/Description/DescriptionTitle.d.ts +6 -1
- package/Description/DescriptionTitle.js +2 -2
- package/Drawer/Drawer.d.ts +39 -34
- package/Drawer/Drawer.js +33 -35
- package/Dropdown/Dropdown.d.ts +16 -1
- package/Dropdown/Dropdown.js +156 -9
- package/Dropdown/DropdownItem.d.ts +26 -2
- package/Dropdown/DropdownItem.js +91 -43
- package/Dropdown/DropdownItemCard.d.ts +3 -2
- package/Dropdown/DropdownItemCard.js +8 -5
- package/Dropdown/dropdownKeydownHandler.d.ts +6 -0
- package/Dropdown/dropdownKeydownHandler.js +14 -7
- package/FilterArea/Filter.d.ts +25 -2
- package/FilterArea/Filter.js +23 -0
- package/FilterArea/FilterArea.d.ts +43 -4
- package/FilterArea/FilterArea.js +35 -2
- package/FilterArea/FilterLine.d.ts +19 -0
- package/FilterArea/FilterLine.js +19 -0
- package/Input/SpinnerButton/SpinnerButton.js +1 -1
- package/Modal/Modal.d.ts +22 -86
- package/Modal/Modal.js +4 -2
- package/Modal/ModalBodyForVerification.js +3 -1
- package/NotificationCenter/NotificationCenter.d.ts +21 -9
- package/NotificationCenter/NotificationCenter.js +22 -10
- package/NotificationCenter/NotificationCenterDrawer.d.ts +52 -1
- package/NotificationCenter/NotificationCenterDrawer.js +2 -2
- package/OverflowTooltip/OverflowTooltip.js +46 -5
- package/PageFooter/PageFooter.js +6 -14
- package/Pagination/PaginationPageSize.js +1 -1
- package/README.md +34 -4
- package/Radio/Radio.js +16 -2
- package/Table/Table.js +1 -1
- package/TimePicker/TimePicker.js +1 -1
- package/TimeRangePicker/TimeRangePicker.js +1 -1
- package/Toggle/Toggle.d.ts +1 -1
- package/Toggle/Toggle.js +1 -1
- package/Upload/Upload.d.ts +13 -7
- package/Upload/Upload.js +55 -20
- package/Upload/UploadItem.js +4 -1
- package/Upload/UploadPictureCard.d.ts +5 -0
- package/Upload/UploadPictureCard.js +8 -5
- package/Upload/Uploader.d.ts +32 -31
- package/Upload/Uploader.js +10 -9
- package/index.d.ts +3 -3
- package/index.js +1 -1
- package/llms.txt +128 -9
- package/package.json +5 -4
package/Drawer/Drawer.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ButtonIconType, ButtonSize, ButtonVariant } from '@mezzanine-ui/core/button';
|
|
2
2
|
import { DrawerSize } from '@mezzanine-ui/core/drawer';
|
|
3
|
+
import type { DropdownOption } from '@mezzanine-ui/core/dropdown';
|
|
3
4
|
import { IconDefinition } from '@mezzanine-ui/icons';
|
|
4
|
-
import {
|
|
5
|
-
import { NativeElementPropsWithoutKeyAndRef } from '../utils/jsx-types';
|
|
5
|
+
import { type ChangeEventHandler } from 'react';
|
|
6
6
|
import { BackdropProps } from '../Backdrop';
|
|
7
|
+
import { NativeElementPropsWithoutKeyAndRef } from '../utils/jsx-types';
|
|
7
8
|
export interface DrawerProps extends NativeElementPropsWithoutKeyAndRef<'div'>, Pick<BackdropProps, 'container' | 'disableCloseOnBackdropClick' | 'disablePortal' | 'onBackdropClick' | 'onClose' | 'open'> {
|
|
8
9
|
/**
|
|
9
10
|
* Key prop for forcing content remount when data changes.
|
|
@@ -116,51 +117,62 @@ export interface DrawerProps extends NativeElementPropsWithoutKeyAndRef<'div'>,
|
|
|
116
117
|
*/
|
|
117
118
|
bottomSecondaryActionVariant?: ButtonVariant;
|
|
118
119
|
/**
|
|
119
|
-
* The label of the all radio in
|
|
120
|
+
* The label of the all radio in filter area.
|
|
120
121
|
*/
|
|
121
|
-
|
|
122
|
+
filterAreaAllRadioLabel?: string;
|
|
122
123
|
/**
|
|
123
|
-
* The label of the custom button in
|
|
124
|
+
* The label of the custom button in filter area.
|
|
124
125
|
*/
|
|
125
|
-
|
|
126
|
+
filterAreaCustomButtonLabel?: string;
|
|
126
127
|
/**
|
|
127
|
-
* The default value of the radio group in
|
|
128
|
+
* The default value of the radio group in filter area.
|
|
128
129
|
*/
|
|
129
|
-
|
|
130
|
+
filterAreaDefaultValue?: string;
|
|
130
131
|
/**
|
|
131
|
-
* Whether the
|
|
132
|
+
* Whether the filter area content is empty (for disabling custom button).
|
|
132
133
|
*/
|
|
133
|
-
|
|
134
|
+
filterAreaIsEmpty?: boolean;
|
|
134
135
|
/**
|
|
135
|
-
* The callback function when the custom button is clicked in
|
|
136
|
+
* The callback function when the custom button is clicked in filter area.
|
|
136
137
|
*/
|
|
137
|
-
|
|
138
|
+
filterAreaOnCustomButtonClick?: VoidFunction;
|
|
138
139
|
/**
|
|
139
|
-
* The callback function when the radio group value changes in
|
|
140
|
+
* The callback function when the radio group value changes in filter area.
|
|
140
141
|
*/
|
|
141
|
-
|
|
142
|
+
filterAreaOnRadioChange?: ChangeEventHandler<HTMLInputElement>;
|
|
142
143
|
/**
|
|
143
|
-
* The label of the read radio in
|
|
144
|
+
* The label of the read radio in filter area.
|
|
144
145
|
*/
|
|
145
|
-
|
|
146
|
+
filterAreaReadRadioLabel?: string;
|
|
146
147
|
/**
|
|
147
|
-
* Controls whether to display the
|
|
148
|
+
* Controls whether to display the filter area.
|
|
148
149
|
* @default false
|
|
149
150
|
*/
|
|
150
|
-
|
|
151
|
+
filterAreaShow?: boolean;
|
|
151
152
|
/**
|
|
152
|
-
* Controls whether to display the unread button in
|
|
153
|
+
* Controls whether to display the unread button in filter area.
|
|
153
154
|
* @default false
|
|
154
155
|
*/
|
|
155
|
-
|
|
156
|
+
filterAreaShowUnreadButton?: boolean;
|
|
157
|
+
/**
|
|
158
|
+
* The label of the unread radio in filter area.
|
|
159
|
+
*/
|
|
160
|
+
filterAreaUnreadRadioLabel?: string;
|
|
161
|
+
/**
|
|
162
|
+
* The value of the radio group in filter area.
|
|
163
|
+
*/
|
|
164
|
+
filterAreaValue?: string;
|
|
156
165
|
/**
|
|
157
|
-
*
|
|
166
|
+
* Options for the filter bar dropdown.
|
|
167
|
+
* When non-empty, the right-side filter area button is replaced by a Dropdown
|
|
168
|
+
* triggered by a `DotHorizontalIcon` icon button.
|
|
158
169
|
*/
|
|
159
|
-
|
|
170
|
+
filterAreaOptions?: DropdownOption[];
|
|
160
171
|
/**
|
|
161
|
-
*
|
|
172
|
+
* Callback fired when a filter bar dropdown option is selected.
|
|
173
|
+
* Only used when `filterAreaOptions` is non-empty.
|
|
162
174
|
*/
|
|
163
|
-
|
|
175
|
+
filterAreaOnSelect?: (option: DropdownOption) => void;
|
|
164
176
|
/**
|
|
165
177
|
* Controls whether to disable closing drawer while escape key down.
|
|
166
178
|
* @default false
|
|
@@ -178,13 +190,6 @@ export interface DrawerProps extends NativeElementPropsWithoutKeyAndRef<'div'>,
|
|
|
178
190
|
* Controls whether to display the header area.
|
|
179
191
|
*/
|
|
180
192
|
isHeaderDisplay?: boolean;
|
|
181
|
-
/**
|
|
182
|
-
* Custom render function for the control bar area.
|
|
183
|
-
* The control bar will be rendered between the header and content areas.
|
|
184
|
-
* If provided, this will override the default control bar rendering and control bar-related props.
|
|
185
|
-
* @returns ReactNode - The custom control bar element
|
|
186
|
-
*/
|
|
187
|
-
renderControlBar?: () => React.ReactNode;
|
|
188
193
|
/**
|
|
189
194
|
* Controls the width of the drawer.
|
|
190
195
|
* @default 'medium'
|
|
@@ -195,7 +200,7 @@ export interface DrawerProps extends NativeElementPropsWithoutKeyAndRef<'div'>,
|
|
|
195
200
|
* 從螢幕右側滑入的抽屜面板元件。
|
|
196
201
|
*
|
|
197
202
|
* 使用 `Backdrop` 作為遮罩層,並以 `Slide` 動畫過渡效果呈現開關狀態。
|
|
198
|
-
*
|
|
203
|
+
* 支援標題列、篩選區域(含分頁 Radio)、底部操作按鈕區域,以及按下 Escape 鍵關閉。
|
|
199
204
|
* 當多個 Drawer 同時開啟時,Escape 鍵只會關閉最上層的 Drawer。
|
|
200
205
|
*
|
|
201
206
|
* @example
|
|
@@ -231,5 +236,5 @@ export interface DrawerProps extends NativeElementPropsWithoutKeyAndRef<'div'>,
|
|
|
231
236
|
* @see {@link Modal} 對話框元件
|
|
232
237
|
* @see {@link Backdrop} 遮罩層元件
|
|
233
238
|
*/
|
|
234
|
-
declare const Drawer:
|
|
239
|
+
declare const Drawer: import("react").ForwardRefExoticComponent<DrawerProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
235
240
|
export default Drawer;
|
package/Drawer/Drawer.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
-
import { forwardRef, useState, useEffect, useMemo } from 'react';
|
|
3
2
|
import { drawerClasses } from '@mezzanine-ui/core/drawer';
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
3
|
+
import { DotHorizontalIcon } from '@mezzanine-ui/icons';
|
|
4
|
+
import { MOTION_EASING, MOTION_DURATION } from '@mezzanine-ui/system/motion';
|
|
5
|
+
import { forwardRef, useState, useEffect, useMemo } from 'react';
|
|
7
6
|
import Button from '../Button/Button.js';
|
|
7
|
+
import ClearActions from '../ClearActions/ClearActions.js';
|
|
8
8
|
import Radio from '../Radio/Radio.js';
|
|
9
9
|
import RadioGroup from '../Radio/RadioGroup.js';
|
|
10
|
-
import {
|
|
10
|
+
import { useDocumentEscapeKeyDown } from '../hooks/useDocumentEscapeKeyDown.js';
|
|
11
|
+
import useTopStack from '../hooks/useTopStack.js';
|
|
12
|
+
import Dropdown from '../Dropdown/Dropdown.js';
|
|
11
13
|
import Backdrop from '../Backdrop/Backdrop.js';
|
|
12
14
|
import Slide from '../Transition/Slide.js';
|
|
13
15
|
import cx from 'clsx';
|
|
@@ -16,7 +18,7 @@ import cx from 'clsx';
|
|
|
16
18
|
* 從螢幕右側滑入的抽屜面板元件。
|
|
17
19
|
*
|
|
18
20
|
* 使用 `Backdrop` 作為遮罩層,並以 `Slide` 動畫過渡效果呈現開關狀態。
|
|
19
|
-
*
|
|
21
|
+
* 支援標題列、篩選區域(含分頁 Radio)、底部操作按鈕區域,以及按下 Escape 鍵關閉。
|
|
20
22
|
* 當多個 Drawer 同時開啟時,Escape 鍵只會關閉最上層的 Drawer。
|
|
21
23
|
*
|
|
22
24
|
* @example
|
|
@@ -53,7 +55,7 @@ import cx from 'clsx';
|
|
|
53
55
|
* @see {@link Backdrop} 遮罩層元件
|
|
54
56
|
*/
|
|
55
57
|
const Drawer = forwardRef((props, ref) => {
|
|
56
|
-
const { bottomGhostActionDisabled, bottomGhostActionIcon, bottomGhostActionIconType, bottomGhostActionLoading, bottomGhostActionSize, bottomGhostActionText, bottomGhostActionVariant = 'base-ghost', bottomOnGhostActionClick, bottomOnPrimaryActionClick, bottomOnSecondaryActionClick, bottomPrimaryActionDisabled, bottomPrimaryActionIcon, bottomPrimaryActionIconType, bottomPrimaryActionLoading, bottomPrimaryActionSize, bottomPrimaryActionText, bottomPrimaryActionVariant = 'base-primary', bottomSecondaryActionDisabled, bottomSecondaryActionIcon, bottomSecondaryActionIconType, bottomSecondaryActionLoading, bottomSecondaryActionSize, bottomSecondaryActionText, bottomSecondaryActionVariant = 'base-secondary', children, className, container, contentKey,
|
|
58
|
+
const { bottomGhostActionDisabled, bottomGhostActionIcon, bottomGhostActionIconType, bottomGhostActionLoading, bottomGhostActionSize, bottomGhostActionText, bottomGhostActionVariant = 'base-ghost', bottomOnGhostActionClick, bottomOnPrimaryActionClick, bottomOnSecondaryActionClick, bottomPrimaryActionDisabled, bottomPrimaryActionIcon, bottomPrimaryActionIconType, bottomPrimaryActionLoading, bottomPrimaryActionSize, bottomPrimaryActionText, bottomPrimaryActionVariant = 'base-primary', bottomSecondaryActionDisabled, bottomSecondaryActionIcon, bottomSecondaryActionIconType, bottomSecondaryActionLoading, bottomSecondaryActionSize, bottomSecondaryActionText, bottomSecondaryActionVariant = 'base-secondary', filterAreaOnSelect, filterAreaOptions = [], children, className, container, contentKey, filterAreaAllRadioLabel, filterAreaCustomButtonLabel = '全部已讀', filterAreaDefaultValue, filterAreaIsEmpty = false, filterAreaOnCustomButtonClick, filterAreaOnRadioChange, filterAreaReadRadioLabel, filterAreaShow = false, filterAreaShowUnreadButton = false, filterAreaUnreadRadioLabel, filterAreaValue, disableCloseOnBackdropClick = false, disableCloseOnEscapeKeyDown = false, disablePortal, headerTitle, isBottomDisplay, isHeaderDisplay, onBackdropClick, onClose, open, size = 'medium', ...rest } = props;
|
|
57
59
|
const [exited, setExited] = useState(true);
|
|
58
60
|
const [openCount, setOpenCount] = useState(0);
|
|
59
61
|
// Track open state changes to auto-remount content when drawer reopens
|
|
@@ -67,47 +69,43 @@ const Drawer = forwardRef((props, ref) => {
|
|
|
67
69
|
* Escape keydown close: escape will only close the top drawer
|
|
68
70
|
*/
|
|
69
71
|
const checkIsOnTheTop = useTopStack(open);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
if (customRenderControlBar) {
|
|
73
|
-
return customRenderControlBar;
|
|
74
|
-
}
|
|
75
|
-
// Default control bar implementation
|
|
76
|
-
if (!controlBarShow) {
|
|
72
|
+
const renderFilterArea = useMemo(() => {
|
|
73
|
+
if (!filterAreaShow) {
|
|
77
74
|
return undefined;
|
|
78
75
|
}
|
|
79
76
|
return () => {
|
|
80
77
|
const radios = [];
|
|
81
|
-
if (
|
|
82
|
-
radios.push(jsx(Radio, { type: "segment", value: "all", children:
|
|
78
|
+
if (filterAreaAllRadioLabel) {
|
|
79
|
+
radios.push(jsx(Radio, { type: "segment", value: "all", children: filterAreaAllRadioLabel }, "all"));
|
|
83
80
|
}
|
|
84
|
-
if (
|
|
85
|
-
radios.push(jsx(Radio, { type: "segment", value: "read", children:
|
|
81
|
+
if (filterAreaReadRadioLabel) {
|
|
82
|
+
radios.push(jsx(Radio, { type: "segment", value: "read", children: filterAreaReadRadioLabel }, "read"));
|
|
86
83
|
}
|
|
87
|
-
if (
|
|
88
|
-
radios.push(jsx(Radio, { type: "segment", value: "unread", children:
|
|
84
|
+
if (filterAreaUnreadRadioLabel && filterAreaShowUnreadButton) {
|
|
85
|
+
radios.push(jsx(Radio, { type: "segment", value: "unread", children: filterAreaUnreadRadioLabel }, "unread"));
|
|
89
86
|
}
|
|
90
87
|
const hasRadios = radios.length > 0;
|
|
91
|
-
const hasButton =
|
|
88
|
+
const hasButton = filterAreaOptions.length > 0 || filterAreaOnCustomButtonClick !== undefined;
|
|
92
89
|
// Don't render if neither radios nor button are provided
|
|
93
90
|
if (!hasRadios && !hasButton) {
|
|
94
91
|
return null;
|
|
95
92
|
}
|
|
96
|
-
return (jsxs("div", { className: cx(drawerClasses.
|
|
93
|
+
return (jsxs("div", { className: cx(drawerClasses.filterArea, !hasRadios && hasButton && drawerClasses.filterAreaButtonOnly), children: [hasRadios && (jsx(RadioGroup, { defaultValue: filterAreaDefaultValue !== null && filterAreaDefaultValue !== void 0 ? filterAreaDefaultValue : 'all', onChange: filterAreaOnRadioChange, size: "minor", type: "segment", value: filterAreaValue, children: radios })), hasButton && (filterAreaOptions.length > 0 ? (jsx(Dropdown, { onSelect: filterAreaOnSelect, options: filterAreaOptions, placement: "bottom-end", children: jsx(Button, { icon: DotHorizontalIcon, iconType: "icon-only", size: "minor", type: "button", variant: "base-ghost" }) })) : (jsx(Button, { disabled: filterAreaIsEmpty, onClick: filterAreaOnCustomButtonClick, size: "minor", type: "button", variant: "base-ghost", children: filterAreaCustomButtonLabel })))] }));
|
|
97
94
|
};
|
|
98
95
|
}, [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
96
|
+
filterAreaAllRadioLabel,
|
|
97
|
+
filterAreaCustomButtonLabel,
|
|
98
|
+
filterAreaDefaultValue,
|
|
99
|
+
filterAreaIsEmpty,
|
|
100
|
+
filterAreaOnCustomButtonClick,
|
|
101
|
+
filterAreaOnRadioChange,
|
|
102
|
+
filterAreaReadRadioLabel,
|
|
103
|
+
filterAreaShow,
|
|
104
|
+
filterAreaShowUnreadButton,
|
|
105
|
+
filterAreaUnreadRadioLabel,
|
|
106
|
+
filterAreaValue,
|
|
107
|
+
filterAreaOnSelect,
|
|
108
|
+
filterAreaOptions,
|
|
111
109
|
]);
|
|
112
110
|
useDocumentEscapeKeyDown(() => {
|
|
113
111
|
if (!open || disableCloseOnEscapeKeyDown || !onClose) {
|
|
@@ -129,7 +127,7 @@ const Drawer = forwardRef((props, ref) => {
|
|
|
129
127
|
}, easing: {
|
|
130
128
|
enter: MOTION_EASING.entrance,
|
|
131
129
|
exit: MOTION_EASING.exit,
|
|
132
|
-
}, in: open, onEntered: () => setExited(false), onExited: () => setExited(true), ref: ref, children: jsxs("div", { ...rest, className: cx(drawerClasses.host, drawerClasses.right, drawerClasses.size(size), className), children: [isHeaderDisplay && (jsxs("div", { className: drawerClasses.header, children: [headerTitle, jsx(ClearActions, { onClick: onClose })] })),
|
|
130
|
+
}, in: open, onEntered: () => setExited(false), onExited: () => setExited(true), ref: ref, children: jsxs("div", { ...rest, className: cx(drawerClasses.host, drawerClasses.right, drawerClasses.size(size), className), children: [isHeaderDisplay && (jsxs("div", { className: drawerClasses.header, children: [headerTitle, jsx(ClearActions, { onClick: onClose })] })), renderFilterArea === null || renderFilterArea === void 0 ? void 0 : renderFilterArea(), jsx("div", { className: drawerClasses.content, children: children }, contentKey !== undefined ? contentKey : openCount), isBottomDisplay && (jsxs("div", { className: drawerClasses.bottom, children: [jsx("div", { children: bottomGhostActionText && bottomOnGhostActionClick && (jsx(Button, { disabled: bottomGhostActionDisabled, icon: bottomGhostActionIcon, iconType: bottomGhostActionIconType, loading: bottomGhostActionLoading, onClick: bottomOnGhostActionClick, size: bottomGhostActionSize, type: "button", variant: bottomGhostActionVariant, children: bottomGhostActionText })) }), jsxs("div", { className: drawerClasses['bottom__actions'], children: [bottomSecondaryActionText && bottomOnSecondaryActionClick && (jsx(Button, { disabled: bottomSecondaryActionDisabled, icon: bottomSecondaryActionIcon, iconType: bottomSecondaryActionIconType, loading: bottomSecondaryActionLoading, onClick: bottomOnSecondaryActionClick, size: bottomSecondaryActionSize, type: "button", variant: bottomSecondaryActionVariant, children: bottomSecondaryActionText })), bottomPrimaryActionText && bottomOnPrimaryActionClick && (jsx(Button, { disabled: bottomPrimaryActionDisabled, icon: bottomPrimaryActionIcon, iconType: bottomPrimaryActionIconType, loading: bottomPrimaryActionLoading, onClick: bottomOnPrimaryActionClick, size: bottomPrimaryActionSize, type: "button", variant: bottomPrimaryActionVariant, children: bottomPrimaryActionText }))] })] }))] }) }) }));
|
|
133
131
|
});
|
|
134
132
|
|
|
135
133
|
export { Drawer as default };
|
package/Dropdown/Dropdown.d.ts
CHANGED
|
@@ -26,9 +26,17 @@ export interface DropdownProps extends DropdownItemSharedProps {
|
|
|
26
26
|
*/
|
|
27
27
|
actionText?: string;
|
|
28
28
|
/**
|
|
29
|
-
* The active option index for hover/focus state.
|
|
29
|
+
* The active option index for hover/focus state and Enter selection.
|
|
30
|
+
* Can be set by both keyboard navigation and mouse hover (e.g. in AutoComplete).
|
|
30
31
|
*/
|
|
31
32
|
activeIndex?: number | null;
|
|
33
|
+
/**
|
|
34
|
+
* The keyboard-only active index.
|
|
35
|
+
* When provided, only this index triggers the focus ring (`--keyboard-active` CSS class).
|
|
36
|
+
* Mouse hover updates `activeIndex` for Enter selection but should not update this.
|
|
37
|
+
* When omitted, falls back to the internal uncontrolled index (set only by built-in keyboard navigation).
|
|
38
|
+
*/
|
|
39
|
+
keyboardActiveIndex?: number | null;
|
|
32
40
|
/**
|
|
33
41
|
* The children of the dropdown.
|
|
34
42
|
* This can be a button or an input.
|
|
@@ -70,6 +78,13 @@ export interface DropdownProps extends DropdownItemSharedProps {
|
|
|
70
78
|
* The max height of the dropdown list.
|
|
71
79
|
*/
|
|
72
80
|
maxHeight?: number | string;
|
|
81
|
+
/**
|
|
82
|
+
* Override the default `min-width` of the dropdown list.
|
|
83
|
+
* Accepts a number (pixels) or any valid CSS length string.
|
|
84
|
+
* Pass `0` to remove the minimum width constraint entirely — useful when `sameWidth` controls the width.
|
|
85
|
+
* @default spacing token `size-container-tiny`
|
|
86
|
+
*/
|
|
87
|
+
minWidth?: number | string;
|
|
73
88
|
/**
|
|
74
89
|
* Whether the dropdown is open (controlled).
|
|
75
90
|
*/
|
package/Dropdown/Dropdown.js
CHANGED
|
@@ -72,7 +72,7 @@ function getElementRef(element) {
|
|
|
72
72
|
* @see {@link AutoComplete} 具備自動補全功能的輸入元件
|
|
73
73
|
*/
|
|
74
74
|
function Dropdown(props) {
|
|
75
|
-
const { activeIndex: activeIndexProp, id, children, options = [], type = 'default', toggleCheckedOnClick, maxHeight, disabled = false, showDropdownActions = false, actionCancelText, actionConfirmText, actionText, actionClearText, actionCustomButtonProps, showActionShowTopBar, isMatchInputValue = false, inputPosition = 'outside', placement = 'bottom-start', customWidth, sameWidth = false, listboxId: listboxIdProp, listboxLabel, onClose, onOpen, open: openProp, onVisibilityChange, onSelect, onActionConfirm, onActionCancel, onActionCustom, onActionClear, onItemHover, zIndex, status, loadingText, emptyText, emptyIcon, loadingPosition = 'full', followText: followTextProp, globalPortal = true, onReachBottom, onLeaveBottom, onScroll, mode, value, scrollbarDefer, scrollbarDisabled, scrollbarMaxWidth, scrollbarOptions, } = props;
|
|
75
|
+
const { activeIndex: activeIndexProp, keyboardActiveIndex: keyboardActiveIndexProp, id, children, options = [], type = 'default', toggleCheckedOnClick, maxHeight, minWidth, disabled = false, showDropdownActions = false, actionCancelText, actionConfirmText, actionText, actionClearText, actionCustomButtonProps, showActionShowTopBar, isMatchInputValue = false, inputPosition = 'outside', placement = 'bottom-start', customWidth, sameWidth = false, listboxId: listboxIdProp, listboxLabel, onClose, onOpen, open: openProp, onVisibilityChange, onSelect, onActionConfirm, onActionCancel, onActionCustom, onActionClear, onItemHover, zIndex, status, loadingText, emptyText, emptyIcon, loadingPosition = 'full', followText: followTextProp, globalPortal = true, onReachBottom, onLeaveBottom, onScroll, mode, value, scrollbarDefer, scrollbarDisabled, scrollbarMaxWidth, scrollbarOptions, } = props;
|
|
76
76
|
const isInline = inputPosition === 'inside';
|
|
77
77
|
const inputId = useId();
|
|
78
78
|
const defaultListboxId = `${inputId}-listbox`;
|
|
@@ -107,14 +107,49 @@ function Dropdown(props) {
|
|
|
107
107
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
|
108
108
|
const isOpenControlled = openProp !== undefined;
|
|
109
109
|
const isOpen = isOpenControlled ? !!openProp : uncontrolledOpen;
|
|
110
|
-
|
|
111
|
-
// Currently not used in handleItemHover to prevent style conflicts
|
|
112
|
-
const [uncontrolledActiveIndex, _setUncontrolledActiveIndex] = useState(activeIndexProp !== null && activeIndexProp !== void 0 ? activeIndexProp : null);
|
|
110
|
+
const [uncontrolledActiveIndex, setUncontrolledActiveIndex] = useState(activeIndexProp !== null && activeIndexProp !== void 0 ? activeIndexProp : null);
|
|
113
111
|
const isActiveIndexControlled = activeIndexProp !== undefined;
|
|
114
112
|
const mergedActiveIndex = isActiveIndexControlled
|
|
115
113
|
? activeIndexProp
|
|
116
114
|
: uncontrolledActiveIndex;
|
|
115
|
+
// For keyboard-only visual focus (focus ring). When not externally controlled,
|
|
116
|
+
// `uncontrolledActiveIndex` is already set only by keyboard (never by hover).
|
|
117
|
+
const mergedKeyboardActiveIndex = keyboardActiveIndexProp !== undefined
|
|
118
|
+
? keyboardActiveIndexProp
|
|
119
|
+
: uncontrolledActiveIndex;
|
|
117
120
|
const containerRef = useRef(null);
|
|
121
|
+
// Expansion state for tree type (lifted here so keyboard nav can track visible options)
|
|
122
|
+
const [expandedNodes, setExpandedNodes] = useState(new Set());
|
|
123
|
+
const handleToggleExpand = useCallback((optionId) => {
|
|
124
|
+
setExpandedNodes((prev) => {
|
|
125
|
+
const next = new Set(prev);
|
|
126
|
+
if (next.has(optionId)) {
|
|
127
|
+
next.delete(optionId);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
next.add(optionId);
|
|
131
|
+
}
|
|
132
|
+
return next;
|
|
133
|
+
});
|
|
134
|
+
}, []);
|
|
135
|
+
// Flat list of navigable options, respecting tree expansion state
|
|
136
|
+
const flatNavigableOptions = useMemo(() => {
|
|
137
|
+
const opts = options;
|
|
138
|
+
if (type === 'grouped') {
|
|
139
|
+
return opts.flatMap((g) => { var _a; return (_a = g.children) !== null && _a !== void 0 ? _a : []; });
|
|
140
|
+
}
|
|
141
|
+
if (type === 'tree') {
|
|
142
|
+
const flatten = (items) => items.flatMap((item) => {
|
|
143
|
+
var _a;
|
|
144
|
+
if (((_a = item.children) === null || _a === void 0 ? void 0 : _a.length) && expandedNodes.has(item.id)) {
|
|
145
|
+
return [item, ...flatten(item.children)];
|
|
146
|
+
}
|
|
147
|
+
return [item];
|
|
148
|
+
});
|
|
149
|
+
return flatten(opts);
|
|
150
|
+
}
|
|
151
|
+
return opts;
|
|
152
|
+
}, [options, type, expandedNodes]);
|
|
118
153
|
const ariaActivedescendant = useMemo(() => {
|
|
119
154
|
if (mergedActiveIndex !== null && mergedActiveIndex >= 0) {
|
|
120
155
|
return `${listboxId}-option-${mergedActiveIndex}`;
|
|
@@ -237,10 +272,16 @@ function Dropdown(props) {
|
|
|
237
272
|
const popperControllerRef = useRef(null);
|
|
238
273
|
// Extract combobox props logic to avoid duplication
|
|
239
274
|
const getComboboxProps = useMemo(() => {
|
|
240
|
-
// Only access the type property to check if it's a Button component
|
|
241
275
|
const isInput = children.type !== Button;
|
|
242
|
-
if (!isInput)
|
|
243
|
-
|
|
276
|
+
if (!isInput) {
|
|
277
|
+
// Button trigger: expose listbox ARIA so screen readers can navigate
|
|
278
|
+
return {
|
|
279
|
+
'aria-haspopup': 'listbox',
|
|
280
|
+
'aria-expanded': isOpen,
|
|
281
|
+
'aria-controls': listboxId,
|
|
282
|
+
'aria-activedescendant': ariaActivedescendant,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
244
285
|
return {
|
|
245
286
|
role: 'combobox',
|
|
246
287
|
'aria-controls': listboxId,
|
|
@@ -253,18 +294,99 @@ function Dropdown(props) {
|
|
|
253
294
|
const handleItemHover = useCallback((index) => {
|
|
254
295
|
onItemHover === null || onItemHover === void 0 ? void 0 : onItemHover(index);
|
|
255
296
|
}, [onItemHover]);
|
|
297
|
+
// Reset active index when dropdown closes (uncontrolled only)
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
if (!isOpen && !isActiveIndexControlled) {
|
|
300
|
+
setUncontrolledActiveIndex(null);
|
|
301
|
+
}
|
|
302
|
+
}, [isOpen, isActiveIndexControlled]);
|
|
303
|
+
// Scroll the active option into view whenever activeIndex changes
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
if (!isOpen || mergedActiveIndex === null)
|
|
306
|
+
return;
|
|
307
|
+
requestAnimationFrame(() => {
|
|
308
|
+
var _a;
|
|
309
|
+
(_a = document
|
|
310
|
+
.getElementById(`${listboxId}-option-${mergedActiveIndex}`)) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ block: 'nearest' });
|
|
311
|
+
});
|
|
312
|
+
}, [mergedActiveIndex, isOpen, listboxId]);
|
|
313
|
+
// Built-in keyboard navigation (only when activeIndex is not controlled externally)
|
|
314
|
+
const handleBuiltinKeyDown = useCallback((event) => {
|
|
315
|
+
var _a;
|
|
316
|
+
if (isActiveIndexControlled)
|
|
317
|
+
return;
|
|
318
|
+
const count = flatNavigableOptions.length;
|
|
319
|
+
if (!isOpen) {
|
|
320
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
setOpen(true);
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (count === 0)
|
|
327
|
+
return;
|
|
328
|
+
switch (event.key) {
|
|
329
|
+
case 'ArrowDown': {
|
|
330
|
+
event.preventDefault();
|
|
331
|
+
setUncontrolledActiveIndex((prev) => prev === null ? 0 : (prev + 1) % count);
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case 'ArrowUp': {
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
setUncontrolledActiveIndex((prev) => prev === null ? count - 1 : (prev - 1 + count) % count);
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case 'Enter': {
|
|
340
|
+
if (mergedActiveIndex !== null && mergedActiveIndex >= 0) {
|
|
341
|
+
const activeOption = flatNavigableOptions[mergedActiveIndex];
|
|
342
|
+
if (activeOption) {
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
if (type === 'tree' && ((_a = activeOption.children) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
345
|
+
handleToggleExpand(activeOption.id);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
onSelect === null || onSelect === void 0 ? void 0 : onSelect(activeOption);
|
|
349
|
+
if (mode !== 'multiple') {
|
|
350
|
+
setOpen(false);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case 'Escape': {
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
setOpen(false);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}, [
|
|
364
|
+
isActiveIndexControlled,
|
|
365
|
+
isOpen,
|
|
366
|
+
flatNavigableOptions,
|
|
367
|
+
mergedActiveIndex,
|
|
368
|
+
type,
|
|
369
|
+
mode,
|
|
370
|
+
handleToggleExpand,
|
|
371
|
+
onSelect,
|
|
372
|
+
setOpen,
|
|
373
|
+
]);
|
|
256
374
|
// Extract shared DropdownItem props to avoid duplication
|
|
257
375
|
const baseDropdownItemProps = useMemo(() => ({
|
|
258
376
|
actionConfig,
|
|
259
377
|
activeIndex: mergedActiveIndex,
|
|
378
|
+
keyboardActiveIndex: mergedKeyboardActiveIndex,
|
|
260
379
|
disabled,
|
|
380
|
+
expandedNodes,
|
|
261
381
|
followText,
|
|
262
382
|
listboxId,
|
|
263
383
|
listboxLabel,
|
|
264
384
|
maxHeight,
|
|
385
|
+
minWidth,
|
|
265
386
|
sameWidth,
|
|
266
387
|
onHover: handleItemHover,
|
|
267
388
|
onSelect,
|
|
389
|
+
onToggleExpand: handleToggleExpand,
|
|
268
390
|
onReachBottom,
|
|
269
391
|
onLeaveBottom,
|
|
270
392
|
onScroll,
|
|
@@ -285,13 +407,17 @@ function Dropdown(props) {
|
|
|
285
407
|
}), [
|
|
286
408
|
actionConfig,
|
|
287
409
|
mergedActiveIndex,
|
|
410
|
+
mergedKeyboardActiveIndex,
|
|
288
411
|
disabled,
|
|
289
412
|
followText,
|
|
290
413
|
listboxId,
|
|
291
414
|
listboxLabel,
|
|
292
415
|
maxHeight,
|
|
416
|
+
minWidth,
|
|
293
417
|
sameWidth,
|
|
294
418
|
handleItemHover,
|
|
419
|
+
handleToggleExpand,
|
|
420
|
+
expandedNodes,
|
|
295
421
|
onSelect,
|
|
296
422
|
onReachBottom,
|
|
297
423
|
onLeaveBottom,
|
|
@@ -317,6 +443,7 @@ function Dropdown(props) {
|
|
|
317
443
|
const childRef = getElementRef(childWithRef);
|
|
318
444
|
const composedRef = composeRefs([anchorRef, childRef]);
|
|
319
445
|
const originalOnClick = childProps.onClick;
|
|
446
|
+
const originalOnKeyDown = childProps.onKeyDown;
|
|
320
447
|
return cloneElement(childWithRef, {
|
|
321
448
|
ref: composedRef,
|
|
322
449
|
...getComboboxProps,
|
|
@@ -328,8 +455,14 @@ function Dropdown(props) {
|
|
|
328
455
|
setOpen((prev) => !prev);
|
|
329
456
|
}
|
|
330
457
|
},
|
|
458
|
+
onKeyDown: (event) => {
|
|
459
|
+
originalOnKeyDown === null || originalOnKeyDown === void 0 ? void 0 : originalOnKeyDown(event);
|
|
460
|
+
if (event === null || event === void 0 ? void 0 : event.defaultPrevented)
|
|
461
|
+
return;
|
|
462
|
+
handleBuiltinKeyDown(event);
|
|
463
|
+
},
|
|
331
464
|
});
|
|
332
|
-
}, [children, getComboboxProps, isInline, setOpen]);
|
|
465
|
+
}, [children, getComboboxProps, handleBuiltinKeyDown, isInline, setOpen]);
|
|
333
466
|
const inlineTriggerElement = useMemo(() => {
|
|
334
467
|
if (!isInline) {
|
|
335
468
|
return null;
|
|
@@ -341,6 +474,7 @@ function Dropdown(props) {
|
|
|
341
474
|
const originalOnBlur = childProps.onBlur;
|
|
342
475
|
const originalOnClick = childProps.onClick;
|
|
343
476
|
const originalOnFocus = childProps.onFocus;
|
|
477
|
+
const originalOnKeyDown = childProps.onKeyDown;
|
|
344
478
|
return cloneElement(childWithRef, {
|
|
345
479
|
ref: composedRef,
|
|
346
480
|
...getComboboxProps,
|
|
@@ -374,8 +508,21 @@ function Dropdown(props) {
|
|
|
374
508
|
return;
|
|
375
509
|
setOpen(true);
|
|
376
510
|
},
|
|
511
|
+
onKeyDown: (event) => {
|
|
512
|
+
originalOnKeyDown === null || originalOnKeyDown === void 0 ? void 0 : originalOnKeyDown(event);
|
|
513
|
+
if (event === null || event === void 0 ? void 0 : event.defaultPrevented)
|
|
514
|
+
return;
|
|
515
|
+
handleBuiltinKeyDown(event);
|
|
516
|
+
},
|
|
377
517
|
});
|
|
378
|
-
}, [
|
|
518
|
+
}, [
|
|
519
|
+
children,
|
|
520
|
+
getComboboxProps,
|
|
521
|
+
handleBuiltinKeyDown,
|
|
522
|
+
isInline,
|
|
523
|
+
isOpenControlled,
|
|
524
|
+
setOpen,
|
|
525
|
+
]);
|
|
379
526
|
useDocumentEvents(() => {
|
|
380
527
|
if (!isOpen) {
|
|
381
528
|
return;
|
|
@@ -9,9 +9,26 @@ export interface DropdownItemProps<T extends DropdownType | undefined = Dropdown
|
|
|
9
9
|
*/
|
|
10
10
|
actionConfig?: DropdownActionProps;
|
|
11
11
|
/**
|
|
12
|
-
* The active option index for hover/focus state.
|
|
12
|
+
* The active option index for hover/focus state and Enter selection.
|
|
13
13
|
*/
|
|
14
14
|
activeIndex: number | null;
|
|
15
|
+
/**
|
|
16
|
+
* Keyboard-only active index. When provided, only this index applies the
|
|
17
|
+
* focus ring (`--keyboard-active`) and active background via `DropdownItemCard`'s `active` prop.
|
|
18
|
+
* Mouse hover should update `activeIndex` (for Enter selection) but NOT this value.
|
|
19
|
+
* Falls back to `activeIndex` when not provided (backward-compatible).
|
|
20
|
+
*/
|
|
21
|
+
keyboardActiveIndex?: number | null;
|
|
22
|
+
/**
|
|
23
|
+
* Controlled set of expanded node IDs for tree type.
|
|
24
|
+
* When provided, expansion state is managed externally.
|
|
25
|
+
*/
|
|
26
|
+
expandedNodes?: Set<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Callback to toggle the expansion of a tree node.
|
|
29
|
+
* Required when `expandedNodes` is provided.
|
|
30
|
+
*/
|
|
31
|
+
onToggleExpand?: (id: string) => void;
|
|
15
32
|
/**
|
|
16
33
|
* The text to follow.
|
|
17
34
|
*/
|
|
@@ -33,6 +50,13 @@ export interface DropdownItemProps<T extends DropdownType | undefined = Dropdown
|
|
|
33
50
|
* The max height of the dropdown list.
|
|
34
51
|
*/
|
|
35
52
|
maxHeight?: number | string;
|
|
53
|
+
/**
|
|
54
|
+
* Override the default `min-width` of the dropdown list.
|
|
55
|
+
* Accepts a number (pixels) or any valid CSS length string.
|
|
56
|
+
* Pass `0` to remove the minimum width constraint entirely.
|
|
57
|
+
* @default spacing token `size-container-tiny`
|
|
58
|
+
*/
|
|
59
|
+
minWidth?: number | string;
|
|
36
60
|
/**
|
|
37
61
|
* Whether to set the same width as its anchor element.
|
|
38
62
|
* @default false
|
|
@@ -40,7 +64,7 @@ export interface DropdownItemProps<T extends DropdownType | undefined = Dropdown
|
|
|
40
64
|
sameWidth?: boolean;
|
|
41
65
|
/**
|
|
42
66
|
* Callback when hovering option index changes.
|
|
43
|
-
|
|
67
|
+
*/
|
|
44
68
|
onHover?: (index: number) => void;
|
|
45
69
|
/**
|
|
46
70
|
* Options to render.
|