actual-css 0.1.0 → 0.3.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 +90 -1
- package/README.md +25 -8
- package/dist/actual-themes.min.css +1 -0
- package/dist/actual.css +1492 -931
- package/dist/actual.js +1 -3
- package/dist/actual.min.css +1 -1
- package/dist/optional.css +1048 -0
- package/dist/optional.min.css +1 -0
- package/docs/design-notes/browser-support.md +95 -6
- package/docs/design-notes/cascade-layer.md +1 -1
- package/docs/design-notes/enhancement-contract.md +101 -0
- package/docs/design-notes/forced-colors.md +105 -0
- package/docs/design-notes/naming.md +3 -1
- package/docs/design-notes/platform-alignment.md +492 -0
- package/docs/design-notes/references.md +8 -0
- package/docs/design-notes/widget-primitives.md +119 -0
- package/docs/navigation.json +135 -0
- package/docs/pages/components/accordion.md +80 -0
- package/docs/pages/components/alert.md +203 -0
- package/docs/pages/components/aura.md +35 -0
- package/docs/pages/components/avatar.md +95 -0
- package/docs/pages/components/badge.md +102 -0
- package/docs/pages/components/breadcrumb.md +19 -0
- package/docs/pages/components/button.md +205 -0
- package/docs/pages/components/card.md +183 -0
- package/docs/pages/components/chat.md +50 -0
- package/docs/pages/components/dialog.md +367 -0
- package/docs/pages/components/drawer.md +113 -0
- package/docs/pages/components/fab.md +118 -0
- package/docs/pages/components/key.md +33 -0
- package/docs/pages/components/meter.md +23 -0
- package/docs/pages/components/navbar.md +92 -0
- package/docs/pages/components/pagination.md +55 -0
- package/docs/pages/components/progress.md +29 -0
- package/docs/pages/components/skeleton.md +38 -0
- package/docs/pages/components/spinner.md +93 -0
- package/docs/pages/components/table.md +102 -0
- package/docs/pages/enhancements/flyout.md +391 -0
- package/docs/pages/enhancements/overview.md +36 -0
- package/docs/pages/enhancements/scrollspy.md +71 -0
- package/docs/pages/enhancements/status-bar.md +72 -0
- package/docs/pages/enhancements/tabs.md +80 -0
- package/docs/pages/enhancements/tooltip.md +130 -0
- package/docs/pages/examples/overview.md +41 -0
- package/docs/pages/forms/choice-cards.md +102 -0
- package/docs/pages/forms/controls.md +32 -0
- package/docs/pages/forms/detached-actions.md +59 -0
- package/docs/pages/forms/floating-field.md +88 -0
- package/docs/pages/forms/form-actions.md +56 -0
- package/docs/pages/forms/input-policy.md +91 -0
- package/docs/pages/forms/joined-controls.md +37 -0
- package/docs/pages/forms/labels.md +42 -0
- package/docs/pages/forms/otp.md +50 -0
- package/docs/pages/forms/overview.md +188 -0
- package/docs/pages/forms/password.md +40 -0
- package/docs/pages/forms/range.md +17 -0
- package/docs/pages/forms/states-and-sizes.md +321 -0
- package/docs/pages/forms/sticky-actions.md +187 -0
- package/docs/pages/forms/switch.md +31 -0
- package/docs/pages/forms/validation.md +241 -0
- package/docs/{mental-model.md → pages/foundations/philosophy.md} +33 -18
- package/docs/pages/foundations/theming.md +92 -0
- package/docs/pages/foundations/tokens.md +421 -0
- package/docs/{typography.md → pages/foundations/typography.md} +27 -9
- package/docs/pages/guides/bootstrap.md +438 -0
- package/docs/pages/guides/introduction.md +157 -0
- package/docs/pages/guides/optional-components.md +33 -0
- package/docs/pages/guides/progressive-enhancement.md +686 -0
- package/docs/pages/guides/tailwind.md +455 -0
- package/docs/pages/layout/app-shell.md +25 -0
- package/docs/pages/layout/center.md +29 -0
- package/docs/pages/layout/cluster.md +57 -0
- package/docs/pages/layout/frame.md +30 -0
- package/docs/pages/layout/grid.md +142 -0
- package/docs/pages/layout/list.md +41 -0
- package/docs/pages/layout/media.md +21 -0
- package/docs/pages/layout/overview.md +37 -0
- package/docs/pages/layout/scroll-snap.md +54 -0
- package/docs/pages/layout/sidebar-layout.md +27 -0
- package/docs/pages/layout/stack.md +18 -0
- package/docs/pages/layout/switcher.md +36 -0
- package/docs/pages/layout/topbar.md +23 -0
- package/docs/pages/patterns/actions.md +52 -0
- package/docs/pages/patterns/code-block.md +36 -0
- package/docs/pages/patterns/exclusive-accordion.md +16 -0
- package/docs/pages/patterns/hierarchical-nav.md +26 -0
- package/docs/pages/patterns/icon-input.md +25 -0
- package/docs/pages/patterns/intent-nesting.md +3 -0
- package/docs/pages/patterns/navigation-list.md +235 -0
- package/docs/pages/patterns/overview.md +58 -0
- package/docs/pages/patterns/structured-lists.md +47 -0
- package/docs/pages/utilities/accessibility.md +34 -0
- package/docs/pages/utilities/flex.md +105 -0
- package/docs/pages/utilities/links.md +18 -0
- package/docs/pages/utilities/list.md +31 -0
- package/docs/pages/utilities/overflow.md +43 -0
- package/docs/pages/utilities/overview.md +18 -0
- package/docs/pages/utilities/shape.md +25 -0
- package/docs/pages/utilities/sizing-wrapping.md +115 -0
- package/docs/pages/utilities/spacing.md +79 -0
- package/docs/pages/utilities/text.md +43 -0
- package/llms.txt +10 -4
- package/package.json +30 -16
- package/src/css/actual.css +12 -1
- package/src/css/base.css +3 -9
- package/src/css/components/accordion.css +18 -30
- package/src/css/components/actions.css +4 -1
- package/src/css/components/alert.css +162 -49
- package/src/css/components/avatar.css +16 -10
- package/src/css/components/badge.css +82 -39
- package/src/css/components/breadcrumb.css +7 -15
- package/src/css/components/busy.css +7 -0
- package/src/css/components/button.css +82 -38
- package/src/css/components/card.css +17 -37
- package/src/css/components/drawer.css +30 -13
- package/src/css/components/flyout.css +39 -110
- package/src/css/components/index.css +1 -0
- package/src/css/components/join.css +14 -3
- package/src/css/components/key.css +2 -0
- package/src/css/components/menu.css +125 -0
- package/src/css/components/meter.css +0 -23
- package/src/css/components/modal.css +162 -7
- package/src/css/components/navbar.css +17 -18
- package/src/css/components/overline.css +0 -7
- package/src/css/components/pagination.css +1 -1
- package/src/css/components/progress.css +8 -26
- package/src/css/components/scrollspy.css +2 -2
- package/src/css/components/skeleton.css +7 -0
- package/src/css/components/spinner.css +10 -11
- package/src/css/components/status-bar.css +18 -28
- package/src/css/components/surface.css +2 -8
- package/src/css/components/tab.css +20 -48
- package/src/css/components/table.css +7 -5
- package/src/css/components/tooltip.css +14 -7
- package/src/css/focus.css +16 -3
- package/src/css/forms/choice-card.css +13 -15
- package/src/css/forms/choice.css +21 -28
- package/src/css/forms/control.css +16 -26
- package/src/css/forms/custom-select.css +9 -11
- package/src/css/forms/form-actions.css +14 -3
- package/src/css/forms/form.css +7 -4
- package/src/css/forms/index.css +5 -4
- package/src/css/forms/input-icon.css +30 -12
- package/src/css/forms/native.css +48 -33
- package/src/css/forms/range.css +103 -0
- package/src/css/forms/select.css +8 -8
- package/src/css/forms/switch.css +14 -13
- package/src/css/forms/validation.css +32 -5
- package/src/css/grid.css +19 -18
- package/src/css/intents.css +2 -2
- package/src/css/layout.css +29 -1
- package/src/css/optional/aura.css +93 -0
- package/src/css/optional/chat.css +105 -0
- package/src/css/optional/fab.css +104 -0
- package/src/css/optional/floating-field.css +87 -0
- package/src/css/optional/index.css +5 -0
- package/src/css/optional/layout-extra.css +7 -4
- package/src/css/optional/otp.css +115 -0
- package/src/css/optional/scroll-snap.css +6 -0
- package/src/css/optional/scroller.css +19 -7
- package/src/css/optional/typography-fluid.css +5 -0
- package/src/css/optional/utilities-extra.css +17 -2
- package/src/css/prose.css +18 -8
- package/src/css/reset.css +0 -10
- package/src/css/theme.css +29 -13
- package/src/css/themes/cyberpunk.css +1 -1
- package/src/css/themes/dim.css +3 -3
- package/src/css/themes/edge.css +11 -37
- package/src/css/themes/index.css +3 -3
- package/src/css/themes/{dark.css → indigo.css} +5 -4
- package/src/css/themes/neon.css +5 -5
- package/src/css/themes/square.css +0 -3
- package/src/css/tokens.css +81 -21
- package/src/css/utilities.css +8 -10
- package/src/css/variants.css +37 -65
- package/src/js/command.js +1 -3
- package/src/js/context-menu.js +72 -53
- package/src/js/dialog.js +68 -79
- package/src/js/dismiss.js +15 -0
- package/src/js/enhance.js +76 -19
- package/src/js/enhancement-loader.js +301 -0
- package/src/js/events.js +3 -0
- package/src/js/filter.js +3 -3
- package/src/js/floating.js +101 -131
- package/src/js/flyout.js +106 -96
- package/src/js/focus-group.js +116 -0
- package/src/js/focus.js +6 -1
- package/src/js/index.js +4 -2
- package/src/js/input.js +1 -1
- package/src/js/keys.js +27 -0
- package/src/js/mask.js +7 -6
- package/src/js/menu.js +89 -35
- package/src/js/parse-config.js +47 -0
- package/src/js/password.js +9 -1
- package/src/js/scrollspy.js +133 -62
- package/src/js/selectors.js +14 -31
- package/src/js/status.js +16 -14
- package/src/js/surface.js +219 -125
- package/src/js/tab.js +46 -66
- package/src/js/tooltip.js +53 -31
- package/src/js/validation.js +45 -51
- package/dist/actual.js.map +0 -31
- package/docs/README.md +0 -26
- package/docs/components.md +0 -860
- package/docs/design-notes/previous-work.md +0 -5
- package/docs/forms.md +0 -1265
- package/docs/javascript.md +0 -426
- package/docs/layout.md +0 -480
- package/docs/patterns.md +0 -415
- package/docs/reserved.md +0 -7
- package/docs/tokens.md +0 -455
- package/docs/ui.md +0 -937
- package/docs/utilities.md +0 -384
- package/src/css/dialog-fallback.css +0 -56
- package/src/js/dialog-fallback.js +0 -163
package/src/js/surface.js
CHANGED
|
@@ -1,17 +1,49 @@
|
|
|
1
|
+
import enhance from "./enhance.js";
|
|
1
2
|
import { EVENTS } from "./events.js";
|
|
2
|
-
import { reposition, repositionAt
|
|
3
|
-
|
|
3
|
+
import { autoUpdate, reposition, repositionAt } from "./floating.js";
|
|
4
|
+
|
|
4
5
|
import { CLASSES } from "./selectors.js";
|
|
5
6
|
|
|
6
|
-
const DEFAULT_BREAKPOINT = 768;
|
|
7
|
-
const BREAKPOINTS = {
|
|
8
|
-
sm: 640,
|
|
9
|
-
md: 768,
|
|
10
|
-
lg: 1024,
|
|
11
|
-
};
|
|
12
7
|
const openSurfaces = new Set();
|
|
13
8
|
const surfaceMap = new WeakMap();
|
|
9
|
+
const surfaceRetainers = new WeakMap();
|
|
14
10
|
const mountedSurfaces = new WeakMap();
|
|
11
|
+
const clickBoundDocuments = new WeakSet();
|
|
12
|
+
|
|
13
|
+
const SURFACE_MARKER = "data-actual-surface";
|
|
14
|
+
const AUTO_CLOSE_VALUES = new Set(["true", "inside", "outside", "false"]);
|
|
15
|
+
const SCROLL_KEYS = new Set([
|
|
16
|
+
"ArrowDown",
|
|
17
|
+
"ArrowLeft",
|
|
18
|
+
"ArrowRight",
|
|
19
|
+
"ArrowUp",
|
|
20
|
+
"End",
|
|
21
|
+
"Home",
|
|
22
|
+
"PageDown",
|
|
23
|
+
"PageUp",
|
|
24
|
+
" ",
|
|
25
|
+
]);
|
|
26
|
+
const reapers = new WeakMap();
|
|
27
|
+
|
|
28
|
+
function normalizeAutoClose(value) {
|
|
29
|
+
const normalized = String(value ?? "outside");
|
|
30
|
+
return AUTO_CLOSE_VALUES.has(normalized) ? normalized : "true";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function reaperFor(menu) {
|
|
34
|
+
const root = menu.ownerDocument?.documentElement;
|
|
35
|
+
if (!root) return null;
|
|
36
|
+
|
|
37
|
+
let reaper = reapers.get(root);
|
|
38
|
+
if (!reaper) {
|
|
39
|
+
reaper = enhance(
|
|
40
|
+
{ [`[${SURFACE_MARKER}]`]: (el) => () => disconnectSurface(el, { restore: false }) },
|
|
41
|
+
root,
|
|
42
|
+
);
|
|
43
|
+
reapers.set(root, reaper);
|
|
44
|
+
}
|
|
45
|
+
return reaper;
|
|
46
|
+
}
|
|
15
47
|
|
|
16
48
|
function waitForAnimations(...elements) {
|
|
17
49
|
const animations = elements
|
|
@@ -52,45 +84,20 @@ function restoreSurface(menu) {
|
|
|
52
84
|
mountedSurfaces.delete(menu);
|
|
53
85
|
}
|
|
54
86
|
|
|
55
|
-
function
|
|
56
|
-
const
|
|
57
|
-
const raw = Number.parseInt(BREAKPOINTS[value] || value, 10);
|
|
58
|
-
return Number.isFinite(raw) ? raw : DEFAULT_BREAKPOINT;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function getMobileMode(menu, mode) {
|
|
62
|
-
return mode || menu.dataset.flyoutMobile || "auto";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function getPlacement(menu, placement) {
|
|
66
|
-
return placement || menu.dataset.flyoutPlacement || "bottom-start";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function getDistance(menu, distance) {
|
|
70
|
-
if (distance != null) return distance;
|
|
71
|
-
const value = Number.parseFloat(menu.dataset.flyoutDistance);
|
|
72
|
-
return Number.isFinite(value) ? value : 4;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function getAutoCloseMode(menu, value) {
|
|
76
|
-
const mode = String(value ?? menu.dataset.flyoutAutoClose ?? "true").toLowerCase();
|
|
77
|
-
if (mode === "inside" || mode === "outside" || mode === "false") return mode;
|
|
78
|
-
return "true";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function shouldUseSheet(menu, mode) {
|
|
82
|
-
const mobile = getMobileMode(menu, mode);
|
|
87
|
+
function shouldUseSheet(menu, state) {
|
|
88
|
+
const mobile = state.mobile || "auto";
|
|
83
89
|
if (mobile === "none" || mobile === "anchored") return false;
|
|
84
90
|
if (mobile === "sheet") return true;
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
const view = menu.ownerDocument?.defaultView;
|
|
93
|
+
if (!view || typeof view.matchMedia !== "function") {
|
|
87
94
|
return false;
|
|
88
95
|
}
|
|
89
96
|
|
|
90
|
-
const breakpoint =
|
|
97
|
+
const breakpoint = state.breakpoint ?? 768;
|
|
91
98
|
return (
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
view.matchMedia("(pointer: coarse)").matches &&
|
|
100
|
+
view.matchMedia(`(max-width: ${breakpoint}px)`).matches
|
|
94
101
|
);
|
|
95
102
|
}
|
|
96
103
|
|
|
@@ -120,16 +127,13 @@ function ensureBackdrop(menu, state) {
|
|
|
120
127
|
const backdrop = menu.ownerDocument.createElement("div");
|
|
121
128
|
backdrop.className = CLASSES.backdrop;
|
|
122
129
|
backdrop.hidden = false;
|
|
123
|
-
backdrop.addEventListener("click", () => {
|
|
124
|
-
closeSurface(menu);
|
|
125
|
-
});
|
|
126
130
|
|
|
127
131
|
menu.before(backdrop);
|
|
128
132
|
state.backdrop = backdrop;
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
function applyPresentation(menu, state) {
|
|
132
|
-
state.isSheet = shouldUseSheet(menu, state
|
|
136
|
+
state.isSheet = shouldUseSheet(menu, state);
|
|
133
137
|
if (state.isSheet) {
|
|
134
138
|
menu.style.removeProperty("left");
|
|
135
139
|
menu.style.removeProperty("top");
|
|
@@ -140,13 +144,13 @@ function applyPresentation(menu, state) {
|
|
|
140
144
|
|
|
141
145
|
function positionSurface(menu) {
|
|
142
146
|
const state = surfaceMap.get(menu);
|
|
143
|
-
if (!state || state.isSheet) return;
|
|
147
|
+
if (!state || state.isSheet) return true;
|
|
144
148
|
|
|
145
149
|
if (state.trigger) {
|
|
146
150
|
const triggerWidth = state.trigger.getBoundingClientRect().width;
|
|
147
|
-
menu.style.setProperty("--
|
|
151
|
+
menu.style.setProperty("--surface-anchor-width", `${triggerWidth}px`);
|
|
148
152
|
} else {
|
|
149
|
-
menu.style.removeProperty("--
|
|
153
|
+
menu.style.removeProperty("--surface-anchor-width");
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
const opts = {
|
|
@@ -159,93 +163,87 @@ function positionSurface(menu) {
|
|
|
159
163
|
};
|
|
160
164
|
|
|
161
165
|
if (state.point) {
|
|
162
|
-
repositionAt(state.point.x, state.point.y, menu, opts);
|
|
163
|
-
return;
|
|
166
|
+
return repositionAt(state.point.x, state.point.y, menu, opts);
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
const anchor = state.trigger || state.source;
|
|
167
|
-
if (anchor) reposition(anchor, menu, opts);
|
|
170
|
+
if (anchor) return reposition(anchor, menu, opts);
|
|
171
|
+
return true;
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
function ensureSurfaceWired(menu) {
|
|
171
175
|
if (surfaceMap.has(menu)) return surfaceMap.get(menu);
|
|
172
176
|
|
|
173
|
-
|
|
177
|
+
ensureDocumentClick(menu);
|
|
174
178
|
const state = {
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
stopTracking: null,
|
|
180
|
+
stopScrollIntent: null,
|
|
181
|
+
unregisterEscape: null,
|
|
177
182
|
backdrop: null,
|
|
178
183
|
trigger: null,
|
|
179
184
|
source: null,
|
|
180
185
|
point: null,
|
|
181
186
|
mobile: "auto",
|
|
187
|
+
breakpoint: 768,
|
|
182
188
|
placement: "bottom-start",
|
|
183
189
|
distance: 4,
|
|
184
190
|
flip: true,
|
|
185
191
|
shift: true,
|
|
186
192
|
shiftPadding: 4,
|
|
187
|
-
autoClose: "
|
|
193
|
+
autoClose: "outside",
|
|
194
|
+
dismissOnScroll: false,
|
|
195
|
+
scrollIntentAt: null,
|
|
188
196
|
isSheet: false,
|
|
189
197
|
closeId: 0,
|
|
190
198
|
};
|
|
191
199
|
|
|
192
|
-
menu.addEventListener(
|
|
193
|
-
EVENTS.reposition,
|
|
194
|
-
(e) => {
|
|
195
|
-
if (menu.hidden) return;
|
|
196
|
-
if (e.detail?.type === "scroll" && surfaceMap.get(menu)?.point) {
|
|
197
|
-
closeSurface(menu);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const state = surfaceMap.get(menu);
|
|
201
|
-
if (!state) return;
|
|
202
|
-
applyPresentation(menu, state);
|
|
203
|
-
positionSurface(menu);
|
|
204
|
-
},
|
|
205
|
-
{ signal: controller.signal },
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
menu.addEventListener(
|
|
209
|
-
EVENTS.hide,
|
|
210
|
-
(e) => {
|
|
211
|
-
if (!menu.hidden) closeSurface(menu, { restoreFocus: e.detail?.type === "escape" });
|
|
212
|
-
},
|
|
213
|
-
{ signal: controller.signal },
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
menu.addEventListener(
|
|
217
|
-
EVENTS.outOfView,
|
|
218
|
-
() => {
|
|
219
|
-
if (!menu.hidden) closeSurface(menu);
|
|
220
|
-
},
|
|
221
|
-
{ signal: controller.signal },
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
menu.addEventListener(
|
|
225
|
-
"keydown",
|
|
226
|
-
(e) => onMenuKeydown(e, { close: (target) => closeSurface(target) }),
|
|
227
|
-
{ signal: controller.signal },
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
menu.addEventListener(
|
|
231
|
-
"click",
|
|
232
|
-
(e) => {
|
|
233
|
-
const state = surfaceMap.get(menu);
|
|
234
|
-
const disabledItem = e.target.closest?.(":disabled, [aria-disabled='true']");
|
|
235
|
-
if (disabledItem && menu.contains(disabledItem)) {
|
|
236
|
-
e.preventDefault();
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
if (!state || state.autoClose === "outside" || state.autoClose === "false") return;
|
|
240
|
-
if (hasMenuItem(e.target)) closeSurface(menu);
|
|
241
|
-
},
|
|
242
|
-
{ signal: controller.signal },
|
|
243
|
-
);
|
|
244
|
-
|
|
245
200
|
surfaceMap.set(menu, state);
|
|
246
201
|
return state;
|
|
247
202
|
}
|
|
248
203
|
|
|
204
|
+
// Escape and positional tracking belong to the *open* surface, not to the
|
|
205
|
+
// wiring. They start when the surface opens and stop when it closes, so a
|
|
206
|
+
// closed surface never participates in the dismissable stack or in
|
|
207
|
+
// scroll/resize work. Reopening re-registers in the correct order.
|
|
208
|
+
function startSurfaceResources(menu, state) {
|
|
209
|
+
state.unregisterEscape = registerEscapeDismissal(menu, (opts) => closeSurface(menu, opts));
|
|
210
|
+
if (state.dismissOnScroll) {
|
|
211
|
+
const controller = new AbortController();
|
|
212
|
+
const arm = (event) => {
|
|
213
|
+
state.scrollIntentAt = event.timeStamp;
|
|
214
|
+
};
|
|
215
|
+
const armExternalPointer = (event) => {
|
|
216
|
+
if (!menu.contains(event.target)) arm(event);
|
|
217
|
+
};
|
|
218
|
+
const armKey = (event) => {
|
|
219
|
+
if (!event.defaultPrevented && SCROLL_KEYS.has(event.key)) arm(event);
|
|
220
|
+
};
|
|
221
|
+
const captureOptions = { signal: controller.signal, capture: true };
|
|
222
|
+
const passiveCaptureOptions = { ...captureOptions, passive: true };
|
|
223
|
+
const doc = menu.ownerDocument;
|
|
224
|
+
doc.addEventListener("pointerdown", armExternalPointer, passiveCaptureOptions);
|
|
225
|
+
doc.addEventListener("touchmove", arm, passiveCaptureOptions);
|
|
226
|
+
doc.addEventListener("wheel", arm, passiveCaptureOptions);
|
|
227
|
+
doc.addEventListener("keydown", armKey, { signal: controller.signal });
|
|
228
|
+
state.stopScrollIntent = () => controller.abort();
|
|
229
|
+
}
|
|
230
|
+
state.stopTracking = autoUpdate(menu, ({ type, targets, timeStamp }) => {
|
|
231
|
+
if (menu.hidden) return;
|
|
232
|
+
if (
|
|
233
|
+
type === "scroll" &&
|
|
234
|
+
state.dismissOnScroll &&
|
|
235
|
+
state.scrollIntentAt != null &&
|
|
236
|
+
timeStamp >= state.scrollIntentAt &&
|
|
237
|
+
[...targets].some((target) => target !== menu && !menu.contains(target))
|
|
238
|
+
) {
|
|
239
|
+
closeSurface(menu);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
applyPresentation(menu, state);
|
|
243
|
+
if (!positionSurface(menu)) closeSurface(menu);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
249
247
|
export function isSurfaceOpen(menu) {
|
|
250
248
|
return menu.classList.contains(CLASSES.open) && !menu.hidden;
|
|
251
249
|
}
|
|
@@ -255,6 +253,31 @@ export function prepareSurface(menu) {
|
|
|
255
253
|
menu.style.position = "fixed";
|
|
256
254
|
menu.hidden = true;
|
|
257
255
|
syncExpanded(menu, false);
|
|
256
|
+
menu.setAttribute(SURFACE_MARKER, "");
|
|
257
|
+
reaperFor(menu)?.refresh(menu);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function retainSurface(panel) {
|
|
261
|
+
if (!panel) return () => {};
|
|
262
|
+
|
|
263
|
+
let entry = surfaceRetainers.get(panel);
|
|
264
|
+
if (!entry) {
|
|
265
|
+
prepareSurface(panel);
|
|
266
|
+
entry = { count: 0 };
|
|
267
|
+
surfaceRetainers.set(panel, entry);
|
|
268
|
+
}
|
|
269
|
+
entry.count++;
|
|
270
|
+
|
|
271
|
+
let released = false;
|
|
272
|
+
return () => {
|
|
273
|
+
if (released) return;
|
|
274
|
+
released = true;
|
|
275
|
+
entry.count--;
|
|
276
|
+
if (entry.count <= 0) {
|
|
277
|
+
disconnectSurface(panel, { restore: false });
|
|
278
|
+
surfaceRetainers.delete(panel);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
258
281
|
}
|
|
259
282
|
|
|
260
283
|
export function openSurface(menu, opts = {}) {
|
|
@@ -269,7 +292,7 @@ export function openSurface(menu, opts = {}) {
|
|
|
269
292
|
if (!menu.dispatchEvent(beforeOpen)) return false;
|
|
270
293
|
|
|
271
294
|
for (const other of openSurfaces) {
|
|
272
|
-
if (other !== menu) closeSurface(other);
|
|
295
|
+
if (other !== menu && other.ownerDocument === menu.ownerDocument) closeSurface(other);
|
|
273
296
|
}
|
|
274
297
|
|
|
275
298
|
const anchor = opts.trigger || opts.source || null;
|
|
@@ -280,10 +303,13 @@ export function openSurface(menu, opts = {}) {
|
|
|
280
303
|
state.source = opts.source || null;
|
|
281
304
|
state.point =
|
|
282
305
|
Number.isFinite(opts.x) && Number.isFinite(opts.y) ? { x: opts.x, y: opts.y } : null;
|
|
283
|
-
state.mobile =
|
|
284
|
-
state.
|
|
285
|
-
state.
|
|
286
|
-
state.
|
|
306
|
+
state.mobile = opts.mobile || "auto";
|
|
307
|
+
state.breakpoint = opts.breakpoint ?? 768;
|
|
308
|
+
state.autoClose = normalizeAutoClose(opts.autoClose);
|
|
309
|
+
state.dismissOnScroll = opts.dismissOnScroll === true;
|
|
310
|
+
state.scrollIntentAt = null;
|
|
311
|
+
state.placement = opts.placement || "bottom-start";
|
|
312
|
+
state.distance = opts.distance ?? 4;
|
|
287
313
|
state.flip = opts.flip !== false;
|
|
288
314
|
state.shift = opts.shift !== false;
|
|
289
315
|
state.shiftPadding = opts.shiftPadding ?? 4;
|
|
@@ -293,10 +319,16 @@ export function openSurface(menu, opts = {}) {
|
|
|
293
319
|
|
|
294
320
|
menu.classList.add(CLASSES.open);
|
|
295
321
|
menu.hidden = false;
|
|
296
|
-
openSurfaces.add(menu);
|
|
297
322
|
applyPresentation(menu, state);
|
|
298
323
|
syncExpanded(menu, true);
|
|
299
|
-
|
|
324
|
+
|
|
325
|
+
if (!positionSurface(menu)) {
|
|
326
|
+
closeSurface(menu, { restoreFocus: false });
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
openSurfaces.add(menu);
|
|
331
|
+
startSurfaceResources(menu, state);
|
|
300
332
|
return true;
|
|
301
333
|
}
|
|
302
334
|
|
|
@@ -317,6 +349,9 @@ export function closeSurface(menu, opts = {}) {
|
|
|
317
349
|
if (backdrop) backdrop.hidden = true;
|
|
318
350
|
openSurfaces.delete(menu);
|
|
319
351
|
syncExpanded(menu, false);
|
|
352
|
+
state?.unregisterEscape?.();
|
|
353
|
+
state?.stopScrollIntent?.();
|
|
354
|
+
state?.stopTracking?.();
|
|
320
355
|
|
|
321
356
|
if (shouldRestoreFocus && state?.restoreFocusTo?.isConnected) {
|
|
322
357
|
state.restoreFocusTo.focus({ preventScroll: true });
|
|
@@ -332,27 +367,86 @@ export function closeSurface(menu, opts = {}) {
|
|
|
332
367
|
});
|
|
333
368
|
}
|
|
334
369
|
|
|
335
|
-
export function disconnectSurface(menu) {
|
|
370
|
+
export function disconnectSurface(menu, { restore = true } = {}) {
|
|
336
371
|
if (!menu) return;
|
|
372
|
+
if (!restore) mountedSurfaces.delete(menu);
|
|
337
373
|
closeSurface(menu);
|
|
338
374
|
const state = surfaceMap.get(menu);
|
|
339
375
|
if (state) {
|
|
340
376
|
state.backdrop?.remove();
|
|
341
|
-
state.
|
|
342
|
-
state.
|
|
377
|
+
state.unregisterEscape?.();
|
|
378
|
+
state.stopScrollIntent?.();
|
|
379
|
+
state.stopTracking?.();
|
|
343
380
|
surfaceMap.delete(menu);
|
|
344
381
|
}
|
|
345
382
|
restoreSurface(menu);
|
|
346
383
|
}
|
|
347
384
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
385
|
+
function onDocumentClick(e) {
|
|
386
|
+
const path = e.composedPath();
|
|
387
|
+
|
|
388
|
+
for (const menu of openSurfaces) {
|
|
389
|
+
if (menu.ownerDocument !== e.currentTarget) continue;
|
|
390
|
+
const state = surfaceMap.get(menu);
|
|
391
|
+
if (!state) continue;
|
|
392
|
+
|
|
393
|
+
const menuIndex = path.indexOf(menu);
|
|
394
|
+
const isInside = menuIndex >= 0;
|
|
395
|
+
const hasCloseTrigger =
|
|
396
|
+
isInside &&
|
|
397
|
+
path
|
|
398
|
+
.slice(0, menuIndex)
|
|
399
|
+
.some((node) => node instanceof Element && node.hasAttribute("data-flyout-close"));
|
|
400
|
+
|
|
401
|
+
if (hasCloseTrigger) {
|
|
355
402
|
closeSurface(menu);
|
|
403
|
+
continue;
|
|
356
404
|
}
|
|
357
|
-
|
|
405
|
+
|
|
406
|
+
const closesInside = state.autoClose === "true" || state.autoClose === "inside";
|
|
407
|
+
const closesOutside = state.autoClose === "true" || state.autoClose === "outside";
|
|
408
|
+
if ((isInside && closesInside) || (!isInside && closesOutside)) closeSurface(menu);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function ensureDocumentClick(menu) {
|
|
413
|
+
const doc = menu.ownerDocument;
|
|
414
|
+
if (!doc || clickBoundDocuments.has(doc)) return;
|
|
415
|
+
clickBoundDocuments.add(doc);
|
|
416
|
+
doc.addEventListener("click", onDocumentClick);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const dismissableStacks = new WeakMap();
|
|
420
|
+
|
|
421
|
+
export function registerEscapeDismissal(element, dismiss) {
|
|
422
|
+
const doc = element.ownerDocument;
|
|
423
|
+
let stack = dismissableStacks.get(doc);
|
|
424
|
+
if (!stack) {
|
|
425
|
+
stack = [];
|
|
426
|
+
dismissableStacks.set(doc, stack);
|
|
427
|
+
doc.addEventListener("keydown", onDocumentEscape);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const entry = { element, dismiss };
|
|
431
|
+
stack.push(entry);
|
|
432
|
+
|
|
433
|
+
return () => {
|
|
434
|
+
const index = stack.indexOf(entry);
|
|
435
|
+
if (index >= 0) stack.splice(index, 1);
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function onDocumentEscape(event) {
|
|
440
|
+
if (event.key !== "Escape" || event.ctrlKey || event.altKey || event.shiftKey) return;
|
|
441
|
+
|
|
442
|
+
const stack = dismissableStacks.get(event.currentTarget);
|
|
443
|
+
const entry = stack.at(-1);
|
|
444
|
+
if (!entry) return;
|
|
445
|
+
|
|
446
|
+
event.preventDefault();
|
|
447
|
+
entry.dismiss({ restoreFocus: true });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function getSurfaceAutoClose(menu) {
|
|
451
|
+
return surfaceMap.get(menu)?.autoClose ?? "outside";
|
|
358
452
|
}
|
package/src/js/tab.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Tabs — in-place panel switcher using real tab semantics.
|
|
3
3
|
*
|
|
4
|
-
* Tab list:
|
|
4
|
+
* Tab list: [data-enhance="tabs"] with role="tablist"
|
|
5
5
|
* Tab: [role="tab"] with aria-selected, aria-controls, id, tabindex
|
|
6
6
|
* Panel: [role="tabpanel"] with id matching aria-controls
|
|
7
7
|
*
|
|
@@ -9,20 +9,24 @@
|
|
|
9
9
|
* ArrowUp/Down for aria-orientation="vertical"
|
|
10
10
|
* ArrowDown (focus selected panel)
|
|
11
11
|
*
|
|
12
|
-
* Self-registers via
|
|
12
|
+
* Self-registers via registerEnhancement: injected tablists wire automatically.
|
|
13
13
|
* The tab→panel map is rebuilt from the live tablist on each interaction,
|
|
14
14
|
* so tabs added after connect are picked up with no extra work.
|
|
15
15
|
* Cleanup is the AbortController.abort() returned to enhance.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
20
|
-
import { CLASSES } from "./selectors.js";
|
|
18
|
+
import { registerEnhancement } from "./enhance.js";
|
|
19
|
+
import { itemForKey } from "./keys.js";
|
|
21
20
|
|
|
22
|
-
const TABLIST_SELECTOR =
|
|
21
|
+
const TABLIST_SELECTOR = '[data-enhance~="tabs"]';
|
|
23
22
|
|
|
23
|
+
// A tablist owns only the tabs directly beneath it, not those of a nested
|
|
24
|
+
// tablist. Filtering by closest owner keeps the outer list from operating on
|
|
25
|
+
// (and re-reacting to events from) tabs that belong to an inner list.
|
|
24
26
|
function tabsOf(list) {
|
|
25
|
-
return [...list.querySelectorAll('[role="tab"]')]
|
|
27
|
+
return [...list.querySelectorAll('[role="tab"]')].filter(
|
|
28
|
+
(tab) => tab.closest(TABLIST_SELECTOR) === list,
|
|
29
|
+
);
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
function panelsOf(tabs) {
|
|
@@ -36,7 +40,7 @@ function panelsOf(tabs) {
|
|
|
36
40
|
|
|
37
41
|
function panelFor(tab) {
|
|
38
42
|
const panelId = tab.getAttribute("aria-controls");
|
|
39
|
-
return panelId ?
|
|
43
|
+
return panelId ? tab.ownerDocument.getElementById(panelId) : null;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
function isTabDisabled(tab) {
|
|
@@ -90,68 +94,46 @@ function initialize(list) {
|
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
function onKeydown(e) {
|
|
97
|
+
const list = e.currentTarget;
|
|
93
98
|
const tab = e.target.closest('[role="tab"]');
|
|
94
|
-
if (!tab) return;
|
|
95
|
-
const list = tab.closest(TABLIST_SELECTOR);
|
|
96
|
-
if (!list) return;
|
|
99
|
+
if (!tab || tab.closest(TABLIST_SELECTOR) !== list) return;
|
|
97
100
|
|
|
98
101
|
const tabs = activatableTabs(list);
|
|
99
102
|
if (!tabs.length || !tabs.includes(tab)) return;
|
|
100
103
|
const isVertical = list.getAttribute("aria-orientation") === "vertical";
|
|
101
|
-
let next;
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
e.preventDefault();
|
|
107
|
-
next = nextItem(tabs, tab, 1, { wrap: true });
|
|
108
|
-
if (next) activateAndFocus(next);
|
|
109
|
-
break;
|
|
110
|
-
case "ArrowLeft":
|
|
111
|
-
if (isVertical) break;
|
|
112
|
-
e.preventDefault();
|
|
113
|
-
next = nextItem(tabs, tab, -1, { wrap: true });
|
|
114
|
-
if (next) activateAndFocus(next);
|
|
115
|
-
break;
|
|
116
|
-
case "ArrowDown":
|
|
117
|
-
if (!isVertical) {
|
|
118
|
-
e.preventDefault();
|
|
119
|
-
const panel = panelFor(tab);
|
|
120
|
-
if (panel) {
|
|
121
|
-
makePanelFocusable(panel);
|
|
122
|
-
panel.focus();
|
|
123
|
-
}
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
e.preventDefault();
|
|
127
|
-
next = nextItem(tabs, tab, 1, { wrap: true });
|
|
128
|
-
if (next) activateAndFocus(next);
|
|
129
|
-
break;
|
|
130
|
-
case "ArrowUp":
|
|
131
|
-
if (!isVertical) break;
|
|
132
|
-
e.preventDefault();
|
|
133
|
-
next = nextItem(tabs, tab, -1, { wrap: true });
|
|
134
|
-
if (next) activateAndFocus(next);
|
|
135
|
-
break;
|
|
136
|
-
case "Home":
|
|
137
|
-
e.preventDefault();
|
|
138
|
-
activateAndFocus(firstItem(tabs));
|
|
139
|
-
break;
|
|
140
|
-
case "End":
|
|
141
|
-
e.preventDefault();
|
|
142
|
-
activateAndFocus(lastItem(tabs));
|
|
143
|
-
break;
|
|
144
|
-
case "Enter":
|
|
145
|
-
case " ":
|
|
105
|
+
if (e.key === "ArrowDown" && !isVertical) {
|
|
106
|
+
const panel = panelFor(tab);
|
|
107
|
+
if (panel) {
|
|
146
108
|
e.preventDefault();
|
|
147
|
-
|
|
148
|
-
|
|
109
|
+
makePanelFocusable(panel);
|
|
110
|
+
panel.focus();
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
activate(tab);
|
|
118
|
+
return;
|
|
149
119
|
}
|
|
120
|
+
|
|
121
|
+
const direction = list.ownerDocument.defaultView?.getComputedStyle(list).direction || "ltr";
|
|
122
|
+
const target = itemForKey(tabs, tab, e.key, {
|
|
123
|
+
orientation: isVertical ? "vertical" : "horizontal",
|
|
124
|
+
wrap: true,
|
|
125
|
+
direction,
|
|
126
|
+
});
|
|
127
|
+
if (!target) return;
|
|
128
|
+
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
activateAndFocus(target);
|
|
150
131
|
}
|
|
151
132
|
|
|
152
133
|
function onClick(e) {
|
|
134
|
+
const list = e.currentTarget;
|
|
153
135
|
const tab = e.target.closest('[role="tab"]');
|
|
154
|
-
if (!tab) return;
|
|
136
|
+
if (!tab || tab.closest(TABLIST_SELECTOR) !== list) return;
|
|
155
137
|
if (isTabDisabled(tab)) {
|
|
156
138
|
e.preventDefault();
|
|
157
139
|
return;
|
|
@@ -162,12 +144,10 @@ function onClick(e) {
|
|
|
162
144
|
tab.focus();
|
|
163
145
|
}
|
|
164
146
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return () => controller.abort();
|
|
172
|
-
},
|
|
147
|
+
registerEnhancement("tabs", (list) => {
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
initialize(list);
|
|
150
|
+
list.addEventListener("click", onClick, { signal: controller.signal });
|
|
151
|
+
list.addEventListener("keydown", onKeydown, { signal: controller.signal });
|
|
152
|
+
return () => controller.abort();
|
|
173
153
|
});
|