@spectral_labs/ui 1.5.1 → 1.5.3

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 (62) hide show
  1. package/dist/components/context-menu/components/context-menu-content.svelte +8 -7
  2. package/dist/components/context-menu/components/context-menu-content.svelte.d.ts.map +1 -1
  3. package/dist/components/context-menu/components/context-menu-root.svelte +9 -7
  4. package/dist/components/context-menu/components/context-menu-root.svelte.d.ts.map +1 -1
  5. package/dist/components/context-menu/index.d.ts +3 -0
  6. package/dist/components/context-menu/index.d.ts.map +1 -1
  7. package/dist/components/context-menu/index.js +2 -0
  8. package/dist/components/dropdown-menu/__tests__/DropdownMenuRadioTest.svelte +90 -0
  9. package/dist/components/dropdown-menu/__tests__/DropdownMenuTest.svelte +22 -1
  10. package/dist/components/dropdown-menu/__tests__/dropdown-menu-radio-group.test.svelte.js +306 -0
  11. package/dist/components/dropdown-menu/__tests__/dropdown-menu.test.svelte.js +438 -1
  12. package/dist/components/dropdown-menu/components/dropdown-menu-checkbox-item.svelte +10 -0
  13. package/dist/components/dropdown-menu/components/dropdown-menu-checkbox-item.svelte.d.ts.map +1 -1
  14. package/dist/components/dropdown-menu/components/dropdown-menu-content.svelte +8 -7
  15. package/dist/components/dropdown-menu/components/dropdown-menu-content.svelte.d.ts.map +1 -1
  16. package/dist/components/dropdown-menu/components/dropdown-menu-item.svelte +8 -0
  17. package/dist/components/dropdown-menu/components/dropdown-menu-item.svelte.d.ts.map +1 -1
  18. package/dist/components/dropdown-menu/components/dropdown-menu-radio-group.svelte +45 -0
  19. package/dist/components/dropdown-menu/components/dropdown-menu-radio-group.svelte.d.ts +5 -0
  20. package/dist/components/dropdown-menu/components/dropdown-menu-radio-group.svelte.d.ts.map +1 -0
  21. package/dist/components/dropdown-menu/components/dropdown-menu-radio-item.svelte +143 -0
  22. package/dist/components/dropdown-menu/components/dropdown-menu-radio-item.svelte.d.ts +5 -0
  23. package/dist/components/dropdown-menu/components/dropdown-menu-radio-item.svelte.d.ts.map +1 -0
  24. package/dist/components/dropdown-menu/components/dropdown-menu-root.svelte +9 -7
  25. package/dist/components/dropdown-menu/components/dropdown-menu-root.svelte.d.ts.map +1 -1
  26. package/dist/components/dropdown-menu/components/dropdown-menu-sub-content.svelte +94 -4
  27. package/dist/components/dropdown-menu/components/dropdown-menu-sub-content.svelte.d.ts.map +1 -1
  28. package/dist/components/dropdown-menu/components/dropdown-menu-sub-trigger.svelte +32 -7
  29. package/dist/components/dropdown-menu/components/dropdown-menu-sub-trigger.svelte.d.ts.map +1 -1
  30. package/dist/components/dropdown-menu/components/dropdown-menu-sub.svelte +180 -6
  31. package/dist/components/dropdown-menu/components/dropdown-menu-sub.svelte.d.ts.map +1 -1
  32. package/dist/components/dropdown-menu/dropdown-menu.svelte.d.ts +48 -2
  33. package/dist/components/dropdown-menu/dropdown-menu.svelte.d.ts.map +1 -1
  34. package/dist/components/dropdown-menu/dropdown-menu.svelte.js +16 -0
  35. package/dist/components/dropdown-menu/index.d.ts +3 -1
  36. package/dist/components/dropdown-menu/index.d.ts.map +1 -1
  37. package/dist/components/dropdown-menu/index.js +2 -0
  38. package/dist/components/dropdown-menu/types.d.ts +37 -1
  39. package/dist/components/dropdown-menu/types.d.ts.map +1 -1
  40. package/dist/components/menubar/__tests__/menubar.test.svelte.js +95 -0
  41. package/dist/components/menubar/components/menubar-content.svelte +8 -7
  42. package/dist/components/menubar/components/menubar-content.svelte.d.ts.map +1 -1
  43. package/dist/components/menubar/components/menubar-menu.svelte +8 -6
  44. package/dist/components/menubar/components/menubar-menu.svelte.d.ts.map +1 -1
  45. package/dist/components/menubar/index.d.ts +3 -1
  46. package/dist/components/menubar/index.d.ts.map +1 -1
  47. package/dist/components/menubar/index.js +2 -0
  48. package/dist/internal/escape-layer/__tests__/escape-layer.test.js +29 -0
  49. package/dist/internal/escape-layer/escape-layer.svelte.d.ts +5 -1
  50. package/dist/internal/escape-layer/escape-layer.svelte.d.ts.map +1 -1
  51. package/dist/internal/escape-layer/escape-layer.svelte.js +20 -13
  52. package/dist/internal/menu-context/index.d.ts +2 -2
  53. package/dist/internal/menu-context/index.d.ts.map +1 -1
  54. package/dist/internal/menu-context/index.js +1 -1
  55. package/dist/internal/menu-context/menu-context.svelte.d.ts +64 -2
  56. package/dist/internal/menu-context/menu-context.svelte.d.ts.map +1 -1
  57. package/dist/internal/menu-context/menu-context.svelte.js +59 -0
  58. package/dist/internal/roving-focus/roving-focus.svelte.d.ts.map +1 -1
  59. package/dist/internal/roving-focus/roving-focus.svelte.js +2 -1
  60. package/dist/internal/roving-focus/types.d.ts +6 -0
  61. package/dist/internal/roving-focus/types.d.ts.map +1 -1
  62. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"dropdown-menu.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/dropdown-menu/dropdown-menu.svelte.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,YAAY,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;CAC/C;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAErE;AAED,wBAAgB,sBAAsB,IAAI,iBAAiB,CAE1D;AAED,wBAAgB,qBAAqB;;;EAKpC;AAID,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,YAAY,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAA;CACxC;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,CAE7E;AAED,wBAAgB,yBAAyB,IAAI,sBAAsB,CAElE"}
1
+ {"version":3,"file":"dropdown-menu.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/dropdown-menu/dropdown-menu.svelte.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,YAAY,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;CAC/C;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAErE;AAED,wBAAgB,sBAAsB,IAAI,iBAAiB,CAE1D;AAED,wBAAgB,qBAAqB;;;EAKpC;AAID,MAAM,WAAW,sBAAsB;IACrC,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IACtC,QAAQ,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IACtC;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAA;IACpC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IACvD,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IACxD,2DAA2D;IAC3D,0BAA0B,EAAE,MAAM,IAAI,CAAA;IACtC,YAAY,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAA;IACvC,YAAY,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;IAC9C;;;OAGG;IACH,cAAc,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IAC7C;;;;OAIG;IACH,cAAc,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;CAC9C;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,CAE7E;AAED,wBAAgB,yBAAyB,IAAI,sBAAsB,CAElE;AAID;;;;;GAKG;AACH,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;IAClC,8EAA8E;IAC9E,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,6BAA6B,GAAG,IAAI,CAE3F;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,IAAI,6BAA6B,CAQhF"}
@@ -20,3 +20,19 @@ export function setDropdownMenuSubContext(state) {
20
20
  export function getDropdownMenuSubContext() {
21
21
  return getContext(SUB_KEY);
22
22
  }
23
+ const RADIO_GROUP_KEY = Symbol('sp-dropdown-radio-group');
24
+ export function setDropdownMenuRadioGroupContext(state) {
25
+ setContext(RADIO_GROUP_KEY, state);
26
+ }
27
+ /**
28
+ * Throws a NAMED error instead of returning `undefined`: an orphan
29
+ * `RadioItem` would otherwise fail later with an opaque property-access
30
+ * TypeError, at a point that no longer names the mistake.
31
+ */
32
+ export function getDropdownMenuRadioGroupContext() {
33
+ const ctx = getContext(RADIO_GROUP_KEY);
34
+ if (!ctx) {
35
+ throw new Error('[spectral-ui] DropdownMenu.RadioItem must be rendered inside a DropdownMenu.RadioGroup.');
36
+ }
37
+ return ctx;
38
+ }
@@ -2,11 +2,13 @@ export { default as CheckboxItem } from './components/dropdown-menu-checkbox-ite
2
2
  export { default as Content } from './components/dropdown-menu-content.svelte';
3
3
  export { default as Item } from './components/dropdown-menu-item.svelte';
4
4
  export { default as Label } from './components/dropdown-menu-label.svelte';
5
+ export { default as RadioGroup } from './components/dropdown-menu-radio-group.svelte';
6
+ export { default as RadioItem } from './components/dropdown-menu-radio-item.svelte';
5
7
  export { default as Root } from './components/dropdown-menu-root.svelte';
6
8
  export { default as Separator } from './components/dropdown-menu-separator.svelte';
7
9
  export { default as SubContent } from './components/dropdown-menu-sub-content.svelte';
8
10
  export { default as SubTrigger } from './components/dropdown-menu-sub-trigger.svelte';
9
11
  export { default as Sub } from './components/dropdown-menu-sub.svelte';
10
12
  export { default as Trigger } from './components/dropdown-menu-trigger.svelte';
11
- export type { DropdownMenuCheckboxItemProps, DropdownMenuContentProps, DropdownMenuItemProps, DropdownMenuLabelProps, DropdownMenuProps, DropdownMenuSeparatorProps, DropdownMenuSubContentProps, DropdownMenuSubProps, DropdownMenuSubTriggerProps, DropdownMenuTriggerProps, } from './types.js';
13
+ export type { DropdownMenuCheckboxItemProps, DropdownMenuContentProps, DropdownMenuItemProps, DropdownMenuLabelProps, DropdownMenuProps, DropdownMenuRadioGroupProps, DropdownMenuRadioItemProps, DropdownMenuSeparatorProps, DropdownMenuSubContentProps, DropdownMenuSubProps, DropdownMenuSubTriggerProps, DropdownMenuTriggerProps, } from './types.js';
12
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/dropdown-menu/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,iDAAiD,CAAA;AACzF,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,2CAA2C,CAAA;AAC9E,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,wCAAwC,CAAA;AACxE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,yCAAyC,CAAA;AAC1E,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,wCAAwC,CAAA;AACxE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,6CAA6C,CAAA;AAClF,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,+CAA+C,CAAA;AACrF,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,+CAA+C,CAAA;AACrF,OAAO,EAAE,OAAO,IAAI,GAAG,EAAE,MAAM,uCAAuC,CAAA;AACtE,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,2CAA2C,CAAA;AAC9E,YAAY,EACV,6BAA6B,EAC7B,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EACtB,iBAAiB,EACjB,0BAA0B,EAC1B,2BAA2B,EAC3B,oBAAoB,EACpB,2BAA2B,EAC3B,wBAAwB,GACzB,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/dropdown-menu/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,iDAAiD,CAAA;AACzF,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,2CAA2C,CAAA;AAC9E,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,wCAAwC,CAAA;AACxE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,yCAAyC,CAAA;AAC1E,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,+CAA+C,CAAA;AACrF,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,8CAA8C,CAAA;AACnF,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,wCAAwC,CAAA;AACxE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,6CAA6C,CAAA;AAClF,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,+CAA+C,CAAA;AACrF,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,+CAA+C,CAAA;AACrF,OAAO,EAAE,OAAO,IAAI,GAAG,EAAE,MAAM,uCAAuC,CAAA;AACtE,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,2CAA2C,CAAA;AAC9E,YAAY,EACV,6BAA6B,EAC7B,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,EAC1B,2BAA2B,EAC3B,oBAAoB,EACpB,2BAA2B,EAC3B,wBAAwB,GACzB,MAAM,YAAY,CAAA"}
@@ -2,6 +2,8 @@ export { default as CheckboxItem } from './components/dropdown-menu-checkbox-ite
2
2
  export { default as Content } from './components/dropdown-menu-content.svelte';
3
3
  export { default as Item } from './components/dropdown-menu-item.svelte';
4
4
  export { default as Label } from './components/dropdown-menu-label.svelte';
5
+ export { default as RadioGroup } from './components/dropdown-menu-radio-group.svelte';
6
+ export { default as RadioItem } from './components/dropdown-menu-radio-item.svelte';
5
7
  export { default as Root } from './components/dropdown-menu-root.svelte';
6
8
  export { default as Separator } from './components/dropdown-menu-separator.svelte';
7
9
  export { default as SubContent } from './components/dropdown-menu-sub-content.svelte';
@@ -1,5 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { HTMLButtonAttributes } from 'svelte/elements';
2
+ import type { HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';
3
3
  import type { Align, Side } from '../../internal/floating-layer/types.js';
4
4
  export interface DropdownMenuProps {
5
5
  open?: boolean;
@@ -55,6 +55,40 @@ export interface DropdownMenuCheckboxItemProps {
55
55
  class?: string;
56
56
  children?: Snippet;
57
57
  }
58
+ /**
59
+ * Extends the native div attributes so the real `role="group"` node can be
60
+ * named: several radio groups in one menu each need their own `aria-label` /
61
+ * `aria-labelledby`, and consumers need `id` / `data-testid` on the group
62
+ * itself.
63
+ */
64
+ export interface DropdownMenuRadioGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children'> {
65
+ /** The selected value. Bindable. */
66
+ value?: string;
67
+ /** Called whenever the selection changes. Never fired for a re-selection. */
68
+ onValueChange?: (value: string) => void;
69
+ class?: string;
70
+ children?: Snippet;
71
+ }
72
+ /**
73
+ * Extends the native div attributes: unlike the older menu rows, a consumer's
74
+ * `data-testid`, `aria-*` and event handlers reach the DOM node
75
+ * (spectral-ui#201 is not reproduced here). Consumer handlers COMPOSE with the
76
+ * built-in ones — they run first, and calling `preventDefault()` cancels the
77
+ * built-in behaviour.
78
+ */
79
+ export interface DropdownMenuRadioItemProps extends Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children'> {
80
+ /** Value identifying this row within its `RadioGroup`. */
81
+ value: string;
82
+ disabled?: boolean;
83
+ /** Override textContent for typeahead matching. */
84
+ textValue?: string;
85
+ /** Close the menu after selection (default true). */
86
+ closeOnSelect?: boolean;
87
+ /** Callback on selection, after the group's value has moved. */
88
+ onSelect?: (event: Event) => void;
89
+ class?: string;
90
+ children?: Snippet;
91
+ }
58
92
  export interface DropdownMenuSubProps {
59
93
  /** Whether the submenu is open. */
60
94
  open?: boolean;
@@ -63,6 +97,8 @@ export interface DropdownMenuSubProps {
63
97
  }
64
98
  export interface DropdownMenuSubTriggerProps {
65
99
  disabled?: boolean;
100
+ /** Override textContent for typeahead matching (parity with `DropdownMenuItemProps`). */
101
+ textValue?: string;
66
102
  class?: string;
67
103
  children?: Snippet;
68
104
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/components/dropdown-menu/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,wCAAwC,CAAA;AAEzE,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uFAAuF;IACvF,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,wBAAyB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,OAAO,CAAC;IACnF,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,WAAW,GAAG,MAAM,CAAA;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iDAAiD;IACjD,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACjC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,mCAAmC;IACnC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/components/dropdown-menu/types.ts"],"names":[],"mappings":"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"}
@@ -229,5 +229,100 @@ describe('menubar', () => {
229
229
  expect(triggers[0].getAttribute('data-state')).toBe('open');
230
230
  expect(triggers[1].getAttribute('data-state')).toBe('closed');
231
231
  });
232
+ it('arrowRight on the sub-trigger moves focus to the first sub item', () => {
233
+ setup({ withSub: true });
234
+ const triggers = target.querySelectorAll('[data-menubar-trigger]');
235
+ triggers[0].click();
236
+ flushSync();
237
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
238
+ subTrigger.focus();
239
+ pressKey('ArrowRight');
240
+ expect(document.activeElement?.textContent?.trim()).toBe('Email');
241
+ });
242
+ it('arrowLeft inside the sub closes it, restores focus to the sub-trigger and keeps the menu', () => {
243
+ setup({ withSub: true });
244
+ const triggers = target.querySelectorAll('[data-menubar-trigger]');
245
+ triggers[0].click();
246
+ flushSync();
247
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
248
+ subTrigger.focus();
249
+ pressKey('ArrowRight');
250
+ expect(document.activeElement?.textContent?.trim()).toBe('Email');
251
+ pressKey('ArrowLeft');
252
+ // Sub closed, focus back on the trigger row — no dead end on body.
253
+ expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
254
+ expect(document.activeElement).toBe(subTrigger);
255
+ // The menubar did NOT switch to the previous menu.
256
+ expect(triggers[0].getAttribute('data-state')).toBe('open');
257
+ expect(triggers[1].getAttribute('data-state')).toBe('closed');
258
+ expect(triggers[2].getAttribute('data-state')).toBe('closed');
259
+ });
260
+ it('hovering a sibling item closes the open sub', () => {
261
+ setup({ withSub: true });
262
+ const triggers = target.querySelectorAll('[data-menubar-trigger]');
263
+ triggers[0].click();
264
+ flushSync();
265
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
266
+ subTrigger.click();
267
+ flushSync();
268
+ expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
269
+ const newItem = Array.from(target.querySelectorAll('.sp-menubar-content .sp-dropdown-menu-item')).find((el) => el.textContent?.trim() === 'New');
270
+ newItem.dispatchEvent(new MouseEvent('pointerenter', { clientX: 0, clientY: 0 }));
271
+ flushSync();
272
+ expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
273
+ // The menubar menu itself stayed open.
274
+ expect(triggers[0].getAttribute('data-state')).toBe('open');
275
+ });
276
+ it('escape with focus in the menu and a hover-opened sub closes the whole menu in ONE keypress and refocuses the menubar trigger', () => {
277
+ setup({ withSub: true });
278
+ const triggers = target.querySelectorAll('[data-menubar-trigger]');
279
+ triggers[0].click();
280
+ flushSync();
281
+ // Autofocus lands on the first parent row.
282
+ expect(document.activeElement?.textContent?.trim()).toBe('New');
283
+ // Pointer path: the sub opens without moving focus.
284
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
285
+ subTrigger.click();
286
+ flushSync();
287
+ expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeTruthy();
288
+ expect(document.activeElement?.textContent?.trim()).toBe('New');
289
+ // ONE Escape: the sub layer defers (focus is NOT inside the sub) and the
290
+ // menubar layer closes the whole menu — focus returns to the MENUBAR
291
+ // trigger, never warped onto the sub trigger.
292
+ pressKey('Escape');
293
+ expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
294
+ expect(target.querySelector('[role="menu"]')).toBeNull();
295
+ expect(document.activeElement).toBe(triggers[0]);
296
+ });
297
+ it('escape with focus INSIDE the sub closes only the sub (menu stays open)', () => {
298
+ setup({ withSub: true });
299
+ const triggers = target.querySelectorAll('[data-menubar-trigger]');
300
+ triggers[0].click();
301
+ flushSync();
302
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
303
+ subTrigger.focus();
304
+ pressKey('ArrowRight');
305
+ expect(document.activeElement?.textContent?.trim()).toBe('Email');
306
+ pressKey('Escape');
307
+ expect(target.querySelector('.sp-dropdown-menu-sub-content')).toBeNull();
308
+ expect(document.activeElement).toBe(subTrigger);
309
+ // The menubar menu itself stayed open.
310
+ expect(triggers[0].getAttribute('data-state')).toBe('open');
311
+ });
312
+ it('parent roving skips sub items while the sub is open', () => {
313
+ setup({ withSub: true });
314
+ const triggers = target.querySelectorAll('[data-menubar-trigger]');
315
+ triggers[0].click();
316
+ flushSync();
317
+ const subTrigger = target.querySelector('.sp-dropdown-sub-trigger');
318
+ // Pointer open keeps focus on the trigger row.
319
+ subTrigger.click();
320
+ flushSync();
321
+ subTrigger.focus();
322
+ // Share is the last row: ArrowDown loops to the first PARENT item,
323
+ // not into the open sub.
324
+ pressKey('ArrowDown');
325
+ expect(document.activeElement?.textContent?.trim()).toBe('New');
326
+ });
232
327
  });
233
328
  });
@@ -4,6 +4,7 @@
4
4
  import { EscapeLayerState } from '../../../internal/escape-layer/index.js'
5
5
  import { FloatingLayerState } from '../../../internal/floating-layer/index.js'
6
6
  import { isBrowser, kbd } from '../../../internal/helpers/index.js'
7
+ import { getOwnLevelMenuItems, isOwnLevelMenuItem } from '../../../internal/menu-context/index.js'
7
8
  import { Portal } from '../../../internal/portal/index.js'
8
9
  import { PresenceLayerState } from '../../../internal/presence-layer/index.js'
9
10
  import { RovingFocusGroup } from '../../../internal/roving-focus/index.js'
@@ -28,6 +29,9 @@
28
29
  orientation: 'vertical',
29
30
  loop: true,
30
31
  candidateSelector: '[data-roving-focus-item]',
32
+ // Sub content renders inline inside this content: exclude its rows from
33
+ // the parent collection (the sub content has its own roving group).
34
+ candidateFilter: (el) => !contentEl || isOwnLevelMenuItem(contentEl, el),
31
35
  })
32
36
 
33
37
  const presence = new PresenceLayerState({
@@ -79,10 +83,8 @@
79
83
  floating.setElements(menu.triggerEl, contentEl)
80
84
  floating.startAutoUpdate()
81
85
 
82
- // Focus first interactive item
83
- const firstItem = contentEl.querySelector<HTMLElement>(
84
- '[data-roving-focus-item]:not([data-disabled])',
85
- )
86
+ // Focus first interactive item (own level only — never a sub row)
87
+ const firstItem = getOwnLevelMenuItems(contentEl)[0]
86
88
  firstItem?.focus()
87
89
  }
88
90
 
@@ -101,9 +103,8 @@
101
103
  if (!isBrowser) return
102
104
  if (!contentEl) return
103
105
 
104
- const items = Array.from(
105
- contentEl.querySelectorAll<HTMLElement>('[data-roving-focus-item]:not([data-disabled])'),
106
- )
106
+ // Typeahead collection scoped to this level (sub rows excluded).
107
+ const items = getOwnLevelMenuItems(contentEl)
107
108
 
108
109
  switch (event.key) {
109
110
  case kbd.ARROW_DOWN: {
@@ -1 +1 @@
1
- {"version":3,"file":"menubar-content.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/menubar/components/menubar-content.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AA4MtD,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"menubar-content.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/menubar/components/menubar-content.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AA8MtD,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { MenubarMenuProps } from '../types.js'
3
- import { setMenuContext } from '../../../internal/menu-context/index.js'
3
+ import { createMenuContext, setMenuContext } from '../../../internal/menu-context/index.js'
4
4
  import {
5
5
  createMenubarMenuIds,
6
6
  getMenubarContext,
@@ -19,11 +19,13 @@
19
19
 
20
20
  // Set the shared MenuContext so dropdown-menu items (Item, CheckboxItem, etc.)
21
21
  // can call close() correctly
22
- setMenuContext({
23
- close() {
24
- root.closeMenu()
25
- },
26
- })
22
+ setMenuContext(
23
+ createMenuContext({
24
+ close() {
25
+ root.closeMenu()
26
+ },
27
+ }),
28
+ )
27
29
 
28
30
  setMenubarMenuContext({
29
31
  value,
@@ -1 +1 @@
1
- {"version":3,"file":"menubar-menu.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/menubar/components/menubar-menu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAsDnD,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"menubar-menu.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/menubar/components/menubar-menu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAwDnD,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -1,11 +1,13 @@
1
1
  export { default as CheckboxItem } from '../dropdown-menu/components/dropdown-menu-checkbox-item.svelte';
2
2
  export { default as Item } from '../dropdown-menu/components/dropdown-menu-item.svelte';
3
3
  export { default as Label } from '../dropdown-menu/components/dropdown-menu-label.svelte';
4
+ export { default as RadioGroup } from '../dropdown-menu/components/dropdown-menu-radio-group.svelte';
5
+ export { default as RadioItem } from '../dropdown-menu/components/dropdown-menu-radio-item.svelte';
4
6
  export { default as Separator } from '../dropdown-menu/components/dropdown-menu-separator.svelte';
5
7
  export { default as SubContent } from '../dropdown-menu/components/dropdown-menu-sub-content.svelte';
6
8
  export { default as SubTrigger } from '../dropdown-menu/components/dropdown-menu-sub-trigger.svelte';
7
9
  export { default as Sub } from '../dropdown-menu/components/dropdown-menu-sub.svelte';
8
- export type { DropdownMenuCheckboxItemProps as MenubarCheckboxItemProps, DropdownMenuItemProps as MenubarItemProps, DropdownMenuLabelProps as MenubarLabelProps, DropdownMenuSeparatorProps as MenubarSeparatorProps, DropdownMenuSubContentProps as MenubarSubContentProps, DropdownMenuSubProps as MenubarSubProps, DropdownMenuSubTriggerProps as MenubarSubTriggerProps, } from '../dropdown-menu/types.js';
10
+ export type { DropdownMenuCheckboxItemProps as MenubarCheckboxItemProps, DropdownMenuItemProps as MenubarItemProps, DropdownMenuLabelProps as MenubarLabelProps, DropdownMenuRadioGroupProps as MenubarRadioGroupProps, DropdownMenuRadioItemProps as MenubarRadioItemProps, DropdownMenuSeparatorProps as MenubarSeparatorProps, DropdownMenuSubContentProps as MenubarSubContentProps, DropdownMenuSubProps as MenubarSubProps, DropdownMenuSubTriggerProps as MenubarSubTriggerProps, } from '../dropdown-menu/types.js';
9
11
  export { default as Content } from './components/menubar-content.svelte';
10
12
  export { default as Menu } from './components/menubar-menu.svelte';
11
13
  export { default as Root } from './components/menubar-root.svelte';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/menubar/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gEAAgE,CAAA;AACxG,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,uDAAuD,CAAA;AACvF,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,wDAAwD,CAAA;AACzF,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,4DAA4D,CAAA;AAEjG,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,8DAA8D,CAAA;AACpG,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,8DAA8D,CAAA;AACpG,OAAO,EAAE,OAAO,IAAI,GAAG,EAAE,MAAM,sDAAsD,CAAA;AAErF,YAAY,EACV,6BAA6B,IAAI,wBAAwB,EACzD,qBAAqB,IAAI,gBAAgB,EACzC,sBAAsB,IAAI,iBAAiB,EAC3C,0BAA0B,IAAI,qBAAqB,EACnD,2BAA2B,IAAI,sBAAsB,EACrD,oBAAoB,IAAI,eAAe,EACvC,2BAA2B,IAAI,sBAAsB,GACtD,MAAM,2BAA2B,CAAA;AAElC,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,qCAAqC,CAAA;AACxE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kCAAkC,CAAA;AAClE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kCAAkC,CAAA;AAElE,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,qCAAqC,CAAA;AAExE,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,GACpB,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/menubar/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gEAAgE,CAAA;AACxG,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,uDAAuD,CAAA;AACvF,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,wDAAwD,CAAA;AACzF,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,8DAA8D,CAAA;AACpG,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,6DAA6D,CAAA;AAClG,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,4DAA4D,CAAA;AAEjG,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,8DAA8D,CAAA;AACpG,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,8DAA8D,CAAA;AACpG,OAAO,EAAE,OAAO,IAAI,GAAG,EAAE,MAAM,sDAAsD,CAAA;AAErF,YAAY,EACV,6BAA6B,IAAI,wBAAwB,EACzD,qBAAqB,IAAI,gBAAgB,EACzC,sBAAsB,IAAI,iBAAiB,EAC3C,2BAA2B,IAAI,sBAAsB,EACrD,0BAA0B,IAAI,qBAAqB,EACnD,0BAA0B,IAAI,qBAAqB,EACnD,2BAA2B,IAAI,sBAAsB,EACrD,oBAAoB,IAAI,eAAe,EACvC,2BAA2B,IAAI,sBAAsB,GACtD,MAAM,2BAA2B,CAAA;AAElC,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,qCAAqC,CAAA;AACxE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kCAAkC,CAAA;AAClE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kCAAkC,CAAA;AAElE,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,qCAAqC,CAAA;AAExE,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,GACpB,MAAM,YAAY,CAAA"}
@@ -2,6 +2,8 @@
2
2
  export { default as CheckboxItem } from '../dropdown-menu/components/dropdown-menu-checkbox-item.svelte';
3
3
  export { default as Item } from '../dropdown-menu/components/dropdown-menu-item.svelte';
4
4
  export { default as Label } from '../dropdown-menu/components/dropdown-menu-label.svelte';
5
+ export { default as RadioGroup } from '../dropdown-menu/components/dropdown-menu-radio-group.svelte';
6
+ export { default as RadioItem } from '../dropdown-menu/components/dropdown-menu-radio-item.svelte';
5
7
  export { default as Separator } from '../dropdown-menu/components/dropdown-menu-separator.svelte';
6
8
  export { default as SubContent } from '../dropdown-menu/components/dropdown-menu-sub-content.svelte';
7
9
  export { default as SubTrigger } from '../dropdown-menu/components/dropdown-menu-sub-trigger.svelte';
@@ -81,4 +81,33 @@ describe('escapeLayer', () => {
81
81
  // Should not throw after cleanup
82
82
  document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
83
83
  });
84
+ it('resolves behavior at keydown time (getter can flip between keypresses)', () => {
85
+ // A layer may pass `behavior` as a getter and decide per keypress — the
86
+ // submenu layer closes itself only when focus lives inside its content,
87
+ // and defers to the parent menu layer otherwise.
88
+ let topBehavior = 'close';
89
+ const onBottom = vi.fn();
90
+ const onTop = vi.fn();
91
+ const bottom = new EscapeLayerState({ enabled: true, onEscapeKeydown: onBottom });
92
+ const top = new EscapeLayerState({
93
+ enabled: true,
94
+ get behavior() {
95
+ return topBehavior;
96
+ },
97
+ onEscapeKeydown: onTop,
98
+ });
99
+ bottom.mount();
100
+ top.mount();
101
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
102
+ expect(onTop).toHaveBeenCalledOnce();
103
+ expect(onBottom).not.toHaveBeenCalled();
104
+ // Same mounted layer, new answer from the getter: the keypress falls
105
+ // through to the bottom layer in the SAME stack state.
106
+ topBehavior = 'defer-otherwise-close';
107
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
108
+ expect(onTop).toHaveBeenCalledOnce();
109
+ expect(onBottom).toHaveBeenCalledOnce();
110
+ top.unmount();
111
+ bottom.unmount();
112
+ });
84
113
  });
@@ -10,7 +10,11 @@ export declare class EscapeLayerState {
10
10
  constructor(props: EscapeLayerProps);
11
11
  mount(): void;
12
12
  unmount(): void;
13
- /** Update behavior dynamically. */
13
+ /**
14
+ * Update behavior dynamically. Writes through to `props.behavior` (behavior
15
+ * is resolved live at keydown time); only meaningful when `behavior` was
16
+ * provided as a plain value, not a getter.
17
+ */
14
18
  updateBehavior(behavior: EscapeKeydownBehavior): void;
15
19
  }
16
20
  /** Reset the layer stack (for testing only). */
@@ -1 +1 @@
1
- {"version":3,"file":"escape-layer.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/internal/escape-layer/escape-layer.svelte.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAkCzE,qBAAa,gBAAgB;IAC3B,KAAK,EAAE,gBAAgB,CAAA;gBAEX,KAAK,EAAE,gBAAgB;IAInC,KAAK,IAAI,IAAI;IAiBb,OAAO,IAAI,IAAI;IAcf,mCAAmC;IACnC,cAAc,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI;CAMtD;AAED,gDAAgD;AAChD,wBAAgB,qBAAqB,IAAI,IAAI,CAK5C"}
1
+ {"version":3,"file":"escape-layer.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/internal/escape-layer/escape-layer.svelte.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AA4CzE,qBAAa,gBAAgB;IAC3B,KAAK,EAAE,gBAAgB,CAAA;gBAEX,KAAK,EAAE,gBAAgB;IAInC,KAAK,IAAI,IAAI;IAYb,OAAO,IAAI,IAAI;IAcf;;;;OAIG;IACH,cAAc,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI;CAGtD;AAED,gDAAgD;AAChD,wBAAgB,qBAAqB,IAAI,IAAI,CAK5C"}
@@ -6,6 +6,15 @@
6
6
  */
7
7
  import { isBrowser, kbd } from '../helpers/index.js';
8
8
  const layerStack = [];
9
+ /**
10
+ * Behavior is resolved at KEYDOWN time, not mount time, so a layer can pass
11
+ * `behavior` as a getter and decide per keypress — e.g. the submenu layer
12
+ * closes only itself when focus lives inside its content, and defers to the
13
+ * parent menu layer otherwise (APG: Escape closes the menu containing focus).
14
+ */
15
+ function resolveBehavior(entry) {
16
+ return entry.state.props.behavior ?? 'close';
17
+ }
9
18
  function handleGlobalKeydown(event) {
10
19
  if (event.key !== kbd.ESCAPE)
11
20
  return;
@@ -14,18 +23,19 @@ function handleGlobalKeydown(event) {
14
23
  // Find the topmost layer that should handle the escape
15
24
  for (let i = layerStack.length - 1; i >= 0; i--) {
16
25
  const entry = layerStack[i];
17
- if (entry.behavior === 'close') {
26
+ const behavior = resolveBehavior(entry);
27
+ if (behavior === 'close') {
18
28
  entry.state.props.onEscapeKeydown?.(event);
19
29
  return;
20
30
  }
21
- if (entry.behavior === 'ignore') {
31
+ if (behavior === 'ignore') {
22
32
  return;
23
33
  }
24
34
  // 'defer-otherwise-close' — continue to next layer
25
35
  }
26
36
  // If all layers deferred, the bottom layer handles it
27
37
  const bottom = layerStack[0];
28
- if (bottom && bottom.behavior === 'defer-otherwise-close') {
38
+ if (bottom && resolveBehavior(bottom) === 'defer-otherwise-close') {
29
39
  bottom.state.props.onEscapeKeydown?.(event);
30
40
  }
31
41
  }
@@ -39,15 +49,11 @@ export class EscapeLayerState {
39
49
  return;
40
50
  if (!this.props.enabled)
41
51
  return;
42
- const entry = {
43
- state: this,
44
- behavior: this.props.behavior ?? 'close',
45
- };
46
52
  // Add global listener if this is the first layer
47
53
  if (layerStack.length === 0) {
48
54
  document.addEventListener('keydown', handleGlobalKeydown, true);
49
55
  }
50
- layerStack.push(entry);
56
+ layerStack.push({ state: this });
51
57
  }
52
58
  unmount() {
53
59
  if (!isBrowser)
@@ -61,12 +67,13 @@ export class EscapeLayerState {
61
67
  document.removeEventListener('keydown', handleGlobalKeydown, true);
62
68
  }
63
69
  }
64
- /** Update behavior dynamically. */
70
+ /**
71
+ * Update behavior dynamically. Writes through to `props.behavior` (behavior
72
+ * is resolved live at keydown time); only meaningful when `behavior` was
73
+ * provided as a plain value, not a getter.
74
+ */
65
75
  updateBehavior(behavior) {
66
- const entry = layerStack.find((e) => e.state === this);
67
- if (entry) {
68
- entry.behavior = behavior;
69
- }
76
+ this.props.behavior = behavior;
70
77
  }
71
78
  }
72
79
  /** Reset the layer stack (for testing only). */
@@ -1,3 +1,3 @@
1
- export { getMenuContext, setMenuContext } from './menu-context.svelte.js';
2
- export type { MenuContext } from './menu-context.svelte.js';
1
+ export { createMenuContext, getMenuContext, getOwnLevelMenuItems, isOwnLevelMenuItem, MENU_SUB_CONTENT_SELECTOR, setMenuContext, } from './menu-context.svelte.js';
2
+ export type { MenuContext, MenuSubHandle } from './menu-context.svelte.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/internal/menu-context/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACzE,YAAY,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/internal/menu-context/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,oBAAoB,EACpB,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,GACf,MAAM,0BAA0B,CAAA;AACjC,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA"}
@@ -1 +1 @@
1
- export { getMenuContext, setMenuContext } from './menu-context.svelte.js';
1
+ export { createMenuContext, getMenuContext, getOwnLevelMenuItems, isOwnLevelMenuItem, MENU_SUB_CONTENT_SELECTOR, setMenuContext, } from './menu-context.svelte.js';
@@ -1,10 +1,72 @@
1
+ import type { Point } from '../grace-area/types.js';
1
2
  /**
2
- * Shared menu context used by both DropdownMenu and ContextMenu.
3
- * Provides the minimal interface needed by shared sub-components (Item, CheckboxItem, etc.).
3
+ * Handle a submenu registers on its parent menu level so that sibling
4
+ * rows can coordinate pointer behavior (macOS/Radix model: hovering
5
+ * another item of the parent menu closes any open submenu, unless the
6
+ * pointer is traveling through the submenu's grace area).
7
+ */
8
+ export interface MenuSubHandle {
9
+ /** Whether `target` belongs to this submenu (its trigger or its content). */
10
+ ownsTarget: (target: EventTarget | null) => boolean;
11
+ /** Whether the pointer is currently traveling through this submenu's grace area. */
12
+ isPointerInGrace: (point: Point) => boolean;
13
+ /**
14
+ * Closes the submenu (pointer path). The submenu still restores focus to
15
+ * its trigger when keyboard focus was inside its content (mixed modality),
16
+ * so focus is never orphaned on `document.body`.
17
+ */
18
+ closeSub: () => void;
19
+ }
20
+ /**
21
+ * Shared menu context used by DropdownMenu, ContextMenu and Menubar menus.
22
+ * Provides the minimal interface needed by shared sub-components
23
+ * (Item, CheckboxItem, Sub, etc.).
4
24
  */
5
25
  export interface MenuContext {
6
26
  close: () => void;
27
+ /** Registers a submenu living at this menu level. Returns an unregister fn. */
28
+ registerSub: (sub: MenuSubHandle) => () => void;
29
+ /**
30
+ * Pointer coordination — called on `pointerenter` of any item row at this
31
+ * level. Closes every registered submenu that neither owns the entered
32
+ * element nor has the pointer inside its grace area.
33
+ */
34
+ onItemEnter: (event: PointerEvent) => void;
35
+ /**
36
+ * Single-open-sub invariant (Radix semantics: one open sub per menu level).
37
+ * Called by a submenu right before it opens: force-closes every OTHER
38
+ * registered submenu at this level, including one spared by a grace-area
39
+ * check (its grace tracking and safety timer are cancelled by its
40
+ * `closeSub`). Covers keyboard opens, which never go through `onItemEnter`.
41
+ */
42
+ closeOtherSubs: (keep: MenuSubHandle) => void;
7
43
  }
44
+ /**
45
+ * Builds a `MenuContext` with a per-menu submenu registry.
46
+ * SSR-safe: no DOM access happens here — only event handlers touch the DOM.
47
+ */
48
+ export declare function createMenuContext(options: {
49
+ close: () => void;
50
+ }): MenuContext;
8
51
  export declare function setMenuContext(ctx: MenuContext): void;
9
52
  export declare function getMenuContext(): MenuContext;
53
+ /** Class hook of the inline submenu content (shared by every menu family). */
54
+ export declare const MENU_SUB_CONTENT_SELECTOR = ".sp-dropdown-menu-sub-content";
55
+ /**
56
+ * Whether `candidate` belongs to `container`'s own menu level.
57
+ *
58
+ * Submenu content renders INLINE inside the parent menu content (no portal),
59
+ * so a naive `querySelectorAll('[data-roving-focus-item]')` on the parent
60
+ * leaks the submenu's items into the parent's roving/typeahead collections.
61
+ * A candidate is "own level" when its closest sub-content ancestor is either
62
+ * absent, the container itself (items directly inside a sub-content), or
63
+ * outside the container.
64
+ */
65
+ export declare function isOwnLevelMenuItem(container: HTMLElement, candidate: HTMLElement): boolean;
66
+ /**
67
+ * Returns the enabled roving candidates that belong to `container`'s own
68
+ * menu level (excluding rows nested in an open submenu). Used for roving
69
+ * focus, typeahead and open-autofocus in every menu content.
70
+ */
71
+ export declare function getOwnLevelMenuItems(container: HTMLElement): HTMLElement[];
10
72
  //# sourceMappingURL=menu-context.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"menu-context.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/internal/menu-context/menu-context.svelte.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAErD;AAED,wBAAgB,cAAc,IAAI,WAAW,CAE5C"}
1
+ {"version":3,"file":"menu-context.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/internal/menu-context/menu-context.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAMnD;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,UAAU,EAAE,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,KAAK,OAAO,CAAA;IACnD,oFAAoF;IACpF,gBAAgB,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAA;IAC3C;;;;OAIG;IACH,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,+EAA+E;IAC/E,WAAW,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,MAAM,IAAI,CAAA;IAC/C;;;;OAIG;IACH,WAAW,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IAC1C;;;;;;OAMG;IACH,cAAc,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAA;CAC9C;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,IAAI,CAAA;CAAE,GAAG,WAAW,CA0B7E;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAErD;AAED,wBAAgB,cAAc,IAAI,WAAW,CAE5C;AAED,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,kCAAkC,CAAA;AAExE;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,GAAG,OAAO,CAG1F;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,WAAW,GAAG,WAAW,EAAE,CAI1E"}