@antadesign/anta 0.3.33 → 0.3.35

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 (205) hide show
  1. package/LICENSE.md +14 -1
  2. package/README.md +9 -0
  3. package/dist/anta_helpers.d.ts +46 -30
  4. package/dist/anta_helpers.js +66 -25
  5. package/dist/bundle.css +1 -1
  6. package/dist/bundle.js +2 -1353
  7. package/dist/components/Banner.d.ts +25 -40
  8. package/dist/components/Banner.js +15 -17
  9. package/dist/components/Breadcrumbs.d.ts +0 -1
  10. package/dist/components/Breadcrumbs.js +0 -1
  11. package/dist/components/Button.d.ts +2 -3
  12. package/dist/components/Button.js +1 -0
  13. package/dist/components/ButtonCopy.js +3 -2
  14. package/dist/components/Calendar.js +13 -13
  15. package/dist/components/Card.d.ts +9 -17
  16. package/dist/components/Card.js +50 -46
  17. package/dist/components/Dialog.d.ts +1 -0
  18. package/dist/components/Dialog.js +16 -10
  19. package/dist/components/Expander.d.ts +6 -21
  20. package/dist/components/Expander.js +24 -23
  21. package/dist/components/Icon.d.ts +8 -14
  22. package/dist/components/Input.d.ts +77 -96
  23. package/dist/components/Input.js +23 -27
  24. package/dist/components/InputAutocomplete.d.ts +29 -10
  25. package/dist/components/InputAutocomplete.js +25 -8
  26. package/dist/components/InputDate.d.ts +0 -1
  27. package/dist/components/InputDate.js +2 -3
  28. package/dist/components/InputTime.d.ts +10 -4
  29. package/dist/components/InputTime.js +20 -16
  30. package/dist/components/Loader.d.ts +2 -2
  31. package/dist/components/Menu.d.ts +5 -4
  32. package/dist/components/Menu.js +13 -8
  33. package/dist/components/MenuItem.js +1 -14
  34. package/dist/components/MenuItemCopy.js +2 -2
  35. package/dist/components/Panel.d.ts +3 -1
  36. package/dist/components/Panel.js +17 -11
  37. package/dist/components/Progress.d.ts +21 -23
  38. package/dist/components/Progress.js +13 -9
  39. package/dist/components/RadioGroup.js +6 -8
  40. package/dist/components/{select-parts.css → Select.css} +1 -1
  41. package/dist/components/Select.d.ts +53 -36
  42. package/dist/components/Select.js +53 -98
  43. package/dist/components/SelectFaceted.d.ts +7 -4
  44. package/dist/components/SelectFaceted.js +21 -10
  45. package/dist/components/Slider.d.ts +28 -11
  46. package/dist/components/Slider.js +20 -20
  47. package/dist/components/Steps.css +1 -1
  48. package/dist/components/Steps.d.ts +0 -1
  49. package/dist/components/Steps.js +1 -12
  50. package/dist/components/Tabs.d.ts +17 -20
  51. package/dist/components/Tabs.js +7 -9
  52. package/dist/components/Text.d.ts +55 -36
  53. package/dist/components/Text.js +35 -22
  54. package/dist/components/Title.d.ts +19 -27
  55. package/dist/components/Title.js +2 -1
  56. package/dist/components/Toaster.js +9 -2
  57. package/dist/components/Tooltip.d.ts +10 -15
  58. package/dist/components/copy-props.d.ts +14 -3
  59. package/dist/components/copy-props.js +14 -3
  60. package/dist/components/select-tree.d.ts +24 -0
  61. package/dist/components/select-tree.js +35 -0
  62. package/dist/element-base.d.ts +10 -0
  63. package/dist/element-base.js +15 -0
  64. package/dist/elements/a-avatar.css +1 -1
  65. package/dist/elements/a-avatar.js +2 -28
  66. package/dist/elements/a-banner.css +1 -1
  67. package/dist/elements/a-banner.d.ts +4 -1
  68. package/dist/elements/a-banner.js +22 -112
  69. package/dist/elements/a-breadcrumbs.d.ts +1 -0
  70. package/dist/elements/a-breadcrumbs.js +1 -0
  71. package/dist/elements/a-button.css +1 -1
  72. package/dist/elements/a-button.d.ts +10 -7
  73. package/dist/elements/a-button.js +18 -1
  74. package/dist/elements/a-calendar.d.ts +14 -13
  75. package/dist/elements/a-calendar.js +29 -24
  76. package/dist/elements/a-card.css +1 -1
  77. package/dist/elements/a-card.d.ts +0 -9
  78. package/dist/elements/a-card.js +0 -184
  79. package/dist/elements/a-checkbox.css +1 -1
  80. package/dist/elements/a-copy.css +1 -1
  81. package/dist/elements/a-copy.d.ts +1 -5
  82. package/dist/elements/a-copy.js +129 -108
  83. package/dist/elements/a-dialog.css +1 -1
  84. package/dist/elements/a-dialog.d.ts +1 -1
  85. package/dist/elements/a-dialog.js +68 -194
  86. package/dist/elements/a-expander.css +1 -1
  87. package/dist/elements/a-expander.d.ts +4 -30
  88. package/dist/elements/a-expander.js +85 -344
  89. package/dist/elements/a-icon.d.ts +5 -23
  90. package/dist/elements/a-icon.js +1 -2
  91. package/dist/elements/a-input-date.d.ts +1 -0
  92. package/dist/elements/a-input-date.js +1 -0
  93. package/dist/elements/a-input-time.css +1 -1
  94. package/dist/elements/a-input-time.d.ts +2 -1
  95. package/dist/elements/a-input-time.js +101 -168
  96. package/dist/elements/a-input.css +1 -1
  97. package/dist/elements/a-input.d.ts +10 -8
  98. package/dist/elements/a-input.js +155 -230
  99. package/dist/elements/a-loader.d.ts +5 -8
  100. package/dist/elements/a-loader.js +1 -2
  101. package/dist/elements/a-menu-item.css +1 -1
  102. package/dist/elements/a-menu-item.d.ts +5 -3
  103. package/dist/elements/a-menu-item.js +5 -1
  104. package/dist/elements/a-menu.css +1 -1
  105. package/dist/elements/a-menu.d.ts +5 -7
  106. package/dist/elements/a-menu.js +383 -299
  107. package/dist/elements/a-panel.css +1 -1
  108. package/dist/elements/a-panel.js +19 -37
  109. package/dist/elements/a-progress.css +1 -1
  110. package/dist/elements/a-progress.d.ts +0 -22
  111. package/dist/elements/a-progress.js +0 -100
  112. package/dist/elements/a-radio-group.d.ts +1 -1
  113. package/dist/elements/a-radio-group.js +6 -4
  114. package/dist/elements/a-select-faceted.d.ts +1 -0
  115. package/dist/elements/a-select-faceted.js +1 -0
  116. package/dist/elements/a-select.d.ts +1 -0
  117. package/dist/elements/a-select.js +1 -0
  118. package/dist/elements/a-slider.css +1 -1
  119. package/dist/elements/a-slider.d.ts +11 -3
  120. package/dist/elements/a-slider.js +244 -242
  121. package/dist/elements/a-steps.d.ts +1 -0
  122. package/dist/elements/a-steps.js +1 -0
  123. package/dist/elements/a-switch.css +1 -1
  124. package/dist/elements/a-tab.css +1 -1
  125. package/dist/elements/a-tabpanel.d.ts +2 -2
  126. package/dist/elements/a-tabpanel.js +13 -3
  127. package/dist/elements/a-tabs.css +1 -1
  128. package/dist/elements/a-tabs.d.ts +1 -1
  129. package/dist/elements/a-tabs.js +6 -4
  130. package/dist/elements/a-tag.css +1 -1
  131. package/dist/elements/a-tag.d.ts +1 -0
  132. package/dist/elements/a-tag.js +1 -0
  133. package/dist/elements/a-text.css +1 -1
  134. package/dist/elements/a-text.d.ts +6 -43
  135. package/dist/elements/a-text.js +110 -193
  136. package/dist/elements/a-title.css +1 -1
  137. package/dist/elements/a-title.d.ts +1 -0
  138. package/dist/elements/a-title.js +1 -0
  139. package/dist/elements/a-toast.css +1 -1
  140. package/dist/elements/a-toast.d.ts +10 -45
  141. package/dist/elements/a-toast.js +80 -66
  142. package/dist/elements/a-toaster.css +1 -1
  143. package/dist/elements/a-toaster.js +9 -44
  144. package/dist/elements/a-tooltip.css +1 -1
  145. package/dist/elements/a-tooltip.d.ts +1 -1
  146. package/dist/elements/a-tooltip.js +142 -136
  147. package/dist/elements/copy-behavior.d.ts +1 -1
  148. package/dist/elements/copy-behavior.js +21 -21
  149. package/dist/elements/index.d.ts +9 -3
  150. package/dist/elements/index.js +6 -6
  151. package/dist/elements/input-focus-modality.d.ts +15 -0
  152. package/dist/elements/input-focus-modality.js +44 -0
  153. package/dist/elements/input-trailing-actions.d.ts +5 -0
  154. package/dist/elements/input-trailing-actions.js +38 -0
  155. package/dist/elements/shadow-aria.d.ts +2 -0
  156. package/dist/elements/shadow-aria.js +2 -1
  157. package/dist/elements/value-input.d.ts +3 -0
  158. package/dist/elements/value-input.js +12 -0
  159. package/dist/general_types.d.ts +103 -96
  160. package/dist/index.d.ts +1 -1
  161. package/dist/input-events.d.ts +4 -0
  162. package/dist/input-events.js +15 -0
  163. package/dist/jsx-runtime.d.ts +70 -23
  164. package/dist/jsx-runtime.js +26 -19
  165. package/dist/native-event-props.d.ts +2 -0
  166. package/dist/native-event-props.js +10 -0
  167. package/dist/reset.css +1 -1
  168. package/dist/theme-antithesis.css +10 -1
  169. package/dist/theme-antune.css +1 -1
  170. package/docs/accessibility.md +17 -3
  171. package/docs/changelog.md +384 -2
  172. package/docs/comparison.md +2 -2
  173. package/docs/components/avatar.md +5 -0
  174. package/docs/components/banner.md +117 -187
  175. package/docs/components/breadcrumbs.md +9 -6
  176. package/docs/components/button.md +22 -0
  177. package/docs/components/card.md +74 -28
  178. package/docs/components/checkbox.md +9 -10
  179. package/docs/components/dialog.md +83 -32
  180. package/docs/components/expander.md +198 -435
  181. package/docs/components/icon.md +11 -17
  182. package/docs/components/input-autocomplete.md +152 -52
  183. package/docs/components/input-date.md +175 -148
  184. package/docs/components/input-time.md +81 -38
  185. package/docs/components/input.md +343 -349
  186. package/docs/components/loader.md +2 -1
  187. package/docs/components/menu.md +35 -6
  188. package/docs/components/panel.md +18 -0
  189. package/docs/components/progress.md +83 -49
  190. package/docs/components/radio.md +39 -43
  191. package/docs/components/select-faceted.md +54 -7
  192. package/docs/components/select.md +390 -386
  193. package/docs/components/slider.md +136 -19
  194. package/docs/components/steps.md +6 -3
  195. package/docs/components/switch.md +8 -9
  196. package/docs/components/tabs.md +110 -104
  197. package/docs/components/text.md +110 -86
  198. package/docs/components/title.md +47 -33
  199. package/docs/components/toaster.md +242 -84
  200. package/docs/components/tooltip.md +204 -113
  201. package/docs/install-config.md +461 -96
  202. package/docs/packages/plot.md +228 -143
  203. package/docs/theming.md +29 -13
  204. package/package.json +43 -4
  205. package/theme-antithesis-license.md +66 -0
@@ -3,10 +3,11 @@ import {
3
3
  SYNC_POPUP_ARIA,
4
4
  anchorRect,
5
5
  isPopupAriaReceiver,
6
+ isComposingKey,
6
7
  isModifiedNavigationKey,
7
8
  setMenuPresence
8
9
  } from "../anta_helpers";
9
- import { AMenuItemElement, MENU_ITEM_SELECTOR, isMenuItemEl, ensureMenuItemKeyListener } from "./a-menu-item";
10
+ import { MENU_ITEM_SELECTOR, isMenuItemEl, isCustomMenuItem, ensureMenuItemKeyListener } from "./a-menu-item";
10
11
  import { activeFocus, cycleFocus, focusParent, tabStops } from "./focus";
11
12
  import { emitCopyRequest } from "./copy-behavior";
12
13
  import "./a-menu.css";
@@ -18,131 +19,201 @@ const TYPEAHEAD_RESET = 500;
18
19
  const SELF_ACTIVATING = 'a-button, a-input[button], button, a[href], input[type="button"], input[type="submit"], input[type="reset"], [role="button"]';
19
20
  const EDITABLE_FIELD = "input:not([readonly]), textarea:not([readonly]), a-input:not([readonly]), a-input-time:not([readonly])";
20
21
  const DECLARATIVE_TRIGGER = "[data-dialog-open], [data-dialog-close], [data-toast-dismiss], [data-banner-dismiss]";
21
- const openStack = [];
22
- let docBound = false;
23
- let boundDoc = null;
24
- let boundView = null;
25
- let mouseX = null;
26
- let mouseY = 0;
27
- function trackMouse(e) {
28
- mouseX = e.pointerType === "mouse" ? e.clientX : null;
29
- if (mouseX !== null) mouseY = e.clientY;
30
- }
31
- function mouseInside(rect) {
32
- return mouseX !== null && mouseX >= rect.left && mouseX < rect.right && mouseY >= rect.top && mouseY < rect.bottom;
33
- }
34
- function bindDocListeners(doc, view) {
35
- if (docBound) return;
36
- doc.addEventListener("pointerdown", onDocPointerDown, true);
37
- doc.addEventListener("pointermove", trackMouse, { capture: true, passive: true });
38
- doc.addEventListener("pointerleave", clearMouse);
39
- doc.addEventListener("keydown", onDocKeyDown, true);
40
- doc.addEventListener("keyup", onDocKeyUp, true);
41
- doc.addEventListener("contextmenu", onDocContextMenu, true);
42
- view.addEventListener("resize", onResize);
43
- boundDoc = doc;
44
- boundView = view;
45
- docBound = true;
46
- }
47
- function unbindDocListeners() {
48
- if (!docBound) return;
49
- boundDoc?.removeEventListener("pointerdown", onDocPointerDown, true);
50
- boundDoc?.removeEventListener("pointermove", trackMouse, true);
51
- boundDoc?.removeEventListener("pointerleave", clearMouse);
52
- boundDoc?.removeEventListener("keydown", onDocKeyDown, true);
53
- boundDoc?.removeEventListener("keyup", onDocKeyUp, true);
54
- boundDoc?.removeEventListener("contextmenu", onDocContextMenu, true);
55
- boundView?.removeEventListener("resize", onResize);
56
- boundDoc = null;
57
- boundView = null;
58
- docBound = false;
59
- mouseX = null;
60
- }
61
- function clearMouse() {
62
- mouseX = null;
63
- }
64
- function menuIndexForEvent(e, primaryClick = false) {
65
- const path = e.composedPath();
66
- for (let i = openStack.length - 1; i >= 0; i--) {
67
- const m = openStack[i];
68
- if (path.includes(m.surface)) return i;
69
- const anchor = m.triggerAnchor;
70
- if (!anchor) continue;
71
- if (primaryClick && m.hasAttribute("context")) continue;
72
- if (path.includes(anchor)) return i;
73
- }
74
- return -1;
22
+ function isElement(node) {
23
+ return !!node && node.nodeType === 1;
75
24
  }
76
25
  function pathCrossesTopLayerBeforeAnchor(e, anchor) {
77
26
  for (const node of e.composedPath()) {
78
27
  if (node === anchor) return false;
79
- if (node instanceof HTMLDialogElement && node.matches(":modal") || node instanceof HTMLElement && node.matches(":popover-open"))
28
+ if (isElement(node) && node.localName === "dialog" && node.matches(":modal") || isElement(node) && node.matches(":popover-open"))
80
29
  return true;
81
30
  }
82
31
  return false;
83
32
  }
84
- function nodeHitsMenus(node) {
85
- for (const m of openStack) {
86
- if (m.contains(node) || m.surface.contains(node)) return true;
87
- }
88
- return false;
89
- }
90
- setMenuPresence({
91
- isOpen: () => openStack.length > 0,
92
- contains: nodeHitsMenus
93
- });
94
- function onDocPointerDown(e) {
95
- trackMouse(e);
96
- if (!openStack.length) return;
97
- const index = menuIndexForEvent(e, e.button === 0);
98
- openStack[index + 1]?.requestClose(e);
99
- }
100
- function onDocContextMenu(e) {
101
- if (!openStack.length) return;
102
- if (menuIndexForEvent(e) === -1) dismiss(e);
103
- }
104
- function onResize() {
105
- if (!openStack.length) return;
106
- dismiss();
33
+ const MENU_COORDINATION = /* @__PURE__ */ Symbol.for("anta.menu-coordination");
34
+ function coordination(doc) {
35
+ return doc[MENU_COORDINATION] ??= createCoordination(doc);
107
36
  }
108
- function onDocKeyDown(e) {
109
- const menu = openStack[openStack.length - 1];
110
- if (!menu) return;
111
- menu.handleKey(e);
112
- }
113
- function onDocKeyUp(e) {
114
- const menu = openStack[openStack.length - 1];
115
- if (!menu) return;
116
- menu.handleKeyUp(e);
117
- }
118
- function dismiss(originEvent) {
119
- openStack[0]?.requestClose(originEvent);
120
- }
121
- function closeAll() {
122
- for (let i = openStack.length - 1; i >= 0; i--) {
123
- const m = openStack[i];
124
- if (m.isOpen && !m._dismissNotified) m.emitChange("closed");
125
- m._doHide();
126
- }
127
- openStack.length = 0;
128
- unbindDocListeners();
129
- }
130
- const anchorToMenu = /* @__PURE__ */ new WeakMap();
131
- function handleIntersection(entries) {
132
- for (const entry of entries) {
133
- const menu = anchorToMenu.get(entry.target);
134
- if (!menu) continue;
135
- if (!menu.listening && entry.isIntersecting) {
136
- requestAnimationFrame(() => menu.setupListeners());
137
- } else if (menu.listening && !entry.isIntersecting) {
138
- requestAnimationFrame(() => {
139
- menu.teardownListeners();
140
- if (menu.isOpen) menu.close();
37
+ function createCoordination(doc) {
38
+ const openStack = [];
39
+ let docBound = false;
40
+ let boundDoc = null;
41
+ let boundView = null;
42
+ let mouseX = null;
43
+ let mouseY = 0;
44
+ function trackMouse(e) {
45
+ mouseX = e.pointerType === "mouse" ? e.clientX : null;
46
+ if (mouseX !== null) mouseY = e.clientY;
47
+ }
48
+ function mouseInside(rect) {
49
+ return mouseX !== null && mouseX >= rect.left && mouseX < rect.right && mouseY >= rect.top && mouseY < rect.bottom;
50
+ }
51
+ function bindDocListeners(doc2, view) {
52
+ if (docBound) return;
53
+ doc2.addEventListener("pointerdown", onDocPointerDown, true);
54
+ doc2.addEventListener("pointermove", trackMouse, { capture: true, passive: true });
55
+ doc2.addEventListener("pointerleave", clearMouse);
56
+ doc2.addEventListener("keydown", onDocKeyDown, true);
57
+ doc2.addEventListener("keyup", onDocKeyUp, true);
58
+ doc2.addEventListener("contextmenu", onDocContextMenu, true);
59
+ view.addEventListener("resize", onResize);
60
+ boundDoc = doc2;
61
+ boundView = view;
62
+ docBound = true;
63
+ }
64
+ function unbindDocListeners() {
65
+ if (!docBound) return;
66
+ boundDoc?.removeEventListener("pointerdown", onDocPointerDown, true);
67
+ boundDoc?.removeEventListener("pointermove", trackMouse, true);
68
+ boundDoc?.removeEventListener("pointerleave", clearMouse);
69
+ boundDoc?.removeEventListener("keydown", onDocKeyDown, true);
70
+ boundDoc?.removeEventListener("keyup", onDocKeyUp, true);
71
+ boundDoc?.removeEventListener("contextmenu", onDocContextMenu, true);
72
+ boundView?.removeEventListener("resize", onResize);
73
+ boundDoc = null;
74
+ boundView = null;
75
+ docBound = false;
76
+ mouseX = null;
77
+ }
78
+ function clearMouse() {
79
+ mouseX = null;
80
+ }
81
+ function menuIndexForEvent(e, primaryClick = false) {
82
+ const path = e.composedPath();
83
+ for (let i = openStack.length - 1; i >= 0; i--) {
84
+ const m = openStack[i];
85
+ if (path.includes(m.surface)) return i;
86
+ const anchor = m.triggerAnchor;
87
+ if (!anchor) continue;
88
+ if (primaryClick && m.hasAttribute("context")) continue;
89
+ if (path.includes(anchor)) return i;
90
+ }
91
+ return -1;
92
+ }
93
+ function nodeHitsMenus(node) {
94
+ for (const m of openStack) {
95
+ if (m.contains(node) || m.surface.contains(node)) return true;
96
+ }
97
+ return false;
98
+ }
99
+ const menuPresence = {
100
+ isOpen: (doc2) => openStack.some((menu) => !doc2 || menu.ownerDocument === doc2),
101
+ contains: nodeHitsMenus
102
+ };
103
+ setMenuPresence(menuPresence, doc);
104
+ function onDocPointerDown(e) {
105
+ trackMouse(e);
106
+ if (!openStack.length) return;
107
+ const index = menuIndexForEvent(e, e.button === 0);
108
+ openStack[index + 1]?.requestClose(e);
109
+ }
110
+ function onDocContextMenu(e) {
111
+ if (!openStack.length) return;
112
+ if (menuIndexForEvent(e) === -1) dismiss(e);
113
+ }
114
+ function onResize() {
115
+ if (!openStack.length) return;
116
+ dismiss();
117
+ }
118
+ function onDocKeyDown(e) {
119
+ const menu = openStack[openStack.length - 1];
120
+ if (!menu) return;
121
+ menu.handleKey(e);
122
+ }
123
+ function onDocKeyUp(e) {
124
+ const menu = openStack[openStack.length - 1];
125
+ if (!menu) return;
126
+ menu.handleKeyUp(e);
127
+ }
128
+ function dismiss(originEvent) {
129
+ openStack[0]?.requestClose(originEvent);
130
+ }
131
+ const parents = /* @__PURE__ */ new Map();
132
+ function watchParent(parent, refresh) {
133
+ const Observer = doc.defaultView?.MutationObserver;
134
+ if (!Observer) return;
135
+ let entry = parents.get(parent);
136
+ if (!entry) {
137
+ const callbacks2 = /* @__PURE__ */ new Set();
138
+ const observer3 = new Observer(() => {
139
+ for (const callback of callbacks2) callback();
141
140
  });
141
+ observer3.observe(parent, { childList: true });
142
+ entry = { observer: observer3, refresh: callbacks2 };
143
+ parents.set(parent, entry);
142
144
  }
145
+ const { observer: observer2, refresh: callbacks } = entry;
146
+ callbacks.add(refresh);
147
+ return () => {
148
+ if (!callbacks.delete(refresh) || callbacks.size) return;
149
+ observer2.disconnect();
150
+ parents.delete(parent);
151
+ };
143
152
  }
153
+ const anchorToMenu = /* @__PURE__ */ new Map();
154
+ const frames = /* @__PURE__ */ new Map();
155
+ let observer;
156
+ function observe(menu, anchor) {
157
+ const previous = anchorToMenu.get(anchor);
158
+ if (previous && previous !== menu) {
159
+ previous.teardownListeners();
160
+ const frame = frames.get(anchor);
161
+ if (frame !== void 0) doc.defaultView?.cancelAnimationFrame(frame);
162
+ frames.delete(anchor);
163
+ observer?.unobserve(anchor);
164
+ }
165
+ anchorToMenu.set(anchor, menu);
166
+ const Observer = doc.defaultView?.IntersectionObserver;
167
+ if (!Observer) {
168
+ menu.setupListeners();
169
+ return;
170
+ }
171
+ observer ??= new Observer((entries) => {
172
+ for (const entry of entries) {
173
+ const menu2 = anchorToMenu.get(entry.target);
174
+ if (!menu2) continue;
175
+ const view = doc.defaultView;
176
+ if (!view) continue;
177
+ const previous2 = frames.get(entry.target);
178
+ if (previous2 !== void 0) view.cancelAnimationFrame(previous2);
179
+ frames.set(entry.target, view.requestAnimationFrame(() => {
180
+ frames.delete(entry.target);
181
+ if (anchorToMenu.get(entry.target) !== menu2 || !menu2.isConnected || menu2.ownerDocument !== doc) return;
182
+ if (entry.isIntersecting) menu2.setupListeners();
183
+ else if (menu2.listening) {
184
+ menu2.teardownListeners();
185
+ if (menu2.isOpen) menu2.close();
186
+ }
187
+ }));
188
+ }
189
+ }, { root: null, rootMargin: "0px", threshold: 0 });
190
+ observer.observe(anchor);
191
+ }
192
+ function unobserve(menu, anchor) {
193
+ if (anchorToMenu.get(anchor) !== menu) return;
194
+ anchorToMenu.delete(anchor);
195
+ const frame = frames.get(anchor);
196
+ if (frame !== void 0) doc.defaultView?.cancelAnimationFrame(frame);
197
+ frames.delete(anchor);
198
+ observer?.unobserve(anchor);
199
+ if (!anchorToMenu.size) {
200
+ observer?.disconnect();
201
+ observer = void 0;
202
+ }
203
+ }
204
+ return {
205
+ stack: openStack,
206
+ observe,
207
+ unobserve,
208
+ watchParent,
209
+ bind: bindDocListeners,
210
+ unbind: unbindDocListeners,
211
+ get mouseX() {
212
+ return mouseX;
213
+ },
214
+ mouseInside
215
+ };
144
216
  }
145
- const lazyObserver = typeof IntersectionObserver !== "undefined" ? new IntersectionObserver(handleIntersection, { root: null, rootMargin: "0px", threshold: 0 }) : null;
146
217
  class AMenuElement extends HTMLElementBase {
147
218
  static observedAttributes = ["placement", "context", "coord", "offset", "nohover", "state", "stop-propagation", "role"];
148
219
  /** Shadow-internal popover surface — the only thing we ever mutate. */
@@ -185,6 +256,8 @@ class AMenuElement extends HTMLElementBase {
185
256
  // layout shifts. Those movements do not all produce a DOM observer callback, so
186
257
  // this frame is active only while the menu is visible.
187
258
  #anchorTrackingFrame;
259
+ // Preserve anchoring mode while a sibling trigger is temporarily absent.
260
+ #anchorRelative = false;
188
261
  #lastAnchorRect;
189
262
  // Vertical-follow baseline captured on each full position(): the frozen
190
263
  // horizontal offset and the gap to the anchor's top. Tracking translates to hold
@@ -211,118 +284,22 @@ class AMenuElement extends HTMLElementBase {
211
284
  } catch {
212
285
  }
213
286
  const shadow = this.attachShadow({ mode: "open" });
214
- const style = document.createElement("style");
215
- style.textContent = `
216
- :host { display: contents; }
217
-
218
- .container {
219
- position: fixed;
220
- left: 0;
221
- top: 0;
222
- margin: 0;
223
- padding: 0;
224
- box-sizing: border-box;
225
- flex-direction: column;
226
- min-width: max(var(--menu-min-width, 88px), var(--_anchor-width, 0px));
227
- max-width: calc(100vw - ${2 * MARGIN}px);
228
- max-height: calc(100dvh - ${2 * MARGIN}px);
229
- overflow: hidden;
230
- background: var(--menu-bg, Canvas);
231
- color: var(--text-2, CanvasText);
232
- border: var(--menu-border, 1px solid);
233
- border-radius: var(--menu-radius, 8px);
234
- box-shadow: var(--menu-shadow, 0 8px 24px rgba(0,0,0,0.2));
235
- -webkit-backdrop-filter: var(--menu-backdrop-filter, blur(20px));
236
- backdrop-filter: var(--menu-backdrop-filter, blur(20px));
237
- outline: none;
238
-
239
- /* Closed / exit state \u2014 fade + a tiny vertical settle (no horizontal
240
- shift). This 140ms governs the EXIT (open \u2192 closed); the enter is a
241
- touch quicker (see :popover-open below) so the menu feels snappy to
242
- open but unhurried to dismiss. 'display' and 'overlay' transition with
243
- 'allow-discrete' so the menu stays in the top layer and visible while
244
- it animates OUT, then hides. */
245
- opacity: 0;
246
- translate: 0 -4px;
247
- transition:
248
- opacity 140ms ease-out,
249
- translate 140ms ease-out,
250
- width var(--menu-width-duration, 160ms) ease,
251
- display 140ms allow-discrete,
252
- overlay 140ms allow-discrete;
253
- }
254
- .container:popover-open {
255
- display: flex;
256
- opacity: 1;
257
- translate: 0 0;
258
- /* Enter (closed \u2192 open) \u2014 quicker than the exit above. */
259
- transition:
260
- opacity 100ms ease-out,
261
- translate 100ms ease-out,
262
- width var(--menu-width-duration, 160ms) ease,
263
- display 100ms allow-discrete,
264
- overlay 100ms allow-discrete;
265
- }
266
- /* Enter: start from the closed state and transition in. */
267
- @starting-style {
268
- .container:popover-open { opacity: 0; translate: 0 -4px; }
269
- }
270
- slot[name="header"],
271
- slot[name="footer"] {
272
- display: block;
273
- flex: none;
274
- }
275
- slot[name="header"]::slotted(a-menu-header),
276
- slot[name="header"]::slotted(a-select-header),
277
- slot[name="footer"]::slotted(a-menu-footer),
278
- slot[name="footer"]::slotted(a-select-footer) {
279
- display: block;
280
- padding: var(--menu-padding, 4px);
281
- }
282
- /* The scrolling body \u2014 only the items scroll; the header / footer slots stay
283
- fixed above and below it. min-height:0 lets it shrink so its own scrollbar
284
- engages instead of the flex item overflowing. The 1px inter-item gap that
285
- used to live on the container lives here now. */
286
- .scroll {
287
- flex: 1 1 auto;
288
- min-height: 0;
289
- display: flex;
290
- flex-direction: column;
291
- gap: 1px;
292
- overflow-y: auto;
293
- overscroll-behavior: contain;
294
- scrollbar-width: thin;
295
- /* The menu's padding lives here (not on the clipping container) so an item's
296
- focus outline has room inside the scroll box instead of being clipped at
297
- the edge; scroll-padding keeps a focused row off the edge when scrolled. */
298
- padding: var(--menu-padding, 4px);
299
- scroll-padding-block: var(--menu-padding, 4px);
300
- /* Soft scroll edges: fade the content into the top / bottom only on the side
301
- with more to scroll (the --fade-* vars, set from JS, are 0 otherwise). The
302
- top also clears a hard --gap-top band (fully transparent, ahead of the ramp)
303
- so a row scrolling up disappears cleanly under a pinned header / filter field
304
- before the fade begins, instead of bleeding right against it. */
305
- --fade-top: 0px;
306
- --fade-bottom: 0px;
307
- --gap-top: 0px;
308
- -webkit-mask-image: linear-gradient(to bottom, transparent 0, transparent var(--gap-top), #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent 100%);
309
- mask-image: linear-gradient(to bottom, transparent 0, transparent var(--gap-top), #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent 100%);
310
- }
311
- `;
312
- this.surface = document.createElement("div");
287
+ this.surface = this.doc.createElement("div");
313
288
  this.surface.className = "container";
314
289
  this.surface.setAttribute("part", "menu");
315
290
  this.surface.setAttribute("popover", "manual");
316
- const header = document.createElement("slot");
291
+ const header = this.doc.createElement("slot");
317
292
  header.setAttribute("name", "header");
318
- const scroll = document.createElement("div");
293
+ header.part.add("header");
294
+ const scroll = this.doc.createElement("div");
319
295
  scroll.className = "scroll";
320
296
  scroll.setAttribute("part", "scroll");
321
- scroll.append(document.createElement("slot"));
297
+ scroll.append(this.doc.createElement("slot"));
322
298
  scroll.addEventListener("scroll", () => this.updateScrollFade(), { passive: true });
323
299
  this.scrollEl = scroll;
324
- const footer = document.createElement("slot");
300
+ const footer = this.doc.createElement("slot");
325
301
  footer.setAttribute("name", "footer");
302
+ footer.part.add("footer");
326
303
  this.surface.append(header, scroll, footer);
327
304
  this.surface.addEventListener("pointerenter", (e) => {
328
305
  if (e.pointerType !== "mouse") return;
@@ -336,24 +313,53 @@ class AMenuElement extends HTMLElementBase {
336
313
  if (this.isSubmenu && this.#isHover) this.scheduleClose();
337
314
  });
338
315
  this.surface.addEventListener("click", this.onSurfaceClick);
339
- shadow.append(style, this.surface);
316
+ this.surface.addEventListener("mousedown", (e) => {
317
+ if (e.button === 0 && this.#comboAnchor) e.preventDefault();
318
+ });
319
+ shadow.append(this.surface);
320
+ }
321
+ #connectedDocument;
322
+ #observedAnchor;
323
+ #unwatchTrigger;
324
+ #stateFrame;
325
+ #positionFrame;
326
+ get #coordination() {
327
+ return coordination(this.#connectedDocument ?? this.doc);
328
+ }
329
+ #closeAll() {
330
+ const state = this.#coordination;
331
+ for (let i = state.stack.length - 1; i >= 0; i--) {
332
+ const menu = state.stack[i];
333
+ if (menu.isOpen && !menu._dismissNotified) menu.emitChange("closed");
334
+ menu._doHide();
335
+ }
336
+ state.stack.length = 0;
337
+ state.unbind();
340
338
  }
341
339
  connectedCallback() {
340
+ this.#connectedDocument = this.doc;
342
341
  ensureMenuItemKeyListener(this.doc);
343
342
  this.#syncStopPropagation();
344
343
  this.#syncAccessibilityRegions();
345
- const anchor = this.triggerAnchor;
346
- if (anchor) {
347
- anchorToMenu.set(anchor, this);
348
- lazyObserver?.observe(anchor);
344
+ this.#syncTriggerAnchor();
345
+ if (!this.isSubmenu && this.parentNode) {
346
+ this.#unwatchTrigger = this.#coordination.watchParent(this.parentNode, () => this.#syncTriggerAnchor());
349
347
  }
350
348
  this.#syncPopupAriaRelations();
351
349
  queueMicrotask(() => {
352
350
  if (this.isConnected) this.#syncPopupAriaRelations();
353
351
  });
354
- if (this.hasAttribute("state")) requestAnimationFrame(() => this.syncState());
352
+ if (this.hasAttribute("state")) this.#stateFrame = this.view.requestAnimationFrame(() => {
353
+ this.#stateFrame = void 0;
354
+ this.syncState();
355
+ });
355
356
  }
356
357
  disconnectedCallback() {
358
+ this.#unwatchTrigger?.();
359
+ this.#unwatchTrigger = void 0;
360
+ const oldView = this.#connectedDocument?.defaultView;
361
+ if (this.#stateFrame !== void 0) oldView?.cancelAnimationFrame(this.#stateFrame);
362
+ this.#stateFrame = void 0;
357
363
  this.hide(false);
358
364
  this.teardownListeners();
359
365
  this.cancelOpenTimer();
@@ -361,11 +367,39 @@ class AMenuElement extends HTMLElementBase {
361
367
  this.accessibilityObserver?.disconnect();
362
368
  this.accessibilityObserver = void 0;
363
369
  this.#clearPopupAriaRelations();
364
- const anchor = this.triggerAnchor;
365
- if (anchor && anchorToMenu.get(anchor) === this) {
366
- anchorToMenu.delete(anchor);
367
- lazyObserver?.unobserve(anchor);
370
+ if (this.#observedAnchor) this.#coordination.unobserve(this, this.#observedAnchor);
371
+ this.#observedAnchor = void 0;
372
+ this.#connectedDocument = void 0;
373
+ }
374
+ /** Rebind only when the authored sibling node changes, after reconciliation.
375
+ * Keep open state intact; focus remains with the application replacing it. */
376
+ #syncTriggerAnchor() {
377
+ if (!this.isConnected) return;
378
+ const anchor = this.triggerAnchor ?? void 0;
379
+ if (anchor === this.#observedAnchor) return;
380
+ if (this.#observedAnchor) this.#coordination.unobserve(this, this.#observedAnchor);
381
+ this.teardownListeners();
382
+ this.listening = false;
383
+ this.cancelOpenTimer();
384
+ this.cancelCloseTimer();
385
+ this.#pendingComboActivate = null;
386
+ this.#observedAnchor = anchor;
387
+ if (anchor) this.#coordination.observe(this, anchor);
388
+ if (this._shown) {
389
+ if (this.#searchField) this.startComboObserver();
390
+ else {
391
+ this.comboObserver?.disconnect();
392
+ this.comboObserver = void 0;
393
+ if (this.activeItem) this.setActive(null);
394
+ }
395
+ if (this.#anchorRelative) {
396
+ if (anchor) {
397
+ this.position();
398
+ this.#startAnchorTracking();
399
+ } else this.#stopAnchorTracking();
400
+ }
368
401
  }
402
+ this.#syncPopupAriaRelations();
369
403
  }
370
404
  attributeChangedCallback(name) {
371
405
  if (name === "state") {
@@ -482,7 +516,7 @@ class AMenuElement extends HTMLElementBase {
482
516
  /** Custom menu rows that support the combobox cursor and selection state. */
483
517
  focusableItems() {
484
518
  return this.navigableItems().filter(
485
- (el) => el instanceof AMenuItemElement
519
+ (el) => isCustomMenuItem(el)
486
520
  );
487
521
  }
488
522
  /** The rows the arrow keys / Home / End / initial-focus step between:
@@ -529,15 +563,21 @@ class AMenuElement extends HTMLElementBase {
529
563
  scrollItemIntoView(item) {
530
564
  const c = this.scrollEl;
531
565
  if (!c) return;
532
- const pad = 4;
566
+ const style = this.view.getComputedStyle(c);
567
+ const padding = (value) => {
568
+ const amount = Number.parseFloat(value);
569
+ return Number.isFinite(amount) ? amount * (value.endsWith("%") ? c.clientHeight / 100 : 1) : 0;
570
+ };
533
571
  const cr = c.getBoundingClientRect();
572
+ const top = cr.top + c.clientTop + padding(style.scrollPaddingTop);
573
+ const bottom = cr.top + c.clientTop + c.clientHeight - padding(style.scrollPaddingBottom);
534
574
  const ir = item.getBoundingClientRect();
535
- if (ir.top < cr.top + pad) c.scrollTop -= cr.top + pad - ir.top;
536
- else if (ir.bottom > cr.bottom - pad) c.scrollTop += ir.bottom - cr.bottom + pad;
575
+ if (ir.top < top) c.scrollTop -= top - ir.top;
576
+ else if (ir.bottom > bottom) c.scrollTop += ir.bottom - bottom;
537
577
  }
538
578
  /** Fade the scrolling body's content into the top / bottom edges — but only on the
539
579
  * side that actually has more to scroll, so a short (non-scrolling) menu and the
540
- * true top / bottom stay crisp. Drives the `--fade-*` / `--gap-top` vars the
580
+ * true top / bottom stay crisp. Drives the private fade/gap properties the
541
581
  * `.scroll` mask reads; runs on scroll and after every (re)position.
542
582
  * Shadow-internal only. */
543
583
  updateScrollFade() {
@@ -545,9 +585,9 @@ class AMenuElement extends HTMLElementBase {
545
585
  if (!el) return;
546
586
  const top = el.scrollTop > 1;
547
587
  const bottom = el.scrollTop + el.clientHeight < el.scrollHeight - 1;
548
- el.style.setProperty("--fade-top", top ? "16px" : "0px");
549
- el.style.setProperty("--gap-top", top ? "3px" : "0px");
550
- el.style.setProperty("--fade-bottom", bottom ? "14px" : "0px");
588
+ el.style.setProperty("--_menu-fade-top", top ? "16px" : "0px");
589
+ el.style.setProperty("--_menu-gap-top", top ? "3px" : "0px");
590
+ el.style.setProperty("--_menu-fade-bottom", bottom ? "14px" : "0px");
551
591
  }
552
592
  /* ------------------------------ combobox mode ------------------------------ */
553
593
  /** Combobox-anchor mode: the trigger anchor ITSELF is the filter field (it
@@ -645,13 +685,17 @@ class AMenuElement extends HTMLElementBase {
645
685
  this.activeItem = item;
646
686
  if (item) {
647
687
  item.active = true;
648
- item.scrollIntoView?.({ block: "nearest" });
688
+ this.scrollItemIntoView(item);
649
689
  }
650
690
  this.#syncPopupAriaRelations();
651
691
  this.dispatchEvent(
652
692
  new CustomEvent("activedescendant", { detail: { id: item?.id ?? null } })
653
693
  );
654
694
  }
695
+ #clearComboCursor() {
696
+ this.#pendingComboActivate = null;
697
+ if (this.activeItem) this.setActive(null);
698
+ }
655
699
  /** Re-seat the cursor on the first option — but only once the filter has input.
656
700
  * An empty filter (e.g. right after opening) shows NO active row, so the first
657
701
  * ArrowDown is what steps onto the list; typing then keeps the top match active.
@@ -659,9 +703,10 @@ class AMenuElement extends HTMLElementBase {
659
703
  * the seat target — the cursor lands on the first real option — but they stay
660
704
  * arrow-reachable. */
661
705
  resetActive() {
706
+ if (this.#comboAnchor) return this.#clearComboCursor();
707
+ this.#pendingComboActivate = null;
662
708
  const q = this.#searchField?.value;
663
709
  if (!q || !q.trim()) return this.setActive(null);
664
- if (this.#comboAnchor) return this.setActive(null);
665
710
  const items = this.focusableItems();
666
711
  this.setActive(items.find((it) => !it.hasAttribute("data-menu-skip-active")) ?? items[0] ?? null);
667
712
  }
@@ -672,6 +717,7 @@ class AMenuElement extends HTMLElementBase {
672
717
  * instead. Any other key falls through to the input (typing). */
673
718
  handleComboKey(e) {
674
719
  const items = this.focusableItems();
720
+ if (this.activeItem && !items.includes(this.activeItem)) this.#clearComboCursor();
675
721
  const idx = this.activeItem ? items.indexOf(this.activeItem) : -1;
676
722
  switch (e.key) {
677
723
  case "ArrowDown":
@@ -709,10 +755,14 @@ class AMenuElement extends HTMLElementBase {
709
755
  /** Combobox keyup: activate the item a filter-field Enter armed. Paired with the
710
756
  * keydown pre-request in `handleComboKey` so lazy content resolves in the gap. */
711
757
  handleKeyUp(e) {
758
+ if (isComposingKey(e)) {
759
+ this.#pendingComboActivate = null;
760
+ return;
761
+ }
712
762
  if (e.key !== "Enter") return;
713
763
  const el = this.#pendingComboActivate;
714
764
  this.#pendingComboActivate = null;
715
- el?.click();
765
+ if (el && this.focusableItems().includes(el)) el.click();
716
766
  }
717
767
  /* ============================ open / close ============================ */
718
768
  /** Public imperative API. Routes through the same intent path as the
@@ -747,6 +797,7 @@ class AMenuElement extends HTMLElementBase {
747
797
  * `statechange`; applies the visibility itself ONLY when uncontrolled and
748
798
  * not vetoed — a controlled menu waits for the consumer to flip `state`. */
749
799
  requestOpen(opts) {
800
+ if (!this.isConnected) return;
750
801
  if (this._shown) {
751
802
  this.show(opts?.coord, opts?.viaKeyboard, opts?.originEvent);
752
803
  return;
@@ -759,14 +810,15 @@ class AMenuElement extends HTMLElementBase {
759
810
  * uncontrolled and not vetoed. */
760
811
  requestClose(originEvent) {
761
812
  if (!this._shown) return;
762
- const idx = openStack.indexOf(this);
813
+ const stack = this.#coordination.stack;
814
+ const idx = stack.indexOf(this);
763
815
  if (idx !== -1) {
764
- for (let i = openStack.length - 1; i > idx; i--) {
765
- const m = openStack[i];
816
+ for (let i = stack.length - 1; i > idx; i--) {
817
+ const m = stack[i];
766
818
  if (m.isOpen && !m._dismissNotified) m.emitChange("closed");
767
819
  m._doHide();
768
820
  }
769
- openStack.length = idx + 1;
821
+ stack.length = idx + 1;
770
822
  }
771
823
  const ok = this.emitChange("closed", { originEvent });
772
824
  if (this.isControlled) {
@@ -778,50 +830,51 @@ class AMenuElement extends HTMLElementBase {
778
830
  /** Apply OPEN to the DOM (no event) — used by uncontrolled intent and by the
779
831
  * controlled `state` sync. */
780
832
  show(coord, viaKeyboard = false, _originEvent) {
833
+ const stack = this.#coordination.stack;
781
834
  if (this.isSubmenu) {
782
835
  const parent = this.#ownerMenu;
783
836
  if (parent) {
784
- const pidx = openStack.indexOf(parent);
837
+ const pidx = stack.indexOf(parent);
785
838
  if (pidx !== -1) {
786
- for (let i = openStack.length - 1; i > pidx; i--) {
787
- const menu = openStack[i];
839
+ for (let i = stack.length - 1; i > pidx; i--) {
840
+ const menu = stack[i];
788
841
  if (menu !== this && menu.isOpen && !menu._dismissNotified) menu.emitChange("closed");
789
842
  menu._doHide();
790
843
  }
791
- openStack.length = pidx + 1;
844
+ stack.length = pidx + 1;
792
845
  }
793
846
  }
794
- } else if (!openStack.includes(this)) {
847
+ } else if (!stack.includes(this)) {
795
848
  const anchor = this.triggerAnchor;
796
849
  let container;
797
850
  if (anchor)
798
- for (let i = openStack.length - 1; i >= 0; i--) {
799
- if (openStack[i] !== this && openStack[i].contains(anchor)) {
800
- container = openStack[i];
851
+ for (let i = stack.length - 1; i >= 0; i--) {
852
+ if (stack[i] !== this && stack[i].contains(anchor)) {
853
+ container = stack[i];
801
854
  break;
802
855
  }
803
856
  }
804
857
  if (container) {
805
- const idx = openStack.indexOf(container);
806
- for (let i = openStack.length - 1; i > idx; i--) {
807
- const m = openStack[i];
858
+ const idx = stack.indexOf(container);
859
+ for (let i = stack.length - 1; i > idx; i--) {
860
+ const m = stack[i];
808
861
  if (m.isOpen && !m._dismissNotified) m.emitChange("closed");
809
862
  m._doHide();
810
863
  }
811
- openStack.length = idx + 1;
864
+ stack.length = idx + 1;
812
865
  } else {
813
- closeAll();
866
+ this.#closeAll();
814
867
  }
815
868
  }
816
- if (openStack.includes(this)) {
869
+ if (stack.includes(this)) {
817
870
  this.position(coord);
818
871
  return;
819
872
  }
820
- const wasEmpty = openStack.length === 0;
873
+ const wasEmpty = stack.length === 0;
821
874
  this._doShow(coord, !wasEmpty);
822
- openStack.push(this);
875
+ stack.push(this);
823
876
  if (wasEmpty) {
824
- bindDocListeners(this.doc, this.view);
877
+ this.#coordination.bind(this.doc, this.view);
825
878
  }
826
879
  const search = this.#searchField;
827
880
  if (search) {
@@ -833,40 +886,39 @@ class AMenuElement extends HTMLElementBase {
833
886
  this.seatInitialFocus(viaKeyboard);
834
887
  }
835
888
  }
836
- /** While a filter field is open, the visible option list changes as the user
837
- * types (the consumer re-renders the matches); re-seat the cursor on the first
838
- * match. `childList` only — an item toggling its own `selected` (multi-select)
839
- * mutates deep in its subtree, not this menu's direct children, so it won't
840
- * spuriously reset the cursor. */
889
+ /** Reconcile changed results while filtering. Slotted filters observe direct
890
+ * children only; autocomplete also observes row content and disabled changes. */
841
891
  startComboObserver() {
842
892
  this.comboObserver?.disconnect();
843
- this.comboObserver = new this.view.MutationObserver(() => {
893
+ this.comboObserver = new this.view.MutationObserver((records) => {
844
894
  if (!this._shown || !this.#searchField) return;
895
+ if (this.#comboAnchor && !records.some(
896
+ (record) => record.type !== "attributes" || isMenuItemEl(record.target) && record.target.parentElement === this
897
+ )) return;
845
898
  this.resetActive();
846
899
  this.position(void 0, false, true);
847
900
  });
848
- this.comboObserver.observe(this, { childList: true });
901
+ this.comboObserver.observe(this, {
902
+ childList: true,
903
+ ...this.#comboAnchor ? { characterData: true, attributes: true, subtree: true, attributeFilter: ["disabled"] } : {}
904
+ });
849
905
  }
850
906
  /** Apply CLOSE without notifying this menu's owner. Notify nested menus so
851
907
  * their controlled state follows the closing parent. */
852
908
  hide(notifyDescendants = true) {
853
- this.comboObserver?.disconnect();
854
- this.comboObserver = void 0;
855
- this.setActive(null);
856
- this._flippedTop = null;
857
- this._flippedSide = null;
858
- const idx = openStack.indexOf(this);
909
+ const stack = this.#coordination.stack;
910
+ const idx = stack.indexOf(this);
859
911
  if (idx === -1) {
860
912
  if (this._shown) this._doHide();
861
913
  return;
862
914
  }
863
- for (let i = openStack.length - 1; i >= idx; i--) {
864
- const menu = openStack[i];
915
+ for (let i = stack.length - 1; i >= idx; i--) {
916
+ const menu = stack[i];
865
917
  if (notifyDescendants && i > idx && menu.isOpen && !menu._dismissNotified) menu.emitChange("closed");
866
918
  menu._doHide();
867
919
  }
868
- openStack.length = idx;
869
- if (openStack.length === 0) unbindDocListeners();
920
+ stack.length = idx;
921
+ if (stack.length === 0) this.#coordination.unbind();
870
922
  }
871
923
  /** Shadow-only show: open the popover and position it. `instant` positions
872
924
  * synchronously (no rAF), so a menu opening over an already-visible one is
@@ -893,15 +945,24 @@ class AMenuElement extends HTMLElementBase {
893
945
  }
894
946
  /** Shadow-only hide. */
895
947
  _doHide() {
948
+ this.comboObserver?.disconnect();
949
+ this.comboObserver = void 0;
950
+ this.#clearComboCursor();
951
+ this._flippedTop = null;
952
+ this._flippedSide = null;
953
+ clearTimeout(this.typeTimer);
954
+ this.typeTimer = void 0;
955
+ this.typeBuffer = "";
956
+ if (this.#positionFrame !== void 0) (this.#connectedDocument?.defaultView ?? this.view).cancelAnimationFrame(this.#positionFrame);
957
+ this.#positionFrame = void 0;
896
958
  const parent = this.parentElement?.closest("a-menu");
897
- if (this._shown && parent && mouseX !== null && mouseInside(this.surface.getBoundingClientRect())) {
959
+ if (this._shown && parent && this.#coordination.mouseX !== null && this.#coordination.mouseInside(this.surface.getBoundingClientRect())) {
898
960
  for (let ancestor = parent; ancestor; ancestor = ancestor.parentElement?.closest("a-menu")) {
899
- if (!ancestor.isSubmenu || !ancestor.#isHover || !ancestor._shown || ancestor.surface.matches(":hover")) continue;
900
- ancestor.#holdHoverClose = true;
901
- ancestor.cancelCloseTimer();
961
+ ancestor.holdHoverClose?.();
902
962
  }
903
963
  }
904
964
  this.#stopAnchorTracking();
965
+ this.#anchorRelative = false;
905
966
  if (this.surface.isConnected && this._shown) this.surface.hidePopover();
906
967
  this._shown = false;
907
968
  this.#holdHoverClose = false;
@@ -933,7 +994,8 @@ class AMenuElement extends HTMLElementBase {
933
994
  * opening point. */
934
995
  #startAnchorTracking(coord) {
935
996
  this.#stopAnchorTracking();
936
- if (coord || this.#isCoord) return;
997
+ this.#anchorRelative = !coord && !this.#isCoord;
998
+ if (!this.#anchorRelative) return;
937
999
  const anchor = this.triggerAnchor;
938
1000
  if (!anchor) return;
939
1001
  this.#lastAnchorRect = this.#anchorRectValues(anchor);
@@ -970,7 +1032,7 @@ class AMenuElement extends HTMLElementBase {
970
1032
  }
971
1033
  #stopAnchorTracking() {
972
1034
  if (this.#anchorTrackingFrame !== void 0) {
973
- this.view.cancelAnimationFrame(this.#anchorTrackingFrame);
1035
+ (this.#connectedDocument?.defaultView ?? this.view).cancelAnimationFrame(this.#anchorTrackingFrame);
974
1036
  this.#anchorTrackingFrame = void 0;
975
1037
  }
976
1038
  this.#lastAnchorRect = void 0;
@@ -995,7 +1057,7 @@ class AMenuElement extends HTMLElementBase {
995
1057
  let left = MARGIN;
996
1058
  let top = MARGIN;
997
1059
  if (coord) {
998
- surface.style.maxHeight = `${Math.max(MIN_HEIGHT, vh - 2 * MARGIN)}px`;
1060
+ surface.style.setProperty("--_menu-max-height", `${Math.max(MIN_HEIGHT, vh - 2 * MARGIN)}px`);
999
1061
  const box = surface.getBoundingClientRect();
1000
1062
  left = coord[0];
1001
1063
  if (left + box.width > vw - MARGIN) left = vw - box.width - MARGIN;
@@ -1006,7 +1068,7 @@ class AMenuElement extends HTMLElementBase {
1006
1068
  } else if (this.isSubmenu) {
1007
1069
  const it = this.triggerAnchor ? anchorRect(this.triggerAnchor) : null;
1008
1070
  if (!it) return;
1009
- surface.style.maxHeight = `${Math.max(MIN_HEIGHT, vh - 2 * MARGIN)}px`;
1071
+ surface.style.setProperty("--_menu-max-height", `${Math.max(MIN_HEIGHT, vh - 2 * MARGIN)}px`);
1010
1072
  const box = surface.getBoundingClientRect();
1011
1073
  left = it.right + this.#offset;
1012
1074
  if (left + box.width > vw - MARGIN) {
@@ -1027,7 +1089,7 @@ class AMenuElement extends HTMLElementBase {
1027
1089
  const p = this.#placement;
1028
1090
  if (p.startsWith("right") || p.startsWith("left")) {
1029
1091
  surface.style.removeProperty("--_anchor-width");
1030
- surface.style.maxHeight = `${Math.max(MIN_HEIGHT, vh - 2 * MARGIN)}px`;
1092
+ surface.style.setProperty("--_menu-max-height", `${Math.max(MIN_HEIGHT, vh - 2 * MARGIN)}px`);
1031
1093
  const box = surface.getBoundingClientRect();
1032
1094
  const spaceRight = vw - a.right - 2 * MARGIN;
1033
1095
  const spaceLeft = a.left - 2 * MARGIN;
@@ -1046,7 +1108,7 @@ class AMenuElement extends HTMLElementBase {
1046
1108
  const spaceBelow = vh - a.bottom - 2 * MARGIN;
1047
1109
  const spaceAbove = a.top - 2 * MARGIN;
1048
1110
  let onTop = p.startsWith("top");
1049
- surface.style.maxHeight = "";
1111
+ surface.style.removeProperty("--_menu-max-height");
1050
1112
  const natural = surface.scrollHeight;
1051
1113
  if (reanchor && this._flippedTop !== null) {
1052
1114
  onTop = this._flippedTop;
@@ -1056,7 +1118,7 @@ class AMenuElement extends HTMLElementBase {
1056
1118
  }
1057
1119
  this._flippedTop = onTop;
1058
1120
  const space = onTop ? spaceAbove : spaceBelow;
1059
- surface.style.maxHeight = `${Math.max(MIN_HEIGHT, Math.floor(space))}px`;
1121
+ surface.style.setProperty("--_menu-max-height", `${Math.max(MIN_HEIGHT, Math.floor(space))}px`);
1060
1122
  const box = surface.getBoundingClientRect();
1061
1123
  const align = p.endsWith("end") ? "end" : p.endsWith("start") ? "start" : "center";
1062
1124
  left = align === "center" ? a.left + a.width / 2 - box.width / 2 : align === "end" ? a.right - box.width : a.left;
@@ -1075,8 +1137,13 @@ class AMenuElement extends HTMLElementBase {
1075
1137
  this.#followGap = appliedTop - anchorRect(this.triggerAnchor).top;
1076
1138
  }
1077
1139
  };
1140
+ if (this.#positionFrame !== void 0) this.view.cancelAnimationFrame(this.#positionFrame);
1141
+ this.#positionFrame = void 0;
1078
1142
  if (sync) run();
1079
- else requestAnimationFrame(run);
1143
+ else this.#positionFrame = this.view.requestAnimationFrame(() => {
1144
+ this.#positionFrame = void 0;
1145
+ run();
1146
+ });
1080
1147
  }
1081
1148
  /* ====================== click / close contract ====================== */
1082
1149
  /**
@@ -1097,20 +1164,20 @@ class AMenuElement extends HTMLElementBase {
1097
1164
  onSurfaceClick = (e) => {
1098
1165
  for (const node of e.composedPath()) {
1099
1166
  if (node === this.surface) break;
1100
- if (node instanceof AMenuElement && node !== this) return;
1167
+ if (isElement(node) && node.localName === "a-menu" && node !== this) return;
1101
1168
  }
1102
1169
  let contain = false;
1103
1170
  let exemptTrigger = false;
1104
1171
  for (const node of e.composedPath()) {
1105
1172
  if (node === this.surface) break;
1106
- if (!(node instanceof Element)) continue;
1173
+ if (!isElement(node)) continue;
1107
1174
  if (node.matches(DECLARATIVE_TRIGGER)) exemptTrigger = true;
1108
- if (node instanceof AMenuItemElement || node.hasAttribute("data-menu-close")) contain = true;
1175
+ if (isCustomMenuItem(node) || node.hasAttribute("data-menu-close")) contain = true;
1109
1176
  }
1110
1177
  if (contain && !exemptTrigger) e.stopPropagation();
1111
1178
  for (const node of e.composedPath()) {
1112
1179
  if (node === this.surface) break;
1113
- if (node instanceof AMenuItemElement) {
1180
+ if (isCustomMenuItem(node)) {
1114
1181
  if (!node.hasAttribute("disabled") && !node.querySelector(":scope > a-menu") && node.closest("a-menu") === this) {
1115
1182
  node.dispatchEvent(
1116
1183
  new MouseEvent("menuselect", {
@@ -1127,9 +1194,9 @@ class AMenuElement extends HTMLElementBase {
1127
1194
  }
1128
1195
  for (const node of e.composedPath()) {
1129
1196
  if (node === this.surface) break;
1130
- if (!(node instanceof Element)) continue;
1197
+ if (!isElement(node)) continue;
1131
1198
  if (node.hasAttribute("data-menu-open")) return;
1132
- if (node instanceof AMenuItemElement) {
1199
+ if (isCustomMenuItem(node)) {
1133
1200
  if (node.hasAttribute("disabled")) {
1134
1201
  e.preventDefault();
1135
1202
  return;
@@ -1147,7 +1214,7 @@ class AMenuElement extends HTMLElementBase {
1147
1214
  this.requestClose(e);
1148
1215
  return;
1149
1216
  }
1150
- const root = openStack[0] ?? this;
1217
+ const root = this.#coordination.stack[0] ?? this;
1151
1218
  root.requestClose(e);
1152
1219
  }
1153
1220
  /** Event types the `stop-propagation` attribute contains at the surface. A bare
@@ -1175,6 +1242,10 @@ class AMenuElement extends HTMLElementBase {
1175
1242
  * (synthesizes a click → routed through onSurfaceClick). Combobox Enter (focus
1176
1243
  * in the filter field) is handled here via handleComboKey / handleKeyUp. */
1177
1244
  handleKey(e) {
1245
+ if (isComposingKey(e)) {
1246
+ this.#pendingComboActivate = null;
1247
+ return;
1248
+ }
1178
1249
  if (isModifiedNavigationKey(e)) return;
1179
1250
  const active = this.doc.activeElement;
1180
1251
  if (e.key === "Escape") {
@@ -1244,7 +1315,7 @@ class AMenuElement extends HTMLElementBase {
1244
1315
  }
1245
1316
  }
1246
1317
  submenuOf(item) {
1247
- if (!item || !(item instanceof AMenuItemElement)) return null;
1318
+ if (!item || !isCustomMenuItem(item)) return null;
1248
1319
  return item.querySelector(":scope > a-menu");
1249
1320
  }
1250
1321
  typeahead(ch, items) {
@@ -1270,7 +1341,7 @@ class AMenuElement extends HTMLElementBase {
1270
1341
  return [r.left, r.bottom];
1271
1342
  }
1272
1343
  setupListeners() {
1273
- if (this.listening) return;
1344
+ if (this.listening || !this.isConnected) return;
1274
1345
  const anchor = this.triggerAnchor;
1275
1346
  if (!anchor) {
1276
1347
  this.listening = true;
@@ -1314,14 +1385,22 @@ class AMenuElement extends HTMLElementBase {
1314
1385
  this.listening = false;
1315
1386
  };
1316
1387
  } else if (this.#comboAnchor) {
1388
+ const onInput = () => this.#clearComboCursor();
1389
+ anchor.addEventListener("input", onInput);
1317
1390
  this.teardown = () => {
1391
+ anchor.removeEventListener("input", onInput);
1318
1392
  this.listening = false;
1319
1393
  };
1320
1394
  this.listening = true;
1321
1395
  return;
1322
1396
  } else {
1323
1397
  const onClick = (e) => {
1324
- if (anchor.matches("a-input") && e.composedPath().some((node) => node instanceof Element && node.getAttribute("slot") === "clear")) return;
1398
+ if (anchor.matches(":disabled, [disabled], [loading]")) return;
1399
+ const path = e.composedPath();
1400
+ const anchorPath = path.slice(0, path.indexOf(anchor));
1401
+ if (anchor.matches("a-input") && anchorPath.some((node) => isElement(node) && node.getAttribute("slot") === "clear")) return;
1402
+ const field = anchor.shadowRoot?.querySelector('[part~="field"]');
1403
+ if (field && !anchorPath.includes(field)) return;
1325
1404
  const viaKeyboard = e.detail === 0;
1326
1405
  const coord = this.#isCoord ? viaKeyboard ? this.#anchorCoord(anchor) : [e.clientX, e.clientY] : void 0;
1327
1406
  if (this._shown) this.requestClose(e);
@@ -1330,7 +1409,7 @@ class AMenuElement extends HTMLElementBase {
1330
1409
  const onKey = (e) => {
1331
1410
  if (isModifiedNavigationKey(e)) return;
1332
1411
  if (this._shown) return;
1333
- if (anchor.hasAttribute("disabled") || anchor.hasAttribute("loading")) return;
1412
+ if (anchor.matches(":disabled, [disabled], [loading]")) return;
1334
1413
  const arrow = e.key === "ArrowDown" || e.key === "ArrowUp";
1335
1414
  if (!arrow && anchor.matches(SELF_ACTIVATING)) return;
1336
1415
  const editable = anchor.matches(EDITABLE_FIELD);
@@ -1369,7 +1448,7 @@ class AMenuElement extends HTMLElementBase {
1369
1448
  if (!this._shown) return;
1370
1449
  if (this.#holdHoverClose) return;
1371
1450
  if (this.#hasKeyboardFocusInside) return;
1372
- if (openStack.some((m) => m !== this && (this.contains(m) || m.triggerAnchor != null && this.contains(m.triggerAnchor)))) {
1451
+ if (this.#coordination.stack.some((m) => m !== this && (this.contains(m) || m.triggerAnchor != null && this.contains(m.triggerAnchor)))) {
1373
1452
  this.cancelCloseTimer();
1374
1453
  return;
1375
1454
  }
@@ -1389,6 +1468,11 @@ class AMenuElement extends HTMLElementBase {
1389
1468
  const active = this.doc.activeElement;
1390
1469
  return !!active && this.contains(active) && active.matches(":focus-visible");
1391
1470
  }
1471
+ holdHoverClose() {
1472
+ if (!this.isSubmenu || !this.#isHover || !this._shown || this.surface.matches(":hover")) return;
1473
+ this.#holdHoverClose = true;
1474
+ this.cancelCloseTimer();
1475
+ }
1392
1476
  cancelOpenTimer() {
1393
1477
  if (this.openTimer !== void 0) {
1394
1478
  clearTimeout(this.openTimer);