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.
- package/CHANGELOG.md +214 -1
- package/README.md +13 -11
- package/components.json +668 -0
- package/dist/actual.css +166 -37
- package/dist/actual.full.css +1005 -550
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/dist/actual.js +1 -1
- package/dist/actual.min.css +1 -1
- package/docs/design-notes/accessibility-decisions.md +33 -0
- package/docs/design-notes/browser-support.md +82 -25
- package/docs/design-notes/column-layout.md +3 -3
- package/docs/design-notes/css-anchor.md +75 -21
- package/docs/design-notes/enhancement-contract.md +14 -6
- package/docs/design-notes/grid.md +4 -4
- package/docs/design-notes/naming.md +6 -1
- package/docs/design-notes/platform-alignment.md +99 -2
- package/docs/design-notes/popover-manual-poc.md +328 -0
- package/docs/design-notes/surface-reparenting.md +131 -0
- package/docs/design-notes/tooltip-coordinate-space.md +132 -0
- package/docs/design-notes/widget-primitives.md +9 -6
- package/docs/pages/components/accordion.md +41 -2
- package/docs/pages/components/alert.md +80 -30
- package/docs/pages/components/app-navigation.md +6 -0
- package/docs/pages/components/avatar.md +6 -6
- package/docs/pages/components/badge.md +101 -16
- package/docs/pages/components/button.md +16 -8
- package/docs/pages/components/card.md +10 -10
- package/docs/pages/components/dialog.md +16 -6
- package/docs/pages/components/fab.md +4 -3
- package/docs/pages/components/list.md +42 -4
- package/docs/pages/components/navbar.md +17 -1
- package/docs/pages/components/pagination.md +2 -2
- package/docs/pages/components/steps.md +19 -0
- package/docs/pages/components/table.md +3 -2
- package/docs/pages/enhancements/flyout.md +144 -24
- package/docs/pages/enhancements/overview.md +25 -0
- package/docs/pages/enhancements/status-bar.md +20 -0
- package/docs/pages/enhancements/tabs.md +45 -37
- package/docs/pages/enhancements/tooltip.md +40 -2
- package/docs/pages/examples/overview.md +38 -5
- package/docs/pages/forms/choice-cards.md +6 -2
- package/docs/pages/forms/combobox.md +1 -1
- package/docs/pages/forms/floating-field.md +4 -3
- package/docs/pages/forms/joined-controls.md +22 -0
- package/docs/pages/forms/otp.md +2 -1
- package/docs/pages/forms/overview.md +1 -1
- package/docs/pages/forms/states-and-sizes.md +16 -18
- package/docs/pages/forms/sticky-actions.md +2 -2
- package/docs/pages/forms/switch.md +1 -1
- package/docs/pages/foundations/philosophy.md +67 -22
- package/docs/pages/foundations/theming.md +10 -2
- package/docs/pages/foundations/tokens.md +118 -21
- package/docs/pages/foundations/typography.md +35 -14
- package/docs/pages/guides/introduction.md +11 -9
- package/docs/pages/guides/progressive-enhancement.md +136 -49
- package/docs/pages/guides/tailwind.md +3 -2
- package/docs/pages/layout/media.md +1 -1
- package/docs/pages/layout/overview.md +11 -1
- package/docs/pages/layout/scroll-snap.md +3 -3
- package/docs/pages/layout/stack.md +17 -9
- package/docs/pages/patterns/icon-input.md +4 -2
- package/docs/pages/patterns/icon-slot.md +2 -0
- package/docs/pages/patterns/intent-nesting.md +1 -1
- package/docs/pages/utilities/overview.md +23 -2
- package/docs/pages/utilities/sizing-wrapping.md +1 -1
- package/docs/pages/utilities/spacing.md +10 -8
- package/llms.txt +2 -1
- package/package.json +19 -8
- package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
- package/src/css/components/accordion.css +42 -3
- package/src/css/components/alert.css +25 -13
- package/src/css/components/app-nav.css +18 -8
- package/src/css/components/avatar.css +13 -1
- package/src/css/components/badge.css +78 -12
- package/src/css/components/breadcrumb.css +2 -3
- package/src/css/components/button.css +9 -16
- package/src/css/components/card.css +9 -2
- package/src/css/components/chat.css +1 -0
- package/src/css/components/data-list.css +1 -1
- package/src/css/components/drawer.css +79 -25
- package/src/css/components/fab.css +0 -6
- package/src/css/components/flyout.css +46 -63
- package/src/css/components/index.css +0 -1
- package/src/css/components/indicator.css +21 -0
- package/src/css/components/join.css +7 -0
- package/src/css/components/key.css +10 -0
- package/src/css/components/list.css +40 -4
- package/src/css/components/menu.css +18 -11
- package/src/css/components/modal.css +48 -33
- package/src/css/components/navbar.css +37 -4
- package/src/css/components/pagination.css +1 -0
- package/src/css/components/spinner.css +6 -6
- package/src/css/components/status-bar.css +0 -5
- package/src/css/components/steps.css +45 -59
- package/src/css/components/tab.css +29 -9
- package/src/css/components/table.css +5 -5
- package/src/css/components/tooltip.css +45 -13
- package/src/css/core/base.css +23 -1
- package/src/css/core/intents.css +17 -6
- package/src/css/core/theme.css +27 -0
- package/src/css/core/tokens.css +35 -15
- package/src/css/core/variants.css +64 -15
- package/src/css/forms/choice-card.css +4 -4
- package/src/css/forms/choice.css +6 -3
- package/src/css/forms/control.css +2 -1
- package/src/css/forms/floating-field.css +3 -3
- package/src/css/forms/form.css +4 -3
- package/src/css/forms/input-icon.css +20 -14
- package/src/css/forms/native.css +6 -1
- package/src/css/forms/otp.css +9 -1
- package/src/css/forms/range.css +2 -0
- package/src/css/forms/select.css +29 -9
- package/src/css/forms/switch.css +19 -5
- package/src/css/layout/app-layout.css +1 -1
- package/src/css/layout/grid.css +50 -76
- package/src/css/layout/scroll-snap.css +11 -7
- package/src/css/layout/scroller.css +3 -4
- package/src/css/layout/stack.css +2 -2
- package/src/css/typography/fluid.css +4 -3
- package/src/css/typography/prose.css +6 -5
- package/src/css/utilities/base.css +2 -1
- package/src/css/utilities/extra.css +20 -17
- package/src/js/context-menu.js +27 -26
- package/src/js/dialog.js +2 -1
- package/src/js/dismiss.js +5 -0
- package/src/js/enhance.js +34 -2
- package/src/js/enhancement-loader.js +36 -43
- package/src/js/events.js +0 -3
- package/src/js/filter.js +0 -1
- package/src/js/floating-space.js +42 -0
- package/src/js/flyout.js +9 -44
- package/src/js/focus-group.js +7 -0
- package/src/js/focus.js +6 -0
- package/src/js/menu.js +10 -1
- package/src/js/selectors.js +5 -9
- package/src/js/status.js +44 -7
- package/src/js/surface.js +106 -132
- package/src/js/tab.js +6 -1
- package/src/js/tooltip.js +152 -53
- package/src/js/validation.js +1 -1
- package/src/css/components/surface.css +0 -49
- 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 {
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
//
|
|
270
|
-
//
|
|
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
|
-
|
|
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
|
|
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) {
|