actual-css 0.2.0 → 0.3.1

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 (214) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/README.md +24 -9
  3. package/dist/actual-themes.min.css +1 -0
  4. package/dist/actual.css +1532 -1114
  5. package/dist/actual.js +1 -3
  6. package/dist/actual.min.css +1 -1
  7. package/dist/optional.css +1048 -0
  8. package/dist/optional.min.css +1 -0
  9. package/docs/design-notes/browser-support.md +94 -14
  10. package/docs/design-notes/cascade-layer.md +7 -6
  11. package/docs/design-notes/enhancement-contract.md +11 -14
  12. package/docs/design-notes/forced-colors.md +105 -0
  13. package/docs/design-notes/naming.md +2 -1
  14. package/docs/design-notes/platform-alignment.md +492 -0
  15. package/docs/design-notes/references.md +8 -0
  16. package/docs/design-notes/widget-primitives.md +37 -1
  17. package/docs/navigation.json +135 -0
  18. package/docs/pages/components/accordion.md +80 -0
  19. package/docs/pages/components/alert.md +203 -0
  20. package/docs/pages/components/aura.md +35 -0
  21. package/docs/pages/components/avatar.md +95 -0
  22. package/docs/pages/components/badge.md +102 -0
  23. package/docs/pages/components/breadcrumb.md +19 -0
  24. package/docs/pages/components/button.md +210 -0
  25. package/docs/pages/components/card.md +195 -0
  26. package/docs/pages/components/chat.md +50 -0
  27. package/docs/pages/components/dialog.md +367 -0
  28. package/docs/pages/components/drawer.md +113 -0
  29. package/docs/pages/components/fab.md +118 -0
  30. package/docs/pages/components/key.md +33 -0
  31. package/docs/pages/components/meter.md +23 -0
  32. package/docs/pages/components/navbar.md +92 -0
  33. package/docs/pages/components/pagination.md +55 -0
  34. package/docs/pages/components/progress.md +29 -0
  35. package/docs/pages/components/skeleton.md +38 -0
  36. package/docs/pages/components/spinner.md +93 -0
  37. package/docs/pages/components/table.md +102 -0
  38. package/docs/pages/enhancements/flyout.md +391 -0
  39. package/docs/pages/enhancements/overview.md +36 -0
  40. package/docs/pages/enhancements/scrollspy.md +71 -0
  41. package/docs/pages/enhancements/status-bar.md +72 -0
  42. package/docs/pages/enhancements/tabs.md +80 -0
  43. package/docs/pages/enhancements/tooltip.md +130 -0
  44. package/docs/pages/examples/overview.md +43 -0
  45. package/docs/pages/forms/choice-cards.md +102 -0
  46. package/docs/pages/forms/controls.md +32 -0
  47. package/docs/pages/forms/detached-actions.md +59 -0
  48. package/docs/pages/forms/floating-field.md +88 -0
  49. package/docs/pages/forms/form-actions.md +56 -0
  50. package/docs/pages/forms/input-policy.md +91 -0
  51. package/docs/pages/forms/joined-controls.md +37 -0
  52. package/docs/pages/forms/labels.md +42 -0
  53. package/docs/pages/forms/otp.md +50 -0
  54. package/docs/pages/forms/overview.md +188 -0
  55. package/docs/pages/forms/password.md +40 -0
  56. package/docs/pages/forms/range.md +17 -0
  57. package/docs/pages/forms/states-and-sizes.md +321 -0
  58. package/docs/pages/forms/sticky-actions.md +187 -0
  59. package/docs/pages/forms/switch.md +31 -0
  60. package/docs/pages/forms/validation.md +241 -0
  61. package/docs/{mental-model.md → pages/foundations/philosophy.md} +33 -18
  62. package/docs/pages/foundations/theming.md +99 -0
  63. package/docs/pages/foundations/tokens.md +421 -0
  64. package/docs/{typography.md → pages/foundations/typography.md} +27 -9
  65. package/docs/pages/guides/bootstrap.md +438 -0
  66. package/docs/pages/guides/introduction.md +157 -0
  67. package/docs/pages/guides/optional-components.md +33 -0
  68. package/docs/pages/guides/progressive-enhancement.md +686 -0
  69. package/docs/pages/guides/tailwind.md +455 -0
  70. package/docs/pages/layout/app-shell.md +25 -0
  71. package/docs/pages/layout/center.md +29 -0
  72. package/docs/pages/layout/cluster.md +59 -0
  73. package/docs/pages/layout/frame.md +30 -0
  74. package/docs/pages/layout/grid.md +142 -0
  75. package/docs/pages/layout/list.md +41 -0
  76. package/docs/pages/layout/media.md +28 -0
  77. package/docs/pages/layout/overview.md +37 -0
  78. package/docs/pages/layout/scroll-snap.md +54 -0
  79. package/docs/pages/layout/sidebar-layout.md +27 -0
  80. package/docs/pages/layout/stack.md +55 -0
  81. package/docs/pages/layout/switcher.md +36 -0
  82. package/docs/pages/layout/topbar.md +23 -0
  83. package/docs/pages/patterns/actions.md +52 -0
  84. package/docs/pages/patterns/code-block.md +36 -0
  85. package/docs/pages/patterns/exclusive-accordion.md +16 -0
  86. package/docs/pages/patterns/hierarchical-nav.md +26 -0
  87. package/docs/pages/patterns/icon-input.md +25 -0
  88. package/docs/pages/patterns/intent-nesting.md +3 -0
  89. package/docs/pages/patterns/navigation-list.md +235 -0
  90. package/docs/pages/patterns/overview.md +58 -0
  91. package/docs/pages/patterns/structured-lists.md +47 -0
  92. package/docs/pages/utilities/accessibility.md +34 -0
  93. package/docs/pages/utilities/flex.md +105 -0
  94. package/docs/pages/utilities/links.md +18 -0
  95. package/docs/pages/utilities/list.md +32 -0
  96. package/docs/pages/utilities/overflow.md +43 -0
  97. package/docs/pages/utilities/overview.md +18 -0
  98. package/docs/pages/utilities/shape.md +25 -0
  99. package/docs/pages/utilities/sizing-wrapping.md +115 -0
  100. package/docs/pages/utilities/spacing.md +79 -0
  101. package/docs/pages/utilities/text.md +43 -0
  102. package/llms.txt +4 -4
  103. package/package.json +21 -12
  104. package/src/css/actual.css +16 -2
  105. package/src/css/base.css +3 -5
  106. package/src/css/components/accordion.css +18 -30
  107. package/src/css/components/actions.css +4 -1
  108. package/src/css/components/alert.css +139 -102
  109. package/src/css/components/avatar.css +16 -10
  110. package/src/css/components/badge.css +69 -42
  111. package/src/css/components/breadcrumb.css +7 -15
  112. package/src/css/components/busy.css +6 -0
  113. package/src/css/components/button.css +84 -34
  114. package/src/css/components/card.css +26 -38
  115. package/src/css/components/drawer.css +30 -13
  116. package/src/css/components/flyout.css +39 -109
  117. package/src/css/components/index.css +1 -0
  118. package/src/css/components/join.css +14 -2
  119. package/src/css/components/key.css +2 -0
  120. package/src/css/components/menu.css +125 -0
  121. package/src/css/components/meter.css +0 -23
  122. package/src/css/components/modal.css +162 -7
  123. package/src/css/components/navbar.css +30 -23
  124. package/src/css/components/overline.css +8 -9
  125. package/src/css/components/pagination.css +1 -1
  126. package/src/css/components/progress.css +8 -26
  127. package/src/css/components/skeleton.css +7 -0
  128. package/src/css/components/spinner.css +6 -7
  129. package/src/css/components/status-bar.css +15 -24
  130. package/src/css/components/surface.css +2 -8
  131. package/src/css/components/tab.css +19 -48
  132. package/src/css/components/table.css +7 -5
  133. package/src/css/components/tooltip.css +14 -7
  134. package/src/css/focus.css +14 -38
  135. package/src/css/forms/choice-card.css +13 -14
  136. package/src/css/forms/choice.css +21 -27
  137. package/src/css/forms/control.css +18 -17
  138. package/src/css/forms/custom-select.css +9 -11
  139. package/src/css/forms/form-actions.css +14 -3
  140. package/src/css/forms/form.css +7 -4
  141. package/src/css/forms/index.css +4 -4
  142. package/src/css/forms/input-icon.css +30 -12
  143. package/src/css/forms/native.css +50 -11
  144. package/src/css/forms/range.css +17 -9
  145. package/src/css/forms/select.css +8 -8
  146. package/src/css/forms/switch.css +14 -12
  147. package/src/css/forms/validation.css +32 -5
  148. package/src/css/grid.css +32 -18
  149. package/src/css/intents.css +2 -2
  150. package/src/css/layout.css +45 -2
  151. package/src/css/optional/aura.css +93 -0
  152. package/src/css/optional/chat.css +105 -0
  153. package/src/css/optional/fab.css +104 -0
  154. package/src/css/optional/floating-field.css +87 -0
  155. package/src/css/optional/index.css +5 -0
  156. package/src/css/optional/layout-extra.css +7 -4
  157. package/src/css/optional/otp.css +115 -0
  158. package/src/css/optional/scroll-snap.css +6 -0
  159. package/src/css/optional/scroller.css +13 -7
  160. package/src/css/optional/typography-fluid.css +5 -0
  161. package/src/css/optional/utilities-extra.css +17 -2
  162. package/src/css/prose.css +18 -8
  163. package/src/css/reset.css +0 -10
  164. package/src/css/theme.css +21 -5
  165. package/src/css/themes/cyberpunk.css +1 -1
  166. package/src/css/themes/dim.css +3 -3
  167. package/src/css/themes/edge.css +8 -10
  168. package/src/css/themes/index.css +8 -4
  169. package/src/css/themes/{dark.css → indigo.css} +5 -4
  170. package/src/css/themes/neon.css +5 -5
  171. package/src/css/themes/square.css +0 -3
  172. package/src/css/tokens.css +74 -20
  173. package/src/css/utilities.css +8 -10
  174. package/src/css/variants.css +51 -66
  175. package/src/js/command.js +1 -3
  176. package/src/js/context-menu.js +67 -35
  177. package/src/js/dialog.js +68 -79
  178. package/src/js/dismiss.js +15 -0
  179. package/src/js/enhance.js +54 -23
  180. package/src/js/enhancement-loader.js +301 -0
  181. package/src/js/events.js +3 -0
  182. package/src/js/filter.js +3 -3
  183. package/src/js/floating.js +101 -131
  184. package/src/js/flyout.js +104 -87
  185. package/src/js/focus-group.js +116 -0
  186. package/src/js/focus.js +6 -1
  187. package/src/js/index.js +4 -2
  188. package/src/js/input.js +1 -1
  189. package/src/js/keys.js +27 -0
  190. package/src/js/mask.js +7 -6
  191. package/src/js/menu.js +87 -34
  192. package/src/js/parse-config.js +47 -0
  193. package/src/js/password.js +9 -1
  194. package/src/js/scrollspy.js +12 -4
  195. package/src/js/selectors.js +0 -7
  196. package/src/js/status.js +3 -1
  197. package/src/js/surface.js +187 -136
  198. package/src/js/tab.js +36 -53
  199. package/src/js/tooltip.js +70 -35
  200. package/src/js/validation.js +34 -8
  201. package/dist/actual.js.map +0 -31
  202. package/docs/README.md +0 -28
  203. package/docs/components.md +0 -860
  204. package/docs/design-notes/previous-work.md +0 -5
  205. package/docs/forms.md +0 -1265
  206. package/docs/javascript.md +0 -496
  207. package/docs/layout.md +0 -480
  208. package/docs/patterns.md +0 -504
  209. package/docs/reserved.md +0 -7
  210. package/docs/tokens.md +0 -455
  211. package/docs/ui.md +0 -929
  212. package/docs/utilities.md +0 -384
  213. package/src/css/dialog-fallback.css +0 -56
  214. package/src/js/dialog-fallback.js +0 -163
package/src/js/flyout.js CHANGED
@@ -1,51 +1,54 @@
1
- /*
2
- * Flyout trigger — trigger adapter over the shared action-surface runtime.
3
- *
4
- * App menus keep roving focus and menuitem activation. Nav panels reuse the
5
- * same surface lifecycle without menu semantics.
6
- */
7
-
8
- import enhance, { registerEnhancement } from "./enhance.js";
1
+ import { registerEnhancement } from "./enhance.js";
9
2
  import { focusFirstDescendant } from "./focus.js";
10
- import { focusFirstMenuItem, focusLastMenuItem, getMenuItems } from "./menu.js";
11
- import { closeSurface, isSurfaceOpen, openSurface, prepareSurface } from "./surface.js";
3
+ import { connectMenu, focusFirstMenuItem, focusLastMenuItem } from "./menu.js";
4
+ import { closeSurface, isSurfaceOpen, openSurface, retainSurface } from "./surface.js";
12
5
 
13
- // trigger -> { flyout, controller }
6
+ const panelRefs = new WeakMap();
14
7
  const triggerMap = new WeakMap();
15
- const FLYOUT_SELECTOR = '[data-enhance~="flyout"]';
16
- const FLYOUT_TRIGGER_SELECTOR = "[aria-controls][aria-expanded]";
17
8
 
18
- function openFlyout(flyout, trigger) {
19
- openSurface(flyout, { trigger, source: trigger });
9
+ function normalizeBreakpoint(raw) {
10
+ const BREAKPOINTS = { sm: 640, md: 768, lg: 1024 };
11
+ const value = Number.parseInt(BREAKPOINTS[raw] || raw, 10);
12
+ return Number.isFinite(value) ? value : undefined;
20
13
  }
21
14
 
22
- function flyoutFor(trigger) {
23
- const flyoutId = trigger.getAttribute("aria-controls");
24
- const flyout = flyoutId && trigger.ownerDocument.getElementById(flyoutId);
25
- return flyout?.matches(FLYOUT_SELECTOR) ? flyout : null;
15
+ function readFlyoutOptions(panel) {
16
+ const opts = { autoClose: "true" };
17
+ const ds = panel.dataset;
18
+ if (ds.flyoutPlacement) opts.placement = ds.flyoutPlacement;
19
+ if (ds.flyoutDistance != null) {
20
+ const d = Number.parseFloat(ds.flyoutDistance);
21
+ if (Number.isFinite(d)) opts.distance = d;
22
+ }
23
+ if (ds.flyoutMobile) opts.mobile = ds.flyoutMobile;
24
+ if (ds.flyoutBreakpoint) {
25
+ const bp = normalizeBreakpoint(ds.flyoutBreakpoint);
26
+ if (bp != null) opts.breakpoint = bp;
27
+ }
28
+ if (ds.flyoutAutoClose) opts.autoClose = ds.flyoutAutoClose;
29
+ return opts;
26
30
  }
27
31
 
28
- function isMenuFlyout(flyout) {
29
- return flyout.matches("menu");
32
+ function panelFor(trigger) {
33
+ const id = trigger.getAttribute("aria-controls");
34
+ return id ? trigger.ownerDocument.getElementById(id) : null;
30
35
  }
31
36
 
32
- function resolveCurrentFlyout(trigger, state) {
33
- if (state.flyout?.isConnected) return state.flyout;
34
-
35
- const flyout = flyoutFor(trigger);
36
- if (!flyout) return null;
37
+ function isMenuFlyout(panel) {
38
+ return panel.matches("menu") || panel.classList.contains("menu");
39
+ }
37
40
 
38
- state.flyout = flyout;
39
- prepareSurface(flyout);
40
- if (!trigger.hasAttribute("aria-haspopup") && isMenuFlyout(flyout)) {
41
- trigger.setAttribute("aria-haspopup", "menu");
42
- }
43
- return flyout;
41
+ function openFlyout(panel, trigger) {
42
+ return openSurface(panel, {
43
+ trigger,
44
+ source: trigger,
45
+ ...readFlyoutOptions(panel),
46
+ });
44
47
  }
45
48
 
46
49
  function openAndFocusPanel(flyout, trigger) {
47
- if (!isSurfaceOpen(flyout)) openFlyout(flyout, trigger);
48
- focusFirstDescendant(flyout);
50
+ if (!isSurfaceOpen(flyout) && !openFlyout(flyout, trigger)) return false;
51
+ return focusFirstDescendant(flyout);
49
52
  }
50
53
 
51
54
  function onTriggerClick(e) {
@@ -53,35 +56,34 @@ function onTriggerClick(e) {
53
56
  const state = triggerMap.get(trigger);
54
57
  if (!state) return;
55
58
  e.stopPropagation();
56
- const flyout = resolveCurrentFlyout(trigger, state);
57
- if (!flyout) return;
58
- if (isSurfaceOpen(flyout)) closeSurface(flyout);
59
- else openFlyout(flyout, trigger);
59
+ const panel = resolvePanel(trigger, state);
60
+ if (!panel) return;
61
+ if (isSurfaceOpen(panel)) closeSurface(panel);
62
+ else openFlyout(panel, trigger);
60
63
  }
61
64
 
62
65
  function onTriggerKeydown(e) {
63
66
  const trigger = e.currentTarget;
64
67
  const state = triggerMap.get(trigger);
65
68
  if (!state) return;
66
- const flyout = resolveCurrentFlyout(trigger, state);
67
- if (!flyout) return;
68
- const items = getMenuItems(flyout);
69
- const isActionList = items.length > 0;
69
+ const panel = resolvePanel(trigger, state);
70
+ if (!panel) return;
71
+ const isActionList = isMenuFlyout(panel);
70
72
 
71
73
  if (!isActionList) {
72
74
  switch (e.key) {
73
75
  case "ArrowDown":
74
76
  e.preventDefault();
75
- openAndFocusPanel(flyout, trigger);
77
+ openAndFocusPanel(panel, trigger);
76
78
  break;
77
79
  case "Enter":
78
80
  case " ":
79
81
  e.preventDefault();
80
- if (isSurfaceOpen(flyout)) closeSurface(flyout);
81
- else openAndFocusPanel(flyout, trigger);
82
+ if (isSurfaceOpen(panel)) closeSurface(panel);
83
+ else openAndFocusPanel(panel, trigger);
82
84
  break;
83
85
  case "Tab":
84
- if (isSurfaceOpen(flyout) && !e.shiftKey && focusFirstDescendant(flyout)) {
86
+ if (isSurfaceOpen(panel) && !e.shiftKey && focusFirstDescendant(panel)) {
85
87
  e.preventDefault();
86
88
  }
87
89
  break;
@@ -92,89 +94,104 @@ function onTriggerKeydown(e) {
92
94
  switch (e.key) {
93
95
  case "ArrowDown":
94
96
  e.preventDefault();
95
- if (!isSurfaceOpen(flyout)) openFlyout(flyout, trigger);
96
- focusFirstMenuItem(flyout);
97
+ if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
98
+ focusFirstMenuItem(panel);
97
99
  break;
98
100
  case "ArrowUp":
99
101
  e.preventDefault();
100
- if (!isSurfaceOpen(flyout)) openFlyout(flyout, trigger);
101
- focusLastMenuItem(flyout);
102
+ if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
103
+ focusLastMenuItem(panel);
102
104
  break;
103
105
  case "Home":
104
106
  e.preventDefault();
105
- if (!isSurfaceOpen(flyout)) openFlyout(flyout, trigger);
106
- focusFirstMenuItem(flyout);
107
+ if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
108
+ focusFirstMenuItem(panel);
107
109
  break;
108
110
  case "End":
109
111
  e.preventDefault();
110
- if (!isSurfaceOpen(flyout)) openFlyout(flyout, trigger);
111
- focusLastMenuItem(flyout);
112
+ if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
113
+ focusLastMenuItem(panel);
112
114
  break;
113
115
  case "Enter":
114
116
  case " ":
115
117
  e.preventDefault();
116
- if (!isSurfaceOpen(flyout)) {
117
- openFlyout(flyout, trigger);
118
- focusFirstMenuItem(flyout);
118
+ if (!isSurfaceOpen(panel)) {
119
+ if (openFlyout(panel, trigger)) focusFirstMenuItem(panel);
119
120
  } else {
120
- closeSurface(flyout);
121
+ closeSurface(panel);
121
122
  }
122
123
  break;
123
124
  case "Tab":
124
- if (isSurfaceOpen(flyout)) closeSurface(flyout);
125
+ if (isSurfaceOpen(panel)) closeSurface(panel);
125
126
  break;
126
127
  }
127
128
  }
128
129
 
129
130
  function connectTrigger(trigger) {
130
131
  if (triggerMap.has(trigger)) return;
131
- const flyout = flyoutFor(trigger);
132
- if (!flyout) return;
133
132
 
134
133
  const controller = new AbortController();
135
- triggerMap.set(trigger, { flyout, controller });
134
+ const state = { panel: null, controller, releaseMenu: null };
135
+ triggerMap.set(trigger, state);
136
+
137
+ resolvePanel(trigger, state);
136
138
 
137
- prepareSurface(flyout);
138
139
  if (!trigger.hasAttribute("aria-expanded")) {
139
140
  trigger.setAttribute("aria-expanded", "false");
140
141
  }
141
- if (!trigger.hasAttribute("aria-haspopup") && isMenuFlyout(flyout)) {
142
- trigger.setAttribute("aria-haspopup", "menu");
143
- }
144
142
 
145
143
  trigger.addEventListener("click", onTriggerClick, { signal: controller.signal });
146
144
  trigger.addEventListener("keydown", onTriggerKeydown, { signal: controller.signal });
147
145
  }
148
146
 
147
+ function resolvePanel(trigger, state) {
148
+ if (state.panel?.isConnected) return state.panel;
149
+
150
+ const panel = panelFor(trigger);
151
+ if (!panel) return null;
152
+
153
+ let panelEntry = panelRefs.get(panel);
154
+ if (!panelEntry) {
155
+ const release = retainSurface(panel);
156
+ panelEntry = { triggers: new Set(), release };
157
+ panelRefs.set(panel, panelEntry);
158
+ }
159
+ panelEntry.triggers.add(trigger);
160
+ state.panel = panel;
161
+
162
+ if (isMenuFlyout(panel)) {
163
+ state.releaseMenu = connectMenu(panel, {
164
+ close: (menu) => closeSurface(menu),
165
+ });
166
+ }
167
+ if (!trigger.hasAttribute("aria-haspopup") && isMenuFlyout(panel)) {
168
+ trigger.setAttribute("aria-haspopup", "menu");
169
+ }
170
+
171
+ return panel;
172
+ }
173
+
149
174
  function disconnectTrigger(trigger) {
150
175
  const state = triggerMap.get(trigger);
151
176
  if (!state) return;
177
+ state.releaseMenu?.();
152
178
  state.controller.abort();
153
- if (state.flyout?.isConnected && isSurfaceOpen(state.flyout)) {
154
- closeSurface(state.flyout, { restoreFocus: false });
179
+ if (state.panel?.isConnected && isSurfaceOpen(state.panel)) {
180
+ closeSurface(state.panel, { restoreFocus: false });
155
181
  }
156
182
  triggerMap.delete(trigger);
157
- }
158
183
 
159
- function connectFlyout(flyout) {
160
- if (!flyout.id) return;
161
- const triggers = flyout.ownerDocument.querySelectorAll(
162
- `${FLYOUT_TRIGGER_SELECTOR}[aria-controls="${CSS.escape(flyout.id)}"]`,
163
- );
164
- for (const trigger of triggers) {
165
- if (!triggerMap.has(trigger)) connectTrigger(trigger);
184
+ const panelEntry = state.panel ? panelRefs.get(state.panel) : undefined;
185
+ if (panelEntry) {
186
+ panelEntry.triggers.delete(trigger);
187
+ if (panelEntry.triggers.size === 0) {
188
+ panelEntry.release();
189
+ panelRefs.delete(state.panel);
190
+ }
166
191
  }
167
192
  }
168
193
 
169
- enhance({
170
- [FLYOUT_TRIGGER_SELECTOR]: (trigger) => {
171
- connectTrigger(trigger);
172
- return () => disconnectTrigger(trigger);
173
- },
174
- });
175
-
176
- registerEnhancement("flyout", (flyout) => {
177
- connectFlyout(flyout);
178
- // Surface owns its own teardown; the marker enhancer in surface.js
179
- // calls disconnectSurface(el, { restore: false }) on sweep.
194
+ registerEnhancement("flyout", (trigger) => {
195
+ connectTrigger(trigger);
196
+ return () => disconnectTrigger(trigger);
180
197
  });
@@ -0,0 +1,116 @@
1
+ import { itemForKey } from "./keys.js";
2
+
3
+ const ORIENTATIONS = new Set(["horizontal", "vertical"]);
4
+
5
+ function restoreTabIndex(item, value) {
6
+ if (value == null) item.removeAttribute("tabindex");
7
+ else item.setAttribute("tabindex", value);
8
+ }
9
+
10
+ export function connectFocusGroup(root, opts = {}) {
11
+ if (!root?.addEventListener || !root?.contains) {
12
+ throw new TypeError("connectFocusGroup() requires a root element.");
13
+ }
14
+ if (typeof opts.getItems !== "function") {
15
+ throw new TypeError("connectFocusGroup() requires getItems().");
16
+ }
17
+
18
+ const orientation = opts.orientation || "horizontal";
19
+ if (!ORIENTATIONS.has(orientation)) {
20
+ throw new TypeError(`Invalid focus-group orientation: ${orientation}`);
21
+ }
22
+
23
+ const controller = new AbortController();
24
+ const externalSignal = opts.signal || null;
25
+ const originalTabIndex = new Map();
26
+ let remembered = null;
27
+ let disconnected = false;
28
+
29
+ function getItems() {
30
+ const seen = new Set();
31
+ return [...(opts.getItems() || [])].filter((item) => {
32
+ if (typeof item?.focus !== "function" || !root.contains(item) || seen.has(item)) return false;
33
+ seen.add(item);
34
+ return true;
35
+ });
36
+ }
37
+
38
+ function restore(item) {
39
+ if (!originalTabIndex.has(item)) return;
40
+ restoreTabIndex(item, originalTabIndex.get(item));
41
+ originalTabIndex.delete(item);
42
+ }
43
+
44
+ function sync(preferred = remembered) {
45
+ if (disconnected) return [];
46
+
47
+ const items = getItems();
48
+ const itemSet = new Set(items);
49
+ for (const item of originalTabIndex.keys()) {
50
+ if (!itemSet.has(item)) restore(item);
51
+ }
52
+
53
+ const active = root.ownerDocument.activeElement;
54
+ const target =
55
+ (itemSet.has(preferred) && preferred) ||
56
+ (itemSet.has(active) && active) ||
57
+ items.find((item) => item.tabIndex === 0) ||
58
+ items[0] ||
59
+ null;
60
+
61
+ for (const item of items) {
62
+ if (!originalTabIndex.has(item)) {
63
+ originalTabIndex.set(item, item.getAttribute("tabindex"));
64
+ }
65
+ const tabIndex = item === target ? 0 : -1;
66
+ item.tabIndex = tabIndex;
67
+ }
68
+ remembered = target;
69
+ return items;
70
+ }
71
+
72
+ function onFocusIn(event) {
73
+ const items = getItems();
74
+ if (items.includes(event.target)) sync(event.target);
75
+ }
76
+
77
+ function onKeydown(event) {
78
+ const items = sync();
79
+ const current = root.ownerDocument.activeElement;
80
+ if (!items.includes(current)) return;
81
+
82
+ const style = root.ownerDocument.defaultView?.getComputedStyle?.(root);
83
+ const direction = style?.direction === "rtl" ? "rtl" : "ltr";
84
+ const target = itemForKey(items, current, event.key, {
85
+ orientation,
86
+ wrap: opts.wrap === true,
87
+ direction,
88
+ });
89
+ if (!target) return;
90
+
91
+ event.preventDefault();
92
+ sync(target);
93
+ target.focus();
94
+ }
95
+
96
+ root.addEventListener("focusin", onFocusIn, { signal: controller.signal });
97
+ root.addEventListener("keydown", onKeydown, { signal: controller.signal });
98
+
99
+ sync();
100
+
101
+ function disconnect() {
102
+ if (disconnected) return;
103
+ disconnected = true;
104
+ externalSignal?.removeEventListener("abort", disconnect);
105
+ controller.abort();
106
+ for (const item of originalTabIndex.keys()) restore(item);
107
+ remembered = null;
108
+ }
109
+
110
+ if (externalSignal) {
111
+ if (externalSignal.aborted) disconnect();
112
+ else externalSignal.addEventListener("abort", disconnect, { once: true });
113
+ }
114
+
115
+ return { sync, disconnect };
116
+ }
package/src/js/focus.js CHANGED
@@ -12,7 +12,12 @@ const FOCUSABLE_SELECTOR = [
12
12
  export function isElementVisible(el) {
13
13
  if (!el || el.hidden) return false;
14
14
  if (typeof el.checkVisibility === "function") return el.checkVisibility();
15
- return el.getClientRects().length > 0;
15
+ if (el.getClientRects().length === 0) return false;
16
+ // checkVisibility() covers visibility:hidden; without it, read the
17
+ // computed direction of the cascade so a hidden element is not focusable.
18
+ const style = el.ownerDocument?.defaultView?.getComputedStyle?.(el);
19
+ if (!style) return true;
20
+ return style.visibility !== "hidden" && style.visibility !== "collapse";
16
21
  }
17
22
 
18
23
  function isFocusable(el) {
package/src/js/index.js CHANGED
@@ -14,8 +14,7 @@
14
14
  import "./flyout.js";
15
15
  import "./context-menu.js";
16
16
  import "./dialog.js";
17
- // Legacy <dialog> shim for browsers below Baseline 2023; omit in custom builds.
18
- import "./dialog-fallback.js";
17
+ import "./dismiss.js";
19
18
  import "./tab.js";
20
19
  import "./tooltip.js";
21
20
  import "./scrollspy.js";
@@ -24,3 +23,6 @@ import "./mask.js";
24
23
  import "./password.js";
25
24
  import "./validation.js";
26
25
  import "./status.js";
26
+ import { watchEnhancementManifests } from "./enhancement-loader.js";
27
+
28
+ watchEnhancementManifests();
package/src/js/input.js CHANGED
@@ -7,7 +7,7 @@ export function selectionStart(el) {
7
7
  }
8
8
 
9
9
  export function setCaret(el, position) {
10
- if (document.activeElement !== el) return;
10
+ if (el.ownerDocument.activeElement !== el) return;
11
11
  if (typeof el.setSelectionRange !== "function") return;
12
12
  try {
13
13
  el.setSelectionRange(position, position);
package/src/js/keys.js CHANGED
@@ -28,3 +28,30 @@ export function nextItem(items, current, delta, opts = {}) {
28
28
  const wrapped = ((next % items.length) + items.length) % items.length;
29
29
  return items[wrapped];
30
30
  }
31
+
32
+ export function itemForKey(items, current, key, opts = {}) {
33
+ const orientation = opts.orientation || "horizontal";
34
+ const wrap = opts.wrap === true;
35
+ const direction = opts.direction || "ltr";
36
+
37
+ switch (key) {
38
+ case "Home":
39
+ return firstItem(items);
40
+ case "End":
41
+ return lastItem(items);
42
+ case "ArrowRight":
43
+ if (orientation !== "horizontal") return null;
44
+ return nextItem(items, current, direction === "rtl" ? -1 : 1, { wrap });
45
+ case "ArrowLeft":
46
+ if (orientation !== "horizontal") return null;
47
+ return nextItem(items, current, direction === "rtl" ? 1 : -1, { wrap });
48
+ case "ArrowDown":
49
+ if (orientation !== "vertical") return null;
50
+ return nextItem(items, current, 1, { wrap });
51
+ case "ArrowUp":
52
+ if (orientation !== "vertical") return null;
53
+ return nextItem(items, current, -1, { wrap });
54
+ default:
55
+ return null;
56
+ }
57
+ }
package/src/js/mask.js CHANGED
@@ -55,21 +55,22 @@ function rawMaskChars(value, mask) {
55
55
  return parseMaskValue(value, mask).raw;
56
56
  }
57
57
 
58
- function appendFollowingLiterals(mask, start) {
58
+ function appendFollowingLiterals(pattern, start) {
59
59
  let out = "";
60
- for (let i = start; i < mask.length && !isToken(mask[i]); i++) {
61
- out += mask[i];
60
+ for (let i = start; i < pattern.length && !isToken(pattern[i]); i++) {
61
+ out += pattern[i];
62
62
  }
63
63
  return out;
64
64
  }
65
65
 
66
66
  function formatRaw(raw, mask, inputType = "") {
67
67
  const autoLiteral = inputType.startsWith("insert");
68
+ const pattern = [...mask];
68
69
  let rawIndex = 0;
69
70
  let out = "";
70
71
 
71
- for (let i = 0; i < mask.length; i++) {
72
- const token = mask[i];
72
+ for (let i = 0; i < pattern.length; i++) {
73
+ const token = pattern[i];
73
74
 
74
75
  if (!isToken(token)) {
75
76
  if (rawIndex < raw.length) out += token;
@@ -89,7 +90,7 @@ function formatRaw(raw, mask, inputType = "") {
89
90
  if (!matched) break;
90
91
 
91
92
  if (autoLiteral && rawIndex >= raw.length) {
92
- out += appendFollowingLiterals(mask, i + 1);
93
+ out += appendFollowingLiterals(pattern, i + 1);
93
94
  break;
94
95
  }
95
96
  }
package/src/js/menu.js CHANGED
@@ -1,15 +1,9 @@
1
1
  import { isElementVisible } from "./focus.js";
2
- import { firstItem, lastItem, nextItem } from "./keys.js";
3
-
4
- const MENU_ITEM = 'button, a, [role="menuitem"]';
5
- // Mirrors the item contract in flyout.css: an item sits directly in the surface,
6
- // or directly in any of its <li> — bare list, or grouped section > ul > li.
7
- // `:scope li` must stay a *descendant* combinator for that second form; the
8
- // grouped flyouts in docs/ui.md lose roving focus with `:scope > li`.
9
- // Scoping is what keeps a neighbouring surface's items out: this selector only
10
- // ever runs against one surface, and hasMenuItem() resolves the clicked item
11
- // through this list rather than through a global closest().
12
- const MENU_ITEM_SELECTOR = `:scope > :is(${MENU_ITEM}), :scope li > :is(${MENU_ITEM})`;
2
+ import { connectFocusGroup } from "./focus-group.js";
3
+ import { firstItem, itemForKey, lastItem } from "./keys.js";
4
+
5
+ const MENU_ITEM_SELECTOR = ":scope > li > .menu-item";
6
+ const MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
13
7
 
14
8
  function isUsableMenuItem(item) {
15
9
  return (
@@ -25,7 +19,7 @@ export function getMenuItems(menu) {
25
19
  }
26
20
 
27
21
  function isAriaMenuItem(item) {
28
- return item.getAttribute("role") === "menuitem";
22
+ return MENU_ITEM_ROLES.has(item.getAttribute("role"));
29
23
  }
30
24
 
31
25
  function activateCurrentItem(menu) {
@@ -40,9 +34,13 @@ export function hasMenuItems(menu) {
40
34
  return getMenuItems(menu).length > 0;
41
35
  }
42
36
 
37
+ function getMenuItem(menu, target) {
38
+ const item = target?.closest?.(".menu-item");
39
+ return item?.parentElement?.parentElement === menu ? item : null;
40
+ }
41
+
43
42
  export function hasMenuItem(menu, target) {
44
- const item = target?.closest?.(MENU_ITEM);
45
- return !!item && getMenuItems(menu).includes(item);
43
+ return !!getMenuItem(menu, target);
46
44
  }
47
45
 
48
46
  export function focusFirstMenuItem(menu) {
@@ -53,28 +51,22 @@ export function focusLastMenuItem(menu) {
53
51
  lastItem(getMenuItems(menu))?.focus();
54
52
  }
55
53
 
56
- export function onMenuKeydown(e, { close }) {
54
+ export function onMenuKeydown(e, { close, navigate = true }) {
57
55
  const menu = e.currentTarget;
58
- const items = getMenuItems(menu);
59
- if (!items.length) return;
56
+ if (navigate) {
57
+ const items = getMenuItems(menu);
58
+ const target = itemForKey(items, menu.ownerDocument.activeElement, e.key, {
59
+ orientation: "vertical",
60
+ wrap: true,
61
+ });
62
+ if (target) {
63
+ e.preventDefault();
64
+ target.focus();
65
+ return;
66
+ }
67
+ }
60
68
 
61
69
  switch (e.key) {
62
- case "ArrowDown":
63
- e.preventDefault();
64
- nextItem(items, menu.ownerDocument.activeElement, 1, { wrap: true })?.focus();
65
- break;
66
- case "ArrowUp":
67
- e.preventDefault();
68
- nextItem(items, menu.ownerDocument.activeElement, -1, { wrap: true })?.focus();
69
- break;
70
- case "Home":
71
- e.preventDefault();
72
- firstItem(items)?.focus();
73
- break;
74
- case "End":
75
- e.preventDefault();
76
- lastItem(items)?.focus();
77
- break;
78
70
  case "Tab":
79
71
  close(menu);
80
72
  break;
@@ -82,8 +74,69 @@ export function onMenuKeydown(e, { close }) {
82
74
  case " ":
83
75
  if (activateCurrentItem(menu)) {
84
76
  e.preventDefault();
85
- close(menu);
86
77
  }
87
78
  break;
88
79
  }
89
80
  }
81
+
82
+ // Menu wiring is a shared, ref-counted resource: a menu may be retained by
83
+ // several triggers (or by flyout and context-menu at once), so the listeners
84
+ // and their AbortController belong to the menu, not to any single caller.
85
+ // Each retain returns a release(); the wiring is torn down on the last one.
86
+ const menuConnections = new WeakMap();
87
+
88
+ export function connectMenu(menu, { close }) {
89
+ let entry = menuConnections.get(menu);
90
+
91
+ if (!entry) {
92
+ const controller = new AbortController();
93
+ const focusGroup = menu.matches('[role="menu"]')
94
+ ? connectFocusGroup(menu, {
95
+ getItems: () => getMenuItems(menu).filter(isAriaMenuItem),
96
+ orientation: "vertical",
97
+ wrap: true,
98
+ signal: controller.signal,
99
+ })
100
+ : null;
101
+ menu.addEventListener(
102
+ "keydown",
103
+ (event) =>
104
+ onMenuKeydown(event, {
105
+ close: () => close(menu),
106
+ navigate: !focusGroup,
107
+ }),
108
+ { signal: controller.signal },
109
+ );
110
+ menu.addEventListener(
111
+ "click",
112
+ (event) => {
113
+ const item = getMenuItem(menu, event.target);
114
+ if (!item) return;
115
+
116
+ if (item.matches(":disabled, [aria-disabled='true']")) {
117
+ event.preventDefault();
118
+ // A disabled command must not reach the surface-level inside-click
119
+ // policy and dismiss the menu as if it had been activated.
120
+ event.stopPropagation();
121
+ return;
122
+ }
123
+ },
124
+ { signal: controller.signal },
125
+ );
126
+ entry = { count: 0, controller, focusGroup };
127
+ menuConnections.set(menu, entry);
128
+ }
129
+
130
+ entry.count++;
131
+ let released = false;
132
+ return () => {
133
+ if (released) return;
134
+ released = true;
135
+ entry.count--;
136
+ if (entry.count <= 0) {
137
+ entry.focusGroup?.disconnect();
138
+ entry.controller.abort();
139
+ menuConnections.delete(menu);
140
+ }
141
+ };
142
+ }