@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.
@@ -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
- const [handleMinus] = modifiers;
18
- setReferenceDate(handleMinus(referenceDate));
19
- };
20
- const onNext = () => {
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
- const [, handleAdd] = modifiers;
25
- setReferenceDate(handleAdd(referenceDate));
26
- };
27
- const onDoublePrev = () => {
28
- const [handleMinus] = modifierGroup[currentMode].double;
29
- setReferenceDate(handleMinus(referenceDate));
30
- };
31
- const onDoubleNext = () => {
32
- const [, handleAdd] = modifierGroup[currentMode].double;
33
- setReferenceDate(handleAdd(referenceDate));
34
- };
35
- const onMonthControlClick = () => pushModeStack('month');
36
- const onYearControlClick = () => pushModeStack('year');
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
- const [handleMinus] = modifiers;
45
- const newFirst = handleMinus(firstReferenceDate);
46
- setFirstReferenceDate(newFirst);
47
- setSecondReferenceDate(getSecondCalendarDate(newFirst));
48
- };
49
- const onFirstNext = () => {
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
- const [, handleAdd] = modifiers;
54
- const newFirst = handleAdd(firstReferenceDate);
55
- setFirstReferenceDate(newFirst);
56
- setSecondReferenceDate(getSecondCalendarDate(newFirst));
57
- };
58
- const onFirstDoublePrev = () => {
59
- const [handleMinus] = modifierGroup[currentMode].double;
60
- const newFirst = handleMinus(firstReferenceDate);
61
- setFirstReferenceDate(newFirst);
62
- setSecondReferenceDate(getSecondCalendarDate(newFirst));
63
- };
64
- const onFirstDoubleNext = () => {
65
- const [, handleAdd] = modifierGroup[currentMode].double;
66
- const newFirst = handleAdd(firstReferenceDate);
67
- setFirstReferenceDate(newFirst);
68
- setSecondReferenceDate(getSecondCalendarDate(newFirst));
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 [handleMinus] = (_a = modifiers.single) !== null && _a !== void 0 ? _a : modifiers.double;
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 [, handleAdd] = (_a = modifiers.single) !== null && _a !== void 0 ? _a : modifiers.double;
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;
@@ -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;
@@ -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;
@@ -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 card = (jsx(DropdownItemCard, { active: isActive, checked: isSelected, disabled: disabled, id: `${listboxId}-option-${optionIndex}`, label: option.name, level: level, mode: mode, name: option.name, onClick: () => {
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
- // Handle scroll to bottom detection
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[]): any;
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): any;
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 & {