@kushagradhawan/kookie-ui 0.1.124 → 0.1.126
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/components.css +93 -0
- package/dist/cjs/components/_internal/base-menu.props.d.ts +52 -1
- package/dist/cjs/components/_internal/base-menu.props.d.ts.map +1 -1
- package/dist/cjs/components/_internal/base-menu.props.js +1 -1
- package/dist/cjs/components/_internal/base-menu.props.js.map +3 -3
- package/dist/cjs/components/_internal/dropdown-menu-drill-down.d.ts +60 -0
- package/dist/cjs/components/_internal/dropdown-menu-drill-down.d.ts.map +1 -0
- package/dist/cjs/components/_internal/dropdown-menu-drill-down.js +2 -0
- package/dist/cjs/components/_internal/dropdown-menu-drill-down.js.map +7 -0
- package/dist/cjs/components/combobox.d.ts.map +1 -1
- package/dist/cjs/components/combobox.js +1 -1
- package/dist/cjs/components/combobox.js.map +3 -3
- package/dist/cjs/components/dropdown-menu.d.ts +28 -7
- package/dist/cjs/components/dropdown-menu.d.ts.map +1 -1
- package/dist/cjs/components/dropdown-menu.js +1 -1
- package/dist/cjs/components/dropdown-menu.js.map +3 -3
- package/dist/cjs/components/dropdown-menu.props.d.ts +1 -1
- package/dist/cjs/components/dropdown-menu.props.d.ts.map +1 -1
- package/dist/cjs/components/dropdown-menu.props.js +1 -1
- package/dist/cjs/components/dropdown-menu.props.js.map +2 -2
- package/dist/cjs/components/icons.d.ts +2 -1
- package/dist/cjs/components/icons.d.ts.map +1 -1
- package/dist/cjs/components/icons.js +1 -1
- package/dist/cjs/components/icons.js.map +3 -3
- package/dist/cjs/components/schemas/shell.schema.d.ts +2 -2
- package/dist/esm/components/_internal/base-menu.props.d.ts +52 -1
- package/dist/esm/components/_internal/base-menu.props.d.ts.map +1 -1
- package/dist/esm/components/_internal/base-menu.props.js +1 -1
- package/dist/esm/components/_internal/base-menu.props.js.map +3 -3
- package/dist/esm/components/_internal/dropdown-menu-drill-down.d.ts +60 -0
- package/dist/esm/components/_internal/dropdown-menu-drill-down.d.ts.map +1 -0
- package/dist/esm/components/_internal/dropdown-menu-drill-down.js +2 -0
- package/dist/esm/components/_internal/dropdown-menu-drill-down.js.map +7 -0
- package/dist/esm/components/combobox.d.ts.map +1 -1
- package/dist/esm/components/combobox.js +1 -1
- package/dist/esm/components/combobox.js.map +3 -3
- package/dist/esm/components/dropdown-menu.d.ts +28 -7
- package/dist/esm/components/dropdown-menu.d.ts.map +1 -1
- package/dist/esm/components/dropdown-menu.js +1 -1
- package/dist/esm/components/dropdown-menu.js.map +3 -3
- package/dist/esm/components/dropdown-menu.props.d.ts +1 -1
- package/dist/esm/components/dropdown-menu.props.d.ts.map +1 -1
- package/dist/esm/components/dropdown-menu.props.js +1 -1
- package/dist/esm/components/dropdown-menu.props.js.map +3 -3
- package/dist/esm/components/icons.d.ts +2 -1
- package/dist/esm/components/icons.d.ts.map +1 -1
- package/dist/esm/components/icons.js +1 -1
- package/dist/esm/components/icons.js.map +3 -3
- package/dist/esm/components/schemas/shell.schema.d.ts +2 -2
- package/package.json +1 -1
- package/schemas/base-button.json +1 -1
- package/schemas/button.json +1 -1
- package/schemas/icon-button.json +1 -1
- package/schemas/index.json +6 -6
- package/schemas/toggle-button.json +1 -1
- package/schemas/toggle-icon-button.json +1 -1
- package/src/components/_internal/base-menu.props.ts +31 -1
- package/src/components/_internal/dropdown-menu-drill-down.tsx +242 -0
- package/src/components/combobox.tsx +176 -80
- package/src/components/dropdown-menu.css +119 -0
- package/src/components/dropdown-menu.props.tsx +2 -0
- package/src/components/dropdown-menu.tsx +217 -27
- package/src/components/icons.tsx +14 -1
- package/styles.css +93 -0
|
@@ -27,6 +27,13 @@ import type { MarginProps } from '../props/margin.props.js';
|
|
|
27
27
|
import type { ComponentPropsWithout, RemovedProps } from '../helpers/component-props.js';
|
|
28
28
|
import type { GetPropDefTypes } from '../props/prop-def.js';
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Pre-compiled regex for className sanitization.
|
|
32
|
+
* Matches size classes like "rt-r-size-1", "rt-r-size-2", etc.
|
|
33
|
+
* Pre-compiling avoids regex compilation on every render.
|
|
34
|
+
*/
|
|
35
|
+
const SIZE_CLASS_REGEX = /^rt-r-size-\d$/;
|
|
36
|
+
|
|
30
37
|
type TextFieldVariant = (typeof textFieldRootPropDefs.variant.values)[number];
|
|
31
38
|
type ComboboxValue = string | null;
|
|
32
39
|
/**
|
|
@@ -97,15 +104,33 @@ type ComboboxRootOwnProps = GetPropDefTypes<typeof comboboxRootPropDefs> & {
|
|
|
97
104
|
};
|
|
98
105
|
|
|
99
106
|
/**
|
|
100
|
-
*
|
|
107
|
+
* Split contexts to minimize re-renders. Each context changes independently:
|
|
108
|
+
* - ConfigContext: Static config that rarely changes (size, color, placeholders, etc.)
|
|
109
|
+
* - SelectionContext: Changes when user selects an item
|
|
110
|
+
* - SearchContext: Changes on every keystroke in the search input
|
|
111
|
+
* - NavigationContext: Changes during keyboard navigation
|
|
101
112
|
*/
|
|
102
|
-
|
|
113
|
+
|
|
114
|
+
/** Static configuration - rarely changes after mount */
|
|
115
|
+
interface ComboboxConfigContextValue {
|
|
116
|
+
size?: ComboboxRootOwnProps['size'];
|
|
117
|
+
highContrast?: boolean;
|
|
118
|
+
placeholder?: string;
|
|
119
|
+
searchPlaceholder?: string;
|
|
120
|
+
filter?: CommandFilter;
|
|
121
|
+
shouldFilter?: boolean;
|
|
122
|
+
loop?: boolean;
|
|
123
|
+
disabled?: boolean;
|
|
124
|
+
resetSearchOnSelect?: boolean;
|
|
125
|
+
color?: ComboboxRootOwnProps['color'];
|
|
126
|
+
listboxId: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Selection state - changes when user picks an option */
|
|
130
|
+
interface ComboboxSelectionContextValue {
|
|
103
131
|
open: boolean;
|
|
104
132
|
setOpen: (open: boolean) => void;
|
|
105
133
|
value: ComboboxValue;
|
|
106
|
-
setValue: (value: ComboboxValue) => void;
|
|
107
|
-
searchValue: string;
|
|
108
|
-
setSearchValue: (value: string) => void;
|
|
109
134
|
/** Label registered by the selected item */
|
|
110
135
|
selectedLabel?: string;
|
|
111
136
|
/** Resolved display value (already computed from string or function) */
|
|
@@ -113,24 +138,73 @@ interface ComboboxContextValue extends ComboboxRootOwnProps {
|
|
|
113
138
|
registerItemLabel: (value: string, label: string) => void;
|
|
114
139
|
unregisterItemLabel: (value: string) => void;
|
|
115
140
|
handleSelect: (value: string) => void;
|
|
116
|
-
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Search state - changes on every keystroke */
|
|
144
|
+
interface ComboboxSearchContextValue {
|
|
145
|
+
searchValue: string;
|
|
146
|
+
setSearchValue: (value: string) => void;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Navigation state - changes during keyboard navigation */
|
|
150
|
+
interface ComboboxNavigationContextValue {
|
|
117
151
|
activeDescendantId: string | undefined;
|
|
118
152
|
setActiveDescendantId: (id: string | undefined) => void;
|
|
119
153
|
}
|
|
120
154
|
|
|
121
|
-
const
|
|
155
|
+
const ComboboxConfigContext = React.createContext<ComboboxConfigContextValue | null>(null);
|
|
156
|
+
const ComboboxSelectionContext = React.createContext<ComboboxSelectionContextValue | null>(null);
|
|
157
|
+
const ComboboxSearchContext = React.createContext<ComboboxSearchContextValue | null>(null);
|
|
158
|
+
const ComboboxNavigationContext = React.createContext<ComboboxNavigationContextValue | null>(null);
|
|
122
159
|
|
|
123
160
|
/**
|
|
124
|
-
* Utility
|
|
161
|
+
* Utility hooks that ensure consumers are wrapped in Combobox.Root.
|
|
162
|
+
* Components should use only the contexts they need to minimize re-renders.
|
|
125
163
|
*/
|
|
126
|
-
const
|
|
127
|
-
const ctx = React.useContext(
|
|
164
|
+
const useComboboxConfigContext = (caller: string) => {
|
|
165
|
+
const ctx = React.useContext(ComboboxConfigContext);
|
|
166
|
+
if (!ctx) {
|
|
167
|
+
throw new Error(`${caller} must be used within Combobox.Root`);
|
|
168
|
+
}
|
|
169
|
+
return ctx;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const useComboboxSelectionContext = (caller: string) => {
|
|
173
|
+
const ctx = React.useContext(ComboboxSelectionContext);
|
|
174
|
+
if (!ctx) {
|
|
175
|
+
throw new Error(`${caller} must be used within Combobox.Root`);
|
|
176
|
+
}
|
|
177
|
+
return ctx;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const useComboboxSearchContext = (caller: string) => {
|
|
181
|
+
const ctx = React.useContext(ComboboxSearchContext);
|
|
182
|
+
if (!ctx) {
|
|
183
|
+
throw new Error(`${caller} must be used within Combobox.Root`);
|
|
184
|
+
}
|
|
185
|
+
return ctx;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const useComboboxNavigationContext = (caller: string) => {
|
|
189
|
+
const ctx = React.useContext(ComboboxNavigationContext);
|
|
128
190
|
if (!ctx) {
|
|
129
191
|
throw new Error(`${caller} must be used within Combobox.Root`);
|
|
130
192
|
}
|
|
131
193
|
return ctx;
|
|
132
194
|
};
|
|
133
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Combined context hook for components that need multiple contexts.
|
|
198
|
+
* Use sparingly - prefer individual context hooks when possible.
|
|
199
|
+
*/
|
|
200
|
+
const useComboboxContext = (caller: string) => {
|
|
201
|
+
const config = useComboboxConfigContext(caller);
|
|
202
|
+
const selection = useComboboxSelectionContext(caller);
|
|
203
|
+
const search = useComboboxSearchContext(caller);
|
|
204
|
+
const navigation = useComboboxNavigationContext(caller);
|
|
205
|
+
return { ...config, ...selection, ...search, ...navigation };
|
|
206
|
+
};
|
|
207
|
+
|
|
134
208
|
/**
|
|
135
209
|
* Context for values that are only available inside Content (e.g., variant, color)
|
|
136
210
|
* so that Input/Item can style themselves consistently.
|
|
@@ -224,6 +298,9 @@ const ComboboxRoot: React.FC<ComboboxRootProps> = (props) => {
|
|
|
224
298
|
|
|
225
299
|
const handleSelect = React.useCallback(
|
|
226
300
|
(nextValue: string) => {
|
|
301
|
+
// Batch state updates to minimize re-renders
|
|
302
|
+
// React 18+ automatically batches these, but we use flushSync-free pattern
|
|
303
|
+
// to ensure predictable update order
|
|
227
304
|
setValue(nextValue);
|
|
228
305
|
setOpen(false);
|
|
229
306
|
if (resetSearchOnSelect) {
|
|
@@ -258,7 +335,10 @@ const ComboboxRoot: React.FC<ComboboxRootProps> = (props) => {
|
|
|
258
335
|
return displayValueProp;
|
|
259
336
|
}, [displayValueProp, value]);
|
|
260
337
|
|
|
261
|
-
|
|
338
|
+
// Split context values for optimal re-render performance
|
|
339
|
+
// Each context only triggers re-renders for components that use it
|
|
340
|
+
|
|
341
|
+
const configContextValue = React.useMemo<ComboboxConfigContextValue>(
|
|
262
342
|
() => ({
|
|
263
343
|
size,
|
|
264
344
|
highContrast,
|
|
@@ -270,55 +350,53 @@ const ComboboxRoot: React.FC<ComboboxRootProps> = (props) => {
|
|
|
270
350
|
disabled,
|
|
271
351
|
resetSearchOnSelect,
|
|
272
352
|
color,
|
|
273
|
-
|
|
353
|
+
listboxId,
|
|
354
|
+
}),
|
|
355
|
+
[size, highContrast, placeholder, searchPlaceholder, filter, shouldFilter, loop, disabled, resetSearchOnSelect, color, listboxId],
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const selectionContextValue = React.useMemo<ComboboxSelectionContextValue>(
|
|
359
|
+
() => ({
|
|
274
360
|
open,
|
|
275
361
|
setOpen,
|
|
276
362
|
value,
|
|
277
|
-
setValue,
|
|
278
|
-
searchValue,
|
|
279
|
-
setSearchValue,
|
|
280
363
|
selectedLabel,
|
|
364
|
+
resolvedDisplayValue,
|
|
281
365
|
registerItemLabel,
|
|
282
366
|
unregisterItemLabel,
|
|
283
367
|
handleSelect,
|
|
284
|
-
listboxId,
|
|
285
|
-
activeDescendantId,
|
|
286
|
-
setActiveDescendantId,
|
|
287
368
|
}),
|
|
288
|
-
[
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
filter,
|
|
294
|
-
shouldFilter,
|
|
295
|
-
loop,
|
|
296
|
-
disabled,
|
|
297
|
-
resetSearchOnSelect,
|
|
298
|
-
color,
|
|
299
|
-
resolvedDisplayValue,
|
|
300
|
-
open,
|
|
301
|
-
setOpen,
|
|
302
|
-
value,
|
|
303
|
-
setValue,
|
|
369
|
+
[open, setOpen, value, selectedLabel, resolvedDisplayValue, registerItemLabel, unregisterItemLabel, handleSelect],
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const searchContextValue = React.useMemo<ComboboxSearchContextValue>(
|
|
373
|
+
() => ({
|
|
304
374
|
searchValue,
|
|
305
375
|
setSearchValue,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
376
|
+
}),
|
|
377
|
+
[searchValue, setSearchValue],
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const navigationContextValue = React.useMemo<ComboboxNavigationContextValue>(
|
|
381
|
+
() => ({
|
|
311
382
|
activeDescendantId,
|
|
312
383
|
setActiveDescendantId,
|
|
313
|
-
|
|
384
|
+
}),
|
|
385
|
+
[activeDescendantId, setActiveDescendantId],
|
|
314
386
|
);
|
|
315
387
|
|
|
316
388
|
return (
|
|
317
|
-
<
|
|
318
|
-
<
|
|
319
|
-
{
|
|
320
|
-
|
|
321
|
-
|
|
389
|
+
<ComboboxConfigContext.Provider value={configContextValue}>
|
|
390
|
+
<ComboboxSelectionContext.Provider value={selectionContextValue}>
|
|
391
|
+
<ComboboxSearchContext.Provider value={searchContextValue}>
|
|
392
|
+
<ComboboxNavigationContext.Provider value={navigationContextValue}>
|
|
393
|
+
<Popover.Root open={open} onOpenChange={setOpen} {...rootProps}>
|
|
394
|
+
{children}
|
|
395
|
+
</Popover.Root>
|
|
396
|
+
</ComboboxNavigationContext.Provider>
|
|
397
|
+
</ComboboxSearchContext.Provider>
|
|
398
|
+
</ComboboxSelectionContext.Provider>
|
|
399
|
+
</ComboboxConfigContext.Provider>
|
|
322
400
|
);
|
|
323
401
|
};
|
|
324
402
|
ComboboxRoot.displayName = 'Combobox.Root';
|
|
@@ -332,9 +410,13 @@ interface ComboboxTriggerProps extends NativeTriggerProps, MarginProps, Combobox
|
|
|
332
410
|
* syncing size/highContrast from Root while exposing select-like states.
|
|
333
411
|
*/
|
|
334
412
|
const ComboboxTrigger = React.forwardRef<ComboboxTriggerElement, ComboboxTriggerProps>((props, forwardedRef) => {
|
|
335
|
-
|
|
413
|
+
// Use specific contexts to minimize re-renders
|
|
414
|
+
const configContext = useComboboxConfigContext('Combobox.Trigger');
|
|
415
|
+
const selectionContext = useComboboxSelectionContext('Combobox.Trigger');
|
|
416
|
+
const navigationContext = useComboboxNavigationContext('Combobox.Trigger');
|
|
417
|
+
|
|
336
418
|
const { children, className, placeholder, disabled, readOnly, error, loading, color, radius, ...triggerProps } = extractProps(
|
|
337
|
-
{ size:
|
|
419
|
+
{ size: configContext.size, highContrast: configContext.highContrast, ...props },
|
|
338
420
|
{ size: comboboxRootPropDefs.size, highContrast: comboboxRootPropDefs.highContrast },
|
|
339
421
|
comboboxTriggerPropDefs,
|
|
340
422
|
marginPropDefs,
|
|
@@ -343,29 +425,29 @@ const ComboboxTrigger = React.forwardRef<ComboboxTriggerElement, ComboboxTrigger
|
|
|
343
425
|
// Extract material and panelBackground separately since they need to be passed as data attributes
|
|
344
426
|
const { material, panelBackground } = props;
|
|
345
427
|
|
|
346
|
-
const isDisabled = disabled ??
|
|
428
|
+
const isDisabled = disabled ?? configContext.disabled;
|
|
347
429
|
|
|
348
430
|
// Use color from props or fall back to context color
|
|
349
|
-
const resolvedColor = color ??
|
|
431
|
+
const resolvedColor = color ?? configContext.color;
|
|
350
432
|
|
|
351
433
|
// Comprehensive ARIA attributes for combobox pattern (WAI-ARIA 1.2)
|
|
352
434
|
const ariaProps = React.useMemo(
|
|
353
435
|
() => ({
|
|
354
436
|
role: 'combobox' as const,
|
|
355
|
-
'aria-expanded':
|
|
437
|
+
'aria-expanded': selectionContext.open,
|
|
356
438
|
'aria-disabled': isDisabled || undefined,
|
|
357
439
|
'aria-haspopup': 'listbox' as const,
|
|
358
|
-
'aria-controls':
|
|
359
|
-
'aria-activedescendant':
|
|
440
|
+
'aria-controls': selectionContext.open ? configContext.listboxId : undefined,
|
|
441
|
+
'aria-activedescendant': selectionContext.open ? navigationContext.activeDescendantId : undefined,
|
|
360
442
|
'aria-autocomplete': 'list' as const,
|
|
361
443
|
}),
|
|
362
|
-
[
|
|
444
|
+
[selectionContext.open, configContext.listboxId, navigationContext.activeDescendantId, isDisabled],
|
|
363
445
|
);
|
|
364
446
|
|
|
365
447
|
const defaultContent = (
|
|
366
448
|
<>
|
|
367
449
|
<span className="rt-SelectTriggerInner">
|
|
368
|
-
<ComboboxValue placeholder={placeholder ??
|
|
450
|
+
<ComboboxValue placeholder={placeholder ?? configContext.placeholder} />
|
|
369
451
|
</span>
|
|
370
452
|
{loading ? (
|
|
371
453
|
<div className="rt-SelectIcon rt-SelectLoadingIcon" aria-hidden="true">
|
|
@@ -414,14 +496,16 @@ interface ComboboxValueProps extends React.ComponentPropsWithoutRef<'span'> {
|
|
|
414
496
|
/**
|
|
415
497
|
* Value mirrors Select.Value by showing the selected item's label
|
|
416
498
|
* or falling back to placeholder text supplied by the consumer or context.
|
|
417
|
-
*
|
|
499
|
+
*
|
|
418
500
|
* Priority: resolvedDisplayValue (explicit) > selectedLabel (from items) > raw value > children > placeholder
|
|
419
501
|
*/
|
|
420
502
|
const ComboboxValue = React.forwardRef<ComboboxValueElement, ComboboxValueProps>(({ placeholder, children, className, ...valueProps }, forwardedRef) => {
|
|
421
|
-
|
|
503
|
+
// Only use the contexts we need - config for placeholder, selection for value display
|
|
504
|
+
const configContext = useComboboxConfigContext('Combobox.Value');
|
|
505
|
+
const selectionContext = useComboboxSelectionContext('Combobox.Value');
|
|
422
506
|
// Priority: explicit displayValue (resolved) > registered label > raw value
|
|
423
|
-
const displayValue =
|
|
424
|
-
const fallback = placeholder ??
|
|
507
|
+
const displayValue = selectionContext.resolvedDisplayValue ?? selectionContext.selectedLabel ?? selectionContext.value ?? undefined;
|
|
508
|
+
const fallback = placeholder ?? configContext.placeholder;
|
|
425
509
|
return (
|
|
426
510
|
<span {...valueProps} ref={forwardedRef} className={classNames('rt-ComboboxValue', className)}>
|
|
427
511
|
{displayValue ?? children ?? fallback}
|
|
@@ -440,27 +524,29 @@ interface ComboboxContentProps extends Omit<ComponentPropsWithout<typeof Popover
|
|
|
440
524
|
* and instantiating cmdk's Command list for roving focus + filtering.
|
|
441
525
|
*/
|
|
442
526
|
const ComboboxContent = React.forwardRef<ComboboxContentElement, ComboboxContentProps>((props, forwardedRef) => {
|
|
443
|
-
|
|
527
|
+
// Only use config context - Content doesn't need selection/search/navigation state
|
|
528
|
+
const configContext = useComboboxConfigContext('Combobox.Content');
|
|
444
529
|
const themeContext = useThemeContext();
|
|
445
530
|
const effectiveMaterial = themeContext.panelBackground;
|
|
446
531
|
|
|
447
|
-
const sizeProp = props.size ??
|
|
532
|
+
const sizeProp = props.size ?? configContext.size ?? comboboxContentPropDefs.size.default;
|
|
448
533
|
const variantProp = props.variant ?? comboboxContentPropDefs.variant.default;
|
|
449
|
-
const highContrastProp = props.highContrast ??
|
|
534
|
+
const highContrastProp = props.highContrast ?? configContext.highContrast ?? comboboxContentPropDefs.highContrast.default;
|
|
450
535
|
|
|
451
536
|
const { className, children, color, forceMount, container, ...contentProps } = extractProps(
|
|
452
537
|
{ ...props, size: sizeProp, variant: variantProp, highContrast: highContrastProp },
|
|
453
538
|
comboboxContentPropDefs,
|
|
454
539
|
);
|
|
455
|
-
const resolvedColor = color ||
|
|
540
|
+
const resolvedColor = color || configContext.color || themeContext.accentColor;
|
|
456
541
|
|
|
457
542
|
// Memoize className sanitization to avoid string operations on every render
|
|
543
|
+
// Uses pre-compiled SIZE_CLASS_REGEX for better performance
|
|
458
544
|
const sanitizedClassName = React.useMemo(() => {
|
|
459
545
|
if (typeof sizeProp !== 'string') return className;
|
|
460
546
|
return className
|
|
461
547
|
?.split(/\s+/)
|
|
462
548
|
.filter(Boolean)
|
|
463
|
-
.filter((token) =>
|
|
549
|
+
.filter((token) => !SIZE_CLASS_REGEX.test(token))
|
|
464
550
|
.join(' ') || undefined;
|
|
465
551
|
}, [className, sizeProp]);
|
|
466
552
|
|
|
@@ -493,9 +579,9 @@ const ComboboxContent = React.forwardRef<ComboboxContentElement, ComboboxContent
|
|
|
493
579
|
<Theme asChild>
|
|
494
580
|
<ComboboxContentContext.Provider value={{ variant: variantProp, size: String(sizeProp), color: resolvedColor, material: effectiveMaterial, highContrast: highContrastProp }}>
|
|
495
581
|
<CommandPrimitive
|
|
496
|
-
loop={
|
|
497
|
-
shouldFilter={
|
|
498
|
-
filter={
|
|
582
|
+
loop={configContext.loop}
|
|
583
|
+
shouldFilter={configContext.shouldFilter}
|
|
584
|
+
filter={configContext.filter}
|
|
499
585
|
className="rt-ComboboxCommand"
|
|
500
586
|
>
|
|
501
587
|
{children}
|
|
@@ -522,7 +608,9 @@ interface ComboboxInputProps extends Omit<React.ComponentPropsWithoutRef<typeof
|
|
|
522
608
|
* automatic focus management and optional adornments.
|
|
523
609
|
*/
|
|
524
610
|
const ComboboxInput = React.forwardRef<ComboboxInputElement, ComboboxInputProps>(({ className, startAdornment, endAdornment, placeholder, variant: inputVariant, value, onValueChange, ...inputProps }, forwardedRef) => {
|
|
525
|
-
|
|
611
|
+
// Use specific contexts - config for size/placeholder, search for search state
|
|
612
|
+
const configContext = useComboboxConfigContext('Combobox.Input');
|
|
613
|
+
const searchContext = useComboboxSearchContext('Combobox.Input');
|
|
526
614
|
const contentContext = useComboboxContentContext();
|
|
527
615
|
const contentVariant = contentContext?.variant ?? 'solid';
|
|
528
616
|
const color = contentContext?.color;
|
|
@@ -537,12 +625,12 @@ const ComboboxInput = React.forwardRef<ComboboxInputElement, ComboboxInputProps>
|
|
|
537
625
|
const textFieldVariant = inputVariant ?? (contentVariant === 'solid' ? 'surface' : 'soft');
|
|
538
626
|
|
|
539
627
|
// Use controlled search value from context, allow override via props
|
|
540
|
-
const searchValue = value ??
|
|
541
|
-
const handleSearchChange = onValueChange ??
|
|
628
|
+
const searchValue = value ?? searchContext.searchValue;
|
|
629
|
+
const handleSearchChange = onValueChange ?? searchContext.setSearchValue;
|
|
542
630
|
|
|
543
631
|
const inputField = (
|
|
544
632
|
<div
|
|
545
|
-
className={classNames('rt-TextFieldRoot', 'rt-ComboboxInputRoot', `rt-r-size-${
|
|
633
|
+
className={classNames('rt-TextFieldRoot', 'rt-ComboboxInputRoot', `rt-r-size-${configContext.size}`, `rt-variant-${textFieldVariant}`)}
|
|
546
634
|
data-accent-color={color}
|
|
547
635
|
data-material={material}
|
|
548
636
|
data-panel-background={material}
|
|
@@ -553,7 +641,7 @@ const ComboboxInput = React.forwardRef<ComboboxInputElement, ComboboxInputProps>
|
|
|
553
641
|
ref={forwardedRef}
|
|
554
642
|
value={searchValue}
|
|
555
643
|
onValueChange={handleSearchChange}
|
|
556
|
-
placeholder={placeholder ??
|
|
644
|
+
placeholder={placeholder ?? configContext.searchPlaceholder}
|
|
557
645
|
className={classNames('rt-reset', 'rt-TextFieldInput', className)}
|
|
558
646
|
/>
|
|
559
647
|
{endAdornment ? (
|
|
@@ -579,7 +667,9 @@ interface ComboboxListProps extends React.ComponentPropsWithoutRef<typeof Comman
|
|
|
579
667
|
* Also handles aria-activedescendant tracking via a single MutationObserver for all items.
|
|
580
668
|
*/
|
|
581
669
|
const ComboboxList = React.forwardRef<ComboboxListElement, ComboboxListProps>(({ className, ...listProps }, forwardedRef) => {
|
|
582
|
-
|
|
670
|
+
// Use specific contexts - config for listboxId, navigation for active descendant
|
|
671
|
+
const configContext = useComboboxConfigContext('Combobox.List');
|
|
672
|
+
const navigationContext = useComboboxNavigationContext('Combobox.List');
|
|
583
673
|
const listRef = React.useRef<HTMLDivElement | null>(null);
|
|
584
674
|
|
|
585
675
|
// Combined ref handling
|
|
@@ -595,6 +685,9 @@ const ComboboxList = React.forwardRef<ComboboxListElement, ComboboxListProps>(({
|
|
|
595
685
|
[forwardedRef],
|
|
596
686
|
);
|
|
597
687
|
|
|
688
|
+
// Destructure to get stable reference for effect dependency
|
|
689
|
+
const { setActiveDescendantId } = navigationContext;
|
|
690
|
+
|
|
598
691
|
/**
|
|
599
692
|
* Single MutationObserver at List level to track aria-activedescendant.
|
|
600
693
|
* This replaces per-item observers for better performance with large lists.
|
|
@@ -606,7 +699,7 @@ const ComboboxList = React.forwardRef<ComboboxListElement, ComboboxListProps>(({
|
|
|
606
699
|
const updateActiveDescendant = () => {
|
|
607
700
|
const selectedItem = listNode.querySelector('[data-selected="true"], [aria-selected="true"]');
|
|
608
701
|
const itemId = selectedItem?.id;
|
|
609
|
-
|
|
702
|
+
setActiveDescendantId(itemId || undefined);
|
|
610
703
|
};
|
|
611
704
|
|
|
612
705
|
// Initial check
|
|
@@ -621,7 +714,7 @@ const ComboboxList = React.forwardRef<ComboboxListElement, ComboboxListProps>(({
|
|
|
621
714
|
});
|
|
622
715
|
|
|
623
716
|
return () => observer.disconnect();
|
|
624
|
-
}, [
|
|
717
|
+
}, [setActiveDescendantId]);
|
|
625
718
|
|
|
626
719
|
return (
|
|
627
720
|
<ScrollArea type="auto" className="rt-ComboboxScrollArea" scrollbars="vertical" size="1">
|
|
@@ -629,7 +722,7 @@ const ComboboxList = React.forwardRef<ComboboxListElement, ComboboxListProps>(({
|
|
|
629
722
|
<CommandPrimitive.List
|
|
630
723
|
{...listProps}
|
|
631
724
|
ref={combinedRef}
|
|
632
|
-
id={
|
|
725
|
+
id={configContext.listboxId}
|
|
633
726
|
role="listbox"
|
|
634
727
|
aria-label="Options"
|
|
635
728
|
className={classNames('rt-ComboboxList', className)}
|
|
@@ -709,13 +802,16 @@ function extractTextFromChildren(children: React.ReactNode): string {
|
|
|
709
802
|
}
|
|
710
803
|
|
|
711
804
|
const ComboboxItem = React.forwardRef<ComboboxItemElement, ComboboxItemProps>(({ className, children, label, value, disabled, onSelect, keywords, ...itemProps }, forwardedRef) => {
|
|
712
|
-
|
|
805
|
+
// Use specific contexts - config for disabled, selection for value/registration
|
|
806
|
+
// This means items only re-render when selection changes, not on search or navigation
|
|
807
|
+
const configContext = useComboboxConfigContext('Combobox.Item');
|
|
808
|
+
const selectionContext = useComboboxSelectionContext('Combobox.Item');
|
|
713
809
|
const contentContext = useComboboxContentContext();
|
|
714
|
-
|
|
810
|
+
|
|
715
811
|
// Memoize label extraction to avoid recursive traversal on every render
|
|
716
812
|
const extractedLabel = React.useMemo(() => extractTextFromChildren(children), [children]);
|
|
717
813
|
const itemLabel = label ?? (extractedLabel || String(value));
|
|
718
|
-
const isSelected = value != null &&
|
|
814
|
+
const isSelected = value != null && selectionContext.value === value;
|
|
719
815
|
const sizeClass = contentContext?.size ? `rt-r-size-${contentContext.size}` : undefined;
|
|
720
816
|
|
|
721
817
|
// Use provided keywords, or default to the item label for search
|
|
@@ -727,7 +823,7 @@ const ComboboxItem = React.forwardRef<ComboboxItemElement, ComboboxItemProps>(({
|
|
|
727
823
|
const itemId = `combobox-item-${generatedId}`;
|
|
728
824
|
|
|
729
825
|
// Destructure stable references to avoid effect re-runs when unrelated context values change
|
|
730
|
-
const { registerItemLabel, unregisterItemLabel, handleSelect: contextHandleSelect } =
|
|
826
|
+
const { registerItemLabel, unregisterItemLabel, handleSelect: contextHandleSelect } = selectionContext;
|
|
731
827
|
|
|
732
828
|
// Register/unregister label for display in trigger
|
|
733
829
|
React.useEffect(() => {
|
|
@@ -745,7 +841,7 @@ const ComboboxItem = React.forwardRef<ComboboxItemElement, ComboboxItemProps>(({
|
|
|
745
841
|
[contextHandleSelect, onSelect],
|
|
746
842
|
);
|
|
747
843
|
|
|
748
|
-
const isDisabled = disabled ??
|
|
844
|
+
const isDisabled = disabled ?? configContext.disabled ?? false;
|
|
749
845
|
|
|
750
846
|
/**
|
|
751
847
|
* Performance notes:
|
|
@@ -44,3 +44,122 @@
|
|
|
44
44
|
height: var(--trigger-icon-size-4);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/***************************************************************************************************
|
|
49
|
+
* *
|
|
50
|
+
* DRILL-DOWN MODE *
|
|
51
|
+
* *
|
|
52
|
+
***************************************************************************************************/
|
|
53
|
+
|
|
54
|
+
/* Root container that holds the main menu items */
|
|
55
|
+
.rt-DropdownMenuDrillDownRoot {
|
|
56
|
+
display: contents;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* When a submenu is active, hide the ROOT's direct children (but not nested panels) */
|
|
60
|
+
.rt-DropdownMenuDrillDownRoot:where([data-drill-down-active]) > * {
|
|
61
|
+
display: none !important;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Keep nested panels visible (they use display: contents) */
|
|
65
|
+
.rt-DropdownMenuDrillDownRoot:where([data-drill-down-active]) > :where(.rt-DropdownMenuDrillDownPanel) {
|
|
66
|
+
display: contents !important;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Submenu panel in drill-down mode */
|
|
70
|
+
.rt-DropdownMenuDrillDownPanel {
|
|
71
|
+
display: contents;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Hide children of INACTIVE drill-down panels */
|
|
75
|
+
.rt-DropdownMenuDrillDownPanel:where(:not([data-drill-down-active])) > * {
|
|
76
|
+
display: none !important;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* But keep nested panels visible (they use display: contents) */
|
|
80
|
+
.rt-DropdownMenuDrillDownPanel:where(:not([data-drill-down-active])) > :where(.rt-DropdownMenuDrillDownPanel) {
|
|
81
|
+
display: contents !important;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Back button item */
|
|
85
|
+
.rt-DropdownMenuDrillDownBackItem {
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: var(--space-2);
|
|
89
|
+
min-height: var(--base-menu-item-height);
|
|
90
|
+
padding-top: var(--base-menu-item-padding-y);
|
|
91
|
+
padding-bottom: var(--base-menu-item-padding-y);
|
|
92
|
+
padding-inline-start: var(--base-menu-item-padding-left);
|
|
93
|
+
padding-inline-end: var(--base-menu-item-padding-right);
|
|
94
|
+
box-sizing: border-box;
|
|
95
|
+
outline: none;
|
|
96
|
+
cursor: var(--cursor-menu-item);
|
|
97
|
+
user-select: none;
|
|
98
|
+
transition: var(--transition-menu);
|
|
99
|
+
font-weight: 500;
|
|
100
|
+
|
|
101
|
+
&:where(:focus-visible) {
|
|
102
|
+
outline: 2px solid var(--focus-8);
|
|
103
|
+
outline-offset: -2px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Enhanced reduced motion support */
|
|
107
|
+
@media (prefers-reduced-motion: reduce) {
|
|
108
|
+
transition: none;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Back icon */
|
|
113
|
+
.rt-DropdownMenuDrillDownBackIcon {
|
|
114
|
+
width: var(--indicator-icon-size-2);
|
|
115
|
+
height: var(--indicator-icon-size-2);
|
|
116
|
+
flex-shrink: 0;
|
|
117
|
+
color: var(--gray-12);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Back label */
|
|
121
|
+
.rt-DropdownMenuDrillDownBackLabel {
|
|
122
|
+
color: var(--gray-12);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Solid variant - drill-down items hover (matches menu item [data-highlighted]) */
|
|
126
|
+
.rt-BaseMenuContent:where(.rt-variant-solid) {
|
|
127
|
+
& :where(.rt-DropdownMenuDrillDownBackItem:hover),
|
|
128
|
+
& :where(.rt-DropdownMenuDrillDownBackItem:focus-visible),
|
|
129
|
+
& :where(.rt-DropdownMenuDrillDownSubTrigger:hover),
|
|
130
|
+
& :where(.rt-DropdownMenuDrillDownSubTrigger:focus-visible) {
|
|
131
|
+
background-color: var(--accent-9);
|
|
132
|
+
color: var(--accent-contrast);
|
|
133
|
+
|
|
134
|
+
& :where(.rt-DropdownMenuDrillDownBackIcon),
|
|
135
|
+
& :where(.rt-DropdownMenuDrillDownBackLabel),
|
|
136
|
+
& :where(.rt-BaseMenuSubTriggerIcon) {
|
|
137
|
+
color: var(--accent-contrast);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Soft variant - drill-down items hover (matches menu item [data-highlighted]) */
|
|
143
|
+
.rt-BaseMenuContent:where(.rt-variant-soft) {
|
|
144
|
+
& :where(.rt-DropdownMenuDrillDownBackItem:hover),
|
|
145
|
+
& :where(.rt-DropdownMenuDrillDownBackItem:focus-visible),
|
|
146
|
+
& :where(.rt-DropdownMenuDrillDownSubTrigger:hover),
|
|
147
|
+
& :where(.rt-DropdownMenuDrillDownSubTrigger:focus-visible) {
|
|
148
|
+
background-color: var(--accent-4);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
:where([data-panel-background='translucent']) & :where(.rt-DropdownMenuDrillDownBackItem:hover),
|
|
152
|
+
:where([data-panel-background='translucent']) & :where(.rt-DropdownMenuDrillDownBackItem:focus-visible),
|
|
153
|
+
:where([data-panel-background='translucent']) & :where(.rt-DropdownMenuDrillDownSubTrigger:hover),
|
|
154
|
+
:where([data-panel-background='translucent']) & :where(.rt-DropdownMenuDrillDownSubTrigger:focus-visible) {
|
|
155
|
+
background-color: var(--accent-a4);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Forced colors support */
|
|
160
|
+
@media (forced-colors: active) {
|
|
161
|
+
.rt-DropdownMenuDrillDownBackItem:where(:focus-visible) {
|
|
162
|
+
outline: 2px solid Highlight;
|
|
163
|
+
outline-offset: 2px;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export {
|
|
2
2
|
baseMenuContentPropDefs as dropdownMenuContentPropDefs,
|
|
3
|
+
baseMenuSubContentPropDefs as dropdownMenuSubContentPropDefs,
|
|
3
4
|
baseMenuItemPropDefs as dropdownMenuItemPropDefs,
|
|
4
5
|
baseMenuCheckboxItemPropDefs as dropdownMenuCheckboxItemPropDefs,
|
|
5
6
|
baseMenuRadioItemPropDefs as dropdownMenuRadioItemPropDefs,
|
|
7
|
+
submenuBehaviors,
|
|
6
8
|
} from './_internal/base-menu.props.js';
|