actual-css 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +214 -1
  2. package/README.md +13 -11
  3. package/components.json +668 -0
  4. package/dist/actual.css +166 -37
  5. package/dist/actual.full.css +1005 -550
  6. package/dist/actual.full.js +1 -1
  7. package/dist/actual.full.min.css +1 -1
  8. package/dist/actual.js +1 -1
  9. package/dist/actual.min.css +1 -1
  10. package/docs/design-notes/accessibility-decisions.md +33 -0
  11. package/docs/design-notes/browser-support.md +82 -25
  12. package/docs/design-notes/column-layout.md +3 -3
  13. package/docs/design-notes/css-anchor.md +75 -21
  14. package/docs/design-notes/enhancement-contract.md +14 -6
  15. package/docs/design-notes/grid.md +4 -4
  16. package/docs/design-notes/naming.md +6 -1
  17. package/docs/design-notes/platform-alignment.md +99 -2
  18. package/docs/design-notes/popover-manual-poc.md +328 -0
  19. package/docs/design-notes/surface-reparenting.md +131 -0
  20. package/docs/design-notes/tooltip-coordinate-space.md +132 -0
  21. package/docs/design-notes/widget-primitives.md +9 -6
  22. package/docs/pages/components/accordion.md +41 -2
  23. package/docs/pages/components/alert.md +80 -30
  24. package/docs/pages/components/app-navigation.md +6 -0
  25. package/docs/pages/components/avatar.md +6 -6
  26. package/docs/pages/components/badge.md +101 -16
  27. package/docs/pages/components/button.md +16 -8
  28. package/docs/pages/components/card.md +10 -10
  29. package/docs/pages/components/dialog.md +16 -6
  30. package/docs/pages/components/fab.md +4 -3
  31. package/docs/pages/components/list.md +42 -4
  32. package/docs/pages/components/navbar.md +17 -1
  33. package/docs/pages/components/pagination.md +2 -2
  34. package/docs/pages/components/steps.md +19 -0
  35. package/docs/pages/components/table.md +3 -2
  36. package/docs/pages/enhancements/flyout.md +144 -24
  37. package/docs/pages/enhancements/overview.md +25 -0
  38. package/docs/pages/enhancements/status-bar.md +20 -0
  39. package/docs/pages/enhancements/tabs.md +45 -37
  40. package/docs/pages/enhancements/tooltip.md +40 -2
  41. package/docs/pages/examples/overview.md +38 -5
  42. package/docs/pages/forms/choice-cards.md +6 -2
  43. package/docs/pages/forms/combobox.md +1 -1
  44. package/docs/pages/forms/floating-field.md +4 -3
  45. package/docs/pages/forms/joined-controls.md +22 -0
  46. package/docs/pages/forms/otp.md +2 -1
  47. package/docs/pages/forms/overview.md +1 -1
  48. package/docs/pages/forms/states-and-sizes.md +16 -18
  49. package/docs/pages/forms/sticky-actions.md +2 -2
  50. package/docs/pages/forms/switch.md +1 -1
  51. package/docs/pages/foundations/philosophy.md +67 -22
  52. package/docs/pages/foundations/theming.md +10 -2
  53. package/docs/pages/foundations/tokens.md +118 -21
  54. package/docs/pages/foundations/typography.md +35 -14
  55. package/docs/pages/guides/introduction.md +11 -9
  56. package/docs/pages/guides/progressive-enhancement.md +136 -49
  57. package/docs/pages/guides/tailwind.md +3 -2
  58. package/docs/pages/layout/media.md +1 -1
  59. package/docs/pages/layout/overview.md +11 -1
  60. package/docs/pages/layout/scroll-snap.md +3 -3
  61. package/docs/pages/layout/stack.md +17 -9
  62. package/docs/pages/patterns/icon-input.md +4 -2
  63. package/docs/pages/patterns/icon-slot.md +2 -0
  64. package/docs/pages/patterns/intent-nesting.md +1 -1
  65. package/docs/pages/utilities/overview.md +23 -2
  66. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  67. package/docs/pages/utilities/spacing.md +10 -8
  68. package/llms.txt +2 -1
  69. package/package.json +19 -8
  70. package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
  71. package/src/css/components/accordion.css +42 -3
  72. package/src/css/components/alert.css +25 -13
  73. package/src/css/components/app-nav.css +18 -8
  74. package/src/css/components/avatar.css +13 -1
  75. package/src/css/components/badge.css +78 -12
  76. package/src/css/components/breadcrumb.css +2 -3
  77. package/src/css/components/button.css +9 -16
  78. package/src/css/components/card.css +9 -2
  79. package/src/css/components/chat.css +1 -0
  80. package/src/css/components/data-list.css +1 -1
  81. package/src/css/components/drawer.css +79 -25
  82. package/src/css/components/fab.css +0 -6
  83. package/src/css/components/flyout.css +46 -63
  84. package/src/css/components/index.css +0 -1
  85. package/src/css/components/indicator.css +21 -0
  86. package/src/css/components/join.css +7 -0
  87. package/src/css/components/key.css +10 -0
  88. package/src/css/components/list.css +40 -4
  89. package/src/css/components/menu.css +18 -11
  90. package/src/css/components/modal.css +48 -33
  91. package/src/css/components/navbar.css +37 -4
  92. package/src/css/components/pagination.css +1 -0
  93. package/src/css/components/spinner.css +6 -6
  94. package/src/css/components/status-bar.css +0 -5
  95. package/src/css/components/steps.css +45 -59
  96. package/src/css/components/tab.css +29 -9
  97. package/src/css/components/table.css +5 -5
  98. package/src/css/components/tooltip.css +45 -13
  99. package/src/css/core/base.css +23 -1
  100. package/src/css/core/intents.css +17 -6
  101. package/src/css/core/theme.css +27 -0
  102. package/src/css/core/tokens.css +35 -15
  103. package/src/css/core/variants.css +64 -15
  104. package/src/css/forms/choice-card.css +4 -4
  105. package/src/css/forms/choice.css +6 -3
  106. package/src/css/forms/control.css +2 -1
  107. package/src/css/forms/floating-field.css +3 -3
  108. package/src/css/forms/form.css +4 -3
  109. package/src/css/forms/input-icon.css +20 -14
  110. package/src/css/forms/native.css +6 -1
  111. package/src/css/forms/otp.css +9 -1
  112. package/src/css/forms/range.css +2 -0
  113. package/src/css/forms/select.css +29 -9
  114. package/src/css/forms/switch.css +19 -5
  115. package/src/css/layout/app-layout.css +1 -1
  116. package/src/css/layout/grid.css +50 -76
  117. package/src/css/layout/scroll-snap.css +11 -7
  118. package/src/css/layout/scroller.css +3 -4
  119. package/src/css/layout/stack.css +2 -2
  120. package/src/css/typography/fluid.css +4 -3
  121. package/src/css/typography/prose.css +6 -5
  122. package/src/css/utilities/base.css +2 -1
  123. package/src/css/utilities/extra.css +20 -17
  124. package/src/js/context-menu.js +27 -26
  125. package/src/js/dialog.js +2 -1
  126. package/src/js/dismiss.js +5 -0
  127. package/src/js/enhance.js +34 -2
  128. package/src/js/enhancement-loader.js +36 -43
  129. package/src/js/events.js +0 -3
  130. package/src/js/filter.js +0 -1
  131. package/src/js/floating-space.js +42 -0
  132. package/src/js/flyout.js +9 -44
  133. package/src/js/focus-group.js +7 -0
  134. package/src/js/focus.js +6 -0
  135. package/src/js/menu.js +10 -1
  136. package/src/js/selectors.js +5 -9
  137. package/src/js/status.js +44 -7
  138. package/src/js/surface.js +106 -132
  139. package/src/js/tab.js +6 -1
  140. package/src/js/tooltip.js +152 -53
  141. package/src/js/validation.js +1 -1
  142. package/src/css/components/surface.css +0 -49
  143. package/src/js/floating.js +0 -432
package/src/js/surface.js CHANGED
@@ -1,15 +1,14 @@
1
+ import { autoUpdate, reposition, repositionAt } from "@lekoala/floating";
1
2
  import enhance from "./enhance.js";
2
3
  import { registerEscapeDismissal } from "./escape.js";
3
4
  import { EVENTS } from "./events.js";
4
- import { autoUpdate, reposition, repositionAt } from "./floating.js";
5
+ import { positionModeFor } from "./floating-space.js";
5
6
 
6
7
  import { CLASSES } from "./selectors.js";
7
- import { waitForTransitions } from "./transition.js";
8
8
 
9
9
  const openSurfaces = new Set();
10
10
  const surfaceMap = new WeakMap();
11
11
  const surfaceRetainers = new WeakMap();
12
- const mountedSurfaces = new WeakMap();
13
12
  const clickBoundDocuments = new WeakSet();
14
13
 
15
14
  const SURFACE_MARKER = "data-actual-surface";
@@ -38,61 +37,57 @@ function reaperFor(menu) {
38
37
 
39
38
  let reaper = reapers.get(root);
40
39
  if (!reaper) {
41
- reaper = enhance(
42
- { [`[${SURFACE_MARKER}]`]: (el) => () => disconnectSurface(el, { restore: false }) },
43
- root,
44
- );
40
+ reaper = enhance({ [`[${SURFACE_MARKER}]`]: (el) => () => disconnectSurface(el) }, root);
45
41
  reapers.set(root, reaper);
46
42
  }
47
43
  return reaper;
48
44
  }
49
45
 
50
- function getSurfaceRoot(menu, anchor) {
51
- return anchor?.closest("dialog") || menu.closest("dialog") || menu.ownerDocument.body;
52
- }
53
-
54
- function mountSurface(menu, anchor) {
55
- if (mountedSurfaces.has(menu)) return;
56
-
57
- const root = getSurfaceRoot(menu, anchor);
58
- const parent = menu.parentNode;
59
- if (!root || !parent || parent === root) return;
60
-
61
- mountedSurfaces.set(menu, { parent, next: menu.nextSibling });
62
- root.append(menu);
63
- }
64
-
65
- function restoreSurface(menu) {
66
- const mount = mountedSurfaces.get(menu);
67
- if (!mount) return;
68
-
69
- if (mount.parent.isConnected) {
70
- const next = mount.next?.parentNode === mount.parent ? mount.next : null;
71
- mount.parent.insertBefore(menu, next);
72
- } else {
73
- menu.remove();
46
+ /*
47
+ * Transport: the top layer, via popover="manual".
48
+ *
49
+ * The runtime owns the whole lifecycle, so "manual" is the only usable mode —
50
+ * it supplies promotion to the top layer and nothing else, leaving the
51
+ * dismissal policy, Escape ordering and focus restoration below untouched.
52
+ *
53
+ * The attribute is set by the runtime, not asked of the author: a surface is
54
+ * still marked up exactly as before. showPopover/hidePopover throw on an
55
+ * out-of-order call (already open, not connected), which is a state Actual's
56
+ * own guards should already have excluded — the try/catch is there so a
57
+ * surprising DOM does not take the lifecycle down with it.
58
+ */
59
+ function showTransport(menu) {
60
+ try {
61
+ menu.showPopover();
62
+ return true;
63
+ } catch {
64
+ return false;
74
65
  }
75
-
76
- mountedSurfaces.delete(menu);
77
66
  }
78
67
 
79
- function shouldUseSheet(menu, state) {
80
- const mobile = state.mobile || "auto";
81
- if (mobile === "none" || mobile === "anchored") return false;
82
- if (mobile === "sheet") return true;
83
-
84
- const view = menu.ownerDocument?.defaultView;
85
- if (!view || typeof view.matchMedia !== "function") {
86
- return false;
68
+ function hideTransport(menu) {
69
+ try {
70
+ menu.hidePopover();
71
+ } catch {
72
+ /* Already closed or detached; nothing left to hide. */
87
73
  }
88
-
89
- const breakpoint = state.breakpoint ?? 768;
90
- return (
91
- view.matchMedia("(pointer: coarse)").matches &&
92
- view.matchMedia(`(max-width: ${breakpoint}px)`).matches
93
- );
94
74
  }
95
75
 
76
+ /*
77
+ * Trigger state sync stays Actual's job, deliberately.
78
+ *
79
+ * A button carrying popovertarget gets an implicit invoker relation and the
80
+ * platform computes aria-expanded on it by itself — measured on the AX tree:
81
+ * expanded false when closed, true when open. Tempting, and out of scope: the
82
+ * public contract here is aria-controls, and popovertarget would replace it.
83
+ * That is a markup change for adopters, not an internal transport detail, so it
84
+ * cannot ride along with a transport swap that changes no HTML.
85
+ *
86
+ * It is also not obviously a win. linkedTriggers() is small, explicit, and
87
+ * already supports several triggers pointing at one panel; popovertarget covers
88
+ * the single-invoker case. Any future move needs to weigh what is lost, not
89
+ * just count the lines saved.
90
+ */
96
91
  function linkedTriggers(menu) {
97
92
  if (!menu.id) return [];
98
93
  return [...menu.ownerDocument.querySelectorAll(`[aria-controls="${CSS.escape(menu.id)}"]`)];
@@ -104,39 +99,9 @@ function syncExpanded(menu, expanded) {
104
99
  }
105
100
  }
106
101
 
107
- function ensureBackdrop(menu, state) {
108
- if (!state.isSheet) {
109
- state.backdrop?.remove();
110
- state.backdrop = null;
111
- return;
112
- }
113
-
114
- if (state.backdrop?.isConnected) {
115
- state.backdrop.hidden = false;
116
- return;
117
- }
118
-
119
- const backdrop = menu.ownerDocument.createElement("div");
120
- backdrop.className = CLASSES.backdrop;
121
- backdrop.hidden = false;
122
-
123
- menu.before(backdrop);
124
- state.backdrop = backdrop;
125
- }
126
-
127
- function applyPresentation(menu, state) {
128
- state.isSheet = shouldUseSheet(menu, state);
129
- if (state.isSheet) {
130
- menu.style.removeProperty("left");
131
- menu.style.removeProperty("top");
132
- }
133
- menu.classList.toggle(CLASSES.sheet, state.isSheet);
134
- ensureBackdrop(menu, state);
135
- }
136
-
137
102
  function positionSurface(menu) {
138
103
  const state = surfaceMap.get(menu);
139
- if (!state || state.isSheet) return true;
104
+ if (!state) return true;
140
105
 
141
106
  if (state.trigger) {
142
107
  const triggerWidth = state.trigger.getBoundingClientRect().width;
@@ -152,6 +117,7 @@ function positionSurface(menu) {
152
117
  shift: state.shift !== false,
153
118
  shiftPadding: state.shiftPadding ?? 4,
154
119
  scope: state.scope,
120
+ coordinateSpace: state.coordinateSpace,
155
121
  };
156
122
 
157
123
  if (state.point) {
@@ -171,12 +137,9 @@ function ensureSurfaceWired(menu) {
171
137
  stopTracking: null,
172
138
  stopScrollIntent: null,
173
139
  unregisterEscape: null,
174
- backdrop: null,
175
140
  trigger: null,
176
141
  source: null,
177
142
  point: null,
178
- mobile: "auto",
179
- breakpoint: 768,
180
143
  placement: "bottom-start",
181
144
  distance: 4,
182
145
  flip: true,
@@ -185,8 +148,7 @@ function ensureSurfaceWired(menu) {
185
148
  autoClose: "outside",
186
149
  dismissOnScroll: false,
187
150
  scrollIntentAt: null,
188
- isSheet: false,
189
- closeId: 0,
151
+ coordinateSpace: "viewport",
190
152
  };
191
153
 
192
154
  surfaceMap.set(menu, state);
@@ -210,6 +172,10 @@ function startSurfaceResources(menu, state) {
210
172
  const armKey = (event) => {
211
173
  if (!event.defaultPrevented && SCROLL_KEYS.has(event.key)) arm(event);
212
174
  };
175
+ const dismissOnScroll = (event) => {
176
+ if (state.scrollIntentAt == null || menu.contains(event.target)) return;
177
+ closeSurface(menu);
178
+ };
213
179
  const captureOptions = { signal: controller.signal, capture: true };
214
180
  const passiveCaptureOptions = { ...captureOptions, passive: true };
215
181
  const doc = menu.ownerDocument;
@@ -217,33 +183,32 @@ function startSurfaceResources(menu, state) {
217
183
  doc.addEventListener("touchmove", arm, passiveCaptureOptions);
218
184
  doc.addEventListener("wheel", arm, passiveCaptureOptions);
219
185
  doc.addEventListener("keydown", armKey, { signal: controller.signal });
186
+ doc.addEventListener("scroll", dismissOnScroll, passiveCaptureOptions);
220
187
  state.stopScrollIntent = () => controller.abort();
221
188
  }
222
- state.stopTracking = autoUpdate(menu, ({ type, targets, timeStamp }) => {
223
- if (menu.hidden) return;
224
- if (
225
- type === "scroll" &&
226
- state.dismissOnScroll &&
227
- state.scrollIntentAt != null &&
228
- timeStamp >= state.scrollIntentAt &&
229
- [...targets].some((target) => target !== menu && !menu.contains(target))
230
- ) {
231
- closeSurface(menu);
232
- return;
233
- }
234
- applyPresentation(menu, state);
189
+ const anchor = state.point ? null : state.trigger || state.source;
190
+ state.stopTracking = autoUpdate(anchor, menu, ({ type }) => {
191
+ if (!isSurfaceOpen(menu)) return;
192
+ if (type === "scroll" && state.dismissOnScroll) return;
235
193
  if (!positionSurface(menu)) closeSurface(menu);
236
194
  });
237
195
  }
238
196
 
239
197
  export function isSurfaceOpen(menu) {
240
- return menu.classList.contains(CLASSES.open) && !menu.hidden;
198
+ return menu.classList.contains(CLASSES.open);
241
199
  }
242
200
 
243
201
  export function prepareSurface(menu) {
244
202
  if (!menu) return;
245
203
  menu.style.position = "fixed";
246
- menu.hidden = true;
204
+ // A closed popover is hidden by the platform, so [hidden] is no longer the
205
+ // closed state. Any left over from author markup would outrank it.
206
+ menu.removeAttribute("hidden");
207
+ // "manual" is written even over an author's own value, because it is the
208
+ // whole of what the transport asks for. Any other mode hands the UA a
209
+ // dismissal policy of its own: light dismiss closes on outside clicks only,
210
+ // which silently defeats data-flyout-auto-close.
211
+ menu.setAttribute("popover", "manual");
247
212
  syncExpanded(menu, false);
248
213
  menu.setAttribute(SURFACE_MARKER, "");
249
214
  reaperFor(menu)?.refresh(menu);
@@ -266,8 +231,10 @@ export function retainSurface(panel) {
266
231
  released = true;
267
232
  entry.count--;
268
233
  if (entry.count <= 0) {
269
- // A released retainer may leave a mounted panel whose original parent
270
- // either still needs it back or has disappeared along with the trigger.
234
+ // Nothing was moved, so nothing has to be put back. What the last
235
+ // release still owes is the teardown: the panel may be open, and an
236
+ // open surface holds an Escape entry and a position tracker, neither of
237
+ // which has an owner once the last retainer is gone.
271
238
  disconnectSurface(panel);
272
239
  surfaceRetainers.delete(panel);
273
240
  }
@@ -285,20 +252,12 @@ export function openSurface(menu, opts = {}) {
285
252
  });
286
253
  if (!menu.dispatchEvent(beforeOpen)) return false;
287
254
 
288
- for (const other of openSurfaces) {
289
- if (other !== menu && other.ownerDocument === menu.ownerDocument) closeSurface(other);
290
- }
291
-
292
- const anchor = opts.trigger || opts.source || null;
293
- mountSurface(menu, anchor);
294
255
  prepareSurface(menu);
295
256
  const state = ensureSurfaceWired(menu);
296
257
  state.trigger = opts.trigger || null;
297
258
  state.source = opts.source || null;
298
259
  state.point =
299
260
  Number.isFinite(opts.x) && Number.isFinite(opts.y) ? { x: opts.x, y: opts.y } : null;
300
- state.mobile = opts.mobile || "auto";
301
- state.breakpoint = opts.breakpoint ?? 768;
302
261
  state.autoClose = normalizeAutoClose(opts.autoClose);
303
262
  state.dismissOnScroll = opts.dismissOnScroll === true;
304
263
  state.scrollIntentAt = null;
@@ -309,11 +268,18 @@ export function openSurface(menu, opts = {}) {
309
268
  state.shiftPadding = opts.shiftPadding ?? 4;
310
269
  state.scope = opts.scope;
311
270
  state.restoreFocusTo = opts.restoreFocusTo || opts.trigger || opts.source || null;
312
- state.closeId++;
313
271
 
272
+ // Points are client coordinates and therefore always viewport-relative.
273
+ // Element anchors choose their coupled coordinate/CSS mode once per open;
274
+ // in particular, a sticky anchor must not switch modes while scrolling.
275
+ const positionAnchor = state.point ? null : state.trigger || state.source;
276
+ const mode = positionModeFor(positionAnchor);
277
+ state.coordinateSpace = mode.space;
278
+ menu.style.position = mode.position;
279
+
280
+ // Promote before measuring: a closed popover has no box to position.
281
+ if (!showTransport(menu)) return false;
314
282
  menu.classList.add(CLASSES.open);
315
- menu.hidden = false;
316
- applyPresentation(menu, state);
317
283
  syncExpanded(menu, true);
318
284
 
319
285
  if (!positionSurface(menu)) {
@@ -321,6 +287,33 @@ export function openSurface(menu, opts = {}) {
321
287
  return false;
322
288
  }
323
289
 
290
+ /*
291
+ * Surfaces are mutually exclusive per document, including when one contains
292
+ * the other's trigger. That is a deliberate contract, not a missing feature:
293
+ * making this loop ancestor-aware is most of a nesting implementation but
294
+ * not all of it. The platform closes a popover with display: none, and what
295
+ * that does to a descendant surface already in the top layer is unmeasured —
296
+ * nothing here cascades a close, so a live Escape entry and position tracker
297
+ * outliving their panel is the expected failure. Measure before relaxing it.
298
+ *
299
+ * The others come down only once this one is up and placed. With
300
+ * popover="manual" nothing requires the old surface to leave the top layer
301
+ * before the new one enters it, so there is no reason to spend it before
302
+ * knowing the open will succeed — a detached anchor or a positioner failure
303
+ * used to dismiss the current menu and then open nothing.
304
+ *
305
+ * restoreFocus: false because these are superseded, not dismissed. Restoring
306
+ * would bounce focus through the outgoing trigger on the way to the new
307
+ * surface, firing focusin on a control the user has just navigated away
308
+ * from; placing focus in the new surface is the caller's job, and both
309
+ * callers in tree do it as soon as this returns.
310
+ */
311
+ for (const other of openSurfaces) {
312
+ if (other !== menu && other.ownerDocument === menu.ownerDocument) {
313
+ closeSurface(other, { restoreFocus: false });
314
+ }
315
+ }
316
+
324
317
  openSurfaces.add(menu);
325
318
  startSurfaceResources(menu, state);
326
319
  return true;
@@ -330,17 +323,10 @@ export function closeSurface(menu, opts = {}) {
330
323
  if (!menu || !isSurfaceOpen(menu)) return;
331
324
 
332
325
  const state = surfaceMap.get(menu);
333
- const closeId = state ? ++state.closeId : 0;
334
- const wasSheet = state?.isSheet === true;
335
326
  const activeElement = menu.ownerDocument.activeElement;
336
327
  const shouldRestoreFocus = opts.restoreFocus ?? menu.contains(activeElement);
337
328
  menu.classList.remove(CLASSES.open);
338
- if (!wasSheet) {
339
- menu.classList.remove(CLASSES.sheet);
340
- }
341
- menu.hidden = true;
342
- const backdrop = state?.backdrop || null;
343
- if (backdrop) backdrop.hidden = true;
329
+ hideTransport(menu);
344
330
  openSurfaces.delete(menu);
345
331
  syncExpanded(menu, false);
346
332
  state?.unregisterEscape?.();
@@ -350,30 +336,18 @@ export function closeSurface(menu, opts = {}) {
350
336
  if (shouldRestoreFocus && state?.restoreFocusTo?.isConnected) {
351
337
  state.restoreFocusTo.focus({ preventScroll: true });
352
338
  }
353
-
354
- waitForTransitions(menu, backdrop).then(() => {
355
- if (!state || state.closeId !== closeId) return;
356
- state.isSheet = false;
357
- menu.classList.remove(CLASSES.sheet);
358
- backdrop?.remove();
359
- if (state.backdrop === backdrop) state.backdrop = null;
360
- restoreSurface(menu);
361
- });
362
339
  }
363
340
 
364
- export function disconnectSurface(menu, { restore = true } = {}) {
341
+ export function disconnectSurface(menu) {
365
342
  if (!menu) return;
366
- if (!restore) mountedSurfaces.delete(menu);
367
343
  closeSurface(menu);
368
344
  const state = surfaceMap.get(menu);
369
345
  if (state) {
370
- state.backdrop?.remove();
371
346
  state.unregisterEscape?.();
372
347
  state.stopScrollIntent?.();
373
348
  state.stopTracking?.();
374
349
  surfaceMap.delete(menu);
375
350
  }
376
- restoreSurface(menu);
377
351
  }
378
352
 
379
353
  function onDocumentClick(e) {
package/src/js/tab.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * Keyboard: ArrowLeft/Right, Home/End (select tab, wrapping)
9
9
  * ArrowUp/Down for aria-orientation="vertical"
10
10
  * ArrowDown (focus selected panel)
11
+ * [hidden], disabled and panel-less tabs are skipped
11
12
  *
12
13
  * Self-registers via registerEnhancement: injected tablists wire automatically.
13
14
  * The tab→panel map is rebuilt from the live tablist on each interaction,
@@ -47,8 +48,12 @@ function isTabDisabled(tab) {
47
48
  return tab.disabled || tab.getAttribute("aria-disabled") === "true";
48
49
  }
49
50
 
51
+ /* A [hidden] tab is not on screen, so it must not be a keyboard stop either:
52
+ an application that filters a tab strip would otherwise leave the hidden
53
+ tabs in the arrow-key sequence, selecting a panel whose tab is invisible.
54
+ Rebuilding from the live tablist is not enough — live is not visible. */
50
55
  function activatableTabs(list) {
51
- return tabsOf(list).filter((tab) => !isTabDisabled(tab) && panelFor(tab));
56
+ return tabsOf(list).filter((tab) => !tab.hidden && !isTabDisabled(tab) && panelFor(tab));
52
57
  }
53
58
 
54
59
  function makePanelFocusable(panel) {