@arun-dev/headless 4.14.0 → 4.15.0

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.
@@ -0,0 +1,8 @@
1
+ // src/core/fold.ts
2
+ function fold(text) {
3
+ return text.normalize("NFD").replace(/\p{M}/gu, "").toLocaleLowerCase();
4
+ }
5
+
6
+ export {
7
+ fold
8
+ };
@@ -133,10 +133,12 @@ function useControlled({
133
133
  return [value, setValue];
134
134
  }
135
135
 
136
- // src/combobox/defaultFilter.ts
136
+ // src/core/fold.ts
137
137
  function fold(text) {
138
138
  return text.normalize("NFD").replace(/\p{M}/gu, "").toLocaleLowerCase();
139
139
  }
140
+
141
+ // src/combobox/defaultFilter.ts
140
142
  function defaultFilter(items, query, itemToString) {
141
143
  const needle = fold(query.trim());
142
144
  if (needle === "") return [...items];
@@ -1,3 +1,6 @@
1
+ import {
2
+ fold
3
+ } from "../chunk-ETNICT5M.js";
1
4
  import {
2
5
  anchorNameFor,
3
6
  anchoredDataAttributes,
@@ -40,9 +43,6 @@ __export(index_parts_exports, {
40
43
  import { useCallback, useDeferredValue, useEffect, useId, useMemo, useRef, useState } from "react";
41
44
 
42
45
  // src/combobox/defaultFilter.ts
43
- function fold(text) {
44
- return text.normalize("NFD").replace(/\p{M}/gu, "").toLocaleLowerCase();
45
- }
46
46
  function defaultFilter(items, query, itemToString) {
47
47
  const needle = fold(query.trim());
48
48
  if (needle === "") return [...items];
@@ -27,8 +27,14 @@ module.exports = __toCommonJS(menu_exports);
27
27
  // src/menu/index.parts.ts
28
28
  var index_parts_exports = {};
29
29
  __export(index_parts_exports, {
30
+ CheckboxItem: () => MenuCheckboxItem,
31
+ Group: () => MenuGroup,
32
+ GroupLabel: () => MenuGroupLabel,
30
33
  Item: () => MenuItem,
34
+ ItemIndicator: () => MenuItemIndicator,
31
35
  Popup: () => MenuPopup,
36
+ RadioGroup: () => MenuRadioGroup,
37
+ RadioItem: () => MenuRadioItem,
32
38
  Root: () => MenuRoot,
33
39
  Trigger: () => MenuTrigger
34
40
  });
@@ -280,9 +286,6 @@ function menuDataAttributes({ open }) {
280
286
  "data-closed": open ? void 0 : ""
281
287
  };
282
288
  }
283
- function menuItemDataAttributes(disabled) {
284
- return { "data-disabled": disabled ? "" : void 0 };
285
- }
286
289
 
287
290
  // src/menu/MenuTrigger.tsx
288
291
  function MenuTrigger({ className, children, render, ...rest }) {
@@ -317,7 +320,58 @@ function MenuTrigger({ className, children, render, ...rest }) {
317
320
  }
318
321
 
319
322
  // src/menu/MenuPopup.tsx
323
+ var import_react6 = require("react");
324
+
325
+ // src/core/typeahead.ts
320
326
  var import_react5 = require("react");
327
+
328
+ // src/core/fold.ts
329
+ function fold(text) {
330
+ return text.normalize("NFD").replace(/\p{M}/gu, "").toLocaleLowerCase();
331
+ }
332
+
333
+ // src/core/typeahead.ts
334
+ var TYPEAHEAD_TIMEOUT = 500;
335
+ function textOf(node) {
336
+ let text = "";
337
+ import_react5.Children.forEach(node, (child) => {
338
+ if (typeof child === "string" || typeof child === "number") text += child;
339
+ else if ((0, import_react5.isValidElement)(child)) text += textOf(child.props.children);
340
+ });
341
+ return text;
342
+ }
343
+ function typeaheadIndex(labels, current, search) {
344
+ const count = labels.length;
345
+ if (count === 0 || search === "") return null;
346
+ const [first = "", ...others] = [...search];
347
+ const repeated = others.every((char) => char === first);
348
+ const needle = fold(repeated ? first : search);
349
+ const start = current < 0 ? 0 : repeated ? current + 1 : current;
350
+ for (let step = 0; step < count; step += 1) {
351
+ const index = (start + step) % count;
352
+ if (fold(labels[index] ?? "").startsWith(needle)) return index;
353
+ }
354
+ return null;
355
+ }
356
+ function useTypeahead() {
357
+ const searchRef = (0, import_react5.useRef)("");
358
+ const timerRef = (0, import_react5.useRef)(void 0);
359
+ (0, import_react5.useEffect)(() => () => clearTimeout(timerRef.current), []);
360
+ return (0, import_react5.useCallback)((event) => {
361
+ const { key } = event;
362
+ if (event.ctrlKey || event.metaKey || event.altKey) return null;
363
+ if ([...key].length !== 1) return null;
364
+ if (key === " " && searchRef.current === "") return null;
365
+ searchRef.current += key;
366
+ clearTimeout(timerRef.current);
367
+ timerRef.current = setTimeout(() => {
368
+ searchRef.current = "";
369
+ }, TYPEAHEAD_TIMEOUT);
370
+ return searchRef.current;
371
+ }, []);
372
+ }
373
+
374
+ // src/menu/MenuPopup.tsx
321
375
  function MenuPopup({
322
376
  side = "bottom",
323
377
  align = "start",
@@ -336,10 +390,11 @@ function MenuPopup({
336
390
  anchorName,
337
391
  triggerRef
338
392
  } = useMenuRootContext("Popup");
339
- const elementRef = (0, import_react5.useRef)(null);
340
- const focusPendingRef = (0, import_react5.useRef)(false);
341
- const [, resync] = (0, import_react5.useReducer)((n) => n + 1, 0);
342
- (0, import_react5.useEffect)(() => {
393
+ const elementRef = (0, import_react6.useRef)(null);
394
+ const focusPendingRef = (0, import_react6.useRef)(false);
395
+ const [, resync] = (0, import_react6.useReducer)((n) => n + 1, 0);
396
+ const typeahead = useTypeahead();
397
+ (0, import_react6.useEffect)(() => {
343
398
  const popup = elementRef.current;
344
399
  if (!popup || typeof popup.showPopover !== "function") return;
345
400
  const shown = popup.matches(":popover-open");
@@ -378,12 +433,18 @@ function MenuPopup({
378
433
  }
379
434
  const current = navigableItems.findIndex((i) => i.ref.current === event.target);
380
435
  if (current < 0 && event.target !== event.currentTarget) return;
381
- const next = rovingIndex(event.key, {
436
+ const search = typeahead(event);
437
+ const next = search === null ? rovingIndex(event.key, {
382
438
  count: navigableItems.length,
383
439
  current,
384
440
  orientation: "vertical",
385
441
  rtl: false
386
- });
442
+ }) : typeaheadIndex(
443
+ navigableItems.map((i) => i.textValue),
444
+ current,
445
+ search
446
+ );
447
+ if (search !== null) event.preventDefault();
387
448
  if (next === null) return;
388
449
  event.preventDefault();
389
450
  navigableItems[next]?.ref.current?.focus();
@@ -399,32 +460,62 @@ function MenuPopup({
399
460
  });
400
461
  }
401
462
 
463
+ // src/menu/useMenuItem.ts
464
+ var import_react7 = require("react");
465
+ function useMenuItem(part, { disabled, textValue, children }) {
466
+ const { setOpen, focusableWhenDisabled, register } = useMenuRootContext(part);
467
+ const ref = (0, import_react7.useRef)(null);
468
+ const text = textValue ?? textOf(children);
469
+ (0, import_react7.useLayoutEffect)(() => register({ disabled, textValue: text, ref }), [register, disabled, text]);
470
+ return {
471
+ setOpen,
472
+ itemProps: {
473
+ type: "button",
474
+ tabIndex: -1,
475
+ disabled: disabled && !focusableWhenDisabled || void 0,
476
+ "aria-disabled": disabled && focusableWhenDisabled || void 0,
477
+ "data-disabled": disabled ? "" : void 0,
478
+ ref
479
+ }
480
+ };
481
+ }
482
+ var MenuItemIndicatorContext = (0, import_react7.createContext)(null);
483
+ function useMenuItemIndicatorContext() {
484
+ const context = (0, import_react7.useContext)(MenuItemIndicatorContext);
485
+ if (context === null) {
486
+ throw new Error(
487
+ "<Menu.ItemIndicator> must be rendered inside <Menu.CheckboxItem> or <Menu.RadioItem>."
488
+ );
489
+ }
490
+ return context;
491
+ }
492
+ function menuCheckableDataAttributes({ checked, disabled }) {
493
+ return {
494
+ "data-checked": checked ? "" : void 0,
495
+ "data-unchecked": checked ? void 0 : "",
496
+ "data-disabled": disabled ? "" : void 0
497
+ };
498
+ }
499
+
402
500
  // src/menu/MenuItem.tsx
403
- var import_react6 = require("react");
404
501
  function MenuItem({
405
502
  disabled = false,
503
+ textValue,
406
504
  onClick,
407
505
  className,
408
506
  children,
409
507
  render,
410
508
  ...rest
411
509
  }) {
412
- const { setOpen, focusableWhenDisabled, register } = useMenuRootContext("Item");
413
- const elementRef = (0, import_react6.useRef)(null);
414
- (0, import_react6.useLayoutEffect)(() => register({ disabled, ref: elementRef }), [register, disabled]);
510
+ const { setOpen, itemProps } = useMenuItem("Item", { disabled, textValue, children });
415
511
  return useRender({
416
512
  render,
417
513
  defaultTagName: "button",
418
514
  props: {
419
- type: "button",
515
+ ...itemProps,
420
516
  role: "menuitem",
421
- tabIndex: -1,
422
- disabled: disabled && !focusableWhenDisabled || void 0,
423
- "aria-disabled": disabled && focusableWhenDisabled || void 0,
424
- ...menuItemDataAttributes(disabled),
425
517
  className,
426
518
  children,
427
- ref: elementRef,
428
519
  onClick() {
429
520
  if (!disabled) setOpen(false);
430
521
  }
@@ -432,6 +523,207 @@ function MenuItem({
432
523
  consumerProps: disabled ? rest : { ...rest, onClick }
433
524
  });
434
525
  }
526
+
527
+ // src/menu/MenuCheckboxItem.tsx
528
+ var import_react8 = require("react");
529
+ var import_jsx_runtime2 = require("react/jsx-runtime");
530
+ function MenuCheckboxItem({
531
+ checked: checkedProp,
532
+ defaultChecked,
533
+ onCheckedChange,
534
+ closeOnClick = false,
535
+ disabled = false,
536
+ textValue,
537
+ onClick,
538
+ className,
539
+ children,
540
+ render,
541
+ ...rest
542
+ }) {
543
+ const { setOpen, itemProps } = useMenuItem("CheckboxItem", { disabled, textValue, children });
544
+ const [checked, setChecked] = useControlled({
545
+ controlled: checkedProp,
546
+ default: defaultChecked ?? false,
547
+ name: "Menu.CheckboxItem",
548
+ state: "checked"
549
+ });
550
+ const state = (0, import_react8.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
551
+ const element = useRender({
552
+ render,
553
+ defaultTagName: "button",
554
+ props: {
555
+ ...itemProps,
556
+ role: "menuitemcheckbox",
557
+ "aria-checked": checked,
558
+ ...menuCheckableDataAttributes(state),
559
+ className,
560
+ children,
561
+ onClick() {
562
+ if (disabled) return;
563
+ setChecked(!checked);
564
+ onCheckedChange?.(!checked);
565
+ if (closeOnClick) setOpen(false);
566
+ }
567
+ },
568
+ consumerProps: disabled ? rest : { ...rest, onClick }
569
+ });
570
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MenuItemIndicatorContext.Provider, { value: state, children: element });
571
+ }
572
+
573
+ // src/menu/MenuRadioGroup.tsx
574
+ var import_react10 = require("react");
575
+
576
+ // src/menu/MenuGroup.tsx
577
+ var import_react9 = require("react");
578
+ var import_jsx_runtime3 = require("react/jsx-runtime");
579
+ var MenuGroupContext = (0, import_react9.createContext)(null);
580
+ function useMenuGroupContext() {
581
+ return (0, import_react9.useContext)(MenuGroupContext);
582
+ }
583
+ function useMenuGroupElement(part, {
584
+ render,
585
+ props,
586
+ consumerProps
587
+ }) {
588
+ useMenuRootContext(part);
589
+ const labelId = `${(0, import_react9.useId)()}-label`;
590
+ const [hasLabel, setHasLabel] = (0, import_react9.useState)(false);
591
+ const registerLabel = (0, import_react9.useCallback)(() => {
592
+ setHasLabel(true);
593
+ return () => setHasLabel(false);
594
+ }, []);
595
+ const context = (0, import_react9.useMemo)(() => ({ labelId, registerLabel }), [labelId, registerLabel]);
596
+ const element = useRender({
597
+ render,
598
+ defaultTagName: "div",
599
+ props: { role: "group", "aria-labelledby": hasLabel ? labelId : void 0, ...props },
600
+ consumerProps
601
+ });
602
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MenuGroupContext.Provider, { value: context, children: element });
603
+ }
604
+ function MenuGroup({ className, children, render, ...rest }) {
605
+ return useMenuGroupElement("Group", {
606
+ render,
607
+ props: { className, children },
608
+ consumerProps: rest
609
+ });
610
+ }
611
+
612
+ // src/menu/MenuRadioGroup.tsx
613
+ var import_jsx_runtime4 = require("react/jsx-runtime");
614
+ var MenuRadioGroupContext = (0, import_react10.createContext)(null);
615
+ function useMenuRadioGroupContext() {
616
+ const context = (0, import_react10.useContext)(MenuRadioGroupContext);
617
+ if (context === null) {
618
+ throw new Error("<Menu.RadioItem> must be rendered inside <Menu.RadioGroup>.");
619
+ }
620
+ return context;
621
+ }
622
+ function MenuRadioGroup({
623
+ value: valueProp,
624
+ defaultValue,
625
+ onValueChange,
626
+ disabled = false,
627
+ className,
628
+ children,
629
+ render,
630
+ ...rest
631
+ }) {
632
+ const [value, setValue] = useControlled({
633
+ controlled: valueProp,
634
+ default: defaultValue ?? null,
635
+ name: "Menu.RadioGroup",
636
+ state: "value"
637
+ });
638
+ const select = (0, import_react10.useCallback)(
639
+ (next) => {
640
+ if (next === value) return;
641
+ setValue(next);
642
+ onValueChange?.(next);
643
+ },
644
+ [value, setValue, onValueChange]
645
+ );
646
+ const context = (0, import_react10.useMemo)(() => ({ value, disabled, select }), [value, disabled, select]);
647
+ const element = useMenuGroupElement("RadioGroup", {
648
+ render,
649
+ props: { "data-disabled": disabled ? "" : void 0, className, children },
650
+ consumerProps: rest
651
+ });
652
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MenuRadioGroupContext.Provider, { value: context, children: element });
653
+ }
654
+
655
+ // src/menu/MenuRadioItem.tsx
656
+ var import_react11 = require("react");
657
+ var import_jsx_runtime5 = require("react/jsx-runtime");
658
+ function MenuRadioItem({
659
+ value,
660
+ closeOnClick = false,
661
+ disabled: disabledProp = false,
662
+ textValue,
663
+ onClick,
664
+ className,
665
+ children,
666
+ render,
667
+ ...rest
668
+ }) {
669
+ const group = useMenuRadioGroupContext();
670
+ const disabled = disabledProp || group.disabled;
671
+ const { setOpen, itemProps } = useMenuItem("RadioItem", { disabled, textValue, children });
672
+ const checked = group.value === value;
673
+ const state = (0, import_react11.useMemo)(() => ({ checked, disabled }), [checked, disabled]);
674
+ const element = useRender({
675
+ render,
676
+ defaultTagName: "button",
677
+ props: {
678
+ ...itemProps,
679
+ role: "menuitemradio",
680
+ "aria-checked": checked,
681
+ ...menuCheckableDataAttributes(state),
682
+ className,
683
+ children,
684
+ onClick() {
685
+ if (disabled) return;
686
+ group.select(value);
687
+ if (closeOnClick) setOpen(false);
688
+ }
689
+ },
690
+ consumerProps: disabled ? rest : { ...rest, onClick }
691
+ });
692
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(MenuItemIndicatorContext.Provider, { value: state, children: element });
693
+ }
694
+
695
+ // src/menu/MenuItemIndicator.tsx
696
+ function MenuItemIndicator({
697
+ className,
698
+ children,
699
+ render,
700
+ ...rest
701
+ }) {
702
+ const state = useMenuItemIndicatorContext();
703
+ return useRender({
704
+ render,
705
+ defaultTagName: "span",
706
+ props: { "aria-hidden": true, ...menuCheckableDataAttributes(state), className, children },
707
+ consumerProps: rest
708
+ });
709
+ }
710
+
711
+ // src/menu/MenuGroupLabel.tsx
712
+ var import_react12 = require("react");
713
+ function MenuGroupLabel({ className, children, render, ...rest }) {
714
+ const group = useMenuGroupContext();
715
+ if (group === null) {
716
+ throw new Error("<Menu.GroupLabel> must be rendered inside <Menu.Group> or <Menu.RadioGroup>.");
717
+ }
718
+ const { labelId, registerLabel } = group;
719
+ (0, import_react12.useLayoutEffect)(registerLabel, [registerLabel]);
720
+ return useRender({
721
+ render,
722
+ defaultTagName: "div",
723
+ props: { id: labelId, className, children },
724
+ consumerProps: rest
725
+ });
726
+ }
435
727
  // Annotate the CommonJS export names for ESM import in node:
436
728
  0 && (module.exports = {
437
729
  Menu
@@ -68,8 +68,9 @@ type MenuPopupProps = MenuPopupOwnProps & Omit<ComponentPropsWithRef<'div'>, key
68
68
  * Popover's popup is (decision 12), and labelled by it.
69
69
  *
70
70
  * Opening moves focus to the first item — the last, when Up on the Trigger opened it. Up
71
- * and Down then move between items, wrapping, with Home and End; disabled items are skipped
72
- * unless the Root sets `focusableWhenDisabled`. Tab closes the menu and lets focus move on.
71
+ * and Down then move between items, wrapping, with Home and End; typing moves to the next item
72
+ * whose text starts with what was typed. Disabled items are skipped by both, unless the Root
73
+ * sets `focusableWhenDisabled`. Tab closes the menu and lets focus move on.
73
74
  *
74
75
  * Light dismiss and Esc close the element before anyone is asked; the `toggle` event
75
76
  * reports it through the Root, and the element reopens if the state stays open. Focus
@@ -83,6 +84,11 @@ type MenuItemOwnProps = {
83
84
  * `focusableWhenDisabled`.
84
85
  */
85
86
  disabled?: boolean;
87
+ /**
88
+ * The text typeahead matches. Defaults to the text in `children`; set it when that text is
89
+ * rendered by a component of your own, or differs from what should be typed.
90
+ */
91
+ textValue?: string;
86
92
  /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
87
93
  render?: ReactElement;
88
94
  /** Ref to the rendered element. Merged with any ref on the `render` element. */
@@ -94,17 +100,145 @@ type MenuItemProps = MenuItemOwnProps & Omit<ComponentPropsWithRef<'button'>, ke
94
100
  * handling here: put the action in `onClick`. Activating it closes the menu — call
95
101
  * `event.preventComponentHandler()` in your `onClick` to keep it open.
96
102
  *
97
- * `tabIndex={-1}`: the menu moves focus between items itself, and Tab leaves the menu.
103
+ * Disabled, your `onClick` is not attached, so an `aria-disabled` item under the Root's
104
+ * `focusableWhenDisabled` cannot run it either.
105
+ */
106
+ declare function MenuItem({ disabled, textValue, onClick, className, children, render, ...rest }: MenuItemProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
107
+
108
+ type MenuCheckboxItemOwnProps = {
109
+ /**
110
+ * Controlled state. Provide `onCheckedChange` alongside it.
111
+ *
112
+ * Never `undefined` once mounted: the mode is latched at mount, as for Checkbox.
113
+ */
114
+ checked?: boolean;
115
+ /** Initial state when uncontrolled. Read once, at mount. */
116
+ defaultChecked?: boolean;
117
+ /** Called with the new state, in both controlled and uncontrolled modes. */
118
+ onCheckedChange?: (checked: boolean) => void;
119
+ /** Closes the menu when activated. Off by default: a setting is often changed with others. */
120
+ closeOnClick?: boolean;
121
+ /**
122
+ * Cannot be activated. Skipped by the arrow keys, unless the Root sets
123
+ * `focusableWhenDisabled`.
124
+ */
125
+ disabled?: boolean;
126
+ /** The text typeahead matches. Defaults to the text in `children`. */
127
+ textValue?: string;
128
+ /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
129
+ render?: ReactElement;
130
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
131
+ ref?: Ref<HTMLElement>;
132
+ };
133
+ type MenuCheckboxItemProps = MenuCheckboxItemOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof MenuCheckboxItemOwnProps>;
134
+ /**
135
+ * A setting that is on or off: `role="menuitemcheckbox"` with `aria-checked`, on a native
136
+ * `<button>`. Activating it toggles the state and leaves the menu open, unless `closeOnClick`.
137
+ * Your `onClick` runs first; `event.preventComponentHandler()` there stops the toggle.
98
138
  *
99
- * Disabled, it is a native `disabled` button, out of focus and activation. With the Root's
100
- * `focusableWhenDisabled` it is `aria-disabled` instead, which keeps it focusable, so the
101
- * activation a native `disabled` would block is blocked here: your `onClick` is not
102
- * attached while it is disabled.
139
+ * Holds its own state through `useControlled`; an ItemIndicator inside reads it.
140
+ */
141
+ declare function MenuCheckboxItem({ checked: checkedProp, defaultChecked, onCheckedChange, closeOnClick, disabled, textValue, onClick, className, children, render, ...rest }: MenuCheckboxItemProps): react.JSX.Element;
142
+
143
+ type MenuRadioGroupOwnProps = {
144
+ /**
145
+ * Controlled value — the `value` of the checked RadioItem, or `null` for none. Provide
146
+ * `onValueChange` alongside it.
147
+ *
148
+ * Never `undefined` once mounted: the mode is latched at mount, as for RadioGroup.
149
+ */
150
+ value?: string | null;
151
+ /** Initial value when uncontrolled. Read once, at mount. */
152
+ defaultValue?: string | null;
153
+ /** Called with the value of the RadioItem being checked, in both modes. */
154
+ onValueChange?: (value: string) => void;
155
+ /** Disables every RadioItem inside. */
156
+ disabled?: boolean;
157
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
158
+ render?: ReactElement;
159
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
160
+ ref?: Ref<HTMLElement>;
161
+ };
162
+ type MenuRadioGroupProps = MenuRadioGroupOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof MenuRadioGroupOwnProps | 'defaultValue'>;
163
+ /**
164
+ * One choice among several: a `role="group"` that owns a value, and the RadioItems inside
165
+ * derive `checked` from it — as RadioGroup does for radios. Named by a GroupLabel inside, as a
166
+ * Group is.
167
+ */
168
+ declare function MenuRadioGroup({ value: valueProp, defaultValue, onValueChange, disabled, className, children, render, ...rest }: MenuRadioGroupProps): react.JSX.Element;
169
+
170
+ type MenuRadioItemOwnProps = {
171
+ /** This item's value in its RadioGroup. Checked while the group's `value` equals it. */
172
+ value: string;
173
+ /** Closes the menu when activated. Off by default, as for CheckboxItem. */
174
+ closeOnClick?: boolean;
175
+ /**
176
+ * Cannot be activated. Skipped by the arrow keys, unless the Root sets
177
+ * `focusableWhenDisabled`. The RadioGroup's `disabled` disables it too.
178
+ */
179
+ disabled?: boolean;
180
+ /** The text typeahead matches. Defaults to the text in `children`. */
181
+ textValue?: string;
182
+ /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
183
+ render?: ReactElement;
184
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
185
+ ref?: Ref<HTMLElement>;
186
+ };
187
+ type MenuRadioItemProps = MenuRadioItemOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof MenuRadioItemOwnProps>;
188
+ /**
189
+ * One choice in a RadioGroup: `role="menuitemradio"` with `aria-checked`, on a native
190
+ * `<button>`. Activating it checks it and leaves the menu open, unless `closeOnClick`; the
191
+ * checked item stays checked when activated again. Your `onClick` runs first;
192
+ * `event.preventComponentHandler()` there stops the change.
193
+ */
194
+ declare function MenuRadioItem({ value, closeOnClick, disabled: disabledProp, textValue, onClick, className, children, render, ...rest }: MenuRadioItemProps): react.JSX.Element;
195
+
196
+ type MenuItemIndicatorOwnProps = {
197
+ /** Element to render instead of the default `<span>`. Props and ref are merged onto it. */
198
+ render?: ReactElement;
199
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
200
+ ref?: Ref<HTMLElement>;
201
+ };
202
+ type MenuItemIndicatorProps = MenuItemIndicatorOwnProps & Omit<ComponentPropsWithRef<'span'>, keyof MenuItemIndicatorOwnProps>;
203
+ /**
204
+ * Where the check goes, in a CheckboxItem or a RadioItem — one part for both, since it reads
205
+ * the same `checked` from either. Ships no glyph, as Checkbox.Indicator ships none: supply it as
206
+ * children, or draw it off the `data-*` attributes, which match the item's.
207
+ *
208
+ * Always rendered, in every state, so an unchecked item keeps its space and its label lines up
209
+ * with the checked ones. Hidden from assistive technology: the item announces `aria-checked`.
210
+ */
211
+ declare function MenuItemIndicator({ className, children, render, ...rest }: MenuItemIndicatorProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
212
+
213
+ type MenuGroupOwnProps = {
214
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
215
+ render?: ReactElement;
216
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
217
+ ref?: Ref<HTMLElement>;
218
+ };
219
+ type MenuGroupProps = MenuGroupOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof MenuGroupOwnProps>;
220
+ /**
221
+ * Items under a label: `role="group"`, named by its GroupLabel — the menu → group → item
222
+ * structure APG allows. The arrow keys and typeahead run through every group's items as one
223
+ * list, past the labels.
224
+ */
225
+ declare function MenuGroup({ className, children, render, ...rest }: MenuGroupProps): react.JSX.Element;
226
+
227
+ type MenuGroupLabelOwnProps = {
228
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
229
+ render?: ReactElement;
230
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
231
+ ref?: Ref<HTMLElement>;
232
+ };
233
+ type MenuGroupLabelProps = MenuGroupLabelOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof MenuGroupLabelOwnProps>;
234
+ /**
235
+ * Names its Group or RadioGroup. Shown in the menu, but not an item: it cannot be activated,
236
+ * and the arrow keys and typeahead pass it.
103
237
  */
104
- declare function MenuItem({ disabled, onClick, className, children, render, ...rest }: MenuItemProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
238
+ declare function MenuGroupLabel({ className, children, render, ...rest }: MenuGroupLabelProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
105
239
 
106
240
  declare namespace index_parts {
107
- export { MenuItem as Item, MenuPopup as Popup, MenuRoot as Root, MenuTrigger as Trigger };
241
+ export { MenuCheckboxItem as CheckboxItem, MenuGroup as Group, MenuGroupLabel as GroupLabel, MenuItem as Item, MenuItemIndicator as ItemIndicator, MenuPopup as Popup, MenuRadioGroup as RadioGroup, MenuRadioItem as RadioItem, MenuRoot as Root, MenuTrigger as Trigger };
108
242
  }
109
243
 
110
244
  /** The state Menu.Root shares with its parts, and projects as `data-*` attributes. */
@@ -112,4 +246,4 @@ type MenuState = {
112
246
  open: boolean;
113
247
  };
114
248
 
115
- export { index_parts as Menu, type MenuAlign, type MenuItemProps, type MenuPopupProps, type MenuRootProps, type MenuSide, type MenuState, type MenuTriggerProps };
249
+ export { index_parts as Menu, type MenuAlign, type MenuCheckboxItemProps, type MenuGroupLabelProps, type MenuGroupProps, type MenuItemIndicatorProps, type MenuItemProps, type MenuPopupProps, type MenuRadioGroupProps, type MenuRadioItemProps, type MenuRootProps, type MenuSide, type MenuState, type MenuTriggerProps };
@@ -68,8 +68,9 @@ type MenuPopupProps = MenuPopupOwnProps & Omit<ComponentPropsWithRef<'div'>, key
68
68
  * Popover's popup is (decision 12), and labelled by it.
69
69
  *
70
70
  * Opening moves focus to the first item — the last, when Up on the Trigger opened it. Up
71
- * and Down then move between items, wrapping, with Home and End; disabled items are skipped
72
- * unless the Root sets `focusableWhenDisabled`. Tab closes the menu and lets focus move on.
71
+ * and Down then move between items, wrapping, with Home and End; typing moves to the next item
72
+ * whose text starts with what was typed. Disabled items are skipped by both, unless the Root
73
+ * sets `focusableWhenDisabled`. Tab closes the menu and lets focus move on.
73
74
  *
74
75
  * Light dismiss and Esc close the element before anyone is asked; the `toggle` event
75
76
  * reports it through the Root, and the element reopens if the state stays open. Focus
@@ -83,6 +84,11 @@ type MenuItemOwnProps = {
83
84
  * `focusableWhenDisabled`.
84
85
  */
85
86
  disabled?: boolean;
87
+ /**
88
+ * The text typeahead matches. Defaults to the text in `children`; set it when that text is
89
+ * rendered by a component of your own, or differs from what should be typed.
90
+ */
91
+ textValue?: string;
86
92
  /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
87
93
  render?: ReactElement;
88
94
  /** Ref to the rendered element. Merged with any ref on the `render` element. */
@@ -94,17 +100,145 @@ type MenuItemProps = MenuItemOwnProps & Omit<ComponentPropsWithRef<'button'>, ke
94
100
  * handling here: put the action in `onClick`. Activating it closes the menu — call
95
101
  * `event.preventComponentHandler()` in your `onClick` to keep it open.
96
102
  *
97
- * `tabIndex={-1}`: the menu moves focus between items itself, and Tab leaves the menu.
103
+ * Disabled, your `onClick` is not attached, so an `aria-disabled` item under the Root's
104
+ * `focusableWhenDisabled` cannot run it either.
105
+ */
106
+ declare function MenuItem({ disabled, textValue, onClick, className, children, render, ...rest }: MenuItemProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
107
+
108
+ type MenuCheckboxItemOwnProps = {
109
+ /**
110
+ * Controlled state. Provide `onCheckedChange` alongside it.
111
+ *
112
+ * Never `undefined` once mounted: the mode is latched at mount, as for Checkbox.
113
+ */
114
+ checked?: boolean;
115
+ /** Initial state when uncontrolled. Read once, at mount. */
116
+ defaultChecked?: boolean;
117
+ /** Called with the new state, in both controlled and uncontrolled modes. */
118
+ onCheckedChange?: (checked: boolean) => void;
119
+ /** Closes the menu when activated. Off by default: a setting is often changed with others. */
120
+ closeOnClick?: boolean;
121
+ /**
122
+ * Cannot be activated. Skipped by the arrow keys, unless the Root sets
123
+ * `focusableWhenDisabled`.
124
+ */
125
+ disabled?: boolean;
126
+ /** The text typeahead matches. Defaults to the text in `children`. */
127
+ textValue?: string;
128
+ /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
129
+ render?: ReactElement;
130
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
131
+ ref?: Ref<HTMLElement>;
132
+ };
133
+ type MenuCheckboxItemProps = MenuCheckboxItemOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof MenuCheckboxItemOwnProps>;
134
+ /**
135
+ * A setting that is on or off: `role="menuitemcheckbox"` with `aria-checked`, on a native
136
+ * `<button>`. Activating it toggles the state and leaves the menu open, unless `closeOnClick`.
137
+ * Your `onClick` runs first; `event.preventComponentHandler()` there stops the toggle.
98
138
  *
99
- * Disabled, it is a native `disabled` button, out of focus and activation. With the Root's
100
- * `focusableWhenDisabled` it is `aria-disabled` instead, which keeps it focusable, so the
101
- * activation a native `disabled` would block is blocked here: your `onClick` is not
102
- * attached while it is disabled.
139
+ * Holds its own state through `useControlled`; an ItemIndicator inside reads it.
140
+ */
141
+ declare function MenuCheckboxItem({ checked: checkedProp, defaultChecked, onCheckedChange, closeOnClick, disabled, textValue, onClick, className, children, render, ...rest }: MenuCheckboxItemProps): react.JSX.Element;
142
+
143
+ type MenuRadioGroupOwnProps = {
144
+ /**
145
+ * Controlled value — the `value` of the checked RadioItem, or `null` for none. Provide
146
+ * `onValueChange` alongside it.
147
+ *
148
+ * Never `undefined` once mounted: the mode is latched at mount, as for RadioGroup.
149
+ */
150
+ value?: string | null;
151
+ /** Initial value when uncontrolled. Read once, at mount. */
152
+ defaultValue?: string | null;
153
+ /** Called with the value of the RadioItem being checked, in both modes. */
154
+ onValueChange?: (value: string) => void;
155
+ /** Disables every RadioItem inside. */
156
+ disabled?: boolean;
157
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
158
+ render?: ReactElement;
159
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
160
+ ref?: Ref<HTMLElement>;
161
+ };
162
+ type MenuRadioGroupProps = MenuRadioGroupOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof MenuRadioGroupOwnProps | 'defaultValue'>;
163
+ /**
164
+ * One choice among several: a `role="group"` that owns a value, and the RadioItems inside
165
+ * derive `checked` from it — as RadioGroup does for radios. Named by a GroupLabel inside, as a
166
+ * Group is.
167
+ */
168
+ declare function MenuRadioGroup({ value: valueProp, defaultValue, onValueChange, disabled, className, children, render, ...rest }: MenuRadioGroupProps): react.JSX.Element;
169
+
170
+ type MenuRadioItemOwnProps = {
171
+ /** This item's value in its RadioGroup. Checked while the group's `value` equals it. */
172
+ value: string;
173
+ /** Closes the menu when activated. Off by default, as for CheckboxItem. */
174
+ closeOnClick?: boolean;
175
+ /**
176
+ * Cannot be activated. Skipped by the arrow keys, unless the Root sets
177
+ * `focusableWhenDisabled`. The RadioGroup's `disabled` disables it too.
178
+ */
179
+ disabled?: boolean;
180
+ /** The text typeahead matches. Defaults to the text in `children`. */
181
+ textValue?: string;
182
+ /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
183
+ render?: ReactElement;
184
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
185
+ ref?: Ref<HTMLElement>;
186
+ };
187
+ type MenuRadioItemProps = MenuRadioItemOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof MenuRadioItemOwnProps>;
188
+ /**
189
+ * One choice in a RadioGroup: `role="menuitemradio"` with `aria-checked`, on a native
190
+ * `<button>`. Activating it checks it and leaves the menu open, unless `closeOnClick`; the
191
+ * checked item stays checked when activated again. Your `onClick` runs first;
192
+ * `event.preventComponentHandler()` there stops the change.
193
+ */
194
+ declare function MenuRadioItem({ value, closeOnClick, disabled: disabledProp, textValue, onClick, className, children, render, ...rest }: MenuRadioItemProps): react.JSX.Element;
195
+
196
+ type MenuItemIndicatorOwnProps = {
197
+ /** Element to render instead of the default `<span>`. Props and ref are merged onto it. */
198
+ render?: ReactElement;
199
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
200
+ ref?: Ref<HTMLElement>;
201
+ };
202
+ type MenuItemIndicatorProps = MenuItemIndicatorOwnProps & Omit<ComponentPropsWithRef<'span'>, keyof MenuItemIndicatorOwnProps>;
203
+ /**
204
+ * Where the check goes, in a CheckboxItem or a RadioItem — one part for both, since it reads
205
+ * the same `checked` from either. Ships no glyph, as Checkbox.Indicator ships none: supply it as
206
+ * children, or draw it off the `data-*` attributes, which match the item's.
207
+ *
208
+ * Always rendered, in every state, so an unchecked item keeps its space and its label lines up
209
+ * with the checked ones. Hidden from assistive technology: the item announces `aria-checked`.
210
+ */
211
+ declare function MenuItemIndicator({ className, children, render, ...rest }: MenuItemIndicatorProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
212
+
213
+ type MenuGroupOwnProps = {
214
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
215
+ render?: ReactElement;
216
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
217
+ ref?: Ref<HTMLElement>;
218
+ };
219
+ type MenuGroupProps = MenuGroupOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof MenuGroupOwnProps>;
220
+ /**
221
+ * Items under a label: `role="group"`, named by its GroupLabel — the menu → group → item
222
+ * structure APG allows. The arrow keys and typeahead run through every group's items as one
223
+ * list, past the labels.
224
+ */
225
+ declare function MenuGroup({ className, children, render, ...rest }: MenuGroupProps): react.JSX.Element;
226
+
227
+ type MenuGroupLabelOwnProps = {
228
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
229
+ render?: ReactElement;
230
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
231
+ ref?: Ref<HTMLElement>;
232
+ };
233
+ type MenuGroupLabelProps = MenuGroupLabelOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof MenuGroupLabelOwnProps>;
234
+ /**
235
+ * Names its Group or RadioGroup. Shown in the menu, but not an item: it cannot be activated,
236
+ * and the arrow keys and typeahead pass it.
103
237
  */
104
- declare function MenuItem({ disabled, onClick, className, children, render, ...rest }: MenuItemProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
238
+ declare function MenuGroupLabel({ className, children, render, ...rest }: MenuGroupLabelProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
105
239
 
106
240
  declare namespace index_parts {
107
- export { MenuItem as Item, MenuPopup as Popup, MenuRoot as Root, MenuTrigger as Trigger };
241
+ export { MenuCheckboxItem as CheckboxItem, MenuGroup as Group, MenuGroupLabel as GroupLabel, MenuItem as Item, MenuItemIndicator as ItemIndicator, MenuPopup as Popup, MenuRadioGroup as RadioGroup, MenuRadioItem as RadioItem, MenuRoot as Root, MenuTrigger as Trigger };
108
242
  }
109
243
 
110
244
  /** The state Menu.Root shares with its parts, and projects as `data-*` attributes. */
@@ -112,4 +246,4 @@ type MenuState = {
112
246
  open: boolean;
113
247
  };
114
248
 
115
- export { index_parts as Menu, type MenuAlign, type MenuItemProps, type MenuPopupProps, type MenuRootProps, type MenuSide, type MenuState, type MenuTriggerProps };
249
+ export { index_parts as Menu, type MenuAlign, type MenuCheckboxItemProps, type MenuGroupLabelProps, type MenuGroupProps, type MenuItemIndicatorProps, type MenuItemProps, type MenuPopupProps, type MenuRadioGroupProps, type MenuRadioItemProps, type MenuRootProps, type MenuSide, type MenuState, type MenuTriggerProps };
@@ -1,3 +1,6 @@
1
+ import {
2
+ fold
3
+ } from "../chunk-ETNICT5M.js";
1
4
  import {
2
5
  anchorNameFor,
3
6
  anchoredDataAttributes,
@@ -18,8 +21,14 @@ import {
18
21
  // src/menu/index.parts.ts
19
22
  var index_parts_exports = {};
20
23
  __export(index_parts_exports, {
24
+ CheckboxItem: () => MenuCheckboxItem,
25
+ Group: () => MenuGroup,
26
+ GroupLabel: () => MenuGroupLabel,
21
27
  Item: () => MenuItem,
28
+ ItemIndicator: () => MenuItemIndicator,
22
29
  Popup: () => MenuPopup,
30
+ RadioGroup: () => MenuRadioGroup,
31
+ RadioItem: () => MenuRadioItem,
23
32
  Root: () => MenuRoot,
24
33
  Trigger: () => MenuTrigger
25
34
  });
@@ -98,9 +107,6 @@ function menuDataAttributes({ open }) {
98
107
  "data-closed": open ? void 0 : ""
99
108
  };
100
109
  }
101
- function menuItemDataAttributes(disabled) {
102
- return { "data-disabled": disabled ? "" : void 0 };
103
- }
104
110
 
105
111
  // src/menu/MenuTrigger.tsx
106
112
  function MenuTrigger({ className, children, render, ...rest }) {
@@ -135,7 +141,51 @@ function MenuTrigger({ className, children, render, ...rest }) {
135
141
  }
136
142
 
137
143
  // src/menu/MenuPopup.tsx
138
- import { useEffect, useReducer, useRef as useRef2 } from "react";
144
+ import { useEffect as useEffect2, useReducer, useRef as useRef3 } from "react";
145
+
146
+ // src/core/typeahead.ts
147
+ import { Children, isValidElement, useCallback as useCallback2, useEffect, useRef as useRef2 } from "react";
148
+ var TYPEAHEAD_TIMEOUT = 500;
149
+ function textOf(node) {
150
+ let text = "";
151
+ Children.forEach(node, (child) => {
152
+ if (typeof child === "string" || typeof child === "number") text += child;
153
+ else if (isValidElement(child)) text += textOf(child.props.children);
154
+ });
155
+ return text;
156
+ }
157
+ function typeaheadIndex(labels, current, search) {
158
+ const count = labels.length;
159
+ if (count === 0 || search === "") return null;
160
+ const [first = "", ...others] = [...search];
161
+ const repeated = others.every((char) => char === first);
162
+ const needle = fold(repeated ? first : search);
163
+ const start = current < 0 ? 0 : repeated ? current + 1 : current;
164
+ for (let step = 0; step < count; step += 1) {
165
+ const index = (start + step) % count;
166
+ if (fold(labels[index] ?? "").startsWith(needle)) return index;
167
+ }
168
+ return null;
169
+ }
170
+ function useTypeahead() {
171
+ const searchRef = useRef2("");
172
+ const timerRef = useRef2(void 0);
173
+ useEffect(() => () => clearTimeout(timerRef.current), []);
174
+ return useCallback2((event) => {
175
+ const { key } = event;
176
+ if (event.ctrlKey || event.metaKey || event.altKey) return null;
177
+ if ([...key].length !== 1) return null;
178
+ if (key === " " && searchRef.current === "") return null;
179
+ searchRef.current += key;
180
+ clearTimeout(timerRef.current);
181
+ timerRef.current = setTimeout(() => {
182
+ searchRef.current = "";
183
+ }, TYPEAHEAD_TIMEOUT);
184
+ return searchRef.current;
185
+ }, []);
186
+ }
187
+
188
+ // src/menu/MenuPopup.tsx
139
189
  function MenuPopup({
140
190
  side = "bottom",
141
191
  align = "start",
@@ -154,10 +204,11 @@ function MenuPopup({
154
204
  anchorName,
155
205
  triggerRef
156
206
  } = useMenuRootContext("Popup");
157
- const elementRef = useRef2(null);
158
- const focusPendingRef = useRef2(false);
207
+ const elementRef = useRef3(null);
208
+ const focusPendingRef = useRef3(false);
159
209
  const [, resync] = useReducer((n) => n + 1, 0);
160
- useEffect(() => {
210
+ const typeahead = useTypeahead();
211
+ useEffect2(() => {
161
212
  const popup = elementRef.current;
162
213
  if (!popup || typeof popup.showPopover !== "function") return;
163
214
  const shown = popup.matches(":popover-open");
@@ -196,12 +247,18 @@ function MenuPopup({
196
247
  }
197
248
  const current = navigableItems.findIndex((i) => i.ref.current === event.target);
198
249
  if (current < 0 && event.target !== event.currentTarget) return;
199
- const next = rovingIndex(event.key, {
250
+ const search = typeahead(event);
251
+ const next = search === null ? rovingIndex(event.key, {
200
252
  count: navigableItems.length,
201
253
  current,
202
254
  orientation: "vertical",
203
255
  rtl: false
204
- });
256
+ }) : typeaheadIndex(
257
+ navigableItems.map((i) => i.textValue),
258
+ current,
259
+ search
260
+ );
261
+ if (search !== null) event.preventDefault();
205
262
  if (next === null) return;
206
263
  event.preventDefault();
207
264
  navigableItems[next]?.ref.current?.focus();
@@ -217,32 +274,62 @@ function MenuPopup({
217
274
  });
218
275
  }
219
276
 
277
+ // src/menu/useMenuItem.ts
278
+ import { createContext as createContext2, useContext as useContext2, useLayoutEffect, useRef as useRef4 } from "react";
279
+ function useMenuItem(part, { disabled, textValue, children }) {
280
+ const { setOpen, focusableWhenDisabled, register } = useMenuRootContext(part);
281
+ const ref = useRef4(null);
282
+ const text = textValue ?? textOf(children);
283
+ useLayoutEffect(() => register({ disabled, textValue: text, ref }), [register, disabled, text]);
284
+ return {
285
+ setOpen,
286
+ itemProps: {
287
+ type: "button",
288
+ tabIndex: -1,
289
+ disabled: disabled && !focusableWhenDisabled || void 0,
290
+ "aria-disabled": disabled && focusableWhenDisabled || void 0,
291
+ "data-disabled": disabled ? "" : void 0,
292
+ ref
293
+ }
294
+ };
295
+ }
296
+ var MenuItemIndicatorContext = createContext2(null);
297
+ function useMenuItemIndicatorContext() {
298
+ const context = useContext2(MenuItemIndicatorContext);
299
+ if (context === null) {
300
+ throw new Error(
301
+ "<Menu.ItemIndicator> must be rendered inside <Menu.CheckboxItem> or <Menu.RadioItem>."
302
+ );
303
+ }
304
+ return context;
305
+ }
306
+ function menuCheckableDataAttributes({ checked, disabled }) {
307
+ return {
308
+ "data-checked": checked ? "" : void 0,
309
+ "data-unchecked": checked ? void 0 : "",
310
+ "data-disabled": disabled ? "" : void 0
311
+ };
312
+ }
313
+
220
314
  // src/menu/MenuItem.tsx
221
- import { useLayoutEffect, useRef as useRef3 } from "react";
222
315
  function MenuItem({
223
316
  disabled = false,
317
+ textValue,
224
318
  onClick,
225
319
  className,
226
320
  children,
227
321
  render,
228
322
  ...rest
229
323
  }) {
230
- const { setOpen, focusableWhenDisabled, register } = useMenuRootContext("Item");
231
- const elementRef = useRef3(null);
232
- useLayoutEffect(() => register({ disabled, ref: elementRef }), [register, disabled]);
324
+ const { setOpen, itemProps } = useMenuItem("Item", { disabled, textValue, children });
233
325
  return useRender({
234
326
  render,
235
327
  defaultTagName: "button",
236
328
  props: {
237
- type: "button",
329
+ ...itemProps,
238
330
  role: "menuitem",
239
- tabIndex: -1,
240
- disabled: disabled && !focusableWhenDisabled || void 0,
241
- "aria-disabled": disabled && focusableWhenDisabled || void 0,
242
- ...menuItemDataAttributes(disabled),
243
331
  className,
244
332
  children,
245
- ref: elementRef,
246
333
  onClick() {
247
334
  if (!disabled) setOpen(false);
248
335
  }
@@ -250,6 +337,207 @@ function MenuItem({
250
337
  consumerProps: disabled ? rest : { ...rest, onClick }
251
338
  });
252
339
  }
340
+
341
+ // src/menu/MenuCheckboxItem.tsx
342
+ import { useMemo as useMemo2 } from "react";
343
+ import { jsx as jsx2 } from "react/jsx-runtime";
344
+ function MenuCheckboxItem({
345
+ checked: checkedProp,
346
+ defaultChecked,
347
+ onCheckedChange,
348
+ closeOnClick = false,
349
+ disabled = false,
350
+ textValue,
351
+ onClick,
352
+ className,
353
+ children,
354
+ render,
355
+ ...rest
356
+ }) {
357
+ const { setOpen, itemProps } = useMenuItem("CheckboxItem", { disabled, textValue, children });
358
+ const [checked, setChecked] = useControlled({
359
+ controlled: checkedProp,
360
+ default: defaultChecked ?? false,
361
+ name: "Menu.CheckboxItem",
362
+ state: "checked"
363
+ });
364
+ const state = useMemo2(() => ({ checked, disabled }), [checked, disabled]);
365
+ const element = useRender({
366
+ render,
367
+ defaultTagName: "button",
368
+ props: {
369
+ ...itemProps,
370
+ role: "menuitemcheckbox",
371
+ "aria-checked": checked,
372
+ ...menuCheckableDataAttributes(state),
373
+ className,
374
+ children,
375
+ onClick() {
376
+ if (disabled) return;
377
+ setChecked(!checked);
378
+ onCheckedChange?.(!checked);
379
+ if (closeOnClick) setOpen(false);
380
+ }
381
+ },
382
+ consumerProps: disabled ? rest : { ...rest, onClick }
383
+ });
384
+ return /* @__PURE__ */ jsx2(MenuItemIndicatorContext.Provider, { value: state, children: element });
385
+ }
386
+
387
+ // src/menu/MenuRadioGroup.tsx
388
+ import { createContext as createContext4, useCallback as useCallback4, useContext as useContext4, useMemo as useMemo4 } from "react";
389
+
390
+ // src/menu/MenuGroup.tsx
391
+ import { createContext as createContext3, useCallback as useCallback3, useContext as useContext3, useId as useId2, useMemo as useMemo3, useState as useState2 } from "react";
392
+ import { jsx as jsx3 } from "react/jsx-runtime";
393
+ var MenuGroupContext = createContext3(null);
394
+ function useMenuGroupContext() {
395
+ return useContext3(MenuGroupContext);
396
+ }
397
+ function useMenuGroupElement(part, {
398
+ render,
399
+ props,
400
+ consumerProps
401
+ }) {
402
+ useMenuRootContext(part);
403
+ const labelId = `${useId2()}-label`;
404
+ const [hasLabel, setHasLabel] = useState2(false);
405
+ const registerLabel = useCallback3(() => {
406
+ setHasLabel(true);
407
+ return () => setHasLabel(false);
408
+ }, []);
409
+ const context = useMemo3(() => ({ labelId, registerLabel }), [labelId, registerLabel]);
410
+ const element = useRender({
411
+ render,
412
+ defaultTagName: "div",
413
+ props: { role: "group", "aria-labelledby": hasLabel ? labelId : void 0, ...props },
414
+ consumerProps
415
+ });
416
+ return /* @__PURE__ */ jsx3(MenuGroupContext.Provider, { value: context, children: element });
417
+ }
418
+ function MenuGroup({ className, children, render, ...rest }) {
419
+ return useMenuGroupElement("Group", {
420
+ render,
421
+ props: { className, children },
422
+ consumerProps: rest
423
+ });
424
+ }
425
+
426
+ // src/menu/MenuRadioGroup.tsx
427
+ import { jsx as jsx4 } from "react/jsx-runtime";
428
+ var MenuRadioGroupContext = createContext4(null);
429
+ function useMenuRadioGroupContext() {
430
+ const context = useContext4(MenuRadioGroupContext);
431
+ if (context === null) {
432
+ throw new Error("<Menu.RadioItem> must be rendered inside <Menu.RadioGroup>.");
433
+ }
434
+ return context;
435
+ }
436
+ function MenuRadioGroup({
437
+ value: valueProp,
438
+ defaultValue,
439
+ onValueChange,
440
+ disabled = false,
441
+ className,
442
+ children,
443
+ render,
444
+ ...rest
445
+ }) {
446
+ const [value, setValue] = useControlled({
447
+ controlled: valueProp,
448
+ default: defaultValue ?? null,
449
+ name: "Menu.RadioGroup",
450
+ state: "value"
451
+ });
452
+ const select = useCallback4(
453
+ (next) => {
454
+ if (next === value) return;
455
+ setValue(next);
456
+ onValueChange?.(next);
457
+ },
458
+ [value, setValue, onValueChange]
459
+ );
460
+ const context = useMemo4(() => ({ value, disabled, select }), [value, disabled, select]);
461
+ const element = useMenuGroupElement("RadioGroup", {
462
+ render,
463
+ props: { "data-disabled": disabled ? "" : void 0, className, children },
464
+ consumerProps: rest
465
+ });
466
+ return /* @__PURE__ */ jsx4(MenuRadioGroupContext.Provider, { value: context, children: element });
467
+ }
468
+
469
+ // src/menu/MenuRadioItem.tsx
470
+ import { useMemo as useMemo5 } from "react";
471
+ import { jsx as jsx5 } from "react/jsx-runtime";
472
+ function MenuRadioItem({
473
+ value,
474
+ closeOnClick = false,
475
+ disabled: disabledProp = false,
476
+ textValue,
477
+ onClick,
478
+ className,
479
+ children,
480
+ render,
481
+ ...rest
482
+ }) {
483
+ const group = useMenuRadioGroupContext();
484
+ const disabled = disabledProp || group.disabled;
485
+ const { setOpen, itemProps } = useMenuItem("RadioItem", { disabled, textValue, children });
486
+ const checked = group.value === value;
487
+ const state = useMemo5(() => ({ checked, disabled }), [checked, disabled]);
488
+ const element = useRender({
489
+ render,
490
+ defaultTagName: "button",
491
+ props: {
492
+ ...itemProps,
493
+ role: "menuitemradio",
494
+ "aria-checked": checked,
495
+ ...menuCheckableDataAttributes(state),
496
+ className,
497
+ children,
498
+ onClick() {
499
+ if (disabled) return;
500
+ group.select(value);
501
+ if (closeOnClick) setOpen(false);
502
+ }
503
+ },
504
+ consumerProps: disabled ? rest : { ...rest, onClick }
505
+ });
506
+ return /* @__PURE__ */ jsx5(MenuItemIndicatorContext.Provider, { value: state, children: element });
507
+ }
508
+
509
+ // src/menu/MenuItemIndicator.tsx
510
+ function MenuItemIndicator({
511
+ className,
512
+ children,
513
+ render,
514
+ ...rest
515
+ }) {
516
+ const state = useMenuItemIndicatorContext();
517
+ return useRender({
518
+ render,
519
+ defaultTagName: "span",
520
+ props: { "aria-hidden": true, ...menuCheckableDataAttributes(state), className, children },
521
+ consumerProps: rest
522
+ });
523
+ }
524
+
525
+ // src/menu/MenuGroupLabel.tsx
526
+ import { useLayoutEffect as useLayoutEffect2 } from "react";
527
+ function MenuGroupLabel({ className, children, render, ...rest }) {
528
+ const group = useMenuGroupContext();
529
+ if (group === null) {
530
+ throw new Error("<Menu.GroupLabel> must be rendered inside <Menu.Group> or <Menu.RadioGroup>.");
531
+ }
532
+ const { labelId, registerLabel } = group;
533
+ useLayoutEffect2(registerLabel, [registerLabel]);
534
+ return useRender({
535
+ render,
536
+ defaultTagName: "div",
537
+ props: { id: labelId, className, children },
538
+ consumerProps: rest
539
+ });
540
+ }
253
541
  export {
254
542
  index_parts_exports as Menu
255
543
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "4.14.0",
3
+ "version": "4.15.0",
4
4
  "description": "Unstyled React behaviour primitives — render engine, controlled/uncontrolled state, and data-* state projection. Ships no CSS and no class names.",
5
5
  "license": "MIT",
6
6
  "type": "module",