@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.
Files changed (64) hide show
  1. package/components.css +93 -0
  2. package/dist/cjs/components/_internal/base-menu.props.d.ts +52 -1
  3. package/dist/cjs/components/_internal/base-menu.props.d.ts.map +1 -1
  4. package/dist/cjs/components/_internal/base-menu.props.js +1 -1
  5. package/dist/cjs/components/_internal/base-menu.props.js.map +3 -3
  6. package/dist/cjs/components/_internal/dropdown-menu-drill-down.d.ts +60 -0
  7. package/dist/cjs/components/_internal/dropdown-menu-drill-down.d.ts.map +1 -0
  8. package/dist/cjs/components/_internal/dropdown-menu-drill-down.js +2 -0
  9. package/dist/cjs/components/_internal/dropdown-menu-drill-down.js.map +7 -0
  10. package/dist/cjs/components/combobox.d.ts.map +1 -1
  11. package/dist/cjs/components/combobox.js +1 -1
  12. package/dist/cjs/components/combobox.js.map +3 -3
  13. package/dist/cjs/components/dropdown-menu.d.ts +28 -7
  14. package/dist/cjs/components/dropdown-menu.d.ts.map +1 -1
  15. package/dist/cjs/components/dropdown-menu.js +1 -1
  16. package/dist/cjs/components/dropdown-menu.js.map +3 -3
  17. package/dist/cjs/components/dropdown-menu.props.d.ts +1 -1
  18. package/dist/cjs/components/dropdown-menu.props.d.ts.map +1 -1
  19. package/dist/cjs/components/dropdown-menu.props.js +1 -1
  20. package/dist/cjs/components/dropdown-menu.props.js.map +2 -2
  21. package/dist/cjs/components/icons.d.ts +2 -1
  22. package/dist/cjs/components/icons.d.ts.map +1 -1
  23. package/dist/cjs/components/icons.js +1 -1
  24. package/dist/cjs/components/icons.js.map +3 -3
  25. package/dist/cjs/components/schemas/shell.schema.d.ts +2 -2
  26. package/dist/esm/components/_internal/base-menu.props.d.ts +52 -1
  27. package/dist/esm/components/_internal/base-menu.props.d.ts.map +1 -1
  28. package/dist/esm/components/_internal/base-menu.props.js +1 -1
  29. package/dist/esm/components/_internal/base-menu.props.js.map +3 -3
  30. package/dist/esm/components/_internal/dropdown-menu-drill-down.d.ts +60 -0
  31. package/dist/esm/components/_internal/dropdown-menu-drill-down.d.ts.map +1 -0
  32. package/dist/esm/components/_internal/dropdown-menu-drill-down.js +2 -0
  33. package/dist/esm/components/_internal/dropdown-menu-drill-down.js.map +7 -0
  34. package/dist/esm/components/combobox.d.ts.map +1 -1
  35. package/dist/esm/components/combobox.js +1 -1
  36. package/dist/esm/components/combobox.js.map +3 -3
  37. package/dist/esm/components/dropdown-menu.d.ts +28 -7
  38. package/dist/esm/components/dropdown-menu.d.ts.map +1 -1
  39. package/dist/esm/components/dropdown-menu.js +1 -1
  40. package/dist/esm/components/dropdown-menu.js.map +3 -3
  41. package/dist/esm/components/dropdown-menu.props.d.ts +1 -1
  42. package/dist/esm/components/dropdown-menu.props.d.ts.map +1 -1
  43. package/dist/esm/components/dropdown-menu.props.js +1 -1
  44. package/dist/esm/components/dropdown-menu.props.js.map +3 -3
  45. package/dist/esm/components/icons.d.ts +2 -1
  46. package/dist/esm/components/icons.d.ts.map +1 -1
  47. package/dist/esm/components/icons.js +1 -1
  48. package/dist/esm/components/icons.js.map +3 -3
  49. package/dist/esm/components/schemas/shell.schema.d.ts +2 -2
  50. package/package.json +1 -1
  51. package/schemas/base-button.json +1 -1
  52. package/schemas/button.json +1 -1
  53. package/schemas/icon-button.json +1 -1
  54. package/schemas/index.json +6 -6
  55. package/schemas/toggle-button.json +1 -1
  56. package/schemas/toggle-icon-button.json +1 -1
  57. package/src/components/_internal/base-menu.props.ts +31 -1
  58. package/src/components/_internal/dropdown-menu-drill-down.tsx +242 -0
  59. package/src/components/combobox.tsx +176 -80
  60. package/src/components/dropdown-menu.css +119 -0
  61. package/src/components/dropdown-menu.props.tsx +2 -0
  62. package/src/components/dropdown-menu.tsx +217 -27
  63. package/src/components/icons.tsx +14 -1
  64. 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
- * Internal context shared by all sub-components to avoid prop drilling.
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
- interface ComboboxContextValue extends ComboboxRootOwnProps {
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
- listboxId: string;
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 ComboboxContext = React.createContext<ComboboxContextValue | null>(null);
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 hook that ensures consumers are wrapped in Combobox.Root.
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 useComboboxContext = (caller: string) => {
127
- const ctx = React.useContext(ComboboxContext);
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
- const contextValue = React.useMemo<ComboboxContextValue>(
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
- resolvedDisplayValue,
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
- size,
290
- highContrast,
291
- placeholder,
292
- searchPlaceholder,
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
- selectedLabel,
307
- registerItemLabel,
308
- unregisterItemLabel,
309
- handleSelect,
310
- listboxId,
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
- <ComboboxContext.Provider value={contextValue}>
318
- <Popover.Root open={open} onOpenChange={setOpen} {...rootProps}>
319
- {children}
320
- </Popover.Root>
321
- </ComboboxContext.Provider>
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
- const context = useComboboxContext('Combobox.Trigger');
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: context.size, highContrast: context.highContrast, ...props },
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 ?? context.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 ?? context.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': context.open,
437
+ 'aria-expanded': selectionContext.open,
356
438
  'aria-disabled': isDisabled || undefined,
357
439
  'aria-haspopup': 'listbox' as const,
358
- 'aria-controls': context.open ? context.listboxId : undefined,
359
- 'aria-activedescendant': context.open ? context.activeDescendantId : undefined,
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
- [context.open, context.listboxId, context.activeDescendantId, isDisabled],
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 ?? context.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
- const context = useComboboxContext('Combobox.Value');
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 = context.resolvedDisplayValue ?? context.selectedLabel ?? context.value ?? undefined;
424
- const fallback = placeholder ?? context.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
- const context = useComboboxContext('Combobox.Content');
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 ?? context.size ?? comboboxContentPropDefs.size.default;
532
+ const sizeProp = props.size ?? configContext.size ?? comboboxContentPropDefs.size.default;
448
533
  const variantProp = props.variant ?? comboboxContentPropDefs.variant.default;
449
- const highContrastProp = props.highContrast ?? context.highContrast ?? comboboxContentPropDefs.highContrast.default;
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 || context.color || themeContext.accentColor;
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) => !/^rt-r-size-\d$/.test(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={context.loop}
497
- shouldFilter={context.shouldFilter}
498
- filter={context.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
- const context = useComboboxContext('Combobox.Input');
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 ?? context.searchValue;
541
- const handleSearchChange = onValueChange ?? context.setSearchValue;
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-${context.size}`, `rt-variant-${textFieldVariant}`)}
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 ?? context.searchPlaceholder}
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
- const context = useComboboxContext('Combobox.List');
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
- context.setActiveDescendantId(itemId || undefined);
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
- }, [context.setActiveDescendantId]);
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={context.listboxId}
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
- const context = useComboboxContext('Combobox.Item');
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 && context.value === value;
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 } = context;
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 ?? context.disabled ?? false;
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';