@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.
Files changed (61) hide show
  1. package/AutoComplete/AutoComplete.d.ts +25 -9
  2. package/AutoComplete/AutoComplete.js +84 -17
  3. package/AutoComplete/AutoCompleteInside.d.ts +54 -0
  4. package/AutoComplete/AutoCompleteInside.js +17 -0
  5. package/AutoComplete/useAutoCompleteKeyboard.d.ts +2 -1
  6. package/AutoComplete/useAutoCompleteKeyboard.js +4 -1
  7. package/Breadcrumb/BreadcrumbDropdown.d.ts +1 -1
  8. package/Breadcrumb/BreadcrumbOverflowMenuDropdown.d.ts +1 -1
  9. package/Breadcrumb/typings.d.ts +1 -1
  10. package/COMPONENTS.md +2 -2
  11. package/Checkbox/Checkbox.js +24 -3
  12. package/Cropper/Cropper.d.ts +1 -1
  13. package/Description/Description.d.ts +1 -1
  14. package/Description/Description.js +1 -1
  15. package/Description/DescriptionTitle.d.ts +6 -1
  16. package/Description/DescriptionTitle.js +2 -2
  17. package/Drawer/Drawer.d.ts +39 -34
  18. package/Drawer/Drawer.js +33 -35
  19. package/Dropdown/Dropdown.d.ts +16 -1
  20. package/Dropdown/Dropdown.js +156 -9
  21. package/Dropdown/DropdownItem.d.ts +26 -2
  22. package/Dropdown/DropdownItem.js +91 -43
  23. package/Dropdown/DropdownItemCard.d.ts +3 -2
  24. package/Dropdown/DropdownItemCard.js +8 -5
  25. package/Dropdown/dropdownKeydownHandler.d.ts +6 -0
  26. package/Dropdown/dropdownKeydownHandler.js +14 -7
  27. package/FilterArea/Filter.d.ts +25 -2
  28. package/FilterArea/Filter.js +23 -0
  29. package/FilterArea/FilterArea.d.ts +43 -4
  30. package/FilterArea/FilterArea.js +35 -2
  31. package/FilterArea/FilterLine.d.ts +19 -0
  32. package/FilterArea/FilterLine.js +19 -0
  33. package/Input/SpinnerButton/SpinnerButton.js +1 -1
  34. package/Modal/Modal.d.ts +22 -86
  35. package/Modal/Modal.js +4 -2
  36. package/Modal/ModalBodyForVerification.js +3 -1
  37. package/NotificationCenter/NotificationCenter.d.ts +21 -9
  38. package/NotificationCenter/NotificationCenter.js +22 -10
  39. package/NotificationCenter/NotificationCenterDrawer.d.ts +52 -1
  40. package/NotificationCenter/NotificationCenterDrawer.js +2 -2
  41. package/OverflowTooltip/OverflowTooltip.js +46 -5
  42. package/PageFooter/PageFooter.js +6 -14
  43. package/Pagination/PaginationPageSize.js +1 -1
  44. package/README.md +34 -4
  45. package/Radio/Radio.js +16 -2
  46. package/Table/Table.js +1 -1
  47. package/TimePicker/TimePicker.js +1 -1
  48. package/TimeRangePicker/TimeRangePicker.js +1 -1
  49. package/Toggle/Toggle.d.ts +1 -1
  50. package/Toggle/Toggle.js +1 -1
  51. package/Upload/Upload.d.ts +13 -7
  52. package/Upload/Upload.js +55 -20
  53. package/Upload/UploadItem.js +4 -1
  54. package/Upload/UploadPictureCard.d.ts +5 -0
  55. package/Upload/UploadPictureCard.js +8 -5
  56. package/Upload/Uploader.d.ts +32 -31
  57. package/Upload/Uploader.js +10 -9
  58. package/index.d.ts +3 -3
  59. package/index.js +1 -1
  60. package/llms.txt +128 -9
  61. package/package.json +5 -4
@@ -1,9 +1,10 @@
1
- import React, { type ChangeEventHandler } from 'react';
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 { ButtonIconType, ButtonSize, ButtonVariant } from '@mezzanine-ui/core/button';
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 control bar.
120
+ * The label of the all radio in filter area.
120
121
  */
121
- controlBarAllRadioLabel?: string;
122
+ filterAreaAllRadioLabel?: string;
122
123
  /**
123
- * The label of the custom button in control bar.
124
+ * The label of the custom button in filter area.
124
125
  */
125
- controlBarCustomButtonLabel?: string;
126
+ filterAreaCustomButtonLabel?: string;
126
127
  /**
127
- * The default value of the radio group in control bar.
128
+ * The default value of the radio group in filter area.
128
129
  */
129
- controlBarDefaultValue?: string;
130
+ filterAreaDefaultValue?: string;
130
131
  /**
131
- * Whether the control bar content is empty (for disabling custom button).
132
+ * Whether the filter area content is empty (for disabling custom button).
132
133
  */
133
- controlBarIsEmpty?: boolean;
134
+ filterAreaIsEmpty?: boolean;
134
135
  /**
135
- * The callback function when the custom button is clicked in control bar.
136
+ * The callback function when the custom button is clicked in filter area.
136
137
  */
137
- controlBarOnCustomButtonClick?: VoidFunction;
138
+ filterAreaOnCustomButtonClick?: VoidFunction;
138
139
  /**
139
- * The callback function when the radio group value changes in control bar.
140
+ * The callback function when the radio group value changes in filter area.
140
141
  */
141
- controlBarOnRadioChange?: ChangeEventHandler<HTMLInputElement>;
142
+ filterAreaOnRadioChange?: ChangeEventHandler<HTMLInputElement>;
142
143
  /**
143
- * The label of the read radio in control bar.
144
+ * The label of the read radio in filter area.
144
145
  */
145
- controlBarReadRadioLabel?: string;
146
+ filterAreaReadRadioLabel?: string;
146
147
  /**
147
- * Controls whether to display the control bar.
148
+ * Controls whether to display the filter area.
148
149
  * @default false
149
150
  */
150
- controlBarShow?: boolean;
151
+ filterAreaShow?: boolean;
151
152
  /**
152
- * Controls whether to display the unread button in control bar.
153
+ * Controls whether to display the unread button in filter area.
153
154
  * @default false
154
155
  */
155
- controlBarShowUnreadButton?: boolean;
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
- * The label of the unread radio in control bar.
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
- controlBarUnreadRadioLabel?: string;
170
+ filterAreaOptions?: DropdownOption[];
160
171
  /**
161
- * The value of the radio group in control bar.
172
+ * Callback fired when a filter bar dropdown option is selected.
173
+ * Only used when `filterAreaOptions` is non-empty.
162
174
  */
163
- controlBarValue?: string;
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
- * 支援標題列、自訂控制列(含分頁 Radio)、底部操作按鈕區域,以及按下 Escape 鍵關閉。
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: React.ForwardRefExoticComponent<DrawerProps & React.RefAttributes<HTMLDivElement>>;
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 { useDocumentEscapeKeyDown } from '../hooks/useDocumentEscapeKeyDown.js';
5
- import useTopStack from '../hooks/useTopStack.js';
6
- import ClearActions from '../ClearActions/ClearActions.js';
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 { MOTION_EASING, MOTION_DURATION } from '@mezzanine-ui/system/motion';
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
- * 支援標題列、自訂控制列(含分頁 Radio)、底部操作按鈕區域,以及按下 Escape 鍵關閉。
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, controlBarAllRadioLabel, controlBarCustomButtonLabel = '全部已讀', controlBarDefaultValue, controlBarIsEmpty = false, controlBarOnCustomButtonClick, controlBarOnRadioChange, controlBarReadRadioLabel, controlBarShow = false, controlBarShowUnreadButton = false, controlBarUnreadRadioLabel, controlBarValue, disableCloseOnBackdropClick = false, disableCloseOnEscapeKeyDown = false, disablePortal, headerTitle, isBottomDisplay, isHeaderDisplay, onBackdropClick, onClose, open, renderControlBar: customRenderControlBar, size = 'medium', ...rest } = props;
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 renderControlBar = useMemo(() => {
71
- // If custom renderControlBar is provided, use it
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 (controlBarAllRadioLabel) {
82
- radios.push(jsx(Radio, { type: "segment", value: "all", children: controlBarAllRadioLabel }, "all"));
78
+ if (filterAreaAllRadioLabel) {
79
+ radios.push(jsx(Radio, { type: "segment", value: "all", children: filterAreaAllRadioLabel }, "all"));
83
80
  }
84
- if (controlBarReadRadioLabel) {
85
- radios.push(jsx(Radio, { type: "segment", value: "read", children: controlBarReadRadioLabel }, "read"));
81
+ if (filterAreaReadRadioLabel) {
82
+ radios.push(jsx(Radio, { type: "segment", value: "read", children: filterAreaReadRadioLabel }, "read"));
86
83
  }
87
- if (controlBarUnreadRadioLabel && controlBarShowUnreadButton) {
88
- radios.push(jsx(Radio, { type: "segment", value: "unread", children: controlBarUnreadRadioLabel }, "unread"));
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 = controlBarOnCustomButtonClick !== undefined;
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.controlBar, !hasRadios && hasButton && drawerClasses.controlBarButtonOnly), children: [hasRadios && (jsx(RadioGroup, { defaultValue: controlBarDefaultValue !== null && controlBarDefaultValue !== void 0 ? controlBarDefaultValue : 'all', onChange: controlBarOnRadioChange, size: "minor", type: "segment", value: controlBarValue, children: radios })), hasButton && (jsx(Button, { disabled: controlBarIsEmpty, onClick: controlBarOnCustomButtonClick, size: "minor", type: "button", variant: "base-ghost", children: controlBarCustomButtonLabel }))] }));
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
- controlBarAllRadioLabel,
100
- controlBarCustomButtonLabel,
101
- controlBarDefaultValue,
102
- controlBarIsEmpty,
103
- controlBarOnCustomButtonClick,
104
- controlBarOnRadioChange,
105
- controlBarReadRadioLabel,
106
- controlBarShow,
107
- controlBarShowUnreadButton,
108
- controlBarUnreadRadioLabel,
109
- controlBarValue,
110
- customRenderControlBar,
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 })] })), renderControlBar === null || renderControlBar === void 0 ? void 0 : renderControlBar(), 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 }))] })] }))] }) }) }));
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 };
@@ -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
  */
@@ -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
- // Keep setter for uncontrolled mode support (e.g., keyboard navigation)
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
- return {};
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
- }, [children, getComboboxProps, isInline, isOpenControlled, setOpen]);
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.