@mezzanine-ui/react 1.0.0-beta.4 → 1.0.0-beta.5
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.js +17 -1
- package/Calendar/Calendar.js +2 -6
- package/Calendar/CalendarCell.d.ts +22 -0
- package/Calendar/CalendarCell.js +6 -2
- package/Calendar/CalendarControls.js +1 -1
- package/Calendar/CalendarDayOfWeek.js +3 -2
- package/Calendar/CalendarDays.js +5 -1
- package/Calendar/CalendarHalfYears.js +13 -7
- package/Calendar/CalendarMonths.js +13 -6
- package/Calendar/CalendarQuarters.js +13 -7
- package/Calendar/CalendarWeeks.js +87 -34
- package/Calendar/CalendarYears.js +13 -12
- package/Calendar/useCalendarControlModifiers.d.ts +1 -1
- package/Calendar/useCalendarControlModifiers.js +12 -12
- package/Calendar/useCalendarControls.d.ts +4 -4
- package/Calendar/useCalendarControls.js +33 -19
- package/Calendar/useRangeCalendarControls.d.ts +8 -8
- package/Calendar/useRangeCalendarControls.js +42 -31
- package/DateRangePicker/useDateRangeCalendarControls.js +8 -2
- package/Dropdown/Dropdown.d.ts +31 -0
- package/Dropdown/Dropdown.js +11 -1
- package/Dropdown/DropdownItem.d.ts +32 -0
- package/Dropdown/DropdownItem.js +112 -10
- package/Dropdown/DropdownItemCard.d.ts +5 -0
- package/Dropdown/DropdownItemCard.js +2 -2
- package/Form/useSelectValueControl.d.ts +3 -4
- package/Form/useSelectValueControl.js +51 -39
- package/Popper/Popper.js +2 -1
- package/Scrollbar/Scrollbar.js +1 -0
- package/Select/Select.d.ts +37 -18
- package/Select/Select.js +165 -51
- package/Select/index.d.ts +8 -9
- package/Select/index.js +3 -3
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
3
3
|
import { useCalendarControlModifiers } from './useCalendarControlModifiers.js';
|
|
4
4
|
import { useCalendarModeStack } from './useCalendarModeStack.js';
|
|
5
5
|
|
|
@@ -10,30 +10,44 @@ function useCalendarControls(referenceDateProp, mode) {
|
|
|
10
10
|
}, [referenceDateProp]);
|
|
11
11
|
const { currentMode, pushModeStack, popModeStack } = useCalendarModeStack(mode || 'day');
|
|
12
12
|
const modifierGroup = useCalendarControlModifiers();
|
|
13
|
-
const onPrev = () => {
|
|
13
|
+
const onPrev = useMemo(() => {
|
|
14
14
|
const modifiers = modifierGroup[currentMode].single;
|
|
15
15
|
if (!modifiers)
|
|
16
16
|
return;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
return () => {
|
|
18
|
+
const [handleMinus] = modifiers;
|
|
19
|
+
setReferenceDate(handleMinus(referenceDate));
|
|
20
|
+
};
|
|
21
|
+
}, [currentMode, modifierGroup, referenceDate]);
|
|
22
|
+
const onNext = useMemo(() => {
|
|
21
23
|
const modifiers = modifierGroup[currentMode].single;
|
|
22
24
|
if (!modifiers)
|
|
23
25
|
return;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
return () => {
|
|
27
|
+
const [, handleAdd] = modifiers;
|
|
28
|
+
setReferenceDate(handleAdd(referenceDate));
|
|
29
|
+
};
|
|
30
|
+
}, [currentMode, modifierGroup, referenceDate]);
|
|
31
|
+
const onDoublePrev = useMemo(() => {
|
|
32
|
+
const modifiers = modifierGroup[currentMode].double;
|
|
33
|
+
if (!modifiers)
|
|
34
|
+
return;
|
|
35
|
+
return () => {
|
|
36
|
+
const [handleMinus] = modifiers;
|
|
37
|
+
setReferenceDate(handleMinus(referenceDate));
|
|
38
|
+
};
|
|
39
|
+
}, [currentMode, modifierGroup, referenceDate]);
|
|
40
|
+
const onDoubleNext = useMemo(() => {
|
|
41
|
+
const modifiers = modifierGroup[currentMode].double;
|
|
42
|
+
if (!modifiers)
|
|
43
|
+
return;
|
|
44
|
+
return () => {
|
|
45
|
+
const [, handleAdd] = modifiers;
|
|
46
|
+
setReferenceDate(handleAdd(referenceDate));
|
|
47
|
+
};
|
|
48
|
+
}, [currentMode, modifierGroup, referenceDate]);
|
|
49
|
+
const onMonthControlClick = useCallback(() => pushModeStack('month'), [pushModeStack]);
|
|
50
|
+
const onYearControlClick = useCallback(() => pushModeStack('year'), [pushModeStack]);
|
|
37
51
|
return {
|
|
38
52
|
currentMode,
|
|
39
53
|
onMonthControlClick,
|
|
@@ -2,14 +2,14 @@ import { DateType, CalendarMode } from '@mezzanine-ui/core/calendar';
|
|
|
2
2
|
export declare function useRangeCalendarControls(referenceDateProp: DateType, mode?: CalendarMode): {
|
|
3
3
|
currentMode: CalendarMode;
|
|
4
4
|
onMonthControlClick: () => void;
|
|
5
|
-
onFirstNext: () => void;
|
|
6
|
-
onFirstPrev: () => void;
|
|
7
|
-
onFirstDoubleNext: () => void;
|
|
8
|
-
onFirstDoublePrev: () => void;
|
|
9
|
-
onSecondNext: () => void;
|
|
10
|
-
onSecondPrev: () => void;
|
|
11
|
-
onSecondDoubleNext: () => void;
|
|
12
|
-
onSecondDoublePrev: () => void;
|
|
5
|
+
onFirstNext: (() => void) | undefined;
|
|
6
|
+
onFirstPrev: (() => void) | undefined;
|
|
7
|
+
onFirstDoubleNext: (() => void) | undefined;
|
|
8
|
+
onFirstDoublePrev: (() => void) | undefined;
|
|
9
|
+
onSecondNext: (() => void) | undefined;
|
|
10
|
+
onSecondPrev: (() => void) | undefined;
|
|
11
|
+
onSecondDoubleNext: (() => void) | undefined;
|
|
12
|
+
onSecondDoublePrev: (() => void) | undefined;
|
|
13
13
|
onYearControlClick: () => void;
|
|
14
14
|
popModeStack: () => void;
|
|
15
15
|
referenceDates: [string, string];
|
|
@@ -37,54 +37,65 @@ function useRangeCalendarControls(referenceDateProp, mode) {
|
|
|
37
37
|
const { currentMode, pushModeStack, popModeStack } = useCalendarModeStack(mode || 'day');
|
|
38
38
|
const modifierGroup = useCalendarControlModifiers();
|
|
39
39
|
// First calendar controls
|
|
40
|
-
const onFirstPrev = () => {
|
|
40
|
+
const onFirstPrev = useMemo(() => {
|
|
41
41
|
const modifiers = modifierGroup[currentMode].single;
|
|
42
42
|
if (!modifiers)
|
|
43
43
|
return;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
return () => {
|
|
45
|
+
const [handleMinus] = modifiers;
|
|
46
|
+
const newFirst = handleMinus(firstReferenceDate);
|
|
47
|
+
setFirstReferenceDate(newFirst);
|
|
48
|
+
setSecondReferenceDate(getSecondCalendarDate(newFirst));
|
|
49
|
+
};
|
|
50
|
+
}, [currentMode, modifierGroup, firstReferenceDate, getSecondCalendarDate]);
|
|
51
|
+
const onFirstNext = useMemo(() => {
|
|
50
52
|
const modifiers = modifierGroup[currentMode].single;
|
|
51
53
|
if (!modifiers)
|
|
52
54
|
return;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
55
|
+
return () => {
|
|
56
|
+
const [, handleAdd] = modifiers;
|
|
57
|
+
const newFirst = handleAdd(firstReferenceDate);
|
|
58
|
+
setFirstReferenceDate(newFirst);
|
|
59
|
+
setSecondReferenceDate(getSecondCalendarDate(newFirst));
|
|
60
|
+
};
|
|
61
|
+
}, [currentMode, modifierGroup, firstReferenceDate, getSecondCalendarDate]);
|
|
62
|
+
const onFirstDoublePrev = useMemo(() => {
|
|
63
|
+
const modifiers = modifierGroup[currentMode].double;
|
|
64
|
+
if (!modifiers)
|
|
65
|
+
return;
|
|
66
|
+
return () => {
|
|
67
|
+
const [handleMinus] = modifiers;
|
|
68
|
+
const newFirst = handleMinus(firstReferenceDate);
|
|
69
|
+
setFirstReferenceDate(newFirst);
|
|
70
|
+
setSecondReferenceDate(getSecondCalendarDate(newFirst));
|
|
71
|
+
};
|
|
72
|
+
}, [currentMode, modifierGroup, firstReferenceDate, getSecondCalendarDate]);
|
|
73
|
+
const onFirstDoubleNext = useMemo(() => {
|
|
74
|
+
const modifiers = modifierGroup[currentMode].double;
|
|
75
|
+
if (!modifiers)
|
|
76
|
+
return;
|
|
77
|
+
return () => {
|
|
78
|
+
const [, handleAdd] = modifiers;
|
|
79
|
+
const newFirst = handleAdd(firstReferenceDate);
|
|
80
|
+
setFirstReferenceDate(newFirst);
|
|
81
|
+
setSecondReferenceDate(getSecondCalendarDate(newFirst));
|
|
82
|
+
};
|
|
83
|
+
}, [currentMode, modifierGroup, firstReferenceDate, getSecondCalendarDate]);
|
|
70
84
|
// Second calendar controls (same behavior as first)
|
|
71
85
|
const onSecondPrev = onFirstPrev;
|
|
72
86
|
const onSecondNext = onFirstNext;
|
|
73
87
|
const onSecondDoublePrev = onFirstDoublePrev;
|
|
74
88
|
const onSecondDoubleNext = onFirstDoubleNext;
|
|
75
|
-
const onMonthControlClick = () => {
|
|
89
|
+
const onMonthControlClick = useCallback(() => {
|
|
76
90
|
setFirstReferenceDate(firstReferenceDate);
|
|
77
91
|
setSecondReferenceDate(addYear(firstReferenceDate, 1));
|
|
78
92
|
pushModeStack('month');
|
|
79
|
-
};
|
|
80
|
-
const onYearControlClick = () => {
|
|
93
|
+
}, [firstReferenceDate, pushModeStack, addYear]);
|
|
94
|
+
const onYearControlClick = useCallback(() => {
|
|
81
95
|
setFirstReferenceDate(firstReferenceDate);
|
|
82
96
|
setSecondReferenceDate(addYear(firstReferenceDate, calendarYearModuler));
|
|
83
97
|
pushModeStack('year');
|
|
84
|
-
};
|
|
85
|
-
// Wrapper functions for updating reference dates
|
|
86
|
-
// These should be used when switching modes (e.g., from month picker back to day mode)
|
|
87
|
-
// They update the target calendar and maintain the offset between calendars
|
|
98
|
+
}, [firstReferenceDate, pushModeStack, addYear]);
|
|
88
99
|
const updateFirstReferenceDate = useCallback((date) => {
|
|
89
100
|
setFirstReferenceDate(date);
|
|
90
101
|
setSecondReferenceDate(getSecondCalendarDate(date));
|
|
@@ -39,7 +39,10 @@ function useDateRangeCalendarControls(referenceDate, mode) {
|
|
|
39
39
|
const onPrevFactory = (target) => () => {
|
|
40
40
|
var _a;
|
|
41
41
|
const modifiers = modifierGroup[currentMode];
|
|
42
|
-
const
|
|
42
|
+
const activeModifiers = (_a = modifiers.single) !== null && _a !== void 0 ? _a : modifiers.double;
|
|
43
|
+
if (!activeModifiers)
|
|
44
|
+
return;
|
|
45
|
+
const [handleMinus] = activeModifiers;
|
|
43
46
|
const newAnchor = handleMinus(referenceDates[target]);
|
|
44
47
|
const newDates = [...referenceDates];
|
|
45
48
|
newDates[target] = newAnchor;
|
|
@@ -54,7 +57,10 @@ function useDateRangeCalendarControls(referenceDate, mode) {
|
|
|
54
57
|
const onNextFactory = (target) => () => {
|
|
55
58
|
var _a;
|
|
56
59
|
const modifiers = modifierGroup[currentMode];
|
|
57
|
-
const
|
|
60
|
+
const activeModifiers = (_a = modifiers.single) !== null && _a !== void 0 ? _a : modifiers.double;
|
|
61
|
+
if (!activeModifiers)
|
|
62
|
+
return;
|
|
63
|
+
const [, handleAdd] = activeModifiers;
|
|
58
64
|
const newAnchor = handleAdd(referenceDates[target]);
|
|
59
65
|
const newDates = [...referenceDates];
|
|
60
66
|
newDates[target] = newAnchor;
|
package/Dropdown/Dropdown.d.ts
CHANGED
|
@@ -179,5 +179,36 @@ export interface DropdownProps extends DropdownItemSharedProps {
|
|
|
179
179
|
* Only fires when `maxHeight` is set and the list is scrollable.
|
|
180
180
|
*/
|
|
181
181
|
onLeaveBottom?: () => void;
|
|
182
|
+
/**
|
|
183
|
+
* Callback fired when the dropdown list is scrolled.
|
|
184
|
+
* Receives the scroll event and computed scroll information.
|
|
185
|
+
*/
|
|
186
|
+
onScroll?: (computed: {
|
|
187
|
+
scrollTop: number;
|
|
188
|
+
maxScrollTop: number;
|
|
189
|
+
}, target: HTMLDivElement) => void;
|
|
190
|
+
/**
|
|
191
|
+
* Whether to defer the initialization of OverlayScrollbars.
|
|
192
|
+
* This can improve initial render performance.
|
|
193
|
+
* @default true
|
|
194
|
+
*/
|
|
195
|
+
scrollbarDefer?: boolean | object;
|
|
196
|
+
/**
|
|
197
|
+
* Whether to disable the custom scrollbar component.
|
|
198
|
+
* When false (default), Scrollbar component will be used when maxHeight is set.
|
|
199
|
+
* When true, falls back to native div scrolling (backward compatible).
|
|
200
|
+
* @default false
|
|
201
|
+
*/
|
|
202
|
+
scrollbarDisabled?: boolean;
|
|
203
|
+
/**
|
|
204
|
+
* The maximum width of the scrollable container.
|
|
205
|
+
* Can be a CSS value string (e.g., '500px', '100%') or a number (treated as pixels).
|
|
206
|
+
*/
|
|
207
|
+
scrollbarMaxWidth?: number | string;
|
|
208
|
+
/**
|
|
209
|
+
* Additional options to pass to OverlayScrollbars.
|
|
210
|
+
* @see https://kingsora.github.io/OverlayScrollbars/#!documentation/options
|
|
211
|
+
*/
|
|
212
|
+
scrollbarOptions?: import('overlayscrollbars').PartialOptions;
|
|
182
213
|
}
|
|
183
214
|
export default function Dropdown(props: DropdownProps): import("react/jsx-runtime").JSX.Element;
|
package/Dropdown/Dropdown.js
CHANGED
|
@@ -14,7 +14,7 @@ import DropdownItem from './DropdownItem.js';
|
|
|
14
14
|
import Popper from '../Popper/Popper.js';
|
|
15
15
|
|
|
16
16
|
function Dropdown(props) {
|
|
17
|
-
const { activeIndex: activeIndexProp, id, children, options = [], type = 'default', maxHeight, disabled = false, showDropdownActions = false, actionCancelText, actionConfirmText, actionText, actionClearText, actionCustomButtonProps, showActionShowTopBar, isMatchInputValue = false, inputPosition = 'outside', placement = 'bottom', customWidth, sameWidth = false, listboxId: listboxIdProp, listboxLabel, onClose, onOpen, open: openProp, onVisibilityChange, onSelect, onActionConfirm, onActionCancel, onActionCustom, onActionClear, onItemHover, zIndex, status, loadingText, emptyText, emptyIcon, followText: followTextProp, disablePortal = false, onReachBottom, onLeaveBottom, mode, value, } = props;
|
|
17
|
+
const { activeIndex: activeIndexProp, id, children, options = [], type = 'default', maxHeight, disabled = false, showDropdownActions = false, actionCancelText, actionConfirmText, actionText, actionClearText, actionCustomButtonProps, showActionShowTopBar, isMatchInputValue = false, inputPosition = 'outside', placement = 'bottom', customWidth, sameWidth = false, listboxId: listboxIdProp, listboxLabel, onClose, onOpen, open: openProp, onVisibilityChange, onSelect, onActionConfirm, onActionCancel, onActionCustom, onActionClear, onItemHover, zIndex, status, loadingText, emptyText, emptyIcon, followText: followTextProp, disablePortal = false, onReachBottom, onLeaveBottom, onScroll, mode, value, scrollbarDefer, scrollbarDisabled, scrollbarMaxWidth, scrollbarOptions, } = props;
|
|
18
18
|
const isInline = inputPosition === 'inside';
|
|
19
19
|
const inputId = useId();
|
|
20
20
|
const defaultListboxId = `${inputId}-listbox`;
|
|
@@ -231,6 +231,7 @@ function Dropdown(props) {
|
|
|
231
231
|
onSelect,
|
|
232
232
|
onReachBottom,
|
|
233
233
|
onLeaveBottom,
|
|
234
|
+
onScroll,
|
|
234
235
|
options,
|
|
235
236
|
type,
|
|
236
237
|
status,
|
|
@@ -239,6 +240,10 @@ function Dropdown(props) {
|
|
|
239
240
|
emptyIcon,
|
|
240
241
|
mode,
|
|
241
242
|
value,
|
|
243
|
+
scrollbarDefer,
|
|
244
|
+
scrollbarDisabled,
|
|
245
|
+
scrollbarMaxWidth,
|
|
246
|
+
scrollbarOptions,
|
|
242
247
|
}), [
|
|
243
248
|
actionConfig,
|
|
244
249
|
mergedActiveIndex,
|
|
@@ -252,6 +257,7 @@ function Dropdown(props) {
|
|
|
252
257
|
onSelect,
|
|
253
258
|
onReachBottom,
|
|
254
259
|
onLeaveBottom,
|
|
260
|
+
onScroll,
|
|
255
261
|
options,
|
|
256
262
|
type,
|
|
257
263
|
status,
|
|
@@ -260,6 +266,10 @@ function Dropdown(props) {
|
|
|
260
266
|
emptyIcon,
|
|
261
267
|
mode,
|
|
262
268
|
value,
|
|
269
|
+
scrollbarDefer,
|
|
270
|
+
scrollbarDisabled,
|
|
271
|
+
scrollbarMaxWidth,
|
|
272
|
+
scrollbarOptions,
|
|
263
273
|
]);
|
|
264
274
|
const triggerElement = useMemo(() => {
|
|
265
275
|
const childWithRef = children;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
import { DropdownItemSharedProps, DropdownOptionsByType, DropdownStatus as DropdownStatusType, DropdownType } from '@mezzanine-ui/core/dropdown/dropdown';
|
|
3
3
|
import { type IconDefinition } from '@mezzanine-ui/icons';
|
|
4
|
+
import type { PartialOptions } from 'overlayscrollbars';
|
|
4
5
|
import { type DropdownActionProps } from './DropdownAction';
|
|
5
6
|
export interface DropdownItemProps<T extends DropdownType | undefined = DropdownType> extends Omit<DropdownItemSharedProps, 'type'> {
|
|
6
7
|
/**
|
|
@@ -83,5 +84,36 @@ export interface DropdownItemProps<T extends DropdownType | undefined = Dropdown
|
|
|
83
84
|
* Only fires when `maxHeight` is set and the list is scrollable.
|
|
84
85
|
*/
|
|
85
86
|
onLeaveBottom?: () => void;
|
|
87
|
+
/**
|
|
88
|
+
* Callback fired when the dropdown list is scrolled.
|
|
89
|
+
* Receives the scroll event and computed scroll information.
|
|
90
|
+
*/
|
|
91
|
+
onScroll?: (computed: {
|
|
92
|
+
scrollTop: number;
|
|
93
|
+
maxScrollTop: number;
|
|
94
|
+
}, target: HTMLDivElement) => void;
|
|
95
|
+
/**
|
|
96
|
+
* Whether to defer the initialization of OverlayScrollbars.
|
|
97
|
+
* This can improve initial render performance.
|
|
98
|
+
* @default true
|
|
99
|
+
*/
|
|
100
|
+
scrollbarDefer?: boolean | object;
|
|
101
|
+
/**
|
|
102
|
+
* Whether to disable the custom scrollbar component.
|
|
103
|
+
* When false (default), Scrollbar component will be used when maxHeight is set.
|
|
104
|
+
* When true, falls back to native div scrolling (backward compatible).
|
|
105
|
+
* @default false
|
|
106
|
+
*/
|
|
107
|
+
scrollbarDisabled?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* The maximum width of the scrollable container.
|
|
110
|
+
* Can be a CSS value string (e.g., '500px', '100%') or a number (treated as pixels).
|
|
111
|
+
*/
|
|
112
|
+
scrollbarMaxWidth?: number | string;
|
|
113
|
+
/**
|
|
114
|
+
* Additional options to pass to OverlayScrollbars.
|
|
115
|
+
* @see https://kingsora.github.io/OverlayScrollbars/#!documentation/options
|
|
116
|
+
*/
|
|
117
|
+
scrollbarOptions?: PartialOptions;
|
|
86
118
|
}
|
|
87
119
|
export default function DropdownItem<T extends DropdownType | undefined = DropdownType>(props: DropdownItemProps<T>): import("react/jsx-runtime").JSX.Element;
|
package/Dropdown/DropdownItem.js
CHANGED
|
@@ -10,7 +10,19 @@ import DropdownAction from './DropdownAction.js';
|
|
|
10
10
|
import DropdownItemCard from './DropdownItemCard.js';
|
|
11
11
|
import DropdownStatus from './DropdownStatus.js';
|
|
12
12
|
import { shortcutTextHandler } from './shortcutTextHandler.js';
|
|
13
|
+
import Scrollbar from '../Scrollbar/Scrollbar.js';
|
|
13
14
|
|
|
15
|
+
// Helper function to recursively get all descendant IDs from a tree option (excluding the option itself)
|
|
16
|
+
function getAllDescendantIds(option) {
|
|
17
|
+
const ids = [];
|
|
18
|
+
if (option.children && option.children.length > 0) {
|
|
19
|
+
option.children.forEach((child) => {
|
|
20
|
+
ids.push(String(child.id));
|
|
21
|
+
ids.push(...getAllDescendantIds(child));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return ids;
|
|
25
|
+
}
|
|
14
26
|
/**
|
|
15
27
|
* Limits DropdownOption array to a maximum depth, truncating extra children levels and showing error message if exceeded.
|
|
16
28
|
* @param input - The original DropdownOption array
|
|
@@ -57,13 +69,16 @@ function truncateArrayDepth(input, maxDepth = 3, warn = true) {
|
|
|
57
69
|
return truncate(input);
|
|
58
70
|
}
|
|
59
71
|
function DropdownItem(props) {
|
|
60
|
-
const { activeIndex, disabled = false, listboxId, listboxLabel, mode = 'single', options, value, type, maxHeight, actionConfig, onHover, onSelect, followText, headerContent, status, loadingText, emptyText, emptyIcon, onReachBottom, onLeaveBottom, } = props;
|
|
72
|
+
const { activeIndex, disabled = false, listboxId, listboxLabel, mode = 'single', options, value, type, maxHeight, actionConfig, onHover, onSelect, followText, headerContent, status, loadingText, emptyText, emptyIcon, onReachBottom, onLeaveBottom, onScroll, scrollbarDefer = true, scrollbarDisabled = false, scrollbarMaxWidth, scrollbarOptions, } = props;
|
|
61
73
|
const optionsContent = truncateArrayDepth(options, 3);
|
|
62
74
|
const listRef = useRef(null);
|
|
63
75
|
const listWrapperRef = useRef(null);
|
|
76
|
+
const viewportRef = useRef(null);
|
|
77
|
+
const wasAtBottomRef = useRef(false);
|
|
64
78
|
const [expandedNodes, setExpandedNodes] = useState(new Set());
|
|
65
79
|
const hasActions = Boolean(actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.showActions);
|
|
66
80
|
const hasHeader = Boolean(headerContent);
|
|
81
|
+
const shouldUseScrollbar = maxHeight && !scrollbarDisabled;
|
|
67
82
|
// Use custom hook to measure element heights
|
|
68
83
|
const [actionRef, actionHeight] = useElementHeight(hasActions && !!maxHeight);
|
|
69
84
|
const [headerRef, headerHeight] = useElementHeight(hasHeader && !!maxHeight);
|
|
@@ -200,17 +215,39 @@ function DropdownItem(props) {
|
|
|
200
215
|
});
|
|
201
216
|
return { elements, nextIndex: currentIndex };
|
|
202
217
|
};
|
|
218
|
+
const calculateNodeSelectionState = useCallback((option, selectedIds) => {
|
|
219
|
+
if (!option.children || option.children.length === 0) {
|
|
220
|
+
const isSelected = selectedIds.includes(String(option.id));
|
|
221
|
+
return { checked: isSelected, indeterminate: false };
|
|
222
|
+
}
|
|
223
|
+
// Get all descendant IDs (excluding the parent node itself)
|
|
224
|
+
const allDescendantIds = getAllDescendantIds(option);
|
|
225
|
+
const selectedDescendants = allDescendantIds.filter((id) => selectedIds.includes(id));
|
|
226
|
+
const totalDescendants = allDescendantIds.length;
|
|
227
|
+
if (totalDescendants === 0) {
|
|
228
|
+
// No descendants, check if parent itself is selected
|
|
229
|
+
const isSelected = selectedIds.includes(String(option.id));
|
|
230
|
+
return { checked: isSelected, indeterminate: false };
|
|
231
|
+
}
|
|
232
|
+
if (selectedDescendants.length === 0) {
|
|
233
|
+
return { checked: false, indeterminate: false };
|
|
234
|
+
}
|
|
235
|
+
if (selectedDescendants.length === totalDescendants) {
|
|
236
|
+
// All descendants are selected
|
|
237
|
+
return { checked: true, indeterminate: false };
|
|
238
|
+
}
|
|
239
|
+
// Some but not all descendants are selected
|
|
240
|
+
return { checked: false, indeterminate: true };
|
|
241
|
+
}, []);
|
|
203
242
|
const renderTreeOptions = (optionList, depth, startIndex) => {
|
|
204
243
|
let currentIndex = startIndex;
|
|
244
|
+
const selectedIds = Array.isArray(value) ? value.map((id) => String(id)) : value ? [String(value)] : [];
|
|
205
245
|
const elements = (optionList !== null && optionList !== void 0 ? optionList : []).flatMap((option) => {
|
|
206
246
|
var _a, _b, _c;
|
|
207
247
|
currentIndex += 1;
|
|
208
248
|
const optionIndex = currentIndex;
|
|
209
249
|
const level = Math.min(depth, 2);
|
|
210
250
|
const isActive = optionIndex === activeIndex;
|
|
211
|
-
const isSelected = Array.isArray(value)
|
|
212
|
-
? value.includes(option.id)
|
|
213
|
-
: value === option.id;
|
|
214
251
|
const hasChildren = Boolean(option.children && option.children.length > 0);
|
|
215
252
|
const isExpanded = hasChildren && expandedNodes.has(option.id);
|
|
216
253
|
let prependIcon = undefined;
|
|
@@ -221,13 +258,31 @@ function DropdownItem(props) {
|
|
|
221
258
|
const shortcutText = option.shortcutText
|
|
222
259
|
? option.shortcutText
|
|
223
260
|
: shortcutTextHandler((_a = option.shortcutKeys) !== null && _a !== void 0 ? _a : []);
|
|
224
|
-
const
|
|
261
|
+
const selectionState = hasChildren && mode === 'multiple'
|
|
262
|
+
? calculateNodeSelectionState(option, selectedIds)
|
|
263
|
+
: {
|
|
264
|
+
checked: selectedIds.includes(String(option.id)),
|
|
265
|
+
indeterminate: false,
|
|
266
|
+
};
|
|
267
|
+
const card = (jsx(DropdownItemCard, { active: isActive, checked: selectionState.checked, indeterminate: selectionState.indeterminate, disabled: disabled, id: `${listboxId}-option-${optionIndex}`, label: option.name, level: level, mode: mode, name: option.name, onClick: () => {
|
|
225
268
|
if (disabled)
|
|
226
269
|
return;
|
|
227
|
-
if (hasChildren && type === 'tree') {
|
|
270
|
+
if (hasChildren && type === 'tree' && mode === 'multiple' && option.showCheckbox) {
|
|
271
|
+
toggleExpand(option.id);
|
|
272
|
+
}
|
|
273
|
+
else if (hasChildren && type === 'tree') {
|
|
228
274
|
toggleExpand(option.id);
|
|
229
275
|
}
|
|
230
276
|
else {
|
|
277
|
+
// In `tree` + `multiple` mode, `DropdownItemCard` already triggers selection via
|
|
278
|
+
// `onCheckedChange` when row is clicked (it toggles checked first, then calls `onClick`),
|
|
279
|
+
// so calling `onSelect` here would cause it to fire twice for leaf nodes.
|
|
280
|
+
if (!(type === 'tree' && mode === 'multiple')) {
|
|
281
|
+
onSelect === null || onSelect === void 0 ? void 0 : onSelect(option);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}, onCheckedChange: () => {
|
|
285
|
+
if (!disabled) {
|
|
231
286
|
onSelect === null || onSelect === void 0 ? void 0 : onSelect(option);
|
|
232
287
|
}
|
|
233
288
|
}, followText: followText, checkSite: checkSite, onMouseEnter: () => onHover === null || onHover === void 0 ? void 0 : onHover(optionIndex), prependIcon: prependIcon, showUnderline: (_b = option.showUnderline) !== null && _b !== void 0 ? _b : false, validate: (_c = option.validate) !== null && _c !== void 0 ? _c : 'default', appendContent: shortcutText }, option.id));
|
|
@@ -292,6 +347,10 @@ function DropdownItem(props) {
|
|
|
292
347
|
maxHeight: `${availableHeight}px`,
|
|
293
348
|
};
|
|
294
349
|
}, [maxHeight, actionHeight, headerHeight]);
|
|
350
|
+
const getIsAtBottom = useCallback((viewport) => {
|
|
351
|
+
const { scrollTop, scrollHeight, clientHeight } = viewport;
|
|
352
|
+
return scrollTop + clientHeight >= scrollHeight - 1;
|
|
353
|
+
}, []);
|
|
295
354
|
useEffect(() => {
|
|
296
355
|
const listElement = listRef.current;
|
|
297
356
|
if (!listElement || disabled) {
|
|
@@ -321,10 +380,17 @@ function DropdownItem(props) {
|
|
|
321
380
|
listElement.removeEventListener('keydown', handleKeyDown);
|
|
322
381
|
};
|
|
323
382
|
}, [disabled, matchShortcut, onSelect, type, toggleExpand, visibleShortcutOptions]);
|
|
324
|
-
|
|
383
|
+
const handleViewportReady = useCallback((viewport) => {
|
|
384
|
+
viewportRef.current = viewport;
|
|
385
|
+
listWrapperRef.current = viewport;
|
|
386
|
+
wasAtBottomRef.current = getIsAtBottom(viewport);
|
|
387
|
+
}, [getIsAtBottom]);
|
|
325
388
|
useEffect(() => {
|
|
389
|
+
if (shouldUseScrollbar) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
326
392
|
const listWrapperElement = listWrapperRef.current;
|
|
327
|
-
if (!listWrapperElement || !maxHeight || (!onReachBottom && !onLeaveBottom)) {
|
|
393
|
+
if (!listWrapperElement || !maxHeight || (!onReachBottom && !onLeaveBottom && !onScroll)) {
|
|
328
394
|
return;
|
|
329
395
|
}
|
|
330
396
|
// Initialize wasAtBottom state by checking current position
|
|
@@ -335,6 +401,14 @@ function DropdownItem(props) {
|
|
|
335
401
|
let wasAtBottom = checkInitialState();
|
|
336
402
|
const handleScroll = () => {
|
|
337
403
|
const { scrollTop, scrollHeight, clientHeight } = listWrapperElement;
|
|
404
|
+
const maxScrollTop = scrollHeight - clientHeight;
|
|
405
|
+
// Call onScroll callback if provided
|
|
406
|
+
if (onScroll) {
|
|
407
|
+
onScroll({
|
|
408
|
+
scrollTop,
|
|
409
|
+
maxScrollTop,
|
|
410
|
+
}, listWrapperElement);
|
|
411
|
+
}
|
|
338
412
|
// Check if scrolled to bottom (with 1px threshold for rounding errors)
|
|
339
413
|
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
|
|
340
414
|
// Trigger onReachBottom when entering bottom state
|
|
@@ -351,9 +425,37 @@ function DropdownItem(props) {
|
|
|
351
425
|
return () => {
|
|
352
426
|
listWrapperElement.removeEventListener('scroll', handleScroll);
|
|
353
427
|
};
|
|
354
|
-
}, [maxHeight, onReachBottom, onLeaveBottom]);
|
|
428
|
+
}, [maxHeight, onReachBottom, onLeaveBottom, onScroll, shouldUseScrollbar]);
|
|
429
|
+
const scrollbarEvents = useMemo(() => {
|
|
430
|
+
if (!shouldUseScrollbar || (!onReachBottom && !onLeaveBottom && !onScroll)) {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
scroll: (_instance, _event) => {
|
|
435
|
+
const viewport = viewportRef.current;
|
|
436
|
+
if (!viewport)
|
|
437
|
+
return;
|
|
438
|
+
const { scrollTop, scrollHeight, clientHeight } = viewport;
|
|
439
|
+
const maxScrollTop = scrollHeight - clientHeight;
|
|
440
|
+
if (onScroll) {
|
|
441
|
+
onScroll({
|
|
442
|
+
scrollTop,
|
|
443
|
+
maxScrollTop,
|
|
444
|
+
}, viewport);
|
|
445
|
+
}
|
|
446
|
+
const isAtBottom = getIsAtBottom(viewport);
|
|
447
|
+
if (isAtBottom && !wasAtBottomRef.current) {
|
|
448
|
+
onReachBottom === null || onReachBottom === void 0 ? void 0 : onReachBottom();
|
|
449
|
+
}
|
|
450
|
+
if (!isAtBottom && wasAtBottomRef.current) {
|
|
451
|
+
onLeaveBottom === null || onLeaveBottom === void 0 ? void 0 : onLeaveBottom();
|
|
452
|
+
}
|
|
453
|
+
wasAtBottomRef.current = isAtBottom;
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}, [getIsAtBottom, shouldUseScrollbar, onReachBottom, onLeaveBottom, onScroll]);
|
|
355
457
|
return (jsxs("ul", { "aria-label": listboxLabel || (optionsContent.length === 0 ? 'Dropdown options' : undefined), className: dropdownClasses.list, id: listboxId, ref: listRef, role: "listbox", style: listStyle, tabIndex: -1, children: [hasHeader && (jsx("li", { className: dropdownClasses.listHeader, role: "presentation", ref: headerRef, children: jsx("div", { className: dropdownClasses.listHeaderInner, children: headerContent }) })), maxHeight
|
|
356
|
-
? (jsx("div", { ref: listWrapperRef, className: dropdownClasses.listWrapper, style: listWrapperStyle, children: shouldShowStatus ? (jsx(DropdownStatus, { status: status, loadingText: loadingText, emptyText: emptyText, emptyIcon: emptyIcon })) : (renderedOptions) }))
|
|
458
|
+
? (shouldUseScrollbar ? (jsx(Scrollbar, { className: dropdownClasses.listWrapper, defer: scrollbarDefer, disabled: false, events: scrollbarEvents, maxHeight: listWrapperStyle === null || listWrapperStyle === void 0 ? void 0 : listWrapperStyle.maxHeight, maxWidth: scrollbarMaxWidth, onViewportReady: handleViewportReady, options: scrollbarOptions, children: shouldShowStatus ? (jsx(DropdownStatus, { status: status, loadingText: loadingText, emptyText: emptyText, emptyIcon: emptyIcon })) : (renderedOptions) })) : (jsx("div", { ref: listWrapperRef, className: dropdownClasses.listWrapper, style: listWrapperStyle, children: shouldShowStatus ? (jsx(DropdownStatus, { status: status, loadingText: loadingText, emptyText: emptyText, emptyIcon: emptyIcon })) : (renderedOptions) })))
|
|
357
459
|
: shouldShowStatus ? (jsx(DropdownStatus, { status: status, loadingText: loadingText, emptyText: emptyText, emptyIcon: emptyIcon })) : (renderedOptions), hasActions && (jsx("div", { ref: actionRef, children: jsx(DropdownAction, { ...actionConfig }) }))] }));
|
|
358
460
|
}
|
|
359
461
|
|
|
@@ -25,6 +25,11 @@ export interface DropdownItemCardProps {
|
|
|
25
25
|
* When provided, the state is controlled externally.
|
|
26
26
|
*/
|
|
27
27
|
checked?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Whether the checkbox is in indeterminate state.
|
|
30
|
+
* Used in tree mode when some but not all children are selected.
|
|
31
|
+
*/
|
|
32
|
+
indeterminate?: boolean;
|
|
28
33
|
/**
|
|
29
34
|
* Additional className for the list item.
|
|
30
35
|
*/
|
|
@@ -10,7 +10,7 @@ import Icon from '../Icon/Icon.js';
|
|
|
10
10
|
import Checkbox from '../Checkbox/Checkbox.js';
|
|
11
11
|
|
|
12
12
|
function DropdownItemCard(props) {
|
|
13
|
-
const { active = false, appendIcon, appendContent, followText, id, label, level: levelProp, mode, name: _name, prependIcon, subTitle, validate, disabled, checked, defaultChecked, checkSite, onCheckedChange, onClick, className, onMouseEnter, showUnderline, } = props;
|
|
13
|
+
const { active = false, appendIcon, appendContent, followText, id, label, level: levelProp, mode, name: _name, prependIcon, subTitle, validate, disabled, checked, defaultChecked, indeterminate = false, checkSite, onCheckedChange, onClick, className, onMouseEnter, showUnderline, } = props;
|
|
14
14
|
const cardLabel = label || '';
|
|
15
15
|
const cardName = _name || cardLabel;
|
|
16
16
|
const level = levelProp || 0;
|
|
@@ -111,7 +111,7 @@ function DropdownItemCard(props) {
|
|
|
111
111
|
[dropdownClasses.cardActive]: active || isChecked,
|
|
112
112
|
[dropdownClasses.cardDisabled]: disabled,
|
|
113
113
|
[dropdownClasses.cardDanger]: validate === 'danger',
|
|
114
|
-
}, className), id: id, role: "option", tabIndex: -1, onMouseEnter: onMouseEnter, onClick: handleClick, onKeyDown: handleKeyDown, children: jsxs("div", { className: dropdownClasses.cardContainer, children: [showPrependContent && (jsxs("div", { className: dropdownClasses.cardPrependContent, children: [prependIcon && jsx(Icon, { icon: prependIcon, color: iconColor }), checkSite === 'prefix' && mode === 'multiple' && (jsx(Checkbox, { checked: isChecked, disabled: disabled, onChange: handleCheckboxChange }))] })), jsxs("div", { className: dropdownClasses.cardBody, children: [cardLabel &&
|
|
114
|
+
}, className), id: id, role: "option", tabIndex: -1, onMouseEnter: onMouseEnter, onClick: handleClick, onKeyDown: handleKeyDown, children: jsxs("div", { className: dropdownClasses.cardContainer, children: [showPrependContent && (jsxs("div", { className: dropdownClasses.cardPrependContent, children: [prependIcon && jsx(Icon, { icon: prependIcon, color: iconColor }), checkSite === 'prefix' && mode === 'multiple' && (jsx(Checkbox, { checked: isChecked, disabled: disabled, indeterminate: indeterminate, onChange: handleCheckboxChange }))] })), jsxs("div", { className: dropdownClasses.cardBody, children: [cardLabel &&
|
|
115
115
|
renderHighlightedText(labelParts, dropdownClasses.cardTitle, labelId), subTitleParts.length > 0 &&
|
|
116
116
|
renderHighlightedText(subTitleParts, dropdownClasses.cardDescription)] }), showAppendContent && (jsxs("div", { className: dropdownClasses.cardAppendContent, children: [appendContent && (jsx(Typography, { color: "text-neutral-light", children: appendContent })), appendIcon && jsx(Icon, { icon: appendIcon, color: iconColor }), checkSite === 'suffix' && isChecked && (jsx(Icon, { icon: CheckedIcon, color: appendIconColor, size: 16 }))] }))] }) }), showUnderline && jsx("div", { className: dropdownClasses.cardUnderline })] }));
|
|
117
117
|
}
|
|
@@ -2,19 +2,18 @@ import { MouseEvent } from 'react';
|
|
|
2
2
|
import { SelectValue } from '../Select/typings';
|
|
3
3
|
export interface UseSelectBaseValueControl {
|
|
4
4
|
onClear?(e: MouseEvent<Element>): void;
|
|
5
|
-
onChange?(newOptions: SelectValue[] | SelectValue): any;
|
|
6
5
|
onClose?(): void;
|
|
7
6
|
}
|
|
8
7
|
export type UseSelectMultipleValueControl = UseSelectBaseValueControl & {
|
|
9
8
|
defaultValue?: SelectValue[];
|
|
10
9
|
mode: 'multiple';
|
|
11
|
-
onChange?(newOptions: SelectValue[]):
|
|
10
|
+
onChange?(newOptions: SelectValue[]): void;
|
|
12
11
|
value?: SelectValue[];
|
|
13
12
|
};
|
|
14
13
|
export type UseSelectSingleValueControl = UseSelectBaseValueControl & {
|
|
15
14
|
defaultValue?: SelectValue;
|
|
16
15
|
mode: 'single';
|
|
17
|
-
onChange?(newOption: SelectValue):
|
|
16
|
+
onChange?(newOption: SelectValue | null): void;
|
|
18
17
|
value?: SelectValue | null;
|
|
19
18
|
};
|
|
20
19
|
export type UseSelectValueControl = UseSelectMultipleValueControl | UseSelectSingleValueControl;
|
|
@@ -22,7 +21,7 @@ export interface SelectBaseValueControl {
|
|
|
22
21
|
onClear(e: MouseEvent<Element>): void;
|
|
23
22
|
}
|
|
24
23
|
export type SelectMultipleValueControl = SelectBaseValueControl & {
|
|
25
|
-
onChange: (v: SelectValue | null) => SelectValue[];
|
|
24
|
+
onChange: (v: SelectValue | SelectValue[] | null) => SelectValue[];
|
|
26
25
|
value: SelectValue[];
|
|
27
26
|
};
|
|
28
27
|
export type SelectSingleValueControl = SelectBaseValueControl & {
|