actual-css 0.1.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 +11 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/actual.css +4929 -0
- package/dist/actual.js +3 -0
- package/dist/actual.js.map +31 -0
- package/dist/actual.min.css +1 -0
- package/docs/README.md +26 -0
- package/docs/components.md +860 -0
- package/docs/design-notes/accessibility-decisions.md +32 -0
- package/docs/design-notes/browser-support.md +93 -0
- package/docs/design-notes/cascade-layer.md +52 -0
- package/docs/design-notes/css-anchor.md +33 -0
- package/docs/design-notes/naming.md +37 -0
- package/docs/design-notes/previous-work.md +5 -0
- package/docs/design-notes/references.md +324 -0
- package/docs/forms.md +1265 -0
- package/docs/javascript.md +426 -0
- package/docs/layout.md +480 -0
- package/docs/mental-model.md +73 -0
- package/docs/patterns.md +415 -0
- package/docs/reserved.md +7 -0
- package/docs/tokens.md +455 -0
- package/docs/typography.md +159 -0
- package/docs/ui.md +937 -0
- package/docs/utilities.md +384 -0
- package/llms.txt +17 -0
- package/package.json +101 -0
- package/src/css/actual.css +17 -0
- package/src/css/actual.full.css +2 -0
- package/src/css/actual.layer.css +3 -0
- package/src/css/base.css +70 -0
- package/src/css/components/accordion.css +97 -0
- package/src/css/components/actions.css +21 -0
- package/src/css/components/alert.css +137 -0
- package/src/css/components/avatar.css +199 -0
- package/src/css/components/badge.css +143 -0
- package/src/css/components/breadcrumb.css +56 -0
- package/src/css/components/busy.css +44 -0
- package/src/css/components/button.css +216 -0
- package/src/css/components/card.css +140 -0
- package/src/css/components/drawer.css +122 -0
- package/src/css/components/flyout.css +243 -0
- package/src/css/components/index.css +41 -0
- package/src/css/components/join.css +106 -0
- package/src/css/components/key.css +25 -0
- package/src/css/components/meter.css +81 -0
- package/src/css/components/modal.css +162 -0
- package/src/css/components/navbar.css +98 -0
- package/src/css/components/overline.css +49 -0
- package/src/css/components/pagination.css +30 -0
- package/src/css/components/progress.css +126 -0
- package/src/css/components/scrollspy.css +27 -0
- package/src/css/components/skeleton.css +67 -0
- package/src/css/components/spinner.css +60 -0
- package/src/css/components/status-bar.css +82 -0
- package/src/css/components/surface.css +48 -0
- package/src/css/components/tab.css +128 -0
- package/src/css/components/table.css +64 -0
- package/src/css/components/tooltip.css +107 -0
- package/src/css/dialog-fallback.css +56 -0
- package/src/css/focus.css +13 -0
- package/src/css/forms/choice-card.css +158 -0
- package/src/css/forms/choice.css +196 -0
- package/src/css/forms/control.css +106 -0
- package/src/css/forms/custom-select.css +93 -0
- package/src/css/forms/form-actions.css +43 -0
- package/src/css/forms/form.css +57 -0
- package/src/css/forms/index.css +15 -0
- package/src/css/forms/input-icon.css +36 -0
- package/src/css/forms/native.css +68 -0
- package/src/css/forms/select.css +36 -0
- package/src/css/forms/switch.css +127 -0
- package/src/css/forms/validation.css +63 -0
- package/src/css/grid.css +95 -0
- package/src/css/intents.css +33 -0
- package/src/css/layout.css +80 -0
- package/src/css/optional/index.css +5 -0
- package/src/css/optional/layout-extra.css +55 -0
- package/src/css/optional/scroll-snap.css +51 -0
- package/src/css/optional/scroller.css +51 -0
- package/src/css/optional/typography-fluid.css +48 -0
- package/src/css/optional/utilities-extra.css +289 -0
- package/src/css/print.css +47 -0
- package/src/css/prose.css +187 -0
- package/src/css/reset.css +56 -0
- package/src/css/theme.css +186 -0
- package/src/css/themes/corporate.css +60 -0
- package/src/css/themes/cyberpunk.css +60 -0
- package/src/css/themes/dark.css +32 -0
- package/src/css/themes/dim.css +33 -0
- package/src/css/themes/edge.css +143 -0
- package/src/css/themes/forest.css +65 -0
- package/src/css/themes/gradient.css +97 -0
- package/src/css/themes/index.css +43 -0
- package/src/css/themes/lavender.css +66 -0
- package/src/css/themes/mono.css +60 -0
- package/src/css/themes/neon.css +66 -0
- package/src/css/themes/ocean.css +60 -0
- package/src/css/themes/petrol.css +64 -0
- package/src/css/themes/spruce.css +64 -0
- package/src/css/themes/square.css +68 -0
- package/src/css/themes/sunset.css +66 -0
- package/src/css/tokens.css +122 -0
- package/src/css/utilities.css +162 -0
- package/src/css/variants.css +118 -0
- package/src/js/command.js +165 -0
- package/src/js/context-menu.js +291 -0
- package/src/js/dialog-fallback.js +163 -0
- package/src/js/dialog.js +503 -0
- package/src/js/enhance.js +208 -0
- package/src/js/events.js +22 -0
- package/src/js/filter.js +122 -0
- package/src/js/floating.js +462 -0
- package/src/js/flyout.js +187 -0
- package/src/js/focus.js +35 -0
- package/src/js/index.js +26 -0
- package/src/js/input.js +47 -0
- package/src/js/keys.js +30 -0
- package/src/js/mask.js +184 -0
- package/src/js/menu.js +88 -0
- package/src/js/password.js +98 -0
- package/src/js/scrollspy.js +121 -0
- package/src/js/selectors.js +59 -0
- package/src/js/status.js +158 -0
- package/src/js/surface.js +358 -0
- package/src/js/tab.js +173 -0
- package/src/js/tooltip.js +374 -0
- package/src/js/validation.js +419 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import enhance from "./enhance.js";
|
|
2
|
+
import { EVENTS } from "./events.js";
|
|
3
|
+
import { focusFirstMenuItem, hasMenuItems } from "./menu.js";
|
|
4
|
+
import { CLASSES } from "./selectors.js";
|
|
5
|
+
import {
|
|
6
|
+
closeSurface,
|
|
7
|
+
disconnectSurface,
|
|
8
|
+
isSurfaceOpen,
|
|
9
|
+
openSurface,
|
|
10
|
+
prepareSurface,
|
|
11
|
+
} from "./surface.js";
|
|
12
|
+
|
|
13
|
+
const LONG_PRESS_MS = 450;
|
|
14
|
+
const MOVE_TOLERANCE = 10;
|
|
15
|
+
const CONTEXT_MENU_SELECTOR = `menu.${CLASSES.flyout}`;
|
|
16
|
+
const CONTEXT_TARGET_SELECTOR = "[data-context-menu]";
|
|
17
|
+
const contextMap = new WeakMap();
|
|
18
|
+
const contextByMenu = new WeakMap();
|
|
19
|
+
|
|
20
|
+
// Returns the detail from the last accepted `actual:context-menu` event for a
|
|
21
|
+
// menu. This is intentionally optional: static menus need no application JS.
|
|
22
|
+
export function contextFor(menu) {
|
|
23
|
+
return contextByMenu.get(menu) || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function menuFor(target) {
|
|
27
|
+
const id = target.getAttribute("data-context-menu");
|
|
28
|
+
const menu = id ? target.ownerDocument.getElementById(id) : null;
|
|
29
|
+
return menu?.matches(CONTEXT_MENU_SELECTOR) ? menu : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shouldIgnoreNativeTarget(target) {
|
|
33
|
+
return !!target.closest?.(
|
|
34
|
+
"a, button, input, textarea, select, option, [contenteditable], [role='button'], [role='link'], [data-context-menu-ignore]",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getContextScope(target) {
|
|
39
|
+
if (!target.hasAttribute("data-context-menu-scope")) return null;
|
|
40
|
+
|
|
41
|
+
const value = target.getAttribute("data-context-menu-scope").trim();
|
|
42
|
+
if (!value || value === "self") return target;
|
|
43
|
+
if (value === "parent") return target.parentElement;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return target.closest(value) || target.ownerDocument.querySelector(value);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getLongPressDelay(target, menu) {
|
|
53
|
+
const value = target.dataset.contextMenuLongPress ?? menu.dataset.contextMenuLongPress;
|
|
54
|
+
if (value == null || value === "false" || value === "none") return null;
|
|
55
|
+
if (value === "") return LONG_PRESS_MS;
|
|
56
|
+
|
|
57
|
+
const delay = Number.parseInt(value, 10);
|
|
58
|
+
return Number.isFinite(delay) ? delay : LONG_PRESS_MS;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function focusMenuContainer(menu) {
|
|
62
|
+
if (!menu.hasAttribute("tabindex")) {
|
|
63
|
+
menu.tabIndex = -1;
|
|
64
|
+
}
|
|
65
|
+
menu.focus();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Pointer-triggered opens (right-click, long-press) focus the menu container,
|
|
69
|
+
// not the first item: a pointer-set focus ring is invisible (:focus-visible
|
|
70
|
+
// doesn't match), so focusing "Open" makes the first ArrowDown look like it
|
|
71
|
+
// skips to the second item. Focusing the container keeps the ring hidden and
|
|
72
|
+
// lets keys.js's out-of-list fallback (indexOf === -1) land ArrowDown/ArrowUp
|
|
73
|
+
// on the first/last item, matching native OS context menus. Keyboard-triggered
|
|
74
|
+
// opens (openFromKeyboard) still focus the first item directly.
|
|
75
|
+
function focusMenu(menu, mode) {
|
|
76
|
+
if (!hasMenuItems(menu)) return;
|
|
77
|
+
if (mode === "first-item") {
|
|
78
|
+
focusFirstMenuItem(menu);
|
|
79
|
+
} else {
|
|
80
|
+
focusMenuContainer(menu);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function requestContextMenu(context, menu, opts = {}) {
|
|
85
|
+
const detail = {
|
|
86
|
+
menu,
|
|
87
|
+
context,
|
|
88
|
+
origin: opts.origin || context,
|
|
89
|
+
trigger: opts.trigger,
|
|
90
|
+
};
|
|
91
|
+
const event = new CustomEvent(EVENTS.contextMenu, {
|
|
92
|
+
bubbles: true,
|
|
93
|
+
cancelable: true,
|
|
94
|
+
detail,
|
|
95
|
+
});
|
|
96
|
+
if (!context.dispatchEvent(event)) return null;
|
|
97
|
+
contextByMenu.set(menu, detail);
|
|
98
|
+
return detail;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function openContextMenu(context, menu, opts = {}) {
|
|
102
|
+
if (!requestContextMenu(context, menu, opts)) return;
|
|
103
|
+
if (isSurfaceOpen(menu)) closeSurface(menu);
|
|
104
|
+
if (
|
|
105
|
+
openSurface(menu, {
|
|
106
|
+
source: context,
|
|
107
|
+
x: opts.x,
|
|
108
|
+
y: opts.y,
|
|
109
|
+
placement: opts.placement || "bottom-start",
|
|
110
|
+
distance: opts.distance ?? 2,
|
|
111
|
+
mobile: opts.mobile || menu.dataset.flyoutMobile || "auto",
|
|
112
|
+
scope: opts.scope || getContextScope(context),
|
|
113
|
+
restoreFocusTo: opts.restoreFocusTo,
|
|
114
|
+
})
|
|
115
|
+
) {
|
|
116
|
+
focusMenu(menu, opts.focus);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function openFromKeyboard(context, menu, origin = context, restoreFocusTo) {
|
|
121
|
+
const rect = origin.getBoundingClientRect();
|
|
122
|
+
openContextMenu(context, menu, {
|
|
123
|
+
x: rect.left,
|
|
124
|
+
y: rect.bottom,
|
|
125
|
+
placement: "bottom-start",
|
|
126
|
+
distance: 4,
|
|
127
|
+
focus: "first-item",
|
|
128
|
+
origin,
|
|
129
|
+
trigger: "keyboard",
|
|
130
|
+
restoreFocusTo,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function clearLongPress(state) {
|
|
135
|
+
if (state.timer) {
|
|
136
|
+
clearTimeout(state.timer);
|
|
137
|
+
state.timer = null;
|
|
138
|
+
}
|
|
139
|
+
state.pointerId = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function connectContextTarget(target) {
|
|
143
|
+
if (contextMap.has(target)) return;
|
|
144
|
+
const menu = menuFor(target);
|
|
145
|
+
if (!menu) return;
|
|
146
|
+
|
|
147
|
+
prepareSurface(menu);
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const state = {
|
|
150
|
+
controller,
|
|
151
|
+
menu,
|
|
152
|
+
timer: null,
|
|
153
|
+
pointerId: null,
|
|
154
|
+
startX: 0,
|
|
155
|
+
startY: 0,
|
|
156
|
+
suppressClickUntil: 0,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
target.addEventListener(
|
|
160
|
+
"contextmenu",
|
|
161
|
+
(e) => {
|
|
162
|
+
if (shouldIgnoreNativeTarget(e.target)) return;
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
e.stopPropagation();
|
|
165
|
+
openContextMenu(target, menu, {
|
|
166
|
+
x: e.clientX,
|
|
167
|
+
y: e.clientY,
|
|
168
|
+
origin: e.target,
|
|
169
|
+
trigger: "pointer",
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
{ signal: controller.signal },
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
target.addEventListener(
|
|
176
|
+
"keydown",
|
|
177
|
+
(e) => {
|
|
178
|
+
if (e.key !== "ContextMenu" && !(e.shiftKey && e.key === "F10")) return;
|
|
179
|
+
if (shouldIgnoreNativeTarget(e.target)) return;
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
openFromKeyboard(target, menu, e.target, target);
|
|
182
|
+
},
|
|
183
|
+
{ signal: controller.signal },
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
target.addEventListener(
|
|
187
|
+
"pointerdown",
|
|
188
|
+
(e) => {
|
|
189
|
+
const delay = getLongPressDelay(target, menu);
|
|
190
|
+
if (e.pointerType !== "touch" || delay == null || shouldIgnoreNativeTarget(e.target)) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
clearLongPress(state);
|
|
195
|
+
state.pointerId = e.pointerId;
|
|
196
|
+
state.startX = e.clientX;
|
|
197
|
+
state.startY = e.clientY;
|
|
198
|
+
state.timer = setTimeout(() => {
|
|
199
|
+
state.timer = null;
|
|
200
|
+
state.suppressClickUntil = Date.now() + 750;
|
|
201
|
+
openContextMenu(target, menu, {
|
|
202
|
+
x: state.startX,
|
|
203
|
+
y: state.startY,
|
|
204
|
+
origin: e.target,
|
|
205
|
+
trigger: "touch",
|
|
206
|
+
});
|
|
207
|
+
}, delay);
|
|
208
|
+
},
|
|
209
|
+
{ signal: controller.signal, passive: true },
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
target.addEventListener(
|
|
213
|
+
"pointermove",
|
|
214
|
+
(e) => {
|
|
215
|
+
if (e.pointerId !== state.pointerId || !state.timer) return;
|
|
216
|
+
if (
|
|
217
|
+
Math.abs(e.clientX - state.startX) > MOVE_TOLERANCE ||
|
|
218
|
+
Math.abs(e.clientY - state.startY) > MOVE_TOLERANCE
|
|
219
|
+
) {
|
|
220
|
+
clearLongPress(state);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{ signal: controller.signal, passive: true },
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
for (const type of ["pointerup", "pointercancel", "pointerleave"]) {
|
|
227
|
+
target.addEventListener(
|
|
228
|
+
type,
|
|
229
|
+
(e) => {
|
|
230
|
+
if (e.pointerId === state.pointerId) clearLongPress(state);
|
|
231
|
+
},
|
|
232
|
+
{ signal: controller.signal, passive: true },
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
target.addEventListener(
|
|
237
|
+
"click",
|
|
238
|
+
(e) => {
|
|
239
|
+
if (Date.now() <= state.suppressClickUntil) {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
e.stopPropagation();
|
|
242
|
+
state.suppressClickUntil = 0;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
{ signal: controller.signal, capture: true },
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
contextMap.set(target, state);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function disconnectContextTarget(target) {
|
|
252
|
+
const state = contextMap.get(target);
|
|
253
|
+
if (!state) return;
|
|
254
|
+
clearLongPress(state);
|
|
255
|
+
state.controller.abort();
|
|
256
|
+
if (contextFor(state.menu)?.context === target) contextByMenu.delete(state.menu);
|
|
257
|
+
contextMap.delete(target);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function connectContextMenu(menu) {
|
|
261
|
+
const controller = new AbortController();
|
|
262
|
+
menu.addEventListener(
|
|
263
|
+
EVENTS.surfaceOpen,
|
|
264
|
+
(event) => {
|
|
265
|
+
const options = event.detail?.options;
|
|
266
|
+
const trigger = options?.trigger;
|
|
267
|
+
const context = trigger?.closest?.(CONTEXT_TARGET_SELECTOR);
|
|
268
|
+
if (!context || menuFor(context) !== menu) return;
|
|
269
|
+
if (!requestContextMenu(context, menu, { origin: trigger, trigger: "button" })) {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
options.source = context;
|
|
274
|
+
options.restoreFocusTo = trigger;
|
|
275
|
+
},
|
|
276
|
+
{ signal: controller.signal },
|
|
277
|
+
);
|
|
278
|
+
return () => {
|
|
279
|
+
controller.abort();
|
|
280
|
+
contextByMenu.delete(menu);
|
|
281
|
+
disconnectSurface(menu);
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
enhance({
|
|
286
|
+
[CONTEXT_TARGET_SELECTOR]: (target) => {
|
|
287
|
+
connectContextTarget(target);
|
|
288
|
+
return () => disconnectContextTarget(target);
|
|
289
|
+
},
|
|
290
|
+
[CONTEXT_MENU_SELECTOR]: (menu) => connectContextMenu(menu),
|
|
291
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dialog fallback — minimal per-element shim for browsers without native
|
|
3
|
+
* <dialog> support (Safari <= 15.3, Firefox <= 97, both below Baseline 2023).
|
|
4
|
+
*
|
|
5
|
+
* Standalone side-effect module: the default runtime imports it, a custom
|
|
6
|
+
* build opts out by omitting it — dialog.js has no dependency on this file
|
|
7
|
+
* and falls back to its acknowledgement alert without it. On supporting
|
|
8
|
+
* browsers importing this module registers nothing.
|
|
9
|
+
*
|
|
10
|
+
* Not a polyfill: no top layer, no focus trap, no inert background. It only
|
|
11
|
+
* provides show()/showModal()/close(), an `open` property, and the
|
|
12
|
+
* form[method=dialog] close-on-submit contract (otherwise those forms really
|
|
13
|
+
* navigate on legacy browsers), so dialog.js can run its normal wiring —
|
|
14
|
+
* backdrop click, Escape cancel, close events, focus restore, and the scroll
|
|
15
|
+
* lock all come from that wiring, not from here.
|
|
16
|
+
*
|
|
17
|
+
* Presentation lives in dialog-fallback.css, keyed on the classes written
|
|
18
|
+
* below. The simulated backdrop is split in two there: a spread box-shadow
|
|
19
|
+
* for the dimming, and a transparent ::before covering the viewport for
|
|
20
|
+
* hit-testing — clicks on it target the dialog element outside its own box,
|
|
21
|
+
* which dialog.js already treats as a backdrop click, and it blocks pointer
|
|
22
|
+
* interaction with the page behind the modal.
|
|
23
|
+
*
|
|
24
|
+
* An adopter-provided polyfill (e.g. dialog-polyfill) still wins: elements it
|
|
25
|
+
* already patched expose showModal() and are left untouched.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { targetFor } from "./command.js";
|
|
29
|
+
import enhance from "./enhance.js";
|
|
30
|
+
import { CLASSES } from "./selectors.js";
|
|
31
|
+
|
|
32
|
+
function supportsDialog() {
|
|
33
|
+
return (
|
|
34
|
+
typeof HTMLDialogElement !== "undefined" &&
|
|
35
|
+
typeof HTMLDialogElement.prototype.showModal === "function"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function focusInitial(dialog) {
|
|
40
|
+
const target = dialog.querySelector("[autofocus]") || dialog;
|
|
41
|
+
if (target === dialog && !dialog.hasAttribute("tabindex")) {
|
|
42
|
+
dialog.setAttribute("tabindex", "-1");
|
|
43
|
+
}
|
|
44
|
+
target.focus?.();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Patch one dialog element with rudimentary show/showModal/close support.
|
|
49
|
+
* No-op when the element is already controllable (native or polyfilled).
|
|
50
|
+
*
|
|
51
|
+
* @param {HTMLElement} dialog A <dialog> element.
|
|
52
|
+
*/
|
|
53
|
+
export default function shimDialog(dialog) {
|
|
54
|
+
if (
|
|
55
|
+
typeof dialog.show === "function" &&
|
|
56
|
+
typeof dialog.showModal === "function" &&
|
|
57
|
+
typeof dialog.close === "function"
|
|
58
|
+
) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const doc = dialog.ownerDocument;
|
|
63
|
+
let escController = null;
|
|
64
|
+
let savedScroll = null;
|
|
65
|
+
|
|
66
|
+
function stopEscapeListener() {
|
|
67
|
+
escController?.abort();
|
|
68
|
+
escController = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleSubmit(event) {
|
|
72
|
+
const form = event.target;
|
|
73
|
+
// Read the attribute: on these browsers "dialog" is an invalid method
|
|
74
|
+
// value, so the reflected form.method property reports "get" and the
|
|
75
|
+
// form would otherwise really submit and navigate away.
|
|
76
|
+
if (form?.getAttribute?.("method")?.toLowerCase() !== "dialog") return;
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
const submitter =
|
|
79
|
+
event.submitter || (doc.activeElement?.form === form ? doc.activeElement : null);
|
|
80
|
+
dialog.close(submitter?.value || "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function handleEscape(event) {
|
|
84
|
+
if (event.key !== "Escape") return;
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
const cancel = new Event("cancel", { cancelable: true });
|
|
87
|
+
// dialog.js always claims the cancel event and decides itself whether to
|
|
88
|
+
// close; only an unwired dialog falls through to the direct close.
|
|
89
|
+
if (dialog.dispatchEvent(cancel)) {
|
|
90
|
+
dialog.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function open(modal) {
|
|
95
|
+
if (dialog.open) return;
|
|
96
|
+
// Old WebKit clamps the scroll position to 0 when the root element gains
|
|
97
|
+
// overflow:hidden (the has-modal-open lock applied right after this),
|
|
98
|
+
// losing the user's place. Remember it and put it back after close.
|
|
99
|
+
const win = doc.defaultView;
|
|
100
|
+
savedScroll = modal && win ? { x: win.scrollX, y: win.scrollY } : null;
|
|
101
|
+
dialog.setAttribute("open", "");
|
|
102
|
+
dialog.classList.toggle(CLASSES.fallbackModal, modal);
|
|
103
|
+
if (modal) {
|
|
104
|
+
escController = new AbortController();
|
|
105
|
+
doc.addEventListener("keydown", handleEscape, { signal: escController.signal });
|
|
106
|
+
}
|
|
107
|
+
focusInitial(dialog);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Object.defineProperty(dialog, "open", {
|
|
111
|
+
configurable: true,
|
|
112
|
+
get() {
|
|
113
|
+
return this.hasAttribute("open");
|
|
114
|
+
},
|
|
115
|
+
set(value) {
|
|
116
|
+
this.toggleAttribute("open", Boolean(value));
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
dialog.returnValue = "";
|
|
121
|
+
dialog.classList.add(CLASSES.dialogFallback);
|
|
122
|
+
dialog.addEventListener("submit", handleSubmit);
|
|
123
|
+
dialog.show = () => open(false);
|
|
124
|
+
dialog.showModal = () => open(true);
|
|
125
|
+
dialog.close = (returnValue = "") => {
|
|
126
|
+
if (!dialog.open) return;
|
|
127
|
+
stopEscapeListener();
|
|
128
|
+
dialog.returnValue = returnValue;
|
|
129
|
+
dialog.removeAttribute("open");
|
|
130
|
+
dialog.classList.remove(CLASSES.fallbackModal);
|
|
131
|
+
dialog.dispatchEvent(new Event("close"));
|
|
132
|
+
// After the close event: dialog.js has lifted the scroll lock and moved
|
|
133
|
+
// focus back to the trigger, so the restored position sticks.
|
|
134
|
+
if (savedScroll) {
|
|
135
|
+
doc.defaultView?.scrollTo?.(savedScroll.x, savedScroll.y);
|
|
136
|
+
savedScroll = null;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (typeof document !== "undefined" && !supportsDialog()) {
|
|
142
|
+
enhance({
|
|
143
|
+
dialog: (el) => {
|
|
144
|
+
shimDialog(el);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// A dialog inserted right before its trigger's click may not be scanned by
|
|
149
|
+
// enhance() yet, so shim command targets in capture phase too — before the
|
|
150
|
+
// delegated command routing resolves them.
|
|
151
|
+
document.addEventListener(
|
|
152
|
+
"click",
|
|
153
|
+
(event) => {
|
|
154
|
+
const trigger = event.target?.closest?.("button[commandfor][command]");
|
|
155
|
+
if (!trigger) return;
|
|
156
|
+
const dialog = targetFor(trigger);
|
|
157
|
+
if (dialog?.localName === "dialog") {
|
|
158
|
+
shimDialog(dialog);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
true,
|
|
162
|
+
);
|
|
163
|
+
}
|