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/context-menu.js
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import enhance from "./enhance.js";
|
|
2
2
|
import { EVENTS } from "./events.js";
|
|
3
|
-
import { focusFirstMenuItem, hasMenuItems } from "./menu.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
closeSurface,
|
|
7
|
-
disconnectSurface,
|
|
8
|
-
isSurfaceOpen,
|
|
9
|
-
openSurface,
|
|
10
|
-
prepareSurface,
|
|
11
|
-
} from "./surface.js";
|
|
3
|
+
import { connectMenu, focusFirstMenuItem, hasMenuItems } from "./menu.js";
|
|
4
|
+
import { closeSurface, isSurfaceOpen, openSurface, retainSurface } from "./surface.js";
|
|
12
5
|
|
|
13
6
|
const LONG_PRESS_MS = 450;
|
|
7
|
+
const LONG_PRESS_CLICK_SUPPRESSION_MS = 750;
|
|
14
8
|
const MOVE_TOLERANCE = 10;
|
|
15
|
-
const CONTEXT_MENU_SELECTOR =
|
|
9
|
+
const CONTEXT_MENU_SELECTOR = 'menu, [role="menu"]';
|
|
16
10
|
const CONTEXT_TARGET_SELECTOR = "[data-context-menu]";
|
|
17
11
|
const contextMap = new WeakMap();
|
|
18
12
|
const contextByMenu = new WeakMap();
|
|
19
13
|
|
|
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
14
|
export function contextFor(menu) {
|
|
23
15
|
return contextByMenu.get(menu) || null;
|
|
24
16
|
}
|
|
@@ -55,27 +47,29 @@ function getLongPressDelay(target, menu) {
|
|
|
55
47
|
if (value === "") return LONG_PRESS_MS;
|
|
56
48
|
|
|
57
49
|
const delay = Number.parseInt(value, 10);
|
|
58
|
-
return Number.isFinite(delay) ? delay : LONG_PRESS_MS;
|
|
50
|
+
return Number.isFinite(delay) ? Math.max(0, delay) : LONG_PRESS_MS;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readPanelOptions(menu) {
|
|
54
|
+
const opts = { autoClose: "true" };
|
|
55
|
+
const ds = menu.dataset;
|
|
56
|
+
if (ds.flyoutMobile) opts.mobile = ds.flyoutMobile;
|
|
57
|
+
if (ds.flyoutAutoClose) opts.autoClose = ds.flyoutAutoClose;
|
|
58
|
+
return opts;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function focusMenuContainer(menu) {
|
|
62
62
|
if (!menu.hasAttribute("tabindex")) {
|
|
63
63
|
menu.tabIndex = -1;
|
|
64
64
|
}
|
|
65
|
-
menu.
|
|
65
|
+
// Moving the mounted menu into the surface root must not scroll the page.
|
|
66
|
+
// Genuine user scrolling is handled separately by the surface policy.
|
|
67
|
+
menu.focus({ preventScroll: true });
|
|
66
68
|
}
|
|
67
69
|
|
|
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
70
|
function focusMenu(menu, mode) {
|
|
76
|
-
if (!hasMenuItems(menu)) return;
|
|
77
71
|
if (mode === "first-item") {
|
|
78
|
-
focusFirstMenuItem(menu);
|
|
72
|
+
if (hasMenuItems(menu)) focusFirstMenuItem(menu);
|
|
79
73
|
} else {
|
|
80
74
|
focusMenuContainer(menu);
|
|
81
75
|
}
|
|
@@ -108,9 +102,11 @@ function openContextMenu(context, menu, opts = {}) {
|
|
|
108
102
|
y: opts.y,
|
|
109
103
|
placement: opts.placement || "bottom-start",
|
|
110
104
|
distance: opts.distance ?? 2,
|
|
111
|
-
|
|
105
|
+
dismissOnScroll: true,
|
|
106
|
+
mobile: opts.mobile || "auto",
|
|
112
107
|
scope: opts.scope || getContextScope(context),
|
|
113
108
|
restoreFocusTo: opts.restoreFocusTo,
|
|
109
|
+
...readPanelOptions(menu),
|
|
114
110
|
})
|
|
115
111
|
) {
|
|
116
112
|
focusMenu(menu, opts.focus);
|
|
@@ -144,11 +140,16 @@ function connectContextTarget(target) {
|
|
|
144
140
|
const menu = menuFor(target);
|
|
145
141
|
if (!menu) return;
|
|
146
142
|
|
|
147
|
-
|
|
143
|
+
const release = retainSurface(menu);
|
|
148
144
|
const controller = new AbortController();
|
|
145
|
+
const releaseMenu = connectMenu(menu, {
|
|
146
|
+
close: (menu) => closeSurface(menu),
|
|
147
|
+
});
|
|
149
148
|
const state = {
|
|
150
149
|
controller,
|
|
150
|
+
releaseMenu,
|
|
151
151
|
menu,
|
|
152
|
+
release,
|
|
152
153
|
timer: null,
|
|
153
154
|
pointerId: null,
|
|
154
155
|
startX: 0,
|
|
@@ -197,7 +198,7 @@ function connectContextTarget(target) {
|
|
|
197
198
|
state.startY = e.clientY;
|
|
198
199
|
state.timer = setTimeout(() => {
|
|
199
200
|
state.timer = null;
|
|
200
|
-
state.suppressClickUntil = Date.now() +
|
|
201
|
+
state.suppressClickUntil = Date.now() + LONG_PRESS_CLICK_SUPPRESSION_MS;
|
|
201
202
|
openContextMenu(target, menu, {
|
|
202
203
|
x: state.startX,
|
|
203
204
|
y: state.startY,
|
|
@@ -245,6 +246,45 @@ function connectContextTarget(target) {
|
|
|
245
246
|
{ signal: controller.signal, capture: true },
|
|
246
247
|
);
|
|
247
248
|
|
|
249
|
+
target.addEventListener(
|
|
250
|
+
"click",
|
|
251
|
+
(e) => {
|
|
252
|
+
const trigger = e.target.closest("[data-context-menu-trigger]");
|
|
253
|
+
if (
|
|
254
|
+
!trigger ||
|
|
255
|
+
trigger.getAttribute("aria-controls") !== target.getAttribute("data-context-menu")
|
|
256
|
+
)
|
|
257
|
+
return;
|
|
258
|
+
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
e.stopPropagation();
|
|
261
|
+
|
|
262
|
+
if (isSurfaceOpen(menu)) {
|
|
263
|
+
closeSurface(menu);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!requestContextMenu(target, menu, { origin: trigger, trigger: "button" })) return;
|
|
268
|
+
|
|
269
|
+
const rect = trigger.getBoundingClientRect();
|
|
270
|
+
if (
|
|
271
|
+
openSurface(menu, {
|
|
272
|
+
source: target,
|
|
273
|
+
x: rect.left,
|
|
274
|
+
y: rect.bottom,
|
|
275
|
+
placement: "bottom-start",
|
|
276
|
+
distance: 4,
|
|
277
|
+
dismissOnScroll: true,
|
|
278
|
+
restoreFocusTo: trigger,
|
|
279
|
+
...readPanelOptions(menu),
|
|
280
|
+
})
|
|
281
|
+
) {
|
|
282
|
+
focusMenuContainer(menu);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
{ signal: controller.signal },
|
|
286
|
+
);
|
|
287
|
+
|
|
248
288
|
contextMap.set(target, state);
|
|
249
289
|
}
|
|
250
290
|
|
|
@@ -253,39 +293,18 @@ function disconnectContextTarget(target) {
|
|
|
253
293
|
if (!state) return;
|
|
254
294
|
clearLongPress(state);
|
|
255
295
|
state.controller.abort();
|
|
256
|
-
|
|
296
|
+
state.releaseMenu?.();
|
|
297
|
+
if (contextFor(state.menu)?.context === target) {
|
|
298
|
+
contextByMenu.delete(state.menu);
|
|
299
|
+
closeSurface(state.menu, { restoreFocus: false });
|
|
300
|
+
}
|
|
301
|
+
state.release();
|
|
257
302
|
contextMap.delete(target);
|
|
258
303
|
}
|
|
259
304
|
|
|
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
305
|
enhance({
|
|
286
306
|
[CONTEXT_TARGET_SELECTOR]: (target) => {
|
|
287
307
|
connectContextTarget(target);
|
|
288
308
|
return () => disconnectContextTarget(target);
|
|
289
309
|
},
|
|
290
|
-
[CONTEXT_MENU_SELECTOR]: (menu) => connectContextMenu(menu),
|
|
291
310
|
});
|
package/src/js/dialog.js
CHANGED
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
* Command semantics are explicit: show-modal calls showModal(), show calls
|
|
9
9
|
* show(). data-dialog-modal still defines the default for direct openDialog()
|
|
10
10
|
* calls when no command mode is forced.
|
|
11
|
-
*
|
|
11
|
+
* data-dialog-dismissible gates backdrop click only: with it, the runtime
|
|
12
|
+
* handles light dismiss (rewriting closedby="any" to "closerequest" so the
|
|
13
|
+
* native dialog does not double-handle). Without it, a native closedby="any"
|
|
14
|
+
* dialog keeps its own light dismiss and other dialogs flash a static
|
|
15
|
+
* indicator on backdrop click. Escape and explicit close requests always
|
|
16
|
+
* close, unless an application cancels the actual:dialog-cancel event, and
|
|
17
|
+
* closedby="none" keeps its native meaning (no Escape, no backdrop close).
|
|
12
18
|
*
|
|
13
19
|
* View transitions (opt-in via data-dialog-view-transition):
|
|
14
20
|
* When the browser supports document.startViewTransition and the user allows
|
|
@@ -16,10 +22,9 @@
|
|
|
16
22
|
* view-transition-name. Otherwise native dialog behavior remains the
|
|
17
23
|
* baseline: the dialog simply opens and closes.
|
|
18
24
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* controllable.
|
|
25
|
+
* The runtime assumes native <dialog> with show()/showModal()/close() — the
|
|
26
|
+
* Minimal browser tier. requestClose() is newer than the floor and is used
|
|
27
|
+
* when present, falling back to close() otherwise.
|
|
23
28
|
*/
|
|
24
29
|
|
|
25
30
|
import { registerCommands, targetFor } from "./command.js";
|
|
@@ -32,35 +37,10 @@ const wiredDialogs = new Set();
|
|
|
32
37
|
const DIALOG_COMMANDS = ["show-modal", "show", "request-close", "close"];
|
|
33
38
|
const DIALOG_SELECTOR = "dialog";
|
|
34
39
|
const DIALOG_TITLE_SELECTOR = "[data-title], h1, h2, h3, h4, h5, h6";
|
|
35
|
-
const DEFAULT_UNSUPPORTED_MESSAGE =
|
|
36
|
-
"This dialog cannot open because this browser does not support this feature.";
|
|
37
40
|
let uid = 0;
|
|
38
41
|
|
|
39
|
-
function supportsDialog() {
|
|
40
|
-
return (
|
|
41
|
-
typeof HTMLDialogElement !== "undefined" &&
|
|
42
|
-
typeof HTMLDialogElement.prototype.showModal === "function"
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
42
|
function isDialogElement(el) {
|
|
46
|
-
return
|
|
47
|
-
typeof Node !== "undefined" && el?.nodeType === Node.ELEMENT_NODE && el.localName === "dialog"
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function canControlDialog(el) {
|
|
52
|
-
// Optional polyfills may patch registered <dialog> elements without making
|
|
53
|
-
// supportsDialog() true globally, so check the actual element before opening.
|
|
54
|
-
return (
|
|
55
|
-
isDialogElement(el) &&
|
|
56
|
-
typeof el.show === "function" &&
|
|
57
|
-
typeof el.showModal === "function" &&
|
|
58
|
-
typeof el.close === "function"
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function isDialog(el) {
|
|
63
|
-
return canControlDialog(el);
|
|
43
|
+
return el?.nodeType === 1 && el.localName === "dialog";
|
|
64
44
|
}
|
|
65
45
|
|
|
66
46
|
function boolData(el, name) {
|
|
@@ -98,13 +78,6 @@ function syncDialogSemantics(dialog, modal = isModal(dialog)) {
|
|
|
98
78
|
}
|
|
99
79
|
}
|
|
100
80
|
|
|
101
|
-
// Reached only when neither the optional dialog-fallback module nor an
|
|
102
|
-
// adopter polyfill made the target controllable on a legacy browser.
|
|
103
|
-
function notifyUnsupportedDialog(trigger) {
|
|
104
|
-
const win = trigger.ownerDocument.defaultView;
|
|
105
|
-
win?.alert?.(DEFAULT_UNSUPPORTED_MESSAGE);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
81
|
function isModal(dialog) {
|
|
109
82
|
return dialog.getAttribute("data-dialog-modal") !== "false";
|
|
110
83
|
}
|
|
@@ -227,16 +200,17 @@ function isOutsideDialog(dialog, event) {
|
|
|
227
200
|
|
|
228
201
|
function flashStatic(dialog) {
|
|
229
202
|
const state = ensureDialogWired(dialog);
|
|
203
|
+
const win = dialog.ownerDocument.defaultView;
|
|
230
204
|
|
|
231
205
|
if (state.staticTimer) {
|
|
232
|
-
|
|
206
|
+
win?.clearTimeout(state.staticTimer);
|
|
233
207
|
}
|
|
234
208
|
|
|
235
209
|
dialog.classList.remove(CLASSES.static);
|
|
236
210
|
void dialog.offsetWidth;
|
|
237
211
|
dialog.classList.add(CLASSES.static);
|
|
238
212
|
|
|
239
|
-
state.staticTimer =
|
|
213
|
+
state.staticTimer = win?.setTimeout(() => {
|
|
240
214
|
dialog.classList.remove(CLASSES.static);
|
|
241
215
|
state.staticTimer = null;
|
|
242
216
|
}, 250);
|
|
@@ -256,7 +230,7 @@ function finishClose(dialog, returnValue = "") {
|
|
|
256
230
|
}
|
|
257
231
|
|
|
258
232
|
export function closeDialog(dialog, returnValue = "") {
|
|
259
|
-
if (!
|
|
233
|
+
if (!isDialogElement(dialog) || !dialog.open) return;
|
|
260
234
|
|
|
261
235
|
const state = ensureDialogWired(dialog);
|
|
262
236
|
if (state.closing) return;
|
|
@@ -285,7 +259,7 @@ export function closeDialog(dialog, returnValue = "") {
|
|
|
285
259
|
}
|
|
286
260
|
|
|
287
261
|
export function requestDialogClose(dialog, returnValue = "") {
|
|
288
|
-
if (!
|
|
262
|
+
if (!isDialogElement(dialog) || !dialog.open) return;
|
|
289
263
|
|
|
290
264
|
if (typeof dialog.requestClose === "function") {
|
|
291
265
|
ensureDialogWired(dialog).returnValue = returnValue;
|
|
@@ -297,14 +271,20 @@ export function requestDialogClose(dialog, returnValue = "") {
|
|
|
297
271
|
}
|
|
298
272
|
|
|
299
273
|
export function openDialog(dialog, trigger = null, forcedModal = null) {
|
|
300
|
-
if (!
|
|
274
|
+
if (!isDialogElement(dialog) || dialog.open || !dialog.isConnected) return;
|
|
301
275
|
|
|
302
276
|
const state = ensureDialogWired(dialog);
|
|
303
277
|
const modal = resolveModalMode(dialog, forcedModal);
|
|
304
278
|
syncDialogSemantics(dialog, modal);
|
|
305
279
|
|
|
306
280
|
state.closing = false;
|
|
307
|
-
state.restoreFocusTo = trigger ||
|
|
281
|
+
state.restoreFocusTo = trigger || dialog.ownerDocument.activeElement;
|
|
282
|
+
|
|
283
|
+
// showModal()/show() focus the dialog, and Chrome resets the viewport scroll
|
|
284
|
+
// while doing so; capture the position first and restore it so the page does
|
|
285
|
+
// not jump to the top behind the modal.
|
|
286
|
+
const viewport = dialog.ownerDocument.defaultView;
|
|
287
|
+
const scrollY = viewport?.scrollY ?? 0;
|
|
308
288
|
|
|
309
289
|
if (modal) {
|
|
310
290
|
dialog.showModal();
|
|
@@ -314,6 +294,10 @@ export function openDialog(dialog, trigger = null, forcedModal = null) {
|
|
|
314
294
|
state.modalOpen = false;
|
|
315
295
|
}
|
|
316
296
|
|
|
297
|
+
if (scrollY > 0) {
|
|
298
|
+
viewport?.scrollTo(0, scrollY);
|
|
299
|
+
}
|
|
300
|
+
|
|
317
301
|
syncModalOpenClass(dialog.ownerDocument);
|
|
318
302
|
}
|
|
319
303
|
|
|
@@ -322,14 +306,19 @@ function handleDialogClick(event) {
|
|
|
322
306
|
|
|
323
307
|
if (event.target !== dialog || !isOutsideDialog(dialog, event)) return;
|
|
324
308
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
309
|
+
// A native closedby="any" dialog owns its light dismiss; the runtime only
|
|
310
|
+
// takes over when the author opted in via data-dialog-dismissible (which
|
|
311
|
+
// rewrites "any" to "closerequest"). Leave the native close alone.
|
|
312
|
+
if (!isDismissible(dialog)) {
|
|
313
|
+
const closedBy = "closedBy" in dialog ? dialog.closedBy : "";
|
|
314
|
+
if (closedBy === "any") return;
|
|
315
|
+
event.preventDefault();
|
|
316
|
+
flashStatic(dialog);
|
|
329
317
|
return;
|
|
330
318
|
}
|
|
331
319
|
|
|
332
|
-
|
|
320
|
+
event.preventDefault();
|
|
321
|
+
requestDialogClose(dialog);
|
|
333
322
|
}
|
|
334
323
|
|
|
335
324
|
function handleDialogSubmit(event) {
|
|
@@ -346,25 +335,25 @@ function handleDialogSubmit(event) {
|
|
|
346
335
|
|
|
347
336
|
function handleDialogCancel(event) {
|
|
348
337
|
const dialog = event.currentTarget;
|
|
338
|
+
const state = dialogMap.get(dialog);
|
|
349
339
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
340
|
+
// Escape and close requests always close, whatever the dismissible flag:
|
|
341
|
+
// data-dialog-dismissible only gates light dismiss (backdrop click, handled
|
|
342
|
+
// in handleDialogClick). Applications intercept via actual:dialog-cancel.
|
|
356
343
|
const request = new CustomEvent(EVENTS.dialogCancel, {
|
|
357
344
|
bubbles: true,
|
|
358
345
|
cancelable: true,
|
|
359
346
|
detail: { dialog, sourceEvent: event },
|
|
360
347
|
});
|
|
361
348
|
if (!dialog.dispatchEvent(request)) {
|
|
349
|
+
// Close was prevented; drop any returnValue staged for it.
|
|
362
350
|
event.preventDefault();
|
|
351
|
+
if (state) state.returnValue = "";
|
|
363
352
|
return;
|
|
364
353
|
}
|
|
365
354
|
|
|
366
355
|
event.preventDefault();
|
|
367
|
-
closeDialog(dialog,
|
|
356
|
+
closeDialog(dialog, state?.returnValue || "");
|
|
368
357
|
}
|
|
369
358
|
|
|
370
359
|
function handleDialogClose(event) {
|
|
@@ -376,7 +365,7 @@ function handleDialogClose(event) {
|
|
|
376
365
|
if (!state) return;
|
|
377
366
|
|
|
378
367
|
if (state.staticTimer) {
|
|
379
|
-
|
|
368
|
+
dialog.ownerDocument.defaultView?.clearTimeout(state.staticTimer);
|
|
380
369
|
state.staticTimer = null;
|
|
381
370
|
}
|
|
382
371
|
|
|
@@ -389,8 +378,11 @@ function handleDialogClose(event) {
|
|
|
389
378
|
const restoreFocusTo = state.restoreFocusTo;
|
|
390
379
|
state.restoreFocusTo = null;
|
|
391
380
|
|
|
392
|
-
|
|
393
|
-
|
|
381
|
+
// The native dialog restores focus to the previously focused element on
|
|
382
|
+
// close; only step in when it has not (e.g. an explicit trigger we tracked)
|
|
383
|
+
// and avoid scrolling the viewport while doing so.
|
|
384
|
+
if (restoreFocusTo?.isConnected && dialog.ownerDocument.activeElement !== restoreFocusTo) {
|
|
385
|
+
restoreFocusTo.focus({ preventScroll: true });
|
|
394
386
|
}
|
|
395
387
|
}
|
|
396
388
|
|
|
@@ -407,18 +399,20 @@ function ensureDialogWired(dialog) {
|
|
|
407
399
|
staticTimer: null,
|
|
408
400
|
};
|
|
409
401
|
|
|
410
|
-
|
|
402
|
+
// A dialog that is already `open` in the markup is non-modal per spec,
|
|
403
|
+
// whatever data-dialog-modal says; infer semantics from its actual state.
|
|
404
|
+
syncDialogSemantics(dialog, dialog.open ? isModalOpen(dialog) : isModal(dialog));
|
|
411
405
|
|
|
412
406
|
dialog.addEventListener("click", handleDialogClick, { signal: controller.signal });
|
|
413
407
|
dialog.addEventListener("submit", handleDialogSubmit, { signal: controller.signal });
|
|
414
408
|
dialog.addEventListener("cancel", handleDialogCancel, { signal: controller.signal });
|
|
415
409
|
dialog.addEventListener("close", handleDialogClose, { signal: controller.signal });
|
|
416
410
|
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
411
|
+
// Only rewrite "any" when the runtime owns light dismiss (dismissible
|
|
412
|
+
// backdrop): the author opted into Actual's controlled behavior. Otherwise
|
|
413
|
+
// closedby keeps its native meaning — "any" closes on backdrop click,
|
|
414
|
+
// "closerequest" on Escape, "none" disables both. Never override "none".
|
|
415
|
+
if (isDismissible(dialog) && "closedBy" in dialog && dialog.closedBy === "any") {
|
|
422
416
|
dialog.closedBy = "closerequest";
|
|
423
417
|
}
|
|
424
418
|
|
|
@@ -437,7 +431,7 @@ function disconnectDialog(dialog) {
|
|
|
437
431
|
if (!state) return;
|
|
438
432
|
|
|
439
433
|
if (state.staticTimer) {
|
|
440
|
-
|
|
434
|
+
dialog.ownerDocument.defaultView?.clearTimeout(state.staticTimer);
|
|
441
435
|
}
|
|
442
436
|
|
|
443
437
|
state.controller.abort();
|
|
@@ -454,7 +448,7 @@ registerCommands(DIALOG_COMMANDS, {
|
|
|
454
448
|
return isDialogElement(dialog) ? dialog : null;
|
|
455
449
|
},
|
|
456
450
|
prepare: (trigger, dialog, command) => {
|
|
457
|
-
if (
|
|
451
|
+
if (isDialogElement(dialog)) {
|
|
458
452
|
ensureDialogWired(dialog);
|
|
459
453
|
}
|
|
460
454
|
|
|
@@ -470,10 +464,7 @@ registerCommands(DIALOG_COMMANDS, {
|
|
|
470
464
|
handle: (event, trigger, dialog, command) => {
|
|
471
465
|
event.preventDefault();
|
|
472
466
|
|
|
473
|
-
if (!
|
|
474
|
-
if (command === "show-modal" || command === "show") {
|
|
475
|
-
notifyUnsupportedDialog(trigger);
|
|
476
|
-
}
|
|
467
|
+
if (!isDialogElement(dialog)) {
|
|
477
468
|
return;
|
|
478
469
|
}
|
|
479
470
|
|
|
@@ -493,11 +484,9 @@ registerCommands(DIALOG_COMMANDS, {
|
|
|
493
484
|
},
|
|
494
485
|
});
|
|
495
486
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
});
|
|
503
|
-
}
|
|
487
|
+
enhance({
|
|
488
|
+
[DIALOG_SELECTOR]: (dialog) => {
|
|
489
|
+
connectDialog(dialog);
|
|
490
|
+
return () => disconnectDialog(dialog);
|
|
491
|
+
},
|
|
492
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { registerCommands } from "./command.js";
|
|
2
|
+
import { EVENTS } from "./events.js";
|
|
3
|
+
|
|
4
|
+
registerCommands("--dismiss", {
|
|
5
|
+
handle(event, trigger, target) {
|
|
6
|
+
event.preventDefault();
|
|
7
|
+
target.hidden = true;
|
|
8
|
+
target.dispatchEvent(
|
|
9
|
+
new CustomEvent(EVENTS.dismiss, {
|
|
10
|
+
bubbles: true,
|
|
11
|
+
detail: { trigger },
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
14
|
+
},
|
|
15
|
+
});
|