actual-css 0.9.2 → 0.11.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 +169 -1
- package/README.md +65 -44
- package/components.json +35 -14
- package/dist/actual.full.css +877 -583
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/design-notes/app-ui.md +1 -1
- package/docs/design-notes/column-layout.md +103 -520
- package/docs/design-notes/enhancement-contract.md +16 -7
- package/docs/design-notes/forced-colors.md +12 -1
- package/docs/design-notes/naming.md +1 -1
- package/docs/design-notes/platform-alignment.md +8 -4
- package/docs/design-notes/rich-text.md +61 -0
- package/docs/design-notes/steps.md +2 -2
- package/docs/design-notes/surface-reparenting.md +8 -9
- package/docs/navigation.json +4 -0
- package/docs/pages/components/accordion.md +55 -1
- package/docs/pages/components/alert.md +9 -15
- package/docs/pages/components/aura.md +12 -13
- package/docs/pages/components/badge.md +16 -7
- package/docs/pages/components/bleed.md +42 -0
- package/docs/pages/components/button.md +30 -4
- package/docs/pages/components/card.md +21 -86
- package/docs/pages/components/chat.md +5 -0
- package/docs/pages/components/close.md +48 -0
- package/docs/pages/components/data-list.md +26 -1
- package/docs/pages/components/dialog.md +21 -18
- package/docs/pages/components/drawer.md +9 -9
- package/docs/pages/components/navbar.md +5 -5
- package/docs/pages/components/pagination.md +15 -15
- package/docs/pages/components/shimmer.md +57 -0
- package/docs/pages/components/skeleton.md +2 -1
- package/docs/pages/components/spinner.md +14 -9
- package/docs/pages/components/table.md +13 -0
- package/docs/pages/enhancements/flyout.md +2 -0
- package/docs/pages/enhancements/overview.md +5 -0
- package/docs/pages/enhancements/tabs.md +4 -0
- package/docs/pages/enhancements/tooltip.md +3 -1
- package/docs/pages/examples/overview.md +2 -1
- package/docs/pages/forms/choice-cards.md +116 -4
- package/docs/pages/forms/combobox.md +39 -12
- package/docs/pages/forms/joined-controls.md +2 -5
- package/docs/pages/forms/overview.md +10 -1
- package/docs/pages/forms/range.md +110 -5
- package/docs/pages/forms/rich-text.md +110 -0
- package/docs/pages/forms/validation.md +5 -1
- package/docs/pages/foundations/theming.md +110 -18
- package/docs/pages/foundations/tokens.md +41 -13
- package/docs/pages/guides/bootstrap.md +2 -1
- package/docs/pages/guides/introduction.md +15 -23
- package/docs/pages/guides/modular-import.md +54 -7
- package/docs/pages/guides/progressive-enhancement.md +19 -17
- package/docs/pages/guides/tailwind.md +21 -7
- package/docs/pages/layout/column-layout.md +92 -54
- package/docs/pages/layout/media.md +1 -1
- package/docs/pages/layout/overview.md +1 -1
- package/docs/pages/utilities/overview.md +1 -1
- package/docs/pages/utilities/sizing-wrapping.md +5 -3
- package/llms.txt +25 -4
- package/package.json +10 -6
- package/reserved-classes.json +6 -30
- package/skills/actual-css/PROJECT.md.example +65 -0
- package/skills/actual-css/README.md +64 -0
- package/skills/actual-css/SKILL.md +234 -0
- package/skills/actual-css/references/accessibility.md +44 -0
- package/skills/actual-css/references/api-and-vocabulary.md +94 -0
- package/skills/actual-css/references/enhancements.md +59 -0
- package/skills/actual-css/references/forms.md +49 -0
- package/skills/actual-css/references/layout-and-theming.md +82 -0
- package/skills/actual-css/references/review-checklist.md +51 -0
- package/skills/actual-css/scripts/inspect-actual-css.mjs +171 -0
- package/src/css/components/accordion.css +44 -7
- package/src/css/components/alert.css +23 -43
- package/src/css/components/app-nav.css +9 -0
- package/src/css/components/avatar.css +1 -1
- package/src/css/components/badge.css +11 -39
- package/src/css/components/bleed.css +79 -0
- package/src/css/components/button.css +6 -17
- package/src/css/components/card.css +7 -40
- package/src/css/components/close.css +91 -0
- package/src/css/components/data-list.css +18 -0
- package/src/css/components/drawer.css +15 -56
- package/src/css/components/fab.css +10 -0
- package/src/css/components/flyout.css +5 -1
- package/src/css/components/index.css +2 -0
- package/src/css/components/join.css +36 -20
- package/src/css/components/menu.css +12 -0
- package/src/css/components/meter.css +1 -1
- package/src/css/components/modal.css +10 -57
- package/src/css/components/navbar.css +27 -0
- package/src/css/components/pagination.css +3 -2
- package/src/css/components/rating.css +1 -1
- package/src/css/components/status-bar.css +2 -2
- package/src/css/components/steps.css +3 -3
- package/src/css/components/tab.css +3 -3
- package/src/css/components/table.css +6 -0
- package/src/css/components/tooltip.css +1 -1
- package/src/css/core/focus.css +19 -3
- package/src/css/core/theme.css +33 -43
- package/src/css/core/tokens.css +9 -5
- package/src/css/core/variants.css +5 -34
- package/src/css/effects/aura.css +51 -33
- package/src/css/effects/index.css +1 -0
- package/src/css/effects/shimmer.css +57 -0
- package/src/css/forms/choice-card.css +142 -24
- package/src/css/forms/choice.css +2 -3
- package/src/css/forms/control.css +17 -10
- package/src/css/forms/custom-select.css +4 -4
- package/src/css/forms/native.css +4 -4
- package/src/css/forms/otp.css +9 -1
- package/src/css/forms/range.css +72 -14
- package/src/css/forms/switch.css +2 -3
- package/src/css/layout/app-layout.css +1 -1
- package/src/css/layout/column-layout.css +26 -111
- package/src/css/utilities/extra.css +7 -6
- package/src/js/command.js +61 -32
- package/src/js/dialog.js +103 -47
- package/src/js/dismiss.js +1 -2
- package/src/js/enhance.js +91 -126
- package/src/js/escape.js +1 -0
- package/src/js/flyout.js +10 -1
- package/src/js/focus-group.js +2 -1
- package/src/js/full.js +1 -0
- package/src/js/keys.js +14 -0
- package/src/js/menu.js +2 -1
- package/src/js/password.js +1 -8
- package/src/js/range.js +151 -0
- package/src/js/scrollspy.js +4 -5
- package/src/js/status.js +21 -10
- package/src/js/surface.js +12 -8
- package/src/js/tab.js +2 -1
- package/src/js/tooltip.js +29 -2
- package/src/js/validation.js +8 -15
- package/dist/actual.css +0 -876
- package/dist/actual.js +0 -1
- package/dist/actual.min.css +0 -1
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* Docs: docs/pages/layout/column-layout.md
|
|
5
5
|
*
|
|
6
6
|
* Neither space-driven like .grid nor count-driven like .grid-N: this is a
|
|
7
|
-
* coordinate system.
|
|
8
|
-
*
|
|
9
|
-
* deliberately no automatic collapse — no threshold,
|
|
10
|
-
* .container-query behavior, no breakpoint variants. A single
|
|
11
|
-
* threshold cannot be right for arbitrary per-region compositions,
|
|
12
|
-
* hidden default would have to be undone rather than extended.
|
|
7
|
+
* coordinate system. Each direct child places itself with two numbers,
|
|
8
|
+
* --column-start and --column-span, and the author owns narrow-container
|
|
9
|
+
* recomposition. There is deliberately no automatic collapse — no threshold,
|
|
10
|
+
* no hidden .container-query behavior, no breakpoint variants. A single
|
|
11
|
+
* framework threshold cannot be right for arbitrary per-region compositions,
|
|
12
|
+
* and a hidden default would have to be undone rather than extended.
|
|
13
13
|
*
|
|
14
14
|
* See docs/design-notes/column-layout.md.
|
|
15
15
|
*
|
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
* media + flexible content -> .media
|
|
22
22
|
* exact custom track templates -> --grid-columns on .grid
|
|
23
23
|
*
|
|
24
|
-
* Child contract: each direct child is placed on the canvas via its own
|
|
25
|
-
* .column-span-N / .column-start-N.
|
|
26
|
-
*
|
|
27
24
|
* Unlike .grid-N, the track template does not read --gap, so the gap
|
|
28
25
|
* utilities are exact here at every width.
|
|
26
|
+
*
|
|
27
|
+
* Public hooks:
|
|
28
|
+
* --column-start (the start line, 1-12; unset = auto-placed)
|
|
29
|
+
* --column-span (the width in canvas units, 1-12; unset = to the canvas end)
|
|
29
30
|
*/
|
|
30
31
|
.column-layout {
|
|
31
32
|
display: grid;
|
|
@@ -33,114 +34,28 @@
|
|
|
33
34
|
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/*
|
|
37
|
-
*
|
|
37
|
+
/* One rule covers the four placements:
|
|
38
|
+
* neither auto-placed, spanning the whole canvas
|
|
39
|
+
* span only auto-placed over N units, so 8 + 4 flows onto one row
|
|
40
|
+
* start only from line S to the canvas end (span 13 - S)
|
|
41
|
+
* start and span explicit; keep S + N - 1 <= 12
|
|
38
42
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* 3. .column-span-* the end only, so it refines either of the above
|
|
43
|
+
* The start stays `auto` unless set: a definite start on every child would
|
|
44
|
+
* take all of them out of auto-placement, and an 8 + 4 pair would stack.
|
|
42
45
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* Zero specificity is the point: an application's own container-query rules
|
|
49
|
-
* override placement additively, with a single-class selector and without
|
|
50
|
-
* having to undo framework specificity first.
|
|
51
|
-
*/
|
|
52
|
-
|
|
53
|
-
/* Tier 1. A child with no placement class stays a readable full-width region
|
|
54
|
-
* instead of collapsing to one twelfth. Expressed as a span, not as
|
|
55
|
-
* `1 / -1`: a definite start line would give every child a definite column
|
|
56
|
-
* position and disable auto-placement, so `.column-span-8` next to
|
|
57
|
-
* `.column-span-4` would stack instead of composing 8 + 4.
|
|
46
|
+
* The reset makes the two hooks inputs of this grid item rather than an
|
|
47
|
+
* inherited context: a nested .column-layout keeps the coordinates it was
|
|
48
|
+
* given for its own placement, and its children start from nothing. Zero
|
|
49
|
+
* specificity, so any author rule on the child wins.
|
|
58
50
|
*
|
|
59
51
|
* min-inline-size: 0 keeps the item box inside its track so the canvas never
|
|
60
52
|
* grows past its container. It does not stop content from painting outside a
|
|
61
53
|
* narrow track — that distinction is the primitive's contract, not a bug. */
|
|
62
54
|
:where(.column-layout) > * {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/* Tier 2. A definite start line, running to the end of the canvas unless a
|
|
68
|
-
* .column-span-* refines the end. `column-start-9` alone is therefore the
|
|
69
|
-
* last four columns rather than an implicit 21-line overflow. */
|
|
70
|
-
:where(.column-start-1) {
|
|
71
|
-
grid-column: 1 / -1;
|
|
72
|
-
}
|
|
73
|
-
:where(.column-start-2) {
|
|
74
|
-
grid-column: 2 / -1;
|
|
75
|
-
}
|
|
76
|
-
:where(.column-start-3) {
|
|
77
|
-
grid-column: 3 / -1;
|
|
78
|
-
}
|
|
79
|
-
:where(.column-start-4) {
|
|
80
|
-
grid-column: 4 / -1;
|
|
81
|
-
}
|
|
82
|
-
:where(.column-start-5) {
|
|
83
|
-
grid-column: 5 / -1;
|
|
84
|
-
}
|
|
85
|
-
:where(.column-start-6) {
|
|
86
|
-
grid-column: 6 / -1;
|
|
87
|
-
}
|
|
88
|
-
:where(.column-start-7) {
|
|
89
|
-
grid-column: 7 / -1;
|
|
90
|
-
}
|
|
91
|
-
:where(.column-start-8) {
|
|
92
|
-
grid-column: 8 / -1;
|
|
93
|
-
}
|
|
94
|
-
:where(.column-start-9) {
|
|
95
|
-
grid-column: 9 / -1;
|
|
96
|
-
}
|
|
97
|
-
:where(.column-start-10) {
|
|
98
|
-
grid-column: 10 / -1;
|
|
99
|
-
}
|
|
100
|
-
:where(.column-start-11) {
|
|
101
|
-
grid-column: 11 / -1;
|
|
102
|
-
}
|
|
103
|
-
:where(.column-start-12) {
|
|
104
|
-
grid-column: 12 / -1;
|
|
105
|
-
}
|
|
55
|
+
--column-start: initial;
|
|
56
|
+
--column-span: initial;
|
|
106
57
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
* just the offending region. */
|
|
111
|
-
:where(.column-span-1) {
|
|
112
|
-
grid-column-end: span 1;
|
|
113
|
-
}
|
|
114
|
-
:where(.column-span-2) {
|
|
115
|
-
grid-column-end: span 2;
|
|
116
|
-
}
|
|
117
|
-
:where(.column-span-3) {
|
|
118
|
-
grid-column-end: span 3;
|
|
119
|
-
}
|
|
120
|
-
:where(.column-span-4) {
|
|
121
|
-
grid-column-end: span 4;
|
|
122
|
-
}
|
|
123
|
-
:where(.column-span-5) {
|
|
124
|
-
grid-column-end: span 5;
|
|
125
|
-
}
|
|
126
|
-
:where(.column-span-6) {
|
|
127
|
-
grid-column-end: span 6;
|
|
128
|
-
}
|
|
129
|
-
:where(.column-span-7) {
|
|
130
|
-
grid-column-end: span 7;
|
|
131
|
-
}
|
|
132
|
-
:where(.column-span-8) {
|
|
133
|
-
grid-column-end: span 8;
|
|
134
|
-
}
|
|
135
|
-
:where(.column-span-9) {
|
|
136
|
-
grid-column-end: span 9;
|
|
137
|
-
}
|
|
138
|
-
:where(.column-span-10) {
|
|
139
|
-
grid-column-end: span 10;
|
|
140
|
-
}
|
|
141
|
-
:where(.column-span-11) {
|
|
142
|
-
grid-column-end: span 11;
|
|
143
|
-
}
|
|
144
|
-
:where(.column-span-12) {
|
|
145
|
-
grid-column-end: span 12;
|
|
58
|
+
grid-column-start: var(--column-start, auto);
|
|
59
|
+
grid-column-end: span var(--column-span, calc(13 - var(--column-start, 1)));
|
|
60
|
+
min-inline-size: 0;
|
|
146
61
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Extra utility helpers — explicit property/value names beside the curated
|
|
3
|
+
* shortcuts of base.css. Both are imported by utilities/index.css and are
|
|
4
|
+
* equally public (docs/design-notes/naming.md § Utilities).
|
|
5
5
|
* They are escape hatches for layout nudges, not a full atomic scale.
|
|
6
6
|
*
|
|
7
7
|
* Core principle:
|
|
@@ -177,8 +177,9 @@
|
|
|
177
177
|
overflow-y: auto;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
/* Text
|
|
181
|
-
.
|
|
180
|
+
/* Text. .text-wrap is the counterpart of base's .text-nowrap: it restores
|
|
181
|
+
wrapping inside a component that sets nowrap (.badge). */
|
|
182
|
+
.text-wrap {
|
|
182
183
|
white-space: normal;
|
|
183
184
|
}
|
|
184
185
|
|
|
@@ -251,7 +252,7 @@
|
|
|
251
252
|
/* Surface. These are semantic surfaces, not arbitrary paint: each owns its
|
|
252
253
|
matching ink and rebinds --heading to that ink, so it stays readable when it
|
|
253
254
|
recreates a light surface inside a context that installed a dark one
|
|
254
|
-
(.
|
|
255
|
+
(e.g. a --surface-solid band). Intent ink is not inherited through a surface utility; use
|
|
255
256
|
.intent-color for that. */
|
|
256
257
|
.background-surface {
|
|
257
258
|
--heading: currentColor;
|
package/src/js/command.js
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* late targets, changed commandfor values, and same-id replacements therefore
|
|
8
8
|
* work immediately without an observer, scan, refresh call, or per-element
|
|
9
9
|
* listener.
|
|
10
|
+
*
|
|
11
|
+
* A routed command cancels the click, so the browser never dispatches the
|
|
12
|
+
* native `command` event to the target, and the router does not re-dispatch
|
|
13
|
+
* one. Listen to the behavior's own events instead. Accessibility state that
|
|
14
|
+
* must exist before interaction (`aria-controls`, `aria-haspopup`,
|
|
15
|
+
* `aria-pressed`) belongs in the markup: the router has no connection step.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
/* One registry per document owns its delegated listener and command handlers.
|
|
@@ -14,18 +20,45 @@
|
|
|
14
20
|
* behavior definitions only, never DOM triggers or resolved targets. */
|
|
15
21
|
const registries = new WeakMap();
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
// The HTML `command` keywords. Any other value that does not start with "--"
|
|
24
|
+
// is the invalid state, which the browser ignores. Extend this list when HTML
|
|
25
|
+
// adds a keyword: registerCommands() throws on an unknown non-"--" name, so a
|
|
26
|
+
// stale list fails loudly instead of silently swallowing the new keyword.
|
|
27
|
+
// A future path that listens to the native `command` event instead of the
|
|
28
|
+
// click would need a per-tier matrix (native support varies) plus
|
|
29
|
+
// no-double-execution tests — the click stays the single routing event until
|
|
30
|
+
// then, which is why a routed command cancels it by design (see above).
|
|
31
|
+
const NATIVE_COMMANDS = new Set([
|
|
32
|
+
"toggle-popover",
|
|
33
|
+
"show-popover",
|
|
34
|
+
"hide-popover",
|
|
35
|
+
"close",
|
|
36
|
+
"request-close",
|
|
37
|
+
"show-modal",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function isCustom(command) {
|
|
41
|
+
return command.startsWith("--");
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
// Native command keywords are ASCII case-insensitive. Custom commands keep
|
|
26
45
|
// their exact spelling, as required by the command invoker contract.
|
|
27
46
|
function commandKey(command) {
|
|
28
|
-
return command
|
|
47
|
+
return isCustom(command) ? command : command.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function commandNames(commands, caller) {
|
|
51
|
+
const names = Array.isArray(commands) ? commands : [commands];
|
|
52
|
+
if (!names.length || names.some((name) => typeof name !== "string")) {
|
|
53
|
+
throw new TypeError(`${caller} requires one or more command names.`);
|
|
54
|
+
}
|
|
55
|
+
const invalid = names.find((name) => !isCustom(name) && !NATIVE_COMMANDS.has(commandKey(name)));
|
|
56
|
+
if (invalid !== undefined) {
|
|
57
|
+
throw new TypeError(
|
|
58
|
+
`${caller}: "${invalid}" is neither a native command nor a custom "--" command.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return [...new Set(names.map(commandKey))];
|
|
29
62
|
}
|
|
30
63
|
|
|
31
64
|
/**
|
|
@@ -46,23 +79,25 @@ export function targetFor(trigger) {
|
|
|
46
79
|
/**
|
|
47
80
|
* Build the button selector corresponding to one or more command names.
|
|
48
81
|
*
|
|
82
|
+
* Native keywords match case-insensitively, custom commands exactly — the
|
|
83
|
+
* same rule the router applies.
|
|
84
|
+
*
|
|
49
85
|
* @param {string | string[]} commands Command name or names to include.
|
|
50
86
|
* @returns {string} A selector matching command buttons with `commandfor`.
|
|
51
|
-
* @throws {TypeError} When
|
|
87
|
+
* @throws {TypeError} When a name is neither native nor a custom `--` command.
|
|
52
88
|
*/
|
|
53
89
|
export function commandSelector(commands) {
|
|
54
|
-
const attributes = commandNames(commands, "commandSelector()").map(
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
const attributes = commandNames(commands, "commandSelector()").map((name) => {
|
|
91
|
+
const value = name.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
92
|
+
return `[command="${value}"${isCustom(name) ? "" : " i"}]`;
|
|
93
|
+
});
|
|
57
94
|
return `button[commandfor]:is(${attributes.join(", ")})`;
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
function triggerFromEvent(event, doc) {
|
|
61
|
-
const ElementClass = doc.defaultView?.Element;
|
|
62
|
-
if (!ElementClass) return null;
|
|
63
|
-
|
|
64
98
|
for (const node of event.composedPath?.() ?? [event.target]) {
|
|
65
|
-
|
|
99
|
+
// nodeType rather than instanceof: nodes from another realm still count.
|
|
100
|
+
if (node.nodeType === 1 && node.matches("button[commandfor][command]")) {
|
|
66
101
|
return node;
|
|
67
102
|
}
|
|
68
103
|
if (node === doc) break;
|
|
@@ -78,7 +113,7 @@ function createRegistry(doc) {
|
|
|
78
113
|
if (event.defaultPrevented) return;
|
|
79
114
|
|
|
80
115
|
const trigger = triggerFromEvent(event, doc);
|
|
81
|
-
if (!trigger || trigger.disabled) return;
|
|
116
|
+
if (!trigger || trigger.matches(":disabled")) return;
|
|
82
117
|
|
|
83
118
|
const command = commandKey(trigger.getAttribute("command"));
|
|
84
119
|
const registration = commands.get(command);
|
|
@@ -87,7 +122,10 @@ function createRegistry(doc) {
|
|
|
87
122
|
const target = registration.resolve(trigger);
|
|
88
123
|
if (!target) return;
|
|
89
124
|
|
|
90
|
-
|
|
125
|
+
// One routed command owns the click, even when the handler throws. This
|
|
126
|
+
// is also what stops a second copy of this module, with its own registry
|
|
127
|
+
// and listener, from handling the same activation.
|
|
128
|
+
event.preventDefault();
|
|
91
129
|
registration.handle(event, trigger, target, command);
|
|
92
130
|
}
|
|
93
131
|
|
|
@@ -98,31 +136,22 @@ function createRegistry(doc) {
|
|
|
98
136
|
/**
|
|
99
137
|
* Register one behavior for one or more command names.
|
|
100
138
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* resolved target, without introducing a connection lifecycle.
|
|
139
|
+
* The router cancels the click once `resolve` returns a target, so `handle`
|
|
140
|
+
* runs with `event.defaultPrevented` already true.
|
|
104
141
|
*
|
|
105
142
|
* @param {string | string[]} commands Command name or names owned by this behavior.
|
|
106
143
|
* @param {object} options Behavior callbacks.
|
|
107
144
|
* @param {(trigger: HTMLButtonElement) => Element | null} [options.resolve=targetFor]
|
|
108
145
|
* Resolver evaluated for every action.
|
|
109
|
-
* @param {(trigger: HTMLButtonElement, target: Element, command: string) => void} [options.prepare]
|
|
110
|
-
* Optional idempotent callback run immediately before the handler.
|
|
111
146
|
* @param {(event: MouseEvent, trigger: HTMLButtonElement, target: Element, command: string) => void} options.handle
|
|
112
147
|
* Command handler.
|
|
113
148
|
* @returns {{ disconnect: () => void }} An idempotent registration teardown handle.
|
|
114
149
|
* @throws {TypeError} When command names or callbacks are invalid.
|
|
115
150
|
* @throws {Error} When a command already has an owner in the current document.
|
|
116
151
|
*/
|
|
117
|
-
export function registerCommands(commands, { resolve = targetFor,
|
|
118
|
-
const names =
|
|
119
|
-
|
|
120
|
-
];
|
|
121
|
-
if (
|
|
122
|
-
typeof resolve !== "function" ||
|
|
123
|
-
(prepare !== undefined && typeof prepare !== "function") ||
|
|
124
|
-
typeof handle !== "function"
|
|
125
|
-
) {
|
|
152
|
+
export function registerCommands(commands, { resolve = targetFor, handle } = {}) {
|
|
153
|
+
const names = commandNames(commands, "registerCommands()");
|
|
154
|
+
if (typeof resolve !== "function" || typeof handle !== "function") {
|
|
126
155
|
throw new TypeError("registerCommands() requires function callbacks.");
|
|
127
156
|
}
|
|
128
157
|
if (typeof document === "undefined") return { disconnect() {} };
|
|
@@ -139,7 +168,7 @@ export function registerCommands(commands, { resolve = targetFor, prepare, handl
|
|
|
139
168
|
throw new Error(`Command "${conflict}" is already registered in this document.`);
|
|
140
169
|
}
|
|
141
170
|
|
|
142
|
-
const registration = { resolve,
|
|
171
|
+
const registration = { resolve, handle };
|
|
143
172
|
for (const name of names) registry.commands.set(name, registration);
|
|
144
173
|
|
|
145
174
|
let connected = true;
|
package/src/js/dialog.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Close button: <button commandfor="dialog-id" command="request-close">
|
|
6
6
|
* Dialog: <dialog id="dialog-id" data-dialog-dismissible>
|
|
7
7
|
*
|
|
8
|
-
* Command semantics are explicit: show-modal calls showModal(), show calls
|
|
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
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*
|
|
25
25
|
* The runtime assumes native <dialog> with show()/showModal()/close() — the
|
|
26
26
|
* Minimal browser tier. requestClose() is newer than the floor and is used
|
|
27
|
-
* when present, falling back to close() otherwise.
|
|
27
|
+
* when present, falling back to a cancel event before close() otherwise.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { registerCommands, targetFor } from "./command.js";
|
|
@@ -34,7 +34,9 @@ import { CLASSES } from "./selectors.js";
|
|
|
34
34
|
|
|
35
35
|
const dialogMap = new WeakMap();
|
|
36
36
|
const wiredDialogs = new Set();
|
|
37
|
-
|
|
37
|
+
// HTML has no non-modal dialog keyword, so non-modal opening is the custom
|
|
38
|
+
// `--show` command.
|
|
39
|
+
const DIALOG_COMMANDS = ["show-modal", "--show", "request-close", "close"];
|
|
38
40
|
const DIALOG_SELECTOR = "dialog";
|
|
39
41
|
const DIALOG_TITLE_SELECTOR = "[data-title], h1, h2, h3, h4, h5, h6";
|
|
40
42
|
const STATIC_SHAKE_MS = 250;
|
|
@@ -48,34 +50,69 @@ function boolData(el, name) {
|
|
|
48
50
|
return el.hasAttribute(name) && el.getAttribute(name) !== "false";
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
function
|
|
53
|
+
function setOwnedAttribute(state, el, name, value) {
|
|
54
|
+
const current = el.getAttribute(name);
|
|
55
|
+
if (current === value) return;
|
|
56
|
+
|
|
57
|
+
const previous = state.ownedAttributes.find((entry) => entry.el === el && entry.name === name);
|
|
58
|
+
if (previous && current !== previous.written) previous.original = current;
|
|
59
|
+
if (value === null) el.removeAttribute(name);
|
|
60
|
+
else el.setAttribute(name, value);
|
|
61
|
+
|
|
62
|
+
if (previous) previous.written = value;
|
|
63
|
+
else state.ownedAttributes.push({ el, name, original: current, written: value });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function restoreOwnedAttributes(state) {
|
|
67
|
+
for (const { el, name, original, written } of state.ownedAttributes) {
|
|
68
|
+
// An author may have changed an attribute while the dialog was connected.
|
|
69
|
+
if (el.getAttribute(name) !== written) continue;
|
|
70
|
+
if (original === null) el.removeAttribute(name);
|
|
71
|
+
else el.setAttribute(name, original);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ensureId(el, prefix, state) {
|
|
52
76
|
if (!el.id) {
|
|
53
77
|
uid++;
|
|
54
|
-
el
|
|
78
|
+
setOwnedAttribute(state, el, "id", `${prefix}-${uid}`);
|
|
55
79
|
}
|
|
56
80
|
return el.id;
|
|
57
81
|
}
|
|
58
82
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
function ownsCurrentAttribute(state, el, name) {
|
|
84
|
+
return state.ownedAttributes.some(
|
|
85
|
+
(entry) => entry.el === el && entry.name === name && el.getAttribute(name) === entry.written,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function syncDialogSemantics(dialog, state, modal = isModal(dialog)) {
|
|
90
|
+
setOwnedAttribute(state, dialog, "aria-modal", modal ? "true" : null);
|
|
65
91
|
|
|
66
|
-
|
|
92
|
+
const ownsLabel = ownsCurrentAttribute(state, dialog, "aria-label");
|
|
93
|
+
const ownsLabelledby = ownsCurrentAttribute(state, dialog, "aria-labelledby");
|
|
94
|
+
if (dialog.hasAttribute("aria-labelledby") && !ownsLabelledby) {
|
|
95
|
+
if (ownsLabel) setOwnedAttribute(state, dialog, "aria-label", null);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (dialog.hasAttribute("aria-label") && !ownsLabel) {
|
|
99
|
+
if (ownsLabelledby) setOwnedAttribute(state, dialog, "aria-labelledby", null);
|
|
67
100
|
return;
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
const label = dialog.getAttribute("data-title")?.trim();
|
|
71
104
|
if (label) {
|
|
72
|
-
dialog
|
|
105
|
+
if (ownsLabelledby) setOwnedAttribute(state, dialog, "aria-labelledby", null);
|
|
106
|
+
setOwnedAttribute(state, dialog, "aria-label", label);
|
|
73
107
|
return;
|
|
74
108
|
}
|
|
75
109
|
|
|
76
110
|
const title = dialog.querySelector(DIALOG_TITLE_SELECTOR);
|
|
111
|
+
if (ownsLabel) setOwnedAttribute(state, dialog, "aria-label", null);
|
|
77
112
|
if (title) {
|
|
78
|
-
dialog
|
|
113
|
+
setOwnedAttribute(state, dialog, "aria-labelledby", ensureId(title, "dialog-title", state));
|
|
114
|
+
} else if (ownsLabelledby) {
|
|
115
|
+
setOwnedAttribute(state, dialog, "aria-labelledby", null);
|
|
79
116
|
}
|
|
80
117
|
}
|
|
81
118
|
|
|
@@ -262,13 +299,19 @@ export function closeDialog(dialog, returnValue = "") {
|
|
|
262
299
|
export function requestDialogClose(dialog, returnValue = "") {
|
|
263
300
|
if (!isDialogElement(dialog) || !dialog.open) return;
|
|
264
301
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
302
|
+
const state = ensureDialogWired(dialog);
|
|
303
|
+
state.returnValue = returnValue;
|
|
304
|
+
state.requestingClose = true;
|
|
305
|
+
try {
|
|
306
|
+
if (typeof dialog.requestClose === "function") {
|
|
307
|
+
dialog.requestClose(returnValue);
|
|
308
|
+
} else {
|
|
309
|
+
// Older browsers still owe consumers the same cancel lifecycle.
|
|
310
|
+
dialog.dispatchEvent(new Event("cancel", { cancelable: true }));
|
|
311
|
+
}
|
|
312
|
+
} finally {
|
|
313
|
+
state.requestingClose = false;
|
|
269
314
|
}
|
|
270
|
-
|
|
271
|
-
closeDialog(dialog, returnValue);
|
|
272
315
|
}
|
|
273
316
|
|
|
274
317
|
export function openDialog(dialog, trigger = null, forcedModal = null) {
|
|
@@ -276,7 +319,7 @@ export function openDialog(dialog, trigger = null, forcedModal = null) {
|
|
|
276
319
|
|
|
277
320
|
const state = ensureDialogWired(dialog);
|
|
278
321
|
const modal = resolveModalMode(dialog, forcedModal);
|
|
279
|
-
syncDialogSemantics(dialog, modal);
|
|
322
|
+
syncDialogSemantics(dialog, state, modal);
|
|
280
323
|
|
|
281
324
|
state.closing = false;
|
|
282
325
|
state.restoreFocusTo = trigger || dialog.ownerDocument.activeElement;
|
|
@@ -285,6 +328,7 @@ export function openDialog(dialog, trigger = null, forcedModal = null) {
|
|
|
285
328
|
// while doing so; capture the position first and restore it so the page does
|
|
286
329
|
// not jump to the top behind the modal.
|
|
287
330
|
const viewport = dialog.ownerDocument.defaultView;
|
|
331
|
+
const scrollX = viewport?.scrollX ?? 0;
|
|
288
332
|
const scrollY = viewport?.scrollY ?? 0;
|
|
289
333
|
|
|
290
334
|
if (modal) {
|
|
@@ -295,17 +339,33 @@ export function openDialog(dialog, trigger = null, forcedModal = null) {
|
|
|
295
339
|
state.modalOpen = false;
|
|
296
340
|
}
|
|
297
341
|
|
|
298
|
-
if (scrollY > 0) {
|
|
299
|
-
viewport?.scrollTo(
|
|
342
|
+
if (scrollX !== 0 || scrollY > 0) {
|
|
343
|
+
viewport?.scrollTo(scrollX, scrollY);
|
|
300
344
|
}
|
|
301
345
|
|
|
302
346
|
syncModalOpenClass(dialog.ownerDocument);
|
|
303
347
|
}
|
|
304
348
|
|
|
349
|
+
function handleDialogPointerDown(event) {
|
|
350
|
+
const dialog = event.currentTarget;
|
|
351
|
+
const state = dialogMap.get(dialog);
|
|
352
|
+
if (!state) return;
|
|
353
|
+
|
|
354
|
+
// Only a press that starts on the backdrop may dismiss on click. A
|
|
355
|
+
// text selection started inside the dialog and released over the backdrop
|
|
356
|
+
// produces a click on <dialog> with outside coordinates, which must not
|
|
357
|
+
// close (or flash static on) the dialog.
|
|
358
|
+
state.backdropPress = event.target === dialog && isOutsideDialog(dialog, event);
|
|
359
|
+
}
|
|
360
|
+
|
|
305
361
|
function handleDialogClick(event) {
|
|
306
362
|
const dialog = event.currentTarget;
|
|
363
|
+
const state = dialogMap.get(dialog);
|
|
364
|
+
const backdropPress = state?.backdropPress === true;
|
|
365
|
+
if (state) state.backdropPress = false;
|
|
307
366
|
|
|
308
367
|
if (event.target !== dialog || !isOutsideDialog(dialog, event)) return;
|
|
368
|
+
if (!backdropPress) return;
|
|
309
369
|
|
|
310
370
|
// A native closedby="any" dialog owns its light dismiss; the runtime only
|
|
311
371
|
// takes over when the author opted in via data-dialog-dismissible (which
|
|
@@ -338,7 +398,15 @@ function handleDialogCancel(event) {
|
|
|
338
398
|
const dialog = event.currentTarget;
|
|
339
399
|
const state = dialogMap.get(dialog);
|
|
340
400
|
|
|
341
|
-
//
|
|
401
|
+
// Older dialogs may dispatch cancel for Escape without implementing
|
|
402
|
+
// closedBy. Explicit requestDialogClose() remains available to close them.
|
|
403
|
+
const closedBy = "closedBy" in dialog ? dialog.closedBy : dialog.getAttribute("closedby");
|
|
404
|
+
if (closedBy === "none" && !state?.requestingClose) {
|
|
405
|
+
event.preventDefault();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Escape and close requests close regardless of the dismissible flag:
|
|
342
410
|
// data-dialog-dismissible only gates light dismiss (backdrop click, handled
|
|
343
411
|
// in handleDialogClick). Applications intercept via actual:dialog-cancel.
|
|
344
412
|
const request = new CustomEvent(EVENTS.dialogCancel, {
|
|
@@ -395,15 +463,19 @@ function ensureDialogWired(dialog) {
|
|
|
395
463
|
controller,
|
|
396
464
|
closing: false,
|
|
397
465
|
modalOpen: isModalOpen(dialog),
|
|
466
|
+
ownedAttributes: [],
|
|
467
|
+
requestingClose: false,
|
|
398
468
|
restoreFocusTo: null,
|
|
399
469
|
returnValue: "",
|
|
400
470
|
staticTimer: null,
|
|
471
|
+
backdropPress: false,
|
|
401
472
|
};
|
|
402
473
|
|
|
403
474
|
// A dialog that is already `open` in the markup is non-modal per spec,
|
|
404
475
|
// whatever data-dialog-modal says; infer semantics from its actual state.
|
|
405
|
-
syncDialogSemantics(dialog, dialog.open ? isModalOpen(dialog) : isModal(dialog));
|
|
476
|
+
syncDialogSemantics(dialog, state, dialog.open ? isModalOpen(dialog) : isModal(dialog));
|
|
406
477
|
|
|
478
|
+
dialog.addEventListener("pointerdown", handleDialogPointerDown, { signal: controller.signal });
|
|
407
479
|
dialog.addEventListener("click", handleDialogClick, { signal: controller.signal });
|
|
408
480
|
dialog.addEventListener("submit", handleDialogSubmit, { signal: controller.signal });
|
|
409
481
|
dialog.addEventListener("cancel", handleDialogCancel, { signal: controller.signal });
|
|
@@ -414,7 +486,7 @@ function ensureDialogWired(dialog) {
|
|
|
414
486
|
// closedby keeps its native meaning — "any" closes on backdrop click,
|
|
415
487
|
// "closerequest" on Escape, "none" disables both. Never override "none".
|
|
416
488
|
if (isDismissible(dialog) && "closedBy" in dialog && dialog.closedBy === "any") {
|
|
417
|
-
dialog
|
|
489
|
+
setOwnedAttribute(state, dialog, "closedby", "closerequest");
|
|
418
490
|
}
|
|
419
491
|
|
|
420
492
|
dialogMap.set(dialog, state);
|
|
@@ -437,6 +509,7 @@ function disconnectDialog(dialog) {
|
|
|
437
509
|
|
|
438
510
|
state.controller.abort();
|
|
439
511
|
dialog.classList.remove(CLASSES.static);
|
|
512
|
+
restoreOwnedAttributes(state);
|
|
440
513
|
state.modalOpen = false;
|
|
441
514
|
dialogMap.delete(dialog);
|
|
442
515
|
wiredDialogs.delete(dialog);
|
|
@@ -448,28 +521,11 @@ registerCommands(DIALOG_COMMANDS, {
|
|
|
448
521
|
const dialog = targetFor(trigger);
|
|
449
522
|
return isDialogElement(dialog) ? dialog : null;
|
|
450
523
|
},
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
trigger.setAttribute("aria-controls", dialog.id);
|
|
457
|
-
|
|
458
|
-
if (
|
|
459
|
-
!trigger.hasAttribute("aria-haspopup") &&
|
|
460
|
-
(command === "show-modal" || command === "show")
|
|
461
|
-
) {
|
|
462
|
-
trigger.setAttribute("aria-haspopup", "dialog");
|
|
463
|
-
}
|
|
464
|
-
},
|
|
465
|
-
handle: (event, trigger, dialog, command) => {
|
|
466
|
-
event.preventDefault();
|
|
467
|
-
|
|
468
|
-
if (!isDialogElement(dialog)) {
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
524
|
+
handle: (_event, trigger, dialog, command) => {
|
|
525
|
+
// A dialog inserted since the last enhance() pass is wired on first use.
|
|
526
|
+
ensureDialogWired(dialog);
|
|
471
527
|
|
|
472
|
-
if (command === "show-modal" || command === "show") {
|
|
528
|
+
if (command === "show-modal" || command === "--show") {
|
|
473
529
|
openDialogWithViewTransition(dialog, trigger, command === "show-modal");
|
|
474
530
|
return;
|
|
475
531
|
}
|
package/src/js/dismiss.js
CHANGED
|
@@ -7,8 +7,7 @@ import { registerCommands } from "./command.js";
|
|
|
7
7
|
import { EVENTS } from "./events.js";
|
|
8
8
|
|
|
9
9
|
registerCommands("--dismiss", {
|
|
10
|
-
handle(
|
|
11
|
-
event.preventDefault();
|
|
10
|
+
handle(_event, trigger, target) {
|
|
12
11
|
target.hidden = true;
|
|
13
12
|
target.dispatchEvent(
|
|
14
13
|
new CustomEvent(EVENTS.dismiss, {
|