actual-css 0.10.0 → 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 +73 -1
- package/README.md +2 -2
- package/components.json +2 -1
- package/dist/actual.full.css +314 -106
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/design-notes/enhancement-contract.md +16 -7
- 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/navigation.json +2 -0
- package/docs/pages/components/aura.md +12 -13
- package/docs/pages/components/badge.md +10 -0
- package/docs/pages/components/button.md +1 -1
- package/docs/pages/components/chat.md +5 -0
- package/docs/pages/components/dialog.md +5 -2
- 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 +11 -6
- package/docs/pages/components/table.md +13 -0
- package/docs/pages/enhancements/flyout.md +2 -0
- package/docs/pages/enhancements/tabs.md +4 -0
- package/docs/pages/forms/choice-cards.md +77 -4
- package/docs/pages/forms/combobox.md +16 -1
- package/docs/pages/forms/overview.md +4 -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 +49 -3
- package/docs/pages/foundations/tokens.md +15 -5
- package/docs/pages/guides/bootstrap.md +2 -1
- package/docs/pages/guides/introduction.md +1 -1
- package/docs/pages/guides/modular-import.md +1 -1
- package/docs/pages/guides/progressive-enhancement.md +13 -14
- package/docs/pages/guides/tailwind.md +4 -2
- package/llms.txt +4 -3
- package/package.json +5 -1
- package/reserved-classes.json +1 -0
- package/src/css/components/avatar.css +1 -1
- package/src/css/components/flyout.css +4 -1
- package/src/css/components/meter.css +1 -1
- 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 +14 -4
- package/src/css/core/theme.css +8 -0
- package/src/css/core/tokens.css +8 -4
- package/src/css/core/variants.css +2 -1
- 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 +71 -23
- package/src/css/forms/control.css +2 -2
- package/src/css/forms/otp.css +9 -1
- package/src/css/forms/range.css +64 -21
- package/src/js/command.js +61 -32
- package/src/js/dialog.js +8 -23
- package/src/js/dismiss.js +1 -2
- package/src/js/enhance.js +84 -123
- package/src/js/full.js +1 -0
- 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 +2 -10
- package/src/js/tooltip.js +11 -1
- package/src/js/validation.js +4 -10
package/src/js/enhance.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Enhance — DOM lifecycle engine for behavioral enhancers.
|
|
3
3
|
*
|
|
4
|
-
* One MutationObserver per root watches DOM insertions/removals
|
|
5
|
-
*
|
|
4
|
+
* One MutationObserver per root watches DOM insertions/removals and serves
|
|
5
|
+
* every enhance() and registerEnhancement() record on that root, each with
|
|
6
|
+
* its own teardown handle. A record pairs a discovery query with
|
|
7
|
+
* selector-keyed initialisers: enhance() queries its own selectors; a named
|
|
8
|
+
* enhancement keys `[data-enhance~="name"]` behind the shared
|
|
9
|
+
* `[data-enhance]` query, so any number of named behaviors cost the root a
|
|
10
|
+
* single discovery gate.
|
|
6
11
|
*
|
|
7
12
|
* Sweep (not removedNodes scanning) is the key design choice: it reasons
|
|
8
13
|
* about final state after a batch, so a moved element (removed then
|
|
@@ -16,16 +21,9 @@
|
|
|
16
21
|
* attribute already set. Behavior attributes are setup-time contracts, not
|
|
17
22
|
* live enable/disable switches.
|
|
18
23
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* Enhancement tokens — the data-enhance layer on top of enhance().
|
|
23
|
-
*
|
|
24
|
-
* enhancementSelector("tabs") returns [data-enhance~="tabs"], the generic
|
|
25
|
-
* opt-in for root-controller behaviors. registerEnhancement() owns a name per
|
|
26
|
-
* root — a second registration for the same name on the same root throws, and
|
|
27
|
-
* disconnect() releases ownership. Third-party behaviors register exactly
|
|
28
|
-
* like built-in ones.
|
|
24
|
+
* registerEnhancement() owns a name per root — a second registration for the
|
|
25
|
+
* same name on the same root throws, and disconnect() releases ownership.
|
|
26
|
+
* Third-party behaviors register exactly like built-in ones.
|
|
29
27
|
*
|
|
30
28
|
* See docs/design-notes/enhancement-contract.md.
|
|
31
29
|
*/
|
|
@@ -39,6 +37,8 @@ const ELEMENT_NODE = 1;
|
|
|
39
37
|
const DOCUMENT_NODE = 9;
|
|
40
38
|
const DOCUMENT_FRAGMENT_NODE = 11;
|
|
41
39
|
|
|
40
|
+
const ENHANCEMENT_NAME = /^[a-z][a-z0-9-]*$/;
|
|
41
|
+
|
|
42
42
|
function noopRuntime() {
|
|
43
43
|
return {
|
|
44
44
|
refresh() {},
|
|
@@ -56,50 +56,30 @@ function canScan(node) {
|
|
|
56
56
|
|
|
57
57
|
function createRegistry(root) {
|
|
58
58
|
const records = new Set();
|
|
59
|
-
let
|
|
60
|
-
|
|
61
|
-
function isValidSelector(selector) {
|
|
62
|
-
try {
|
|
63
|
-
root.querySelector?.(selector);
|
|
64
|
-
if (root.nodeType === ELEMENT_NODE) root.matches(selector);
|
|
65
|
-
return true;
|
|
66
|
-
} catch (error) {
|
|
67
|
-
console.error(`Invalid enhancer selector "${selector}"`, error);
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function updateSelectorString() {
|
|
73
|
-
selectorString = [...records].flatMap((record) => record.selectors).join(",");
|
|
74
|
-
}
|
|
59
|
+
let discovery = "";
|
|
75
60
|
|
|
76
61
|
function start(record, el) {
|
|
77
|
-
if (el?.nodeType !== ELEMENT_NODE) return;
|
|
78
62
|
let active = record.instances.get(el);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (!el.matches(selector)) continue;
|
|
82
|
-
if (active?.has(selector)) continue;
|
|
63
|
+
for (const key of record.keys) {
|
|
64
|
+
if (active?.has(key) || !el.matches(key)) continue;
|
|
83
65
|
|
|
84
66
|
let cleanup;
|
|
85
67
|
try {
|
|
86
|
-
cleanup = record.enhancers[
|
|
68
|
+
cleanup = record.enhancers[key](el);
|
|
87
69
|
} catch (error) {
|
|
88
|
-
console.error(`Enhancer
|
|
70
|
+
console.error(`Enhancer "${key}" failed`, error);
|
|
89
71
|
continue;
|
|
90
72
|
}
|
|
91
73
|
if (!active) {
|
|
92
74
|
active = new Map();
|
|
93
75
|
record.instances.set(el, active);
|
|
94
76
|
}
|
|
95
|
-
active.set(
|
|
77
|
+
active.set(key, typeof cleanup === "function" ? cleanup : null);
|
|
96
78
|
}
|
|
97
79
|
}
|
|
98
80
|
|
|
99
81
|
function stop(record, el) {
|
|
100
|
-
const
|
|
101
|
-
if (!active) return;
|
|
102
|
-
for (const cleanup of active.values()) {
|
|
82
|
+
for (const cleanup of record.instances.get(el)?.values() ?? []) {
|
|
103
83
|
try {
|
|
104
84
|
cleanup?.();
|
|
105
85
|
} catch (error) {
|
|
@@ -109,24 +89,13 @@ function createRegistry(root) {
|
|
|
109
89
|
record.instances.delete(el);
|
|
110
90
|
}
|
|
111
91
|
|
|
112
|
-
function
|
|
113
|
-
if (!canScan(node)) return;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function scan(node) {
|
|
123
|
-
if (!canScan(node) || !selectorString) return;
|
|
124
|
-
if (node.nodeType === ELEMENT_NODE) {
|
|
125
|
-
for (const record of records) start(record, node);
|
|
126
|
-
}
|
|
127
|
-
node.querySelectorAll?.(selectorString).forEach((el) => {
|
|
128
|
-
for (const record of records) start(record, el);
|
|
129
|
-
});
|
|
92
|
+
function scan(node, targets, selector) {
|
|
93
|
+
if (!canScan(node) || !selector) return;
|
|
94
|
+
const visit = (el) => {
|
|
95
|
+
for (const record of targets) start(record, el);
|
|
96
|
+
};
|
|
97
|
+
if (node.nodeType === ELEMENT_NODE) visit(node);
|
|
98
|
+
node.querySelectorAll(selector).forEach(visit);
|
|
130
99
|
}
|
|
131
100
|
|
|
132
101
|
function sweepDisconnected() {
|
|
@@ -137,11 +106,16 @@ function createRegistry(root) {
|
|
|
137
106
|
}
|
|
138
107
|
}
|
|
139
108
|
|
|
109
|
+
function updateDiscovery() {
|
|
110
|
+
// Named records all share `[data-enhance]`; the Set collapses them.
|
|
111
|
+
discovery = [...new Set(Array.from(records, (record) => record.query))].join(",");
|
|
112
|
+
}
|
|
113
|
+
|
|
140
114
|
const observer = new MutationObserver((mutationRecords) => {
|
|
141
115
|
let hasRemoval = false;
|
|
142
116
|
for (const mutationRecord of mutationRecords) {
|
|
143
117
|
for (const node of mutationRecord.addedNodes) {
|
|
144
|
-
scan(node);
|
|
118
|
+
scan(node, records, discovery);
|
|
145
119
|
}
|
|
146
120
|
if (mutationRecord.removedNodes.length > 0) {
|
|
147
121
|
hasRemoval = true;
|
|
@@ -151,61 +125,52 @@ function createRegistry(root) {
|
|
|
151
125
|
});
|
|
152
126
|
|
|
153
127
|
return {
|
|
154
|
-
add(
|
|
155
|
-
const
|
|
156
|
-
if (!selectors.length) return null;
|
|
157
|
-
const validEnhancers = Object.fromEntries(
|
|
158
|
-
selectors.map((selector) => [selector, enhancers[selector]]),
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
const record = {
|
|
162
|
-
disconnected: false,
|
|
163
|
-
enhancers: validEnhancers,
|
|
164
|
-
instances: new Map(),
|
|
165
|
-
selectors,
|
|
166
|
-
selectorString: selectors.join(","),
|
|
167
|
-
};
|
|
128
|
+
add(spec) {
|
|
129
|
+
const record = { ...spec, disconnected: false, instances: new Map() };
|
|
168
130
|
records.add(record);
|
|
169
131
|
// Observe only once there is a record to act on; an enhance() call with
|
|
170
132
|
// no valid selector must not leave an idle observer behind.
|
|
171
133
|
if (records.size === 1) observer.observe(root, { childList: true, subtree: true });
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
registries.delete(root);
|
|
191
|
-
}
|
|
134
|
+
updateDiscovery();
|
|
135
|
+
scan(root, [record], record.query);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
refresh(node) {
|
|
139
|
+
if (!record.disconnected) scan(node, [record], record.query);
|
|
140
|
+
},
|
|
141
|
+
disconnect() {
|
|
142
|
+
if (record.disconnected) return;
|
|
143
|
+
record.disconnected = true;
|
|
144
|
+
for (const el of Array.from(record.instances.keys())) stop(record, el);
|
|
145
|
+
records.delete(record);
|
|
146
|
+
updateDiscovery();
|
|
147
|
+
if (records.size) return;
|
|
148
|
+
observer.disconnect();
|
|
149
|
+
registries.delete(root);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
192
152
|
},
|
|
193
153
|
};
|
|
194
154
|
}
|
|
195
155
|
|
|
196
|
-
|
|
156
|
+
function registryFor(root) {
|
|
157
|
+
let registry = registries.get(root);
|
|
158
|
+
if (!registry) {
|
|
159
|
+
registry = createRegistry(root);
|
|
160
|
+
registries.set(root, registry);
|
|
161
|
+
}
|
|
162
|
+
return registry;
|
|
163
|
+
}
|
|
197
164
|
|
|
198
|
-
|
|
165
|
+
// The name check is what keeps the interpolation safe: a quote or space in a
|
|
166
|
+
// name would otherwise break or widen the selector.
|
|
167
|
+
function enhancementSelector(name) {
|
|
199
168
|
if (!ENHANCEMENT_NAME.test(name)) {
|
|
200
169
|
throw new TypeError(`Invalid enhancement name: ${name}`);
|
|
201
170
|
}
|
|
202
171
|
return `[data-enhance~="${name}"]`;
|
|
203
172
|
}
|
|
204
173
|
|
|
205
|
-
export function hasEnhancement(el, name) {
|
|
206
|
-
return el.matches(enhancementSelector(name));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
174
|
/**
|
|
210
175
|
* Apply a named opt-in once; presentation selectors remain application-owned.
|
|
211
176
|
* @param {string} name
|
|
@@ -216,12 +181,12 @@ export function hasEnhancement(el, name) {
|
|
|
216
181
|
export function applyEnhancement(name, selector, root) {
|
|
217
182
|
if (typeof document === "undefined") return [];
|
|
218
183
|
|
|
219
|
-
enhancementSelector(name);
|
|
184
|
+
const enhanced = enhancementSelector(name);
|
|
220
185
|
root ??= document.documentElement;
|
|
221
186
|
const elements = [...root.querySelectorAll(selector)];
|
|
222
187
|
|
|
223
188
|
for (const el of elements) {
|
|
224
|
-
if (
|
|
189
|
+
if (el.matches(enhanced)) continue;
|
|
225
190
|
const tokens = el.getAttribute("data-enhance");
|
|
226
191
|
el.setAttribute("data-enhance", tokens ? `${tokens} ${name}` : name);
|
|
227
192
|
}
|
|
@@ -240,7 +205,7 @@ export function applyEnhancement(name, selector, root) {
|
|
|
240
205
|
export function registerEnhancement(name, init, root) {
|
|
241
206
|
if (typeof document === "undefined") return noopRuntime();
|
|
242
207
|
|
|
243
|
-
enhancementSelector(name);
|
|
208
|
+
const selector = enhancementSelector(name);
|
|
244
209
|
if (typeof init !== "function") {
|
|
245
210
|
throw new TypeError("registerEnhancement() requires a function init.");
|
|
246
211
|
}
|
|
@@ -255,7 +220,11 @@ export function registerEnhancement(name, init, root) {
|
|
|
255
220
|
throw new Error(`Enhancement "${name}" is already registered on this root.`);
|
|
256
221
|
}
|
|
257
222
|
|
|
258
|
-
const runtime =
|
|
223
|
+
const runtime = registryFor(root).add({
|
|
224
|
+
keys: [selector],
|
|
225
|
+
enhancers: { [selector]: init },
|
|
226
|
+
query: "[data-enhance]",
|
|
227
|
+
});
|
|
259
228
|
names.set(name, runtime);
|
|
260
229
|
|
|
261
230
|
return {
|
|
@@ -268,34 +237,26 @@ export function registerEnhancement(name, init, root) {
|
|
|
268
237
|
};
|
|
269
238
|
}
|
|
270
239
|
|
|
271
|
-
function registryFor(root) {
|
|
272
|
-
let registry = registries.get(root);
|
|
273
|
-
if (!registry) {
|
|
274
|
-
registry = createRegistry(root);
|
|
275
|
-
registries.set(root, registry);
|
|
276
|
-
}
|
|
277
|
-
return registry;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
240
|
/**
|
|
281
241
|
* @param {Record<string, (el: Element) => (() => void) | void>} enhancers
|
|
282
242
|
* @param {Document | Element | DocumentFragment} [root]
|
|
283
243
|
* @returns {{ refresh: (node: Node) => void, disconnect: () => void }}
|
|
284
244
|
*/
|
|
285
245
|
export default function enhance(enhancers, root) {
|
|
286
|
-
if (typeof document === "undefined")
|
|
287
|
-
return noopRuntime();
|
|
288
|
-
}
|
|
246
|
+
if (typeof document === "undefined") return noopRuntime();
|
|
289
247
|
|
|
290
248
|
root ??= document.documentElement;
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
249
|
+
const keys = Object.keys(enhancers).filter((selector) => {
|
|
250
|
+
try {
|
|
251
|
+
root.querySelector?.(selector);
|
|
252
|
+
if (root.nodeType === ELEMENT_NODE) root.matches(selector);
|
|
253
|
+
return true;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error(`Invalid enhancer selector "${selector}"`, error);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
if (!keys.length) return noopRuntime();
|
|
294
260
|
|
|
295
|
-
return {
|
|
296
|
-
refresh: (node) => registry.refresh(record, node),
|
|
297
|
-
disconnect: () => {
|
|
298
|
-
registry.remove(record);
|
|
299
|
-
},
|
|
300
|
-
};
|
|
261
|
+
return registryFor(root).add({ keys, enhancers, query: keys.join(",") });
|
|
301
262
|
}
|
package/src/js/full.js
CHANGED
package/src/js/password.js
CHANGED
|
@@ -74,14 +74,7 @@ function watchSubmitRoot(input) {
|
|
|
74
74
|
|
|
75
75
|
registerCommands(PASSWORD_COMMANDS, {
|
|
76
76
|
resolve: resolvePasswordInput,
|
|
77
|
-
|
|
78
|
-
trigger.setAttribute("aria-controls", input.id);
|
|
79
|
-
if (!trigger.hasAttribute("aria-pressed")) {
|
|
80
|
-
trigger.setAttribute("aria-pressed", "false");
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
handle: (event, _trigger, input) => {
|
|
84
|
-
event.preventDefault();
|
|
77
|
+
handle: (_event, _trigger, input) => {
|
|
85
78
|
const reveal = input.type === "password";
|
|
86
79
|
if (reveal) watchSubmitRoot(input);
|
|
87
80
|
setRevealed(input, reveal);
|
package/src/js/range.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Range — fill share and named-value semantics for range inputs.
|
|
3
|
+
*
|
|
4
|
+
* The `range` enhancement token is the opt-in; it stays independent from
|
|
5
|
+
* the `.range` presentation class:
|
|
6
|
+
*
|
|
7
|
+
* <input class="range" data-enhance="range" type="range" min="0" max="100">
|
|
8
|
+
*
|
|
9
|
+
* Every opted-in input gets `--range-progress`, the fill share the track
|
|
10
|
+
* gradient in range.css reads. Without this module the variable stays at
|
|
11
|
+
* its `0%` default and the track renders plain — the control is fully
|
|
12
|
+
* usable, only unfilled.
|
|
13
|
+
*
|
|
14
|
+
* When the input references a datalist whose option carries a `label`, the
|
|
15
|
+
* module also sets `aria-valuetext` (the announced value when the numeric
|
|
16
|
+
* value really means a text, e.g. `2` means "High") and mirrors the label
|
|
17
|
+
* into each associated `<output for="…">`. Those writes are owned: when a
|
|
18
|
+
* later value has no label, the module restores the pre-existing
|
|
19
|
+
* `aria-valuetext` (or removes the attribute if it was absent) and each
|
|
20
|
+
* output's original text — but only where the current value is still the
|
|
21
|
+
* module's own last write, so author state is never destroyed.
|
|
22
|
+
* Options without a label leave author-written `aria-valuetext` and outputs
|
|
23
|
+
* untouched.
|
|
24
|
+
*
|
|
25
|
+
* <output for="effort">High</output>
|
|
26
|
+
* <input data-enhance="range" type="range" id="effort" min="0" max="3" list="effort-levels" aria-label="Effort">
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { registerEnhancement } from "./enhance.js";
|
|
30
|
+
|
|
31
|
+
const RANGE_SELECTOR = 'input[type="range"]';
|
|
32
|
+
|
|
33
|
+
// Writes this module owns per input: for the valuetext and each output,
|
|
34
|
+
// the value before the first write (original) and the last value written
|
|
35
|
+
// (last). A later unlabeled value restores an entry only when its current
|
|
36
|
+
// value is still the module's last write — author state, whether present
|
|
37
|
+
// from the start or written since, is never destroyed. Entries live on the
|
|
38
|
+
// node, not the tree, so a disconnected output is still restored.
|
|
39
|
+
const owned = new WeakMap();
|
|
40
|
+
|
|
41
|
+
function toFinite(value, fallback) {
|
|
42
|
+
// Absent min/max read back as "" (Number("") is 0, not missing).
|
|
43
|
+
if (value === "" || value === null || value === undefined) return fallback;
|
|
44
|
+
const parsed = Number(value);
|
|
45
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function labelFor(input) {
|
|
49
|
+
const listId = input.getAttribute("list");
|
|
50
|
+
if (!listId) return undefined;
|
|
51
|
+
const list = input.getRootNode().getElementById?.(listId);
|
|
52
|
+
if (!list) return undefined;
|
|
53
|
+
const option = [...(list.options ?? [])].find((entry) => entry.value === input.value);
|
|
54
|
+
const label = option?.label ?? option?.getAttribute?.("label") ?? "";
|
|
55
|
+
return label ? label : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function outputsFor(input) {
|
|
59
|
+
if (!input.id) return [];
|
|
60
|
+
const root = input.getRootNode();
|
|
61
|
+
if (typeof root.querySelectorAll !== "function") return [];
|
|
62
|
+
return [...root.querySelectorAll(`output[for~="${CSS.escape(input.id)}"]`)];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function syncRange(input) {
|
|
66
|
+
const min = toFinite(input.min, 0);
|
|
67
|
+
const max = toFinite(input.max, 100);
|
|
68
|
+
const value = toFinite(input.value, min);
|
|
69
|
+
const share = max > min ? ((value - min) / (max - min)) * 100 : 0;
|
|
70
|
+
const clamped = Math.min(100, Math.max(0, share));
|
|
71
|
+
input.style.setProperty("--range-progress", `${clamped}%`);
|
|
72
|
+
|
|
73
|
+
const label = labelFor(input);
|
|
74
|
+
if (label === undefined) {
|
|
75
|
+
releaseOwned(input);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const record = owned.get(input) ?? { aria: null, outputs: new Map() };
|
|
79
|
+
if (!record.aria) {
|
|
80
|
+
record.aria = { original: input.getAttribute("aria-valuetext"), last: label };
|
|
81
|
+
} else {
|
|
82
|
+
record.aria.last = label;
|
|
83
|
+
}
|
|
84
|
+
input.setAttribute("aria-valuetext", label);
|
|
85
|
+
for (const output of outputsFor(input)) {
|
|
86
|
+
const entry = record.outputs.get(output) ?? { original: output.textContent, last: label };
|
|
87
|
+
entry.last = label;
|
|
88
|
+
record.outputs.set(output, entry);
|
|
89
|
+
output.textContent = label;
|
|
90
|
+
}
|
|
91
|
+
owned.set(input, record);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function releaseOwned(input) {
|
|
95
|
+
const record = owned.get(input);
|
|
96
|
+
if (!record) return;
|
|
97
|
+
// Restore only what this module wrote: an author may have set its own
|
|
98
|
+
// value since, and that state is not ours to destroy. A null original
|
|
99
|
+
// means the attribute was absent before the first write.
|
|
100
|
+
if (record.aria && input.getAttribute("aria-valuetext") === record.aria.last) {
|
|
101
|
+
if (record.aria.original === null) {
|
|
102
|
+
input.removeAttribute("aria-valuetext");
|
|
103
|
+
} else {
|
|
104
|
+
input.setAttribute("aria-valuetext", record.aria.original);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const [output, entry] of record.outputs) {
|
|
108
|
+
if (output.textContent === entry.last) output.textContent = entry.original;
|
|
109
|
+
}
|
|
110
|
+
owned.delete(input);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Private on purpose: `owned` holds one record per input, so a second,
|
|
114
|
+
// independent connection would restore and drop state the enhancement still
|
|
115
|
+
// owns. The lifecycle is the only caller. A programmatic value change goes
|
|
116
|
+
// through dispatchInput() from ./input.js, which every listener sees.
|
|
117
|
+
function connectRange(input) {
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
syncRange(input);
|
|
120
|
+
input.addEventListener("input", () => syncRange(input), {
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
});
|
|
123
|
+
// Listen on the root so form="…" and moves between forms keep working.
|
|
124
|
+
input.getRootNode().addEventListener(
|
|
125
|
+
"reset",
|
|
126
|
+
(event) => {
|
|
127
|
+
if (event.target !== input.form) return;
|
|
128
|
+
// Reset fires before native values change. A new task also lets later
|
|
129
|
+
// listeners cancel it; a microtask can run before the native reset.
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
if (
|
|
132
|
+
!controller.signal.aborted &&
|
|
133
|
+
!event.defaultPrevented &&
|
|
134
|
+
input.isConnected &&
|
|
135
|
+
input.form === event.target
|
|
136
|
+
) {
|
|
137
|
+
syncRange(input);
|
|
138
|
+
}
|
|
139
|
+
}, 0);
|
|
140
|
+
},
|
|
141
|
+
{ capture: true, signal: controller.signal },
|
|
142
|
+
);
|
|
143
|
+
return () => {
|
|
144
|
+
releaseOwned(input);
|
|
145
|
+
controller.abort();
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
registerEnhancement("range", (input) =>
|
|
150
|
+
input.matches(RANGE_SELECTOR) ? connectRange(input) : undefined,
|
|
151
|
+
);
|
package/src/js/scrollspy.js
CHANGED
|
@@ -154,11 +154,10 @@ function setupNav(nav) {
|
|
|
154
154
|
measure();
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
// The scroll root
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// contract (re-inject the nav or call refreshScrollspy after such a move).
|
|
157
|
+
// The scroll root is resolved once when the enhancement connects, and the
|
|
158
|
+
// scroll listener stays on it. Changing data-scrollspy-root or replacing its
|
|
159
|
+
// target is outside the connection contract; recreate the enhancement with
|
|
160
|
+
// the new markup. refreshScrollspy() only rebuilds the link->section map.
|
|
162
161
|
const scrollTarget = rootFor(nav) ?? nav.ownerDocument.defaultView;
|
|
163
162
|
scrollTarget?.addEventListener("scroll", () => schedule(), {
|
|
164
163
|
passive: true,
|
package/src/js/status.js
CHANGED
|
@@ -247,10 +247,6 @@ function resolveStatusTrigger(trigger) {
|
|
|
247
247
|
return target && target === statusTarget() ? target : null;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
function connectStatusTrigger(trigger, target) {
|
|
251
|
-
trigger.setAttribute("aria-controls", target.id);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
250
|
if (typeof document !== "undefined") {
|
|
255
251
|
document.addEventListener(EVENTS.invalid, (event) => {
|
|
256
252
|
const message = event.detail?.message;
|
|
@@ -273,9 +269,7 @@ if (typeof document !== "undefined") {
|
|
|
273
269
|
|
|
274
270
|
registerCommands(STATUS_SHOW_COMMANDS, {
|
|
275
271
|
resolve: resolveStatusTrigger,
|
|
276
|
-
|
|
277
|
-
handle: (event, trigger) => {
|
|
278
|
-
event.preventDefault();
|
|
272
|
+
handle: (_event, trigger) => {
|
|
279
273
|
dispatchStatusEvent(trigger, {
|
|
280
274
|
message: trigger.getAttribute("data-status-message"),
|
|
281
275
|
intent: trigger.getAttribute("data-status-intent") ?? undefined,
|
|
@@ -286,9 +280,7 @@ if (typeof document !== "undefined") {
|
|
|
286
280
|
|
|
287
281
|
registerCommands(STATUS_CLEAR_COMMANDS, {
|
|
288
282
|
resolve: resolveStatusTrigger,
|
|
289
|
-
|
|
290
|
-
handle: (event, trigger) => {
|
|
291
|
-
event.preventDefault();
|
|
283
|
+
handle: (_event, trigger) => {
|
|
292
284
|
dispatchStatusEvent(trigger, {});
|
|
293
285
|
},
|
|
294
286
|
});
|
package/src/js/tooltip.js
CHANGED
|
@@ -29,7 +29,10 @@
|
|
|
29
29
|
* tooltips therefore keep their initial text; use an explicit
|
|
30
30
|
* tooltip when its content must update dynamically.
|
|
31
31
|
*
|
|
32
|
-
* Show: hover + focus (150ms delay), click toggle, or always
|
|
32
|
+
* Show: hover + keyboard focus (150ms delay), click toggle, or always
|
|
33
|
+
* visible. Focus counts only when it matches :focus-visible, so
|
|
34
|
+
* a pointer press, or focus handed back by a closing dialog after
|
|
35
|
+
* a pointer close, shows nothing.
|
|
33
36
|
* Hide: focus leaving the trigger (focusout), pointer leave, Escape
|
|
34
37
|
*
|
|
35
38
|
* Geometry: a trigger scrolled out of the positioning boundary takes its tip
|
|
@@ -519,6 +522,13 @@ function handleTriggerIntent(e) {
|
|
|
519
522
|
)
|
|
520
523
|
return;
|
|
521
524
|
|
|
525
|
+
// why: focus is a show intent only when it is keyboard focus. A pointer
|
|
526
|
+
// press focuses the trigger too, and a dialog or drawer closed with the
|
|
527
|
+
// pointer hands focus back to its invoker; neither asks for a label the
|
|
528
|
+
// user just acted on. :focus-visible is the browser's own modality verdict,
|
|
529
|
+
// the one the focus rings already follow, so Tab still shows the tip.
|
|
530
|
+
if (e.type === "focusin" && !e.target.matches(":focus-visible")) return;
|
|
531
|
+
|
|
522
532
|
const tip = ensureTip(trigger);
|
|
523
533
|
if (!tip) return;
|
|
524
534
|
|
package/src/js/validation.js
CHANGED
|
@@ -11,19 +11,20 @@
|
|
|
11
11
|
* Opt in with data-enhance="validation". The .needs-validation class
|
|
12
12
|
* remains valid for presentation-only (CSS :user-invalid feedback without
|
|
13
13
|
* the JS behavior). Importing the module registers the behavior; there is
|
|
14
|
-
* no init call.
|
|
14
|
+
* no init call. To opt existing forms in by selector, use
|
|
15
|
+
* applyEnhancement("validation", selector) so they join the enhance()
|
|
16
|
+
* lifecycle (inserted forms connect, removed forms tear down).
|
|
15
17
|
*
|
|
16
18
|
* The marker is a static, init-time contract. Removing the form from the
|
|
17
19
|
* DOM tears down everything, including the managed novalidate attribute.
|
|
18
20
|
*/
|
|
19
21
|
|
|
20
|
-
import {
|
|
22
|
+
import { registerEnhancement } from "./enhance.js";
|
|
21
23
|
import { EVENTS } from "./events.js";
|
|
22
24
|
import { CLASSES } from "./selectors.js";
|
|
23
25
|
|
|
24
26
|
const NOVALIDATE = "novalidate";
|
|
25
27
|
const WAS_VALIDATED_CLASS = CLASSES.wasValidated;
|
|
26
|
-
const VALIDATION_SELECTOR = enhancementSelector("validation");
|
|
27
28
|
const FIELD_CLASS = CLASSES.field;
|
|
28
29
|
const DANGER_CLASS = CLASSES.danger;
|
|
29
30
|
const MANAGED_NOVALIDATE_ATTR = "validationManagedNovalidate";
|
|
@@ -398,13 +399,6 @@ export class FormValidator {
|
|
|
398
399
|
rules[name] = callback;
|
|
399
400
|
}
|
|
400
401
|
|
|
401
|
-
static init(selector = `form${VALIDATION_SELECTOR}`) {
|
|
402
|
-
if (typeof document === "undefined") return;
|
|
403
|
-
for (const form of document.querySelectorAll(selector)) {
|
|
404
|
-
if (isFormElement(form)) connectForm(form);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
402
|
static setFieldError(el, message) {
|
|
409
403
|
el.setCustomValidity(message || "invalid");
|
|
410
404
|
el.dataset.validationErrors = "server";
|