praxis-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/_shared/diagnostics.d.ts +312 -0
- package/dist/_shared/diagnostics.js +360 -0
- package/dist/build-runtime-CJ_nQEaZ.js +5065 -0
- package/dist/codemod/index.d.ts +2 -0
- package/dist/codemod/index.js +176520 -0
- package/dist/contract/index.d.ts +677 -0
- package/dist/contract/index.js +341 -0
- package/dist/eslint/index.d.ts +90 -0
- package/dist/eslint/index.js +1047 -0
- package/dist/guards/index.d.ts +78 -0
- package/dist/guards/index.js +118 -0
- package/dist/html/index.d.ts +151 -0
- package/dist/html/index.js +1244 -0
- package/dist/index-BIBd_iPD.d.ts +951 -0
- package/dist/lit/index.d.ts +862 -0
- package/dist/lit/index.js +4893 -0
- package/dist/preact/index.d.ts +796 -0
- package/dist/preact/index.js +5043 -0
- package/dist/react/index.d.ts +28 -0
- package/dist/react/index.js +205 -0
- package/dist/react/legacy.d.ts +29 -0
- package/dist/react/legacy.js +80 -0
- package/dist/solid/index.d.ts +728 -0
- package/dist/solid/index.js +4821 -0
- package/dist/svelte/Polymorphic.svelte +190 -0
- package/dist/svelte/_polymorphic-runtime.d.ts +102 -0
- package/dist/svelte/_polymorphic-runtime.js +371 -0
- package/dist/svelte/index.d.ts +994 -0
- package/dist/svelte/index.js +4482 -0
- package/dist/tailwind/index.d.ts +197 -0
- package/dist/tailwind/index.js +767 -0
- package/dist/tailwind/safelist.css +20 -0
- package/dist/ts-plugin/index.cjs +166 -0
- package/dist/ts-plugin/index.d.cts +9 -0
- package/dist/utils/index.d.ts +19 -0
- package/dist/utils/index.js +21 -0
- package/dist/vite-plugin/index.d.ts +200 -0
- package/dist/vite-plugin/index.js +2106 -0
- package/dist/vue/index.d.ts +729 -0
- package/dist/vue/index.js +4945 -0
- package/dist/web/index.d.ts +832 -0
- package/dist/web/index.js +4868 -0
- package/package.json +258 -0
|
@@ -0,0 +1,4868 @@
|
|
|
1
|
+
import { clsx } from "clsx";
|
|
2
|
+
import { DiagnosticCategory, DiagnosticCode, resolveDiagnostics, silentDiagnostics, throwDiagnostics, throwDiagnostics as throwDiagnostics$1, warnDiagnostics } from "../_shared/diagnostics.js";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
//#region ../../lib/adapter-utils/src/invariant.ts
|
|
5
|
+
function panic(message) {
|
|
6
|
+
throw new Error(message);
|
|
7
|
+
}
|
|
8
|
+
function invariant(condition, message) {
|
|
9
|
+
if (!condition) panic(message);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region ../../lib/adapter-utils/src/runtime/define-component.ts
|
|
13
|
+
function defineContractComponent(options) {
|
|
14
|
+
return (factory) => factory(options);
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region ../../lib/adapter-utils/src/runtime/assemble-compound-component.ts
|
|
18
|
+
/**
|
|
19
|
+
* Assembles a compound component by attaching sub-components to a
|
|
20
|
+
* generated root component, producing APIs such as `Card.Header`,
|
|
21
|
+
* `Card.Content`, and `Card.Footer`.
|
|
22
|
+
*
|
|
23
|
+
* Mutates and returns `root` in place, following the convention used by
|
|
24
|
+
* other component post-processing helpers.
|
|
25
|
+
*/
|
|
26
|
+
function assembleCompoundComponent(root, subComponents) {
|
|
27
|
+
if (!subComponents) return root;
|
|
28
|
+
return Object.assign(root, subComponents);
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region ../../lib/primitive/src/tag/resolve-tag.ts
|
|
32
|
+
function makeResolveTag(defaultTag) {
|
|
33
|
+
return function tag(as) {
|
|
34
|
+
return as ?? defaultTag;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region ../../lib/primitive/src/rule/rule-brand.ts
|
|
39
|
+
const RULE_BRAND = Symbol("praxis-kit/dynamic-rule");
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region ../../lib/primitive/src/rule/dynamic.ts
|
|
42
|
+
function dynamic(resolve) {
|
|
43
|
+
return {
|
|
44
|
+
[RULE_BRAND]: true,
|
|
45
|
+
resolve
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region ../../lib/foundation/src/iterate.ts
|
|
50
|
+
function find(iterable, callback) {
|
|
51
|
+
for (const value of iterable) {
|
|
52
|
+
const result = callback(value);
|
|
53
|
+
if (result != null) return result;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function some(iterable, predicate) {
|
|
58
|
+
for (const value of iterable) if (predicate(value)) return true;
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
function every(iterable, predicate) {
|
|
62
|
+
let index = 0;
|
|
63
|
+
for (const value of iterable) if (!predicate(value, index++)) return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
function* filter(iterable, predicate) {
|
|
67
|
+
let index = 0;
|
|
68
|
+
for (const value of iterable) if (predicate(value, index++)) yield value;
|
|
69
|
+
}
|
|
70
|
+
function* map(iterable, callback) {
|
|
71
|
+
let index = 0;
|
|
72
|
+
for (const value of iterable) yield callback(value, index++);
|
|
73
|
+
}
|
|
74
|
+
function forEach(iterable, callback) {
|
|
75
|
+
let index = 0;
|
|
76
|
+
for (const value of iterable) callback(value, index++);
|
|
77
|
+
}
|
|
78
|
+
function reduce(iterable, initial, callback) {
|
|
79
|
+
let accumulator = initial;
|
|
80
|
+
let index = 0;
|
|
81
|
+
for (const value of iterable) accumulator = callback(accumulator, value, index++);
|
|
82
|
+
return accumulator;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Transforms an iterable into a Record.
|
|
86
|
+
*
|
|
87
|
+
* The callback returns a `[key, value]` tuple for each element. Returning
|
|
88
|
+
* `null` aborts the collection and causes `collect()` to return `null`.
|
|
89
|
+
*/
|
|
90
|
+
function collect(iterable, callback) {
|
|
91
|
+
const result = {};
|
|
92
|
+
let index = 0;
|
|
93
|
+
for (const value of iterable) {
|
|
94
|
+
const entry = callback(value, index++);
|
|
95
|
+
if (entry === null) return null;
|
|
96
|
+
result[entry[0]] = entry[1];
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
function findLast(value, callback) {
|
|
101
|
+
for (let index = value.length - 1; index >= 0; index--) {
|
|
102
|
+
const result = callback(value[index], index);
|
|
103
|
+
if (result != null) return result;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function* items(collection) {
|
|
108
|
+
for (let i = 0; i < collection.length; i++) {
|
|
109
|
+
const item = collection.item(i);
|
|
110
|
+
if (item !== null) yield item;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function nodeList(list) {
|
|
114
|
+
return { *[Symbol.iterator]() {
|
|
115
|
+
for (let i = 0; i < list.length; i++) {
|
|
116
|
+
const node = list.item(i);
|
|
117
|
+
if (node !== null) yield node;
|
|
118
|
+
}
|
|
119
|
+
} };
|
|
120
|
+
}
|
|
121
|
+
function mapEntries(m) {
|
|
122
|
+
return m.entries();
|
|
123
|
+
}
|
|
124
|
+
function set(s) {
|
|
125
|
+
return s.values();
|
|
126
|
+
}
|
|
127
|
+
function hasOwn(object, key) {
|
|
128
|
+
return Object.hasOwn(object, key);
|
|
129
|
+
}
|
|
130
|
+
function* entries(object) {
|
|
131
|
+
for (const key in object) {
|
|
132
|
+
if (!hasOwn(object, key)) continue;
|
|
133
|
+
yield [key, object[key]];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function* keys(object) {
|
|
137
|
+
for (const [key] of entries(object)) yield key;
|
|
138
|
+
}
|
|
139
|
+
function* values(object) {
|
|
140
|
+
for (const [, value] of entries(object)) yield value;
|
|
141
|
+
}
|
|
142
|
+
function mapValues(object, callback) {
|
|
143
|
+
const result = {};
|
|
144
|
+
for (const [key, value] of entries(object)) result[key] = callback(value, key);
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
function forEachEntry(object, callback) {
|
|
148
|
+
for (const [key, value] of entries(object)) callback(key, value);
|
|
149
|
+
}
|
|
150
|
+
function forEachKey(object, callback) {
|
|
151
|
+
for (const key of keys(object)) callback(key);
|
|
152
|
+
}
|
|
153
|
+
function forEachValue(object, callback) {
|
|
154
|
+
for (const value of values(object)) callback(value);
|
|
155
|
+
}
|
|
156
|
+
function forEachSet(s, callback) {
|
|
157
|
+
for (const value of s) callback(value);
|
|
158
|
+
}
|
|
159
|
+
const iterate = Object.freeze({
|
|
160
|
+
entries,
|
|
161
|
+
filter,
|
|
162
|
+
find,
|
|
163
|
+
findLast,
|
|
164
|
+
forEach,
|
|
165
|
+
forEachEntry,
|
|
166
|
+
forEachKey,
|
|
167
|
+
forEachSet,
|
|
168
|
+
forEachValue,
|
|
169
|
+
items,
|
|
170
|
+
keys,
|
|
171
|
+
map,
|
|
172
|
+
mapEntries,
|
|
173
|
+
mapValues,
|
|
174
|
+
nodeList,
|
|
175
|
+
reduce,
|
|
176
|
+
collect,
|
|
177
|
+
set,
|
|
178
|
+
some,
|
|
179
|
+
every,
|
|
180
|
+
values
|
|
181
|
+
});
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region ../../lib/foundation/src/assert-never.ts
|
|
184
|
+
function assertNever(value) {
|
|
185
|
+
throw new Error(`Unexpected value: ${String(value)}`);
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region ../../lib/foundation/src/cn.ts
|
|
189
|
+
function cn(...inputs) {
|
|
190
|
+
return clsx(...inputs);
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region ../../lib/foundation/src/lru-cache.ts
|
|
194
|
+
/**
|
|
195
|
+
* A bounded cache that evicts the least recently used entry once `maxSize` is
|
|
196
|
+
* exceeded. Backed by a single `Map`, relying on its insertion-order iteration:
|
|
197
|
+
* `get()` promotes a hit to most-recently-used by deleting and re-inserting the
|
|
198
|
+
* key (moving it to the tail), and `set()` evicts the head (oldest) key when
|
|
199
|
+
* over capacity.
|
|
200
|
+
*
|
|
201
|
+
* Consolidates a pattern that was hand-rolled independently in three places
|
|
202
|
+
* (`StaticClassResolver`, `VariantClassResolver`, `AriaPolicyEngine#planCache`)
|
|
203
|
+
* before this existed.
|
|
204
|
+
*/
|
|
205
|
+
var LRUCache = class {
|
|
206
|
+
#maxSize;
|
|
207
|
+
#store = /* @__PURE__ */ new Map();
|
|
208
|
+
constructor(maxSize) {
|
|
209
|
+
if (!Number.isInteger(maxSize) || maxSize < 1) throw new RangeError("LRUCache maxSize must be a positive integer.");
|
|
210
|
+
this.#maxSize = maxSize;
|
|
211
|
+
}
|
|
212
|
+
get(key) {
|
|
213
|
+
if (!this.#store.has(key)) return void 0;
|
|
214
|
+
const value = this.#store.get(key);
|
|
215
|
+
this.#store.delete(key);
|
|
216
|
+
this.#store.set(key, value);
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
set(key, value) {
|
|
220
|
+
this.#store.delete(key);
|
|
221
|
+
this.#store.set(key, value);
|
|
222
|
+
if (this.#store.size > this.#maxSize) {
|
|
223
|
+
const lru = this.#store.keys().next().value;
|
|
224
|
+
if (lru !== void 0) this.#store.delete(lru);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
has(key) {
|
|
228
|
+
return this.#store.has(key);
|
|
229
|
+
}
|
|
230
|
+
delete(key) {
|
|
231
|
+
return this.#store.delete(key);
|
|
232
|
+
}
|
|
233
|
+
get size() {
|
|
234
|
+
return this.#store.size;
|
|
235
|
+
}
|
|
236
|
+
clear() {
|
|
237
|
+
this.#store.clear();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region ../../lib/foundation/src/type-guards.ts
|
|
242
|
+
function isString(value) {
|
|
243
|
+
return typeof value === "string";
|
|
244
|
+
}
|
|
245
|
+
function isNumber(value) {
|
|
246
|
+
return typeof value === "number";
|
|
247
|
+
}
|
|
248
|
+
function isFunction(value) {
|
|
249
|
+
return typeof value === "function";
|
|
250
|
+
}
|
|
251
|
+
function isObject(value, excludeArrays = false) {
|
|
252
|
+
if (value === null || typeof value !== "object") return false;
|
|
253
|
+
return excludeArrays ? !Array.isArray(value) : true;
|
|
254
|
+
}
|
|
255
|
+
function isDefined(value) {
|
|
256
|
+
return value !== void 0;
|
|
257
|
+
}
|
|
258
|
+
function isUndefined(value) {
|
|
259
|
+
return value === void 0;
|
|
260
|
+
}
|
|
261
|
+
function isNull(value) {
|
|
262
|
+
return value === null;
|
|
263
|
+
}
|
|
264
|
+
function isNonNull(value) {
|
|
265
|
+
return value != null;
|
|
266
|
+
}
|
|
267
|
+
function isNullish(value) {
|
|
268
|
+
return isNull(value) || isUndefined(value);
|
|
269
|
+
}
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region ../../lib/primitive/src/rule/is-dynamic-rule.ts
|
|
272
|
+
function isDynamicRule(rule) {
|
|
273
|
+
return isObject(rule, true) && Reflect.get(rule, RULE_BRAND) === true;
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region ../../lib/primitive/src/rule/resolve-rule.ts
|
|
277
|
+
function resolveRule(rule, context) {
|
|
278
|
+
return isDynamicRule(rule) ? rule.resolve(context) : rule;
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region ../../lib/primitive/src/utils/merge-props.ts
|
|
282
|
+
function mergeProps(defaultProps, props) {
|
|
283
|
+
return {
|
|
284
|
+
...defaultProps ?? {},
|
|
285
|
+
...props
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region ../../lib/primitive/src/constants/aria/global-aria-attributes.ts
|
|
290
|
+
const GLOBAL_ARIA_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
291
|
+
"aria-atomic",
|
|
292
|
+
"aria-busy",
|
|
293
|
+
"aria-controls",
|
|
294
|
+
"aria-current",
|
|
295
|
+
"aria-describedby",
|
|
296
|
+
"aria-description",
|
|
297
|
+
"aria-details",
|
|
298
|
+
"aria-disabled",
|
|
299
|
+
"aria-errormessage",
|
|
300
|
+
"aria-flowto",
|
|
301
|
+
"aria-hidden",
|
|
302
|
+
"aria-keyshortcuts",
|
|
303
|
+
"aria-label",
|
|
304
|
+
"aria-labelledby",
|
|
305
|
+
"aria-live",
|
|
306
|
+
"aria-owns",
|
|
307
|
+
"aria-relevant",
|
|
308
|
+
"aria-roledescription"
|
|
309
|
+
]);
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region ../../lib/primitive/src/constants/aria/implicit-role-record.ts
|
|
312
|
+
/**
|
|
313
|
+
* A **deliberately partial** static tag→role model — not "the ARIA validator".
|
|
314
|
+
*
|
|
315
|
+
* HTML implicit roles fall into four kinds; this file only covers the first:
|
|
316
|
+
*
|
|
317
|
+
* 1. **static** — role depends on the tag alone (`nav → navigation`,
|
|
318
|
+
* `article → article`). Only these belong in `IMPLICIT_ROLE_RECORD`.
|
|
319
|
+
* 2. **attribute-dependent** — role depends on an attribute value
|
|
320
|
+
* (`a`/`area` is `link` *with* `href`, `generic` without — see
|
|
321
|
+
* `getAnchorImplicitRole`; `input` per `type`, see `INPUT_TYPE_ROLE_MAP` +
|
|
322
|
+
* `getInputImplicitRole`; `img` per `alt`; `select` is `combobox` or
|
|
323
|
+
* `listbox` per `multiple`/`size`, see `getSelectImplicitRole`).
|
|
324
|
+
* 3. **context-dependent** — role depends on ancestry
|
|
325
|
+
* (`section`/`form` become landmarks only when they have an accessible name;
|
|
326
|
+
* `header`/`footer` are `banner`/`contentinfo` only at the top level — see
|
|
327
|
+
* `getConditionalImplicitRole`).
|
|
328
|
+
* 4. **state-/naming-dependent** — role depends on runtime state or naming.
|
|
329
|
+
*
|
|
330
|
+
* Entries here that are *actually* attribute-dependent (`td`, `th`) are the "no
|
|
331
|
+
* attributes / defaults" case; callers that know the attributes must prefer the
|
|
332
|
+
* conditional helpers. Do not add an entry whose real role needs more than the
|
|
333
|
+
* tag — `a`/`area` and `select` are deliberately absent for that reason (a bare
|
|
334
|
+
* `<a>` is `generic`, not `link`; a bare `<select>` is `combobox`, not
|
|
335
|
+
* `listbox`, per ARIA-in-HTML).
|
|
336
|
+
*/
|
|
337
|
+
const IMPLICIT_ROLE_RECORD = Object.freeze({
|
|
338
|
+
article: "article",
|
|
339
|
+
aside: "complementary",
|
|
340
|
+
footer: "contentinfo",
|
|
341
|
+
header: "banner",
|
|
342
|
+
main: "main",
|
|
343
|
+
nav: "navigation",
|
|
344
|
+
button: "button",
|
|
345
|
+
textarea: "textbox",
|
|
346
|
+
h1: "heading",
|
|
347
|
+
h2: "heading",
|
|
348
|
+
h3: "heading",
|
|
349
|
+
h4: "heading",
|
|
350
|
+
h5: "heading",
|
|
351
|
+
h6: "heading",
|
|
352
|
+
ul: "list",
|
|
353
|
+
ol: "list",
|
|
354
|
+
li: "listitem",
|
|
355
|
+
table: "table",
|
|
356
|
+
tr: "row",
|
|
357
|
+
td: "cell",
|
|
358
|
+
th: "columnheader",
|
|
359
|
+
dialog: "dialog",
|
|
360
|
+
fieldset: "group",
|
|
361
|
+
figure: "figure",
|
|
362
|
+
meter: "meter",
|
|
363
|
+
output: "status",
|
|
364
|
+
progress: "progressbar"
|
|
365
|
+
});
|
|
366
|
+
const INPUT_TYPE_ROLE_MAP = Object.freeze({
|
|
367
|
+
checkbox: "checkbox",
|
|
368
|
+
radio: "radio",
|
|
369
|
+
range: "slider",
|
|
370
|
+
number: "spinbutton",
|
|
371
|
+
search: "searchbox",
|
|
372
|
+
text: "textbox",
|
|
373
|
+
email: "textbox",
|
|
374
|
+
tel: "textbox",
|
|
375
|
+
url: "textbox",
|
|
376
|
+
button: "button",
|
|
377
|
+
submit: "button",
|
|
378
|
+
reset: "button",
|
|
379
|
+
image: "button"
|
|
380
|
+
});
|
|
381
|
+
/**
|
|
382
|
+
* Roles whose implicit assignment this library treats as **not overridable** by
|
|
383
|
+
* an explicit `role` attribute (a warning, not a hard block).
|
|
384
|
+
*
|
|
385
|
+
* DELIBERATE POLICY, wider than ARIA-in-HTML (audited 2026-09, see
|
|
386
|
+
* docs/accessibility/html-aria-audit.md D1). ARIA-in-HTML *permits* specific role overrides
|
|
387
|
+
* on landmark elements (e.g. `<nav role="tablist">`); Praxis flags them anyway
|
|
388
|
+
* via `landmarkRoleRule`, because overriding a landmark's role silently drops it
|
|
389
|
+
* from the screen-reader landmark menu, and the APG's own menu/tab/tree patterns
|
|
390
|
+
* never put those roles on a landmark element. The allowed-role tables in
|
|
391
|
+
* `role-restrictions.ts` remain spec-accurate; this set only drives the softer
|
|
392
|
+
* landmark-override advisory. `<header>`/`<footer>` are `banner`/`contentinfo`
|
|
393
|
+
* only at the top level (see `getConditionalImplicitRole`).
|
|
394
|
+
*/
|
|
395
|
+
const STRONG_ROLES = Object.freeze([
|
|
396
|
+
"main",
|
|
397
|
+
"navigation",
|
|
398
|
+
"complementary",
|
|
399
|
+
"contentinfo",
|
|
400
|
+
"banner"
|
|
401
|
+
]);
|
|
402
|
+
const STANDALONE_ROLES = Object.freeze(["article"]);
|
|
403
|
+
const STRONG_ROLES_SET = new Set(STRONG_ROLES);
|
|
404
|
+
const STANDALONE_ROLES_SET = new Set(STANDALONE_ROLES);
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region ../../lib/primitive/src/constants/aria/role-restricted-attributes.ts
|
|
407
|
+
const ROLE_RESTRICTED_ATTRIBUTES = /* @__PURE__ */ new Map([
|
|
408
|
+
["aria-activedescendant", /* @__PURE__ */ new Set([
|
|
409
|
+
"application",
|
|
410
|
+
"combobox",
|
|
411
|
+
"grid",
|
|
412
|
+
"group",
|
|
413
|
+
"listbox",
|
|
414
|
+
"menu",
|
|
415
|
+
"menubar",
|
|
416
|
+
"radiogroup",
|
|
417
|
+
"row",
|
|
418
|
+
"searchbox",
|
|
419
|
+
"spinbutton",
|
|
420
|
+
"tablist",
|
|
421
|
+
"textbox",
|
|
422
|
+
"toolbar",
|
|
423
|
+
"tree",
|
|
424
|
+
"treegrid"
|
|
425
|
+
])],
|
|
426
|
+
["aria-autocomplete", /* @__PURE__ */ new Set([
|
|
427
|
+
"combobox",
|
|
428
|
+
"searchbox",
|
|
429
|
+
"textbox"
|
|
430
|
+
])],
|
|
431
|
+
["aria-checked", /* @__PURE__ */ new Set([
|
|
432
|
+
"checkbox",
|
|
433
|
+
"menuitemcheckbox",
|
|
434
|
+
"menuitemradio",
|
|
435
|
+
"option",
|
|
436
|
+
"radio",
|
|
437
|
+
"switch",
|
|
438
|
+
"treeitem"
|
|
439
|
+
])],
|
|
440
|
+
["aria-colcount", /* @__PURE__ */ new Set([
|
|
441
|
+
"grid",
|
|
442
|
+
"table",
|
|
443
|
+
"treegrid"
|
|
444
|
+
])],
|
|
445
|
+
["aria-colindex", /* @__PURE__ */ new Set([
|
|
446
|
+
"cell",
|
|
447
|
+
"columnheader",
|
|
448
|
+
"gridcell",
|
|
449
|
+
"row",
|
|
450
|
+
"rowheader"
|
|
451
|
+
])],
|
|
452
|
+
["aria-colspan", /* @__PURE__ */ new Set([
|
|
453
|
+
"cell",
|
|
454
|
+
"columnheader",
|
|
455
|
+
"gridcell",
|
|
456
|
+
"rowheader"
|
|
457
|
+
])],
|
|
458
|
+
["aria-expanded", /* @__PURE__ */ new Set([
|
|
459
|
+
"application",
|
|
460
|
+
"button",
|
|
461
|
+
"checkbox",
|
|
462
|
+
"columnheader",
|
|
463
|
+
"combobox",
|
|
464
|
+
"gridcell",
|
|
465
|
+
"link",
|
|
466
|
+
"listbox",
|
|
467
|
+
"menuitem",
|
|
468
|
+
"menuitemcheckbox",
|
|
469
|
+
"menuitemradio",
|
|
470
|
+
"row",
|
|
471
|
+
"rowheader",
|
|
472
|
+
"switch",
|
|
473
|
+
"tab",
|
|
474
|
+
"treeitem"
|
|
475
|
+
])],
|
|
476
|
+
["aria-haspopup", /* @__PURE__ */ new Set([
|
|
477
|
+
"application",
|
|
478
|
+
"button",
|
|
479
|
+
"columnheader",
|
|
480
|
+
"combobox",
|
|
481
|
+
"gridcell",
|
|
482
|
+
"link",
|
|
483
|
+
"menuitem",
|
|
484
|
+
"menuitemcheckbox",
|
|
485
|
+
"menuitemradio",
|
|
486
|
+
"rowheader",
|
|
487
|
+
"searchbox",
|
|
488
|
+
"slider",
|
|
489
|
+
"tab",
|
|
490
|
+
"textbox",
|
|
491
|
+
"treeitem"
|
|
492
|
+
])],
|
|
493
|
+
["aria-invalid", /* @__PURE__ */ new Set([
|
|
494
|
+
"application",
|
|
495
|
+
"checkbox",
|
|
496
|
+
"columnheader",
|
|
497
|
+
"combobox",
|
|
498
|
+
"gridcell",
|
|
499
|
+
"listbox",
|
|
500
|
+
"menuitemcheckbox",
|
|
501
|
+
"menuitemradio",
|
|
502
|
+
"radiogroup",
|
|
503
|
+
"rowheader",
|
|
504
|
+
"searchbox",
|
|
505
|
+
"slider",
|
|
506
|
+
"spinbutton",
|
|
507
|
+
"switch",
|
|
508
|
+
"textbox",
|
|
509
|
+
"tree",
|
|
510
|
+
"treegrid"
|
|
511
|
+
])],
|
|
512
|
+
["aria-level", /* @__PURE__ */ new Set([
|
|
513
|
+
"heading",
|
|
514
|
+
"listitem",
|
|
515
|
+
"row",
|
|
516
|
+
"tablist",
|
|
517
|
+
"treeitem"
|
|
518
|
+
])],
|
|
519
|
+
["aria-modal", /* @__PURE__ */ new Set(["alertdialog", "dialog"])],
|
|
520
|
+
["aria-multiline", /* @__PURE__ */ new Set(["searchbox", "textbox"])],
|
|
521
|
+
["aria-multiselectable", /* @__PURE__ */ new Set([
|
|
522
|
+
"grid",
|
|
523
|
+
"listbox",
|
|
524
|
+
"tablist",
|
|
525
|
+
"tree",
|
|
526
|
+
"treegrid"
|
|
527
|
+
])],
|
|
528
|
+
["aria-orientation", /* @__PURE__ */ new Set([
|
|
529
|
+
"listbox",
|
|
530
|
+
"menu",
|
|
531
|
+
"menubar",
|
|
532
|
+
"radiogroup",
|
|
533
|
+
"scrollbar",
|
|
534
|
+
"separator",
|
|
535
|
+
"slider",
|
|
536
|
+
"tablist",
|
|
537
|
+
"toolbar",
|
|
538
|
+
"tree",
|
|
539
|
+
"treegrid"
|
|
540
|
+
])],
|
|
541
|
+
["aria-placeholder", /* @__PURE__ */ new Set(["searchbox", "textbox"])],
|
|
542
|
+
["aria-posinset", /* @__PURE__ */ new Set([
|
|
543
|
+
"article",
|
|
544
|
+
"listitem",
|
|
545
|
+
"menuitem",
|
|
546
|
+
"menuitemcheckbox",
|
|
547
|
+
"menuitemradio",
|
|
548
|
+
"option",
|
|
549
|
+
"radio",
|
|
550
|
+
"row",
|
|
551
|
+
"tab",
|
|
552
|
+
"treeitem"
|
|
553
|
+
])],
|
|
554
|
+
["aria-pressed", /* @__PURE__ */ new Set(["button"])],
|
|
555
|
+
["aria-readonly", /* @__PURE__ */ new Set([
|
|
556
|
+
"checkbox",
|
|
557
|
+
"columnheader",
|
|
558
|
+
"combobox",
|
|
559
|
+
"grid",
|
|
560
|
+
"gridcell",
|
|
561
|
+
"listbox",
|
|
562
|
+
"menuitemcheckbox",
|
|
563
|
+
"menuitemradio",
|
|
564
|
+
"radiogroup",
|
|
565
|
+
"rowheader",
|
|
566
|
+
"searchbox",
|
|
567
|
+
"slider",
|
|
568
|
+
"spinbutton",
|
|
569
|
+
"switch",
|
|
570
|
+
"textbox",
|
|
571
|
+
"treegrid"
|
|
572
|
+
])],
|
|
573
|
+
["aria-required", /* @__PURE__ */ new Set([
|
|
574
|
+
"checkbox",
|
|
575
|
+
"columnheader",
|
|
576
|
+
"combobox",
|
|
577
|
+
"gridcell",
|
|
578
|
+
"listbox",
|
|
579
|
+
"menuitemcheckbox",
|
|
580
|
+
"menuitemradio",
|
|
581
|
+
"radiogroup",
|
|
582
|
+
"rowheader",
|
|
583
|
+
"searchbox",
|
|
584
|
+
"spinbutton",
|
|
585
|
+
"switch",
|
|
586
|
+
"textbox",
|
|
587
|
+
"tree",
|
|
588
|
+
"treegrid"
|
|
589
|
+
])],
|
|
590
|
+
["aria-rowcount", /* @__PURE__ */ new Set([
|
|
591
|
+
"grid",
|
|
592
|
+
"table",
|
|
593
|
+
"treegrid"
|
|
594
|
+
])],
|
|
595
|
+
["aria-rowindex", /* @__PURE__ */ new Set([
|
|
596
|
+
"cell",
|
|
597
|
+
"columnheader",
|
|
598
|
+
"gridcell",
|
|
599
|
+
"row",
|
|
600
|
+
"rowheader"
|
|
601
|
+
])],
|
|
602
|
+
["aria-rowspan", /* @__PURE__ */ new Set([
|
|
603
|
+
"cell",
|
|
604
|
+
"columnheader",
|
|
605
|
+
"gridcell",
|
|
606
|
+
"rowheader"
|
|
607
|
+
])],
|
|
608
|
+
["aria-selected", /* @__PURE__ */ new Set([
|
|
609
|
+
"columnheader",
|
|
610
|
+
"gridcell",
|
|
611
|
+
"option",
|
|
612
|
+
"row",
|
|
613
|
+
"rowheader",
|
|
614
|
+
"tab",
|
|
615
|
+
"treeitem"
|
|
616
|
+
])],
|
|
617
|
+
["aria-setsize", /* @__PURE__ */ new Set([
|
|
618
|
+
"article",
|
|
619
|
+
"listitem",
|
|
620
|
+
"menuitem",
|
|
621
|
+
"menuitemcheckbox",
|
|
622
|
+
"menuitemradio",
|
|
623
|
+
"option",
|
|
624
|
+
"radio",
|
|
625
|
+
"row",
|
|
626
|
+
"tab",
|
|
627
|
+
"treeitem"
|
|
628
|
+
])],
|
|
629
|
+
["aria-sort", /* @__PURE__ */ new Set(["columnheader", "rowheader"])],
|
|
630
|
+
["aria-valuemax", /* @__PURE__ */ new Set([
|
|
631
|
+
"meter",
|
|
632
|
+
"progressbar",
|
|
633
|
+
"scrollbar",
|
|
634
|
+
"separator",
|
|
635
|
+
"slider",
|
|
636
|
+
"spinbutton"
|
|
637
|
+
])],
|
|
638
|
+
["aria-valuemin", /* @__PURE__ */ new Set([
|
|
639
|
+
"meter",
|
|
640
|
+
"progressbar",
|
|
641
|
+
"scrollbar",
|
|
642
|
+
"separator",
|
|
643
|
+
"slider",
|
|
644
|
+
"spinbutton"
|
|
645
|
+
])],
|
|
646
|
+
["aria-valuenow", /* @__PURE__ */ new Set([
|
|
647
|
+
"meter",
|
|
648
|
+
"progressbar",
|
|
649
|
+
"scrollbar",
|
|
650
|
+
"separator",
|
|
651
|
+
"slider",
|
|
652
|
+
"spinbutton"
|
|
653
|
+
])],
|
|
654
|
+
["aria-valuetext", /* @__PURE__ */ new Set([
|
|
655
|
+
"meter",
|
|
656
|
+
"progressbar",
|
|
657
|
+
"scrollbar",
|
|
658
|
+
"separator",
|
|
659
|
+
"slider",
|
|
660
|
+
"spinbutton"
|
|
661
|
+
])]
|
|
662
|
+
]);
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region ../../lib/primitive/src/constants/html/void-tags.ts
|
|
665
|
+
/**
|
|
666
|
+
* HTML void elements.
|
|
667
|
+
*
|
|
668
|
+
* Void elements cannot have child nodes and therefore all share the same
|
|
669
|
+
* empty content model — a WHATWG-stable fact, not a praxis-kit opinion.
|
|
670
|
+
* Single source of truth shared by the contract engine's built-in void-tag
|
|
671
|
+
* children contract (`@praxis-kit/core`'s `VOID_TAGS` re-export) and the
|
|
672
|
+
* Tailwind pipeline's flex/grid-on-void-tag warning, so the two can't drift.
|
|
673
|
+
*/
|
|
674
|
+
const VOID_TAGS = [
|
|
675
|
+
"area",
|
|
676
|
+
"base",
|
|
677
|
+
"br",
|
|
678
|
+
"col",
|
|
679
|
+
"embed",
|
|
680
|
+
"hr",
|
|
681
|
+
"img",
|
|
682
|
+
"input",
|
|
683
|
+
"link",
|
|
684
|
+
"meta",
|
|
685
|
+
"param",
|
|
686
|
+
"source",
|
|
687
|
+
"track",
|
|
688
|
+
"wbr"
|
|
689
|
+
];
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region ../../lib/primitive/src/guards/aria/is-aria-attribute.ts
|
|
692
|
+
function isGlobalAriaAttribute(attr) {
|
|
693
|
+
return GLOBAL_ARIA_ATTRIBUTES.has(attr);
|
|
694
|
+
}
|
|
695
|
+
function isAriaAttributeValidForRole(attr, role) {
|
|
696
|
+
const allowedRoles = ROLE_RESTRICTED_ATTRIBUTES.get(attr);
|
|
697
|
+
if (isUndefined(allowedRoles)) return true;
|
|
698
|
+
if (isUndefined(role)) return false;
|
|
699
|
+
return allowedRoles.has(role);
|
|
700
|
+
}
|
|
701
|
+
//#endregion
|
|
702
|
+
//#region ../../lib/primitive/src/guards/aria/is-aria-role.ts
|
|
703
|
+
function lookupImplicitRole(tag) {
|
|
704
|
+
return IMPLICIT_ROLE_RECORD[tag];
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Returns whether the element has a strong implicit ARIA role.
|
|
708
|
+
*
|
|
709
|
+
* Strong implicit roles cannot be overridden with an explicit `role`
|
|
710
|
+
* attribute unless the HTML specification explicitly permits it.
|
|
711
|
+
*/
|
|
712
|
+
function isStrongImplicitRole(tag) {
|
|
713
|
+
const role = lookupImplicitRole(tag);
|
|
714
|
+
return !isNullish(role) && STRONG_ROLES_SET.has(role);
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Returns whether the element's implicit ARIA role is considered
|
|
718
|
+
* standalone for accessibility validation purposes.
|
|
719
|
+
*/
|
|
720
|
+
function hasStandaloneRole(tag) {
|
|
721
|
+
const role = lookupImplicitRole(tag);
|
|
722
|
+
return !isNullish(role) && STANDALONE_ROLES_SET.has(role);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Input types for which the presence of a `list` attribute changes the implicit
|
|
726
|
+
* ARIA role from `textbox` to `combobox`, per the ARIA-in-HTML specification.
|
|
727
|
+
* `packages/core`'s `inputElementSpec` keeps its own copy of this set (it must
|
|
728
|
+
* not import the guard module) — the two are pinned to the spec by tests on both
|
|
729
|
+
* sides and must stay in sync.
|
|
730
|
+
*/
|
|
731
|
+
const LIST_ELIGIBLE_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
732
|
+
"text",
|
|
733
|
+
"search",
|
|
734
|
+
"tel",
|
|
735
|
+
"url",
|
|
736
|
+
"email"
|
|
737
|
+
]);
|
|
738
|
+
/**
|
|
739
|
+
* Returns the implicit ARIA role for an `<input>` element.
|
|
740
|
+
*
|
|
741
|
+
* An omitted `type` attribute defaults to `text` (HTML), so a bare `<input>`
|
|
742
|
+
* resolves to `textbox` — `<input role="textbox">` is then caught as redundant.
|
|
743
|
+
*
|
|
744
|
+
* For text-like input types associated with a `<datalist>` via the
|
|
745
|
+
* `list` attribute, the implicit role becomes `combobox` instead of
|
|
746
|
+
* `textbox`, per the ARIA-in-HTML specification.
|
|
747
|
+
*
|
|
748
|
+
* Returns `undefined` for input types that do not expose an implicit
|
|
749
|
+
* ARIA role (for example `color`, `date`, or `hidden`).
|
|
750
|
+
*/
|
|
751
|
+
function getInputImplicitRole(type, list) {
|
|
752
|
+
const resolvedType = isNullish(type) ? "text" : type;
|
|
753
|
+
if (!isString(resolvedType)) return void 0;
|
|
754
|
+
const role = INPUT_TYPE_ROLE_MAP[resolvedType];
|
|
755
|
+
if (!role) return void 0;
|
|
756
|
+
if (!isNullish(list) && LIST_ELIGIBLE_INPUT_TYPES.has(resolvedType)) return "combobox";
|
|
757
|
+
return role;
|
|
758
|
+
}
|
|
759
|
+
function isBooleanAttrOn(value) {
|
|
760
|
+
if (value === false || value === void 0 || value === null) return false;
|
|
761
|
+
if (typeof value === "string") return value.toLowerCase() !== "false";
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Returns the implicit ARIA role for a `<select>` element.
|
|
766
|
+
*
|
|
767
|
+
* Per HTML-AAM / ARIA-in-HTML a `<select>` is a **`combobox`** in its default
|
|
768
|
+
* drop-down form, and a **`listbox`** only when it is a list box — i.e. it has a
|
|
769
|
+
* `multiple` attribute, or a `size` attribute whose value is greater than 1.
|
|
770
|
+
*
|
|
771
|
+
* Kept as a helper (not an `IMPLICIT_ROLE_RECORD` entry) because the role needs
|
|
772
|
+
* more than the tag name — the same reason `<input>` is handled by
|
|
773
|
+
* `getInputImplicitRole`.
|
|
774
|
+
*/
|
|
775
|
+
function getSelectImplicitRole(multiple, size) {
|
|
776
|
+
if (isBooleanAttrOn(multiple)) return "listbox";
|
|
777
|
+
const sizeNum = typeof size === "number" ? size : typeof size === "string" ? Number(size) : NaN;
|
|
778
|
+
if (Number.isFinite(sizeNum) && sizeNum > 1) return "listbox";
|
|
779
|
+
return "combobox";
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Returns the implicit ARIA role for an `<a>` or `<area>` element.
|
|
783
|
+
*
|
|
784
|
+
* Per ARIA-in-HTML, an `<a>`/`<area>` **with an `href`** is a `link`; **without
|
|
785
|
+
* an `href`** it is `generic` (it has no interactive semantics — it is just a
|
|
786
|
+
* styled span). Kept as a helper (not an `IMPLICIT_ROLE_RECORD` entry) because
|
|
787
|
+
* the role needs the `href` attribute, not just the tag: a bare `<a>` is *not* a
|
|
788
|
+
* `link`, so `<a role="button">` on it is not a redundant override.
|
|
789
|
+
*
|
|
790
|
+
* A `href=""` (empty string) still counts — it is a valid same-page link.
|
|
791
|
+
*/
|
|
792
|
+
function getAnchorImplicitRole(href) {
|
|
793
|
+
return isNullish(href) ? "generic" : "link";
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Returns the conditional implicit landmark role for `<section>` and
|
|
797
|
+
* `<form>` elements.
|
|
798
|
+
*
|
|
799
|
+
* Per HTML-AAM, these elements expose their landmark roles only when
|
|
800
|
+
* they have an accessible name provided by `aria-label` or
|
|
801
|
+
* `aria-labelledby`.
|
|
802
|
+
*/
|
|
803
|
+
function getConditionalImplicitRole(tag, ariaLabel, ariaLabelledBy) {
|
|
804
|
+
if (!(isString(ariaLabel) && ariaLabel.trim().length > 0 || isString(ariaLabelledBy) && ariaLabelledBy.trim().length > 0)) return void 0;
|
|
805
|
+
if (tag === "section") return "region";
|
|
806
|
+
if (tag === "form") return "form";
|
|
807
|
+
}
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region ../../lib/primitive/src/guards/children/component-id.ts
|
|
810
|
+
/**
|
|
811
|
+
* Well-known Symbol stamped onto every component created by praxis-kit factories.
|
|
812
|
+
* Stores the factory's defaultTag — the tag that renders when no `as` prop is given.
|
|
813
|
+
* HOC wrappers must propagate it: `Wrapped[COMPONENT_DEFAULT_TAG] = Original[COMPONENT_DEFAULT_TAG]`.
|
|
814
|
+
*/
|
|
815
|
+
const COMPONENT_DEFAULT_TAG = Symbol.for("praxis.component-default-tag");
|
|
816
|
+
//#endregion
|
|
817
|
+
//#region ../../lib/primitive/src/guards/children/is-tag.ts
|
|
818
|
+
function getAsProp(child) {
|
|
819
|
+
if (!isObject(child) || !("props" in child)) return void 0;
|
|
820
|
+
const { props } = child;
|
|
821
|
+
if (!isObject(props)) return void 0;
|
|
822
|
+
const as = Reflect.get(props, "as");
|
|
823
|
+
return isString(as) && as !== "" ? as : void 0;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Resolves the effective HTML tag for a vnode:
|
|
827
|
+
* - native element: returns its type string directly
|
|
828
|
+
* - praxis-kit component: resolves `as ?? defaultTag`, mirroring render-time logic
|
|
829
|
+
* - anything else: returns undefined
|
|
830
|
+
*/
|
|
831
|
+
function getTag(child) {
|
|
832
|
+
if (!isObject(child) || !("type" in child)) return void 0;
|
|
833
|
+
const { type: t } = child;
|
|
834
|
+
if (isString(t)) return t;
|
|
835
|
+
if (typeof t === "function" || isObject(t)) {
|
|
836
|
+
const defaultTag = Reflect.get(t, COMPONENT_DEFAULT_TAG);
|
|
837
|
+
if (!isString(defaultTag)) return void 0;
|
|
838
|
+
return getAsProp(child) ?? defaultTag;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function isTag(...args) {
|
|
842
|
+
if (isString(args[0])) {
|
|
843
|
+
const set = new Set(args);
|
|
844
|
+
return (child) => {
|
|
845
|
+
const tag = getTag(child);
|
|
846
|
+
return tag !== void 0 && set.has(tag);
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
const [child, ...tags] = args;
|
|
850
|
+
const set = new Set(tags);
|
|
851
|
+
const tag = getTag(child);
|
|
852
|
+
return tag !== void 0 && set.has(tag);
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region ../../lib/adapter-utils/src/runtime/is-factory-options-like.ts
|
|
856
|
+
/**
|
|
857
|
+
* Loosely validates one recognized `FactoryOptions` field's runtime shape.
|
|
858
|
+
* "Loosely" because the field's declared type is itself parameterized by a
|
|
859
|
+
* generic (Props, Variants, TPlugin, ...) that's erased at runtime — these
|
|
860
|
+
* checks confirm the field is the right *kind* of value (object, function,
|
|
861
|
+
* string), not that it satisfies its exact generic instantiation, which no
|
|
862
|
+
* runtime check can ever do.
|
|
863
|
+
*
|
|
864
|
+
* Covers every field `FactoryOptions` itself declares — identical across
|
|
865
|
+
* every adapter, since they all extend the same core type. Adapter-specific
|
|
866
|
+
* additions (React's `slotComponent`/`artifact`, every adapter's own
|
|
867
|
+
* `filterProps`, etc.) are passed in by the caller as extra entries.
|
|
868
|
+
*/
|
|
869
|
+
const FACTORY_OPTIONS_FIELD_VALIDATORS = {
|
|
870
|
+
tag: (v) => v === void 0 || isString(v),
|
|
871
|
+
name: (v) => v === void 0 || isString(v),
|
|
872
|
+
defaults: (v) => v === void 0 || isObject(v),
|
|
873
|
+
normalize: (v) => v === void 0 || isFunction(v),
|
|
874
|
+
styling: (v) => v === void 0 || isObject(v),
|
|
875
|
+
enforcement: (v) => v === void 0 || isObject(v),
|
|
876
|
+
diagnostics: (v) => v === void 0 || isObject(v),
|
|
877
|
+
subComponents: (v) => v === void 0 || isObject(v),
|
|
878
|
+
onElement: (v) => v === void 0 || isFunction(v)
|
|
879
|
+
};
|
|
880
|
+
/**
|
|
881
|
+
* Type guard narrowing the generic `FactoryOptions` shape down to an
|
|
882
|
+
* adapter-specific factory options type `T` — the type each adapter's
|
|
883
|
+
* `buildRuntime` is declared against.
|
|
884
|
+
*
|
|
885
|
+
* Walks every own enumerable property on `options` and checks it against
|
|
886
|
+
* `FACTORY_OPTIONS_FIELD_VALIDATORS` plus `extraFieldValidators` (the
|
|
887
|
+
* adapter's own additions on top of `FactoryOptions`): an unrecognized key,
|
|
888
|
+
* or a recognized key holding a value of the wrong kind, fails the guard.
|
|
889
|
+
* This is a genuine structural check, not a relabeled assertion — though it
|
|
890
|
+
* necessarily stops at each field's runtime *kind*, since the field's actual
|
|
891
|
+
* generic instantiation is erased at runtime and no guard can validate it.
|
|
892
|
+
* That part remains the caller's responsibility, same as with any other
|
|
893
|
+
* generic function in TypeScript.
|
|
894
|
+
*/
|
|
895
|
+
function isFactoryOptionsLike(options, extraFieldValidators) {
|
|
896
|
+
if (!isObject(options)) return false;
|
|
897
|
+
const validators = {
|
|
898
|
+
...FACTORY_OPTIONS_FIELD_VALIDATORS,
|
|
899
|
+
...extraFieldValidators
|
|
900
|
+
};
|
|
901
|
+
for (const [key, value] of Object.entries(options)) {
|
|
902
|
+
const validate = validators[key];
|
|
903
|
+
if (!validate || !validate(value)) return false;
|
|
904
|
+
}
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
//#endregion
|
|
908
|
+
//#region ../../lib/contract/src/aria/aria-role-policy.ts
|
|
909
|
+
function getImplicitRole(tag, props) {
|
|
910
|
+
if (tag in IMPLICIT_ROLE_RECORD) return IMPLICIT_ROLE_RECORD[tag];
|
|
911
|
+
if (tag === "a" || tag === "area") return getAnchorImplicitRole(props?.href);
|
|
912
|
+
if (tag === "input") return getInputImplicitRole(props?.type, props?.list);
|
|
913
|
+
if (tag === "select") return getSelectImplicitRole(props?.multiple, props?.size);
|
|
914
|
+
if (tag === "img") return props?.alt === "" ? "none" : "img";
|
|
915
|
+
if (tag === "section" || tag === "form") return getConditionalImplicitRole(tag, props?.["aria-label"], props?.["aria-labelledby"]);
|
|
916
|
+
}
|
|
917
|
+
//#endregion
|
|
918
|
+
//#region ../../lib/contract/src/diagnostics/anchor-accessibility.ts
|
|
919
|
+
function createDiagnostic(input) {
|
|
920
|
+
return {
|
|
921
|
+
category: DiagnosticCategory.Accessibility,
|
|
922
|
+
...input
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
const AnchorAccessibilityDiagnostics = {
|
|
926
|
+
roleButtonWithHref() {
|
|
927
|
+
return createDiagnostic({
|
|
928
|
+
code: DiagnosticCode.A11yAnchorRoleButtonWithHref,
|
|
929
|
+
severity: "warning",
|
|
930
|
+
message: "role=\"button\" on an <a> with an href overrides its navigation semantics for assistive technology; use a real <button> element, or remove href, if this element should not navigate.",
|
|
931
|
+
rationale: "Assistive technology announces role=\"button\" as a button, not a link — but the element still follows the link when activated by mouse or keyboard, which is confusing and inconsistent with how a button is expected to behave.",
|
|
932
|
+
suggestions: [{
|
|
933
|
+
title: "Use a real <button>",
|
|
934
|
+
description: "If this element should not navigate, render a <button> instead of an <a>."
|
|
935
|
+
}, {
|
|
936
|
+
title: "Remove role=\"button\"",
|
|
937
|
+
description: "If this element should navigate, keep the default link role."
|
|
938
|
+
}]
|
|
939
|
+
});
|
|
940
|
+
},
|
|
941
|
+
ariaDisabledInert() {
|
|
942
|
+
return createDiagnostic({
|
|
943
|
+
code: DiagnosticCode.A11yAnchorAriaDisabledInert,
|
|
944
|
+
severity: "warning",
|
|
945
|
+
message: "aria-disabled does not prevent an <a> from being focused, clicked, or navigated via keyboard; remove href, or prevent navigation yourself, if this link should be inert.",
|
|
946
|
+
rationale: "Unlike a native form control, an <a> has no disabled state the browser enforces — aria-disabled only changes what assistive technology announces, not what actually happens when the link is activated by mouse or keyboard.",
|
|
947
|
+
suggestions: [{
|
|
948
|
+
title: "Remove href",
|
|
949
|
+
description: "If this element is temporarily unavailable, remove href until it can be activated."
|
|
950
|
+
}, {
|
|
951
|
+
title: "Prevent navigation when disabled",
|
|
952
|
+
description: "Prevent the click and keyboard activation when the link is disabled so it behaves consistently for all users."
|
|
953
|
+
}]
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
//#endregion
|
|
958
|
+
//#region ../../lib/contract/src/diagnostics/aria.ts
|
|
959
|
+
const AriaDiagnostics = {
|
|
960
|
+
/** Generic bridge for violations produced by external AriaRule functions. */
|
|
961
|
+
fromViolation(v) {
|
|
962
|
+
return {
|
|
963
|
+
code: DiagnosticCode.AriaViolation,
|
|
964
|
+
category: DiagnosticCategory.ARIA,
|
|
965
|
+
message: v.message
|
|
966
|
+
};
|
|
967
|
+
},
|
|
968
|
+
attributeInvalid(key, role) {
|
|
969
|
+
return {
|
|
970
|
+
code: DiagnosticCode.AriaAttributeInvalid,
|
|
971
|
+
category: DiagnosticCategory.ARIA,
|
|
972
|
+
message: `"${key}" is not valid on role="${role}". It will be removed.`,
|
|
973
|
+
rationale: "Invalid ARIA attributes are ignored by assistive technology and may trigger accessibility-tree warnings in browser devtools.",
|
|
974
|
+
suggestions: [{
|
|
975
|
+
title: "Remove the attribute",
|
|
976
|
+
description: `"${key}" is not in the allowed attribute set for role="${role}".`
|
|
977
|
+
}]
|
|
978
|
+
};
|
|
979
|
+
},
|
|
980
|
+
missingLiveRegion(role, impliedLive) {
|
|
981
|
+
return {
|
|
982
|
+
code: DiagnosticCode.AriaMissingLiveRegion,
|
|
983
|
+
category: DiagnosticCategory.ARIA,
|
|
984
|
+
message: `role="${role}" implies aria-live="${impliedLive}" but it is missing. It has been injected.`,
|
|
985
|
+
rationale: "Live-region roles announce dynamic content changes to screen readers. Without aria-live the politeness level is unspecified and announcements may be silent.",
|
|
986
|
+
suggestions: [{
|
|
987
|
+
title: `Add aria-live="${impliedLive}"`,
|
|
988
|
+
description: `role="${role}" conventionally implies aria-live="${impliedLive}".`,
|
|
989
|
+
fix: `aria-live="${impliedLive}"`
|
|
990
|
+
}]
|
|
991
|
+
};
|
|
992
|
+
},
|
|
993
|
+
missingAtomic(role) {
|
|
994
|
+
return {
|
|
995
|
+
code: DiagnosticCode.AriaMissingAtomic,
|
|
996
|
+
category: DiagnosticCategory.ARIA,
|
|
997
|
+
message: `role="${role}" is a live region. Consider setting aria-atomic="true" if the full region should be announced as a unit, or aria-atomic="false" if only changed nodes should be read.`,
|
|
998
|
+
rationale: "aria-atomic controls whether assistive technology announces the entire live region or only the changed nodes. Omitting it leaves the behaviour browser-defined."
|
|
999
|
+
};
|
|
1000
|
+
},
|
|
1001
|
+
relevantInvalidTokens(invalid) {
|
|
1002
|
+
const quoted = invalid.map((t) => `"${t}"`).join(", ");
|
|
1003
|
+
return {
|
|
1004
|
+
code: DiagnosticCode.AriaRelevantInvalidToken,
|
|
1005
|
+
category: DiagnosticCategory.ARIA,
|
|
1006
|
+
message: `aria-relevant contains invalid token(s): ${quoted}. Valid tokens are: additions, removals, text, all.`,
|
|
1007
|
+
rationale: "aria-relevant accepts a space-separated list of change types. Unrecognised tokens are silently ignored by assistive technology, making the attribute ineffective.",
|
|
1008
|
+
suggestions: [{
|
|
1009
|
+
title: "Use only valid tokens",
|
|
1010
|
+
description: "Valid values are: additions, removals, text, all (or a space-separated combination)."
|
|
1011
|
+
}]
|
|
1012
|
+
};
|
|
1013
|
+
},
|
|
1014
|
+
relevantSuperseded() {
|
|
1015
|
+
return {
|
|
1016
|
+
code: DiagnosticCode.AriaRelevantSuperseded,
|
|
1017
|
+
category: DiagnosticCategory.ARIA,
|
|
1018
|
+
message: "aria-relevant includes \"all\" alongside other tokens. \"all\" supersedes additions, removals, and text — use aria-relevant=\"all\" alone.",
|
|
1019
|
+
rationale: "\"all\" is equivalent to \"additions removals text\". Combining it with other tokens is redundant and may confuse readers of the markup.",
|
|
1020
|
+
suggestions: [{
|
|
1021
|
+
title: "Use aria-relevant=\"all\"",
|
|
1022
|
+
fix: "aria-relevant=\"all\""
|
|
1023
|
+
}]
|
|
1024
|
+
};
|
|
1025
|
+
},
|
|
1026
|
+
missingAccessibleName(tag) {
|
|
1027
|
+
return {
|
|
1028
|
+
code: DiagnosticCode.AriaMissingAccessibleName,
|
|
1029
|
+
category: DiagnosticCategory.ARIA,
|
|
1030
|
+
message: `<${tag}> has no accessible name. Add aria-label or aria-labelledby.`,
|
|
1031
|
+
rationale: "Elements with a landmark or interactive role must have an accessible name so that assistive technology can identify them when presenting the page outline.",
|
|
1032
|
+
suggestions: [{
|
|
1033
|
+
title: "Add aria-label",
|
|
1034
|
+
description: `Add aria-label="…" directly to the <${tag}> element.`
|
|
1035
|
+
}, {
|
|
1036
|
+
title: "Add aria-labelledby",
|
|
1037
|
+
description: "Point aria-labelledby at the id of an existing heading or label element."
|
|
1038
|
+
}]
|
|
1039
|
+
};
|
|
1040
|
+
},
|
|
1041
|
+
nameProhibited(attr, role) {
|
|
1042
|
+
return {
|
|
1043
|
+
code: DiagnosticCode.AriaNameProhibited,
|
|
1044
|
+
category: DiagnosticCategory.ARIA,
|
|
1045
|
+
message: `"${attr}" is prohibited on role="${role}" — this role does not support a name from the author. It will be removed.`,
|
|
1046
|
+
rationale: "WAI-ARIA 1.2 lists a set of roles (generic, presentation/none, and the inline text-level roles) as Name Prohibited: aria-label / aria-labelledby on them are a conformance error and are ignored by assistive technology.",
|
|
1047
|
+
suggestions: [{
|
|
1048
|
+
title: "Remove the attribute",
|
|
1049
|
+
description: `role="${role}" cannot be named. If the element needs an accessible name, give it a role that supports one (or use visible text / a wrapping labelled element).`
|
|
1050
|
+
}]
|
|
1051
|
+
};
|
|
1052
|
+
},
|
|
1053
|
+
attributeOnPresentational(attr, tag) {
|
|
1054
|
+
return {
|
|
1055
|
+
code: DiagnosticCode.AriaAttributeOnPresentational,
|
|
1056
|
+
category: DiagnosticCategory.ARIA,
|
|
1057
|
+
message: `"${attr}" is not allowed on a presentational <${tag}>. Presentational elements are invisible to assistive technology.`,
|
|
1058
|
+
rationale: "role=\"none\" and role=\"presentation\" (including <img alt=\"\">) remove an element from the accessibility tree. ARIA attributes on such elements are ignored by assistive technology.",
|
|
1059
|
+
suggestions: [{
|
|
1060
|
+
title: "Remove the attribute",
|
|
1061
|
+
description: `"${attr}" has no effect when the element has role="none" or role="presentation".`
|
|
1062
|
+
}]
|
|
1063
|
+
};
|
|
1064
|
+
},
|
|
1065
|
+
ariaHiddenOnFocusable(tag) {
|
|
1066
|
+
return {
|
|
1067
|
+
code: DiagnosticCode.AriaHiddenOnFocusable,
|
|
1068
|
+
category: DiagnosticCategory.ARIA,
|
|
1069
|
+
message: `aria-hidden="true" must not be used on focusable <${tag}> elements. Screen reader users who navigate by keyboard will encounter the element but receive no information about it.`,
|
|
1070
|
+
rationale: "aria-hidden removes an element from the accessibility tree while leaving it keyboard-reachable. This creates a \"ghost\" — a focusable element assistive technology cannot describe.",
|
|
1071
|
+
suggestions: [{
|
|
1072
|
+
title: "Remove aria-hidden",
|
|
1073
|
+
description: "If the element should be hidden from all users, use the HTML hidden attribute or CSS display:none instead."
|
|
1074
|
+
}, {
|
|
1075
|
+
title: "Make the element non-focusable",
|
|
1076
|
+
description: "If the element is intentionally decorative, add tabindex=\"-1\" and disable it so it is not reachable by keyboard."
|
|
1077
|
+
}]
|
|
1078
|
+
};
|
|
1079
|
+
},
|
|
1080
|
+
invalidAttributeValue(attr, value, expected) {
|
|
1081
|
+
const got = value === null ? "null" : value === void 0 ? "undefined" : typeof value === "string" ? `"${value}"` : String(value);
|
|
1082
|
+
return {
|
|
1083
|
+
code: DiagnosticCode.AriaInvalidAttributeValue,
|
|
1084
|
+
category: DiagnosticCategory.ARIA,
|
|
1085
|
+
message: `"${attr}" has an invalid value (${got}). Expected: ${expected}.`,
|
|
1086
|
+
rationale: "ARIA attributes with invalid values are silently ignored by assistive technology, making the markup semantically inert.",
|
|
1087
|
+
suggestions: [{
|
|
1088
|
+
title: `Use a valid value for ${attr}`,
|
|
1089
|
+
description: `Valid values are: ${expected}.`
|
|
1090
|
+
}]
|
|
1091
|
+
};
|
|
1092
|
+
},
|
|
1093
|
+
redundantAriaLevel(tag, level) {
|
|
1094
|
+
return {
|
|
1095
|
+
code: DiagnosticCode.AriaRedundantLevelAttribute,
|
|
1096
|
+
category: DiagnosticCategory.ARIA,
|
|
1097
|
+
message: `aria-level="${level}" is redundant on <${tag}>: the element already has an implicit heading level of ${level}. Remove the attribute.`,
|
|
1098
|
+
rationale: "Restating the implicit aria-level adds noise without semantic value. Use aria-level only to override the native heading level (e.g. aria-level=\"3\" on <h2>).",
|
|
1099
|
+
suggestions: [{
|
|
1100
|
+
title: "Remove aria-level",
|
|
1101
|
+
description: `<${tag}> already implies aria-level="${level}".`
|
|
1102
|
+
}]
|
|
1103
|
+
};
|
|
1104
|
+
},
|
|
1105
|
+
requiredProperty(attr, role) {
|
|
1106
|
+
return {
|
|
1107
|
+
code: DiagnosticCode.AriaRequiredProperty,
|
|
1108
|
+
category: DiagnosticCategory.ARIA,
|
|
1109
|
+
message: `"${attr}" is required for role="${role}" but is missing.`,
|
|
1110
|
+
rationale: `WAI-ARIA 1.2 specifies required states and properties for certain roles. Without "${attr}", assistive technology cannot correctly communicate the element's state to users.`,
|
|
1111
|
+
suggestions: [{
|
|
1112
|
+
title: `Add ${attr}`,
|
|
1113
|
+
description: `role="${role}" requires "${attr}" to be present.`
|
|
1114
|
+
}]
|
|
1115
|
+
};
|
|
1116
|
+
},
|
|
1117
|
+
invalidRole(role, tag) {
|
|
1118
|
+
return {
|
|
1119
|
+
code: DiagnosticCode.AriaInvalidRole,
|
|
1120
|
+
category: DiagnosticCategory.ARIA,
|
|
1121
|
+
message: `Invalid role "${role ?? ""}" on <${tag}>.`,
|
|
1122
|
+
rationale: "An unrecognised or misapplied ARIA role is ignored by assistive technology and may degrade the accessibility of the element."
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region ../../lib/contract/src/diagnostics/contract.ts
|
|
1128
|
+
const ContractDiagnostics = {
|
|
1129
|
+
unexpectedChild(typeName, index, context) {
|
|
1130
|
+
return {
|
|
1131
|
+
code: DiagnosticCode.UnexpectedChild,
|
|
1132
|
+
category: DiagnosticCategory.Contract,
|
|
1133
|
+
component: context,
|
|
1134
|
+
message: `${context}: unexpected child "${typeName}" at index ${index}.`
|
|
1135
|
+
};
|
|
1136
|
+
},
|
|
1137
|
+
ambiguousChild(typeName, index, ruleNames, context) {
|
|
1138
|
+
const quoted = ruleNames.map((n) => `"${n}"`).join(" and ");
|
|
1139
|
+
return {
|
|
1140
|
+
code: DiagnosticCode.AmbiguousChild,
|
|
1141
|
+
category: DiagnosticCategory.Contract,
|
|
1142
|
+
component: context,
|
|
1143
|
+
message: `${context}: child "${typeName}" at index ${index} matches multiple child rules: ${quoted}.`
|
|
1144
|
+
};
|
|
1145
|
+
},
|
|
1146
|
+
cardinalityMin(ruleName, min, context) {
|
|
1147
|
+
return {
|
|
1148
|
+
code: DiagnosticCode.CardinalityMin,
|
|
1149
|
+
category: DiagnosticCategory.Contract,
|
|
1150
|
+
component: context,
|
|
1151
|
+
message: `${context}: "${ruleName}" requires at least ${min}.`
|
|
1152
|
+
};
|
|
1153
|
+
},
|
|
1154
|
+
cardinalityMax(ruleName, max, context) {
|
|
1155
|
+
return {
|
|
1156
|
+
code: DiagnosticCode.CardinalityMax,
|
|
1157
|
+
category: DiagnosticCategory.Contract,
|
|
1158
|
+
component: context,
|
|
1159
|
+
message: `${context}: "${ruleName}" allows at most ${max}.`
|
|
1160
|
+
};
|
|
1161
|
+
},
|
|
1162
|
+
positionViolation(ruleName, position, index, context) {
|
|
1163
|
+
return {
|
|
1164
|
+
code: DiagnosticCode.PositionViolation,
|
|
1165
|
+
category: DiagnosticCategory.Contract,
|
|
1166
|
+
component: context,
|
|
1167
|
+
message: `${context}: "${ruleName}" must be ${position}, got index ${index}`
|
|
1168
|
+
};
|
|
1169
|
+
},
|
|
1170
|
+
unknownVariantDim(component, label, dim) {
|
|
1171
|
+
return {
|
|
1172
|
+
code: DiagnosticCode.ContractUnknownVariantDim,
|
|
1173
|
+
category: DiagnosticCategory.Contract,
|
|
1174
|
+
message: `${component}: ${label} references unknown variant "${dim}".`
|
|
1175
|
+
};
|
|
1176
|
+
},
|
|
1177
|
+
unknownVariantValue(component, label, dim, value, valid) {
|
|
1178
|
+
return {
|
|
1179
|
+
code: DiagnosticCode.ContractUnknownVariantValue,
|
|
1180
|
+
category: DiagnosticCategory.Contract,
|
|
1181
|
+
message: `${component}: ${label} sets "${dim}" to unknown value "${value}" (valid: ${valid.join(", ")}).`
|
|
1182
|
+
};
|
|
1183
|
+
},
|
|
1184
|
+
unknownRecipeKey(component, key) {
|
|
1185
|
+
return {
|
|
1186
|
+
code: DiagnosticCode.ContractUnknownRecipeKey,
|
|
1187
|
+
category: DiagnosticCategory.Contract,
|
|
1188
|
+
message: `${component}: unknown recipeKey "${key}" — no preset with that name exists.`
|
|
1189
|
+
};
|
|
1190
|
+
},
|
|
1191
|
+
invalidVariantValue(component, key, value) {
|
|
1192
|
+
return {
|
|
1193
|
+
code: DiagnosticCode.ContractInvalidVariantValue,
|
|
1194
|
+
category: DiagnosticCategory.Contract,
|
|
1195
|
+
message: `${component}: variant "${key}=${value}" is not a defined value for the "${key}" dimension.`
|
|
1196
|
+
};
|
|
1197
|
+
},
|
|
1198
|
+
allowedAsViolation(tag, allowedAs, component) {
|
|
1199
|
+
const allowed = allowedAs.map((t) => `"${String(t)}"`).join(", ");
|
|
1200
|
+
return {
|
|
1201
|
+
code: DiagnosticCode.AllowedAsViolation,
|
|
1202
|
+
category: DiagnosticCategory.Contract,
|
|
1203
|
+
component,
|
|
1204
|
+
message: `<${component}>: "as" prop received "${tag}" but only [${allowed}] are allowed.`
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
//#endregion
|
|
1209
|
+
//#region ../../lib/contract/src/diagnostics/html.ts
|
|
1210
|
+
const ATTRIBUTE_IGNORED_CODES = {
|
|
1211
|
+
checked: DiagnosticCode.HtmlInputCheckedIgnoredForType,
|
|
1212
|
+
multiple: DiagnosticCode.HtmlInputMultipleIgnoredForType,
|
|
1213
|
+
maxLength: DiagnosticCode.HtmlInputMaxLengthIgnoredForType,
|
|
1214
|
+
minLength: DiagnosticCode.HtmlInputMinLengthIgnoredForType,
|
|
1215
|
+
pattern: DiagnosticCode.HtmlInputPatternIgnoredForType,
|
|
1216
|
+
min: DiagnosticCode.HtmlInputMinIgnoredForType,
|
|
1217
|
+
max: DiagnosticCode.HtmlInputMaxIgnoredForType,
|
|
1218
|
+
step: DiagnosticCode.HtmlInputStepIgnoredForType,
|
|
1219
|
+
accept: DiagnosticCode.HtmlInputAcceptIgnoredForType,
|
|
1220
|
+
capture: DiagnosticCode.HtmlInputCaptureIgnoredForType,
|
|
1221
|
+
size: DiagnosticCode.HtmlInputSizeIgnoredForType,
|
|
1222
|
+
alt: DiagnosticCode.HtmlInputAltIgnoredForType,
|
|
1223
|
+
height: DiagnosticCode.HtmlInputHeightIgnoredForType,
|
|
1224
|
+
width: DiagnosticCode.HtmlInputWidthIgnoredForType
|
|
1225
|
+
};
|
|
1226
|
+
const HtmlDiagnostics = {
|
|
1227
|
+
emptyRole(tag) {
|
|
1228
|
+
return {
|
|
1229
|
+
code: DiagnosticCode.HtmlEmptyRole,
|
|
1230
|
+
category: DiagnosticCategory.HTML,
|
|
1231
|
+
severity: "warning",
|
|
1232
|
+
message: `<${tag}> has an explicit empty role="". Omit the attribute instead.`
|
|
1233
|
+
};
|
|
1234
|
+
},
|
|
1235
|
+
implicitRoleRedundant(tag, implicitRole) {
|
|
1236
|
+
return {
|
|
1237
|
+
code: DiagnosticCode.HtmlImplicitRoleRedundant,
|
|
1238
|
+
category: DiagnosticCategory.HTML,
|
|
1239
|
+
severity: "warning",
|
|
1240
|
+
message: `<${tag}> already has implicit role="${implicitRole}". Avoid redundant role assignment.`
|
|
1241
|
+
};
|
|
1242
|
+
},
|
|
1243
|
+
implicitRoleOverride(tag, implicitRole, role) {
|
|
1244
|
+
return {
|
|
1245
|
+
code: DiagnosticCode.HtmlImplicitRoleOverride,
|
|
1246
|
+
category: DiagnosticCategory.HTML,
|
|
1247
|
+
severity: "error",
|
|
1248
|
+
message: `<${tag}> should not override its implicit role="${implicitRole}" with role="${role}".`
|
|
1249
|
+
};
|
|
1250
|
+
},
|
|
1251
|
+
standaloneRegionOverride(tag, implicitRole) {
|
|
1252
|
+
return {
|
|
1253
|
+
code: DiagnosticCode.HtmlStandaloneRegionOverride,
|
|
1254
|
+
category: DiagnosticCategory.HTML,
|
|
1255
|
+
severity: "error",
|
|
1256
|
+
message: `<${tag}> is a self-contained element with implicit role="${implicitRole}". Assigning role="region" has been removed.`
|
|
1257
|
+
};
|
|
1258
|
+
},
|
|
1259
|
+
landmarkRoleOverride(tag, implicitRole, role) {
|
|
1260
|
+
return {
|
|
1261
|
+
code: DiagnosticCode.HtmlLandmarkRoleOverride,
|
|
1262
|
+
category: DiagnosticCategory.HTML,
|
|
1263
|
+
severity: "error",
|
|
1264
|
+
message: `<${tag}> has a fixed landmark role="${implicitRole}". role="${role}" overrides it and confuses assistive technology. The override has been removed.`
|
|
1265
|
+
};
|
|
1266
|
+
},
|
|
1267
|
+
invalidChild(child, parent, allowed) {
|
|
1268
|
+
return {
|
|
1269
|
+
code: DiagnosticCode.HtmlInvalidChild,
|
|
1270
|
+
category: DiagnosticCategory.HTML,
|
|
1271
|
+
severity: "error",
|
|
1272
|
+
message: `<${child}> is not a valid direct child of <${parent}>. Allowed: ${allowed}.`
|
|
1273
|
+
};
|
|
1274
|
+
},
|
|
1275
|
+
roleNotPermitted(tag, role, allowedRoles) {
|
|
1276
|
+
const allowed = allowedRoles.length > 0 ? allowedRoles.map((r) => `"${r}"`).join(", ") : "none — no explicit role is permitted on this element";
|
|
1277
|
+
return {
|
|
1278
|
+
code: DiagnosticCode.HtmlRoleNotPermitted,
|
|
1279
|
+
category: DiagnosticCategory.HTML,
|
|
1280
|
+
severity: "error",
|
|
1281
|
+
message: `role="${role}" is not permitted on <${tag}>. Allowed alternate role(s): ${allowed}.`,
|
|
1282
|
+
rationale: "The WAI-ARIA \"ARIA in HTML\" specification restricts which explicit roles a native element may take. A role outside that set is ignored or produces undefined behavior in assistive technology."
|
|
1283
|
+
};
|
|
1284
|
+
},
|
|
1285
|
+
input: {
|
|
1286
|
+
unsupportedType(type) {
|
|
1287
|
+
return {
|
|
1288
|
+
code: DiagnosticCode.HtmlInputUnsupportedType,
|
|
1289
|
+
category: DiagnosticCategory.HTML,
|
|
1290
|
+
severity: "warning",
|
|
1291
|
+
message: `type="${type}" is not a value defined by the HTML specification. Browsers silently fall back to type="text".`,
|
|
1292
|
+
rationale: "An unrecognized input type is not invalid markup — the spec requires the \"text\" fallback — but it usually means a typo, since the input keeps working while silently losing the intended type-specific behavior (validation, virtual keyboard, picker UI).",
|
|
1293
|
+
suggestions: [{
|
|
1294
|
+
title: "Check for a typo in the type value",
|
|
1295
|
+
description: `"${type}" does not match any HTML5 input type.`
|
|
1296
|
+
}]
|
|
1297
|
+
};
|
|
1298
|
+
},
|
|
1299
|
+
attributeIgnoredForType(attribute, type, allowedTypes) {
|
|
1300
|
+
const allowed = allowedTypes.map((t) => `"${t}"`).join(", ");
|
|
1301
|
+
return {
|
|
1302
|
+
code: ATTRIBUTE_IGNORED_CODES[attribute],
|
|
1303
|
+
category: DiagnosticCategory.HTML,
|
|
1304
|
+
severity: "warning",
|
|
1305
|
+
message: `"${attribute}" is ignored on <input type="${type}">.`,
|
|
1306
|
+
rationale: `"${attribute}" only has an effect when type is one of: ${allowed}. Browsers silently ignore it on other input types.`,
|
|
1307
|
+
suggestions: [{
|
|
1308
|
+
title: `Remove "${attribute}"`,
|
|
1309
|
+
description: `"${attribute}" only affects <input> when type is one of: ${allowed}.`
|
|
1310
|
+
}]
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
anchor: { dangerousHref(href) {
|
|
1315
|
+
return {
|
|
1316
|
+
code: DiagnosticCode.HtmlAnchorDangerousHref,
|
|
1317
|
+
category: DiagnosticCategory.HTML,
|
|
1318
|
+
severity: "warning",
|
|
1319
|
+
message: `href="${href}" uses a scheme that executes attacker-controlled content when navigated to. It has been removed.`,
|
|
1320
|
+
rationale: "\"javascript:\", \"data:\", and \"vbscript:\" URLs are a common XSS vector when href comes from untrusted input — none of the three are needed for legitimate navigation."
|
|
1321
|
+
};
|
|
1322
|
+
} }
|
|
1323
|
+
};
|
|
1324
|
+
//#endregion
|
|
1325
|
+
//#region ../../lib/contract/src/diagnostics/input-accessibility.ts
|
|
1326
|
+
function accessibilityFact(input) {
|
|
1327
|
+
return {
|
|
1328
|
+
category: DiagnosticCategory.Accessibility,
|
|
1329
|
+
...input
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const InputAccessibilityDiagnostics = {
|
|
1333
|
+
missingAccessibleName() {
|
|
1334
|
+
return accessibilityFact({
|
|
1335
|
+
code: DiagnosticCode.A11yInputMissingAccessibleName,
|
|
1336
|
+
severity: "warning",
|
|
1337
|
+
message: "This input has no accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
|
|
1338
|
+
rationale: "Assistive technology announces a form field by its accessible name; without one, users of screen readers cannot tell what the field is for.",
|
|
1339
|
+
suggestions: [{
|
|
1340
|
+
title: "Add aria-label",
|
|
1341
|
+
description: "Set aria-label=\"…\" directly on the input."
|
|
1342
|
+
}, {
|
|
1343
|
+
title: "Add an associated <label>",
|
|
1344
|
+
description: "Wrap the input in a <label>, or point a <label for=\"…\"> at its id."
|
|
1345
|
+
}]
|
|
1346
|
+
});
|
|
1347
|
+
},
|
|
1348
|
+
placeholderIsNotLabel() {
|
|
1349
|
+
return accessibilityFact({
|
|
1350
|
+
code: DiagnosticCode.A11yInputPlaceholderNotLabel,
|
|
1351
|
+
severity: "warning",
|
|
1352
|
+
message: "Placeholder text does not provide an accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
|
|
1353
|
+
rationale: "Placeholder text disappears as users interact with the field and is not treated as the control's accessible name by many assistive technologies.",
|
|
1354
|
+
suggestions: [{
|
|
1355
|
+
title: "Add aria-label",
|
|
1356
|
+
description: "Set aria-label=\"…\" directly on the input."
|
|
1357
|
+
}, {
|
|
1358
|
+
title: "Add an associated <label>",
|
|
1359
|
+
description: "Wrap the input in a <label>, or point a <label for=\"…\"> at its id."
|
|
1360
|
+
}]
|
|
1361
|
+
});
|
|
1362
|
+
},
|
|
1363
|
+
passwordMissingAutocomplete() {
|
|
1364
|
+
return accessibilityFact({
|
|
1365
|
+
code: DiagnosticCode.A11yInputPasswordAutocomplete,
|
|
1366
|
+
severity: "warning",
|
|
1367
|
+
message: "Password inputs should specify an autoComplete value.",
|
|
1368
|
+
rationale: "Without an explicit autocomplete hint, password managers and browsers cannot reliably tell a sign-in field apart from a password-creation field.",
|
|
1369
|
+
suggestions: [{
|
|
1370
|
+
title: "Set autoComplete=\"current-password\"",
|
|
1371
|
+
description: "Use this for sign-in forms."
|
|
1372
|
+
}, {
|
|
1373
|
+
title: "Set autoComplete=\"new-password\"",
|
|
1374
|
+
description: "Use this for sign-up / change-password forms."
|
|
1375
|
+
}]
|
|
1376
|
+
});
|
|
1377
|
+
},
|
|
1378
|
+
requiredReadOnlyConflict() {
|
|
1379
|
+
return accessibilityFact({
|
|
1380
|
+
code: DiagnosticCode.A11yInputRequiredReadOnlyConflict,
|
|
1381
|
+
severity: "warning",
|
|
1382
|
+
message: "\"required\" has no effect while this field is readOnly — the two attributes together usually signal an unintended state.",
|
|
1383
|
+
rationale: "The combination is valid HTML, but a readOnly control cannot be edited, so its \"required\" constraint can never be satisfied or violated through user interaction. Consider using disabled instead of readOnly, or only applying required when the field is editable."
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
//#endregion
|
|
1388
|
+
//#region ../../lib/contract/src/strict/invariant-base.ts
|
|
1389
|
+
var InvariantBase = class {
|
|
1390
|
+
diagnostics;
|
|
1391
|
+
constructor(diagnostics) {
|
|
1392
|
+
this.diagnostics = diagnostics;
|
|
1393
|
+
}
|
|
1394
|
+
get warnActive() {
|
|
1395
|
+
return this.diagnostics.warnActive;
|
|
1396
|
+
}
|
|
1397
|
+
violate(input) {
|
|
1398
|
+
this.diagnostics.error(input);
|
|
1399
|
+
}
|
|
1400
|
+
warn(input) {
|
|
1401
|
+
this.diagnostics.warn(input);
|
|
1402
|
+
}
|
|
1403
|
+
invariant(condition, input) {
|
|
1404
|
+
if (!condition) this.violate(input);
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
//#endregion
|
|
1408
|
+
//#region ../../lib/contract/src/aria/spec/roles/required-properties.ts
|
|
1409
|
+
const REQUIRED_ARIA_PROPERTIES = {
|
|
1410
|
+
combobox: ["aria-expanded"],
|
|
1411
|
+
option: ["aria-selected"],
|
|
1412
|
+
slider: ["aria-valuenow"],
|
|
1413
|
+
scrollbar: ["aria-controls", "aria-valuenow"],
|
|
1414
|
+
spinbutton: ["aria-valuenow"]
|
|
1415
|
+
};
|
|
1416
|
+
//#endregion
|
|
1417
|
+
//#region ../../lib/contract/src/aria/spec/roles/name-required.ts
|
|
1418
|
+
const NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
|
|
1419
|
+
//#endregion
|
|
1420
|
+
//#region ../../lib/contract/src/aria/spec/roles/name-prohibited.ts
|
|
1421
|
+
const NAME_PROHIBITED_ROLES = /* @__PURE__ */ new Set([
|
|
1422
|
+
"caption",
|
|
1423
|
+
"code",
|
|
1424
|
+
"deletion",
|
|
1425
|
+
"emphasis",
|
|
1426
|
+
"generic",
|
|
1427
|
+
"insertion",
|
|
1428
|
+
"none",
|
|
1429
|
+
"paragraph",
|
|
1430
|
+
"presentation",
|
|
1431
|
+
"strong",
|
|
1432
|
+
"subscript",
|
|
1433
|
+
"superscript"
|
|
1434
|
+
]);
|
|
1435
|
+
const NAME_PROHIBITED_ATTRIBUTES = ["aria-label", "aria-labelledby"];
|
|
1436
|
+
//#endregion
|
|
1437
|
+
//#region ../../lib/contract/src/aria/spec/validators/required-properties-validator.ts
|
|
1438
|
+
const NO_VIOLATIONS$1 = [{ valid: true }];
|
|
1439
|
+
function requiredAttributeByRole(roles, attribute) {
|
|
1440
|
+
return Object.fromEntries([...roles].map((role) => [role, [attribute]]));
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Reports missing attributes required by an element's effective ARIA role.
|
|
1444
|
+
*
|
|
1445
|
+
* Shared by validators that differ only in their role-to-attribute mapping and diagnostic
|
|
1446
|
+
* generation.
|
|
1447
|
+
*/
|
|
1448
|
+
function checkRequiredAttributes(requirement, { props, effectiveRole }) {
|
|
1449
|
+
if (!effectiveRole) return NO_VIOLATIONS$1;
|
|
1450
|
+
const requiredAttributes = requirement.attributesByRole[effectiveRole];
|
|
1451
|
+
if (!requiredAttributes) return NO_VIOLATIONS$1;
|
|
1452
|
+
const results = [];
|
|
1453
|
+
for (const attribute of requiredAttributes) {
|
|
1454
|
+
if (attribute in props) continue;
|
|
1455
|
+
results.push({
|
|
1456
|
+
valid: false,
|
|
1457
|
+
fixable: false,
|
|
1458
|
+
severity: "warning",
|
|
1459
|
+
attribute,
|
|
1460
|
+
diagnostic: requirement.diagnosticFor(attribute, effectiveRole)
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
return results;
|
|
1464
|
+
}
|
|
1465
|
+
//#endregion
|
|
1466
|
+
//#region ../../lib/contract/src/aria/spec/roles/live-region.ts
|
|
1467
|
+
const LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
|
|
1468
|
+
["alert", "assertive"],
|
|
1469
|
+
["status", "polite"],
|
|
1470
|
+
["log", "polite"],
|
|
1471
|
+
["timer", "off"]
|
|
1472
|
+
]);
|
|
1473
|
+
const ATOMIC_REQUIREMENTS = requiredAttributeByRole(LIVE_REGION_ROLES.keys(), "aria-atomic");
|
|
1474
|
+
//#endregion
|
|
1475
|
+
//#region ../../lib/contract/src/aria/spec/attributes/aria-value-types.ts
|
|
1476
|
+
const ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
|
|
1477
|
+
["aria-atomic", { kind: "boolean" }],
|
|
1478
|
+
["aria-busy", { kind: "boolean" }],
|
|
1479
|
+
["aria-disabled", { kind: "boolean" }],
|
|
1480
|
+
["aria-expanded", { kind: "boolean" }],
|
|
1481
|
+
["aria-hidden", { kind: "boolean" }],
|
|
1482
|
+
["aria-modal", { kind: "boolean" }],
|
|
1483
|
+
["aria-multiline", { kind: "boolean" }],
|
|
1484
|
+
["aria-multiselectable", { kind: "boolean" }],
|
|
1485
|
+
["aria-readonly", { kind: "boolean" }],
|
|
1486
|
+
["aria-required", { kind: "boolean" }],
|
|
1487
|
+
["aria-selected", { kind: "boolean" }],
|
|
1488
|
+
["aria-checked", { kind: "tristate" }],
|
|
1489
|
+
["aria-pressed", { kind: "tristate" }],
|
|
1490
|
+
["aria-valuenow", { kind: "number" }],
|
|
1491
|
+
["aria-valuemin", { kind: "number" }],
|
|
1492
|
+
["aria-valuemax", { kind: "number" }],
|
|
1493
|
+
["aria-level", {
|
|
1494
|
+
kind: "integer",
|
|
1495
|
+
min: 1
|
|
1496
|
+
}],
|
|
1497
|
+
["aria-posinset", {
|
|
1498
|
+
kind: "integer",
|
|
1499
|
+
min: 1
|
|
1500
|
+
}],
|
|
1501
|
+
["aria-setsize", {
|
|
1502
|
+
kind: "integer",
|
|
1503
|
+
min: -1
|
|
1504
|
+
}],
|
|
1505
|
+
["aria-rowcount", {
|
|
1506
|
+
kind: "integer",
|
|
1507
|
+
min: -1
|
|
1508
|
+
}],
|
|
1509
|
+
["aria-colcount", {
|
|
1510
|
+
kind: "integer",
|
|
1511
|
+
min: -1
|
|
1512
|
+
}],
|
|
1513
|
+
["aria-rowindex", {
|
|
1514
|
+
kind: "integer",
|
|
1515
|
+
min: 1
|
|
1516
|
+
}],
|
|
1517
|
+
["aria-colindex", {
|
|
1518
|
+
kind: "integer",
|
|
1519
|
+
min: 1
|
|
1520
|
+
}],
|
|
1521
|
+
["aria-rowspan", {
|
|
1522
|
+
kind: "integer",
|
|
1523
|
+
min: 0
|
|
1524
|
+
}],
|
|
1525
|
+
["aria-colspan", {
|
|
1526
|
+
kind: "integer",
|
|
1527
|
+
min: 0
|
|
1528
|
+
}],
|
|
1529
|
+
["aria-autocomplete", {
|
|
1530
|
+
kind: "enum",
|
|
1531
|
+
values: /* @__PURE__ */ new Set([
|
|
1532
|
+
"inline",
|
|
1533
|
+
"list",
|
|
1534
|
+
"both",
|
|
1535
|
+
"none"
|
|
1536
|
+
])
|
|
1537
|
+
}],
|
|
1538
|
+
["aria-current", {
|
|
1539
|
+
kind: "enum",
|
|
1540
|
+
values: /* @__PURE__ */ new Set([
|
|
1541
|
+
"page",
|
|
1542
|
+
"step",
|
|
1543
|
+
"location",
|
|
1544
|
+
"date",
|
|
1545
|
+
"time",
|
|
1546
|
+
"true",
|
|
1547
|
+
"false"
|
|
1548
|
+
])
|
|
1549
|
+
}],
|
|
1550
|
+
["aria-haspopup", {
|
|
1551
|
+
kind: "enum",
|
|
1552
|
+
values: /* @__PURE__ */ new Set([
|
|
1553
|
+
"false",
|
|
1554
|
+
"true",
|
|
1555
|
+
"menu",
|
|
1556
|
+
"listbox",
|
|
1557
|
+
"tree",
|
|
1558
|
+
"grid",
|
|
1559
|
+
"dialog"
|
|
1560
|
+
])
|
|
1561
|
+
}],
|
|
1562
|
+
["aria-invalid", {
|
|
1563
|
+
kind: "enum",
|
|
1564
|
+
values: /* @__PURE__ */ new Set([
|
|
1565
|
+
"grammar",
|
|
1566
|
+
"false",
|
|
1567
|
+
"spelling",
|
|
1568
|
+
"true"
|
|
1569
|
+
])
|
|
1570
|
+
}],
|
|
1571
|
+
["aria-live", {
|
|
1572
|
+
kind: "enum",
|
|
1573
|
+
values: /* @__PURE__ */ new Set([
|
|
1574
|
+
"assertive",
|
|
1575
|
+
"off",
|
|
1576
|
+
"polite"
|
|
1577
|
+
])
|
|
1578
|
+
}],
|
|
1579
|
+
["aria-orientation", {
|
|
1580
|
+
kind: "enum",
|
|
1581
|
+
values: /* @__PURE__ */ new Set([
|
|
1582
|
+
"horizontal",
|
|
1583
|
+
"vertical",
|
|
1584
|
+
"undefined"
|
|
1585
|
+
])
|
|
1586
|
+
}],
|
|
1587
|
+
["aria-sort", {
|
|
1588
|
+
kind: "enum",
|
|
1589
|
+
values: /* @__PURE__ */ new Set([
|
|
1590
|
+
"ascending",
|
|
1591
|
+
"descending",
|
|
1592
|
+
"none",
|
|
1593
|
+
"other"
|
|
1594
|
+
])
|
|
1595
|
+
}]
|
|
1596
|
+
]);
|
|
1597
|
+
//#endregion
|
|
1598
|
+
//#region ../../lib/contract/src/aria/spec/attributes/aria-relevant-tokens.ts
|
|
1599
|
+
const VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set([
|
|
1600
|
+
"additions",
|
|
1601
|
+
"removals",
|
|
1602
|
+
"text",
|
|
1603
|
+
"all"
|
|
1604
|
+
]);
|
|
1605
|
+
//#endregion
|
|
1606
|
+
//#region ../../lib/contract/src/aria/spec/elements/heading-implicit-levels.ts
|
|
1607
|
+
const HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
|
|
1608
|
+
["h1", 1],
|
|
1609
|
+
["h2", 2],
|
|
1610
|
+
["h3", 3],
|
|
1611
|
+
["h4", 4],
|
|
1612
|
+
["h5", 5],
|
|
1613
|
+
["h6", 6]
|
|
1614
|
+
]);
|
|
1615
|
+
//#endregion
|
|
1616
|
+
//#region ../../lib/contract/src/aria/spec/elements/focusable.ts
|
|
1617
|
+
function parseTabIndex(raw) {
|
|
1618
|
+
if (typeof raw === "number") return Number.isInteger(raw) ? raw : void 0;
|
|
1619
|
+
if (typeof raw !== "string") return void 0;
|
|
1620
|
+
const trimmed = raw.trim();
|
|
1621
|
+
if (trimmed === "") return void 0;
|
|
1622
|
+
const n = Number(trimmed);
|
|
1623
|
+
return Number.isInteger(n) ? n : void 0;
|
|
1624
|
+
}
|
|
1625
|
+
function isDisabled(props) {
|
|
1626
|
+
const d = props["disabled"];
|
|
1627
|
+
return d !== void 0 && d !== false && d !== null;
|
|
1628
|
+
}
|
|
1629
|
+
function isContentEditable(props) {
|
|
1630
|
+
const c = props["contenteditable"] ?? props["contentEditable"];
|
|
1631
|
+
return c === "" || c === true || c === "true" || c === "plaintext-only";
|
|
1632
|
+
}
|
|
1633
|
+
function hasHref(props) {
|
|
1634
|
+
return props["href"] !== void 0 && props["href"] !== null;
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Whether an element is reachable in the sequential keyboard tab order — a native control its
|
|
1638
|
+
* own attributes leave tabbable, `tabindex >= 0`, or `contenteditable`.
|
|
1639
|
+
*
|
|
1640
|
+
* This is **tabbability, not raw focusability**: `tabindex="-1"` is deliberately treated as *not*
|
|
1641
|
+
* qualifying, even though such an element can still receive programmatic focus. The one consumer
|
|
1642
|
+
* (`#checkAriaHiddenOnFocusable`) cares about content a keyboard user can land on, and `../pk`'s
|
|
1643
|
+
* behavior — `aria-hidden` on `<h2 tabindex="-1">` is not flagged — depends on this. If the
|
|
1644
|
+
* contract system ever needs to separate focusability from tabbability, split this into
|
|
1645
|
+
* `isPotentiallyFocusable` / `isPotentiallyTabbable`; until then one predicate with this policy
|
|
1646
|
+
* is enough.
|
|
1647
|
+
*
|
|
1648
|
+
* Absent `tabindex`, `contenteditable` makes anything tabbable; otherwise the answer is per
|
|
1649
|
+
* native tag, gated on that tag's own attributes.
|
|
1650
|
+
*/
|
|
1651
|
+
function isPotentiallyFocusable(tag, props) {
|
|
1652
|
+
const tabindex = parseTabIndex(props["tabindex"] ?? props["tabIndex"]);
|
|
1653
|
+
if (tabindex !== void 0) return tabindex >= 0;
|
|
1654
|
+
if (isContentEditable(props)) return true;
|
|
1655
|
+
switch (tag) {
|
|
1656
|
+
case "a":
|
|
1657
|
+
case "area": return hasHref(props);
|
|
1658
|
+
case "input": return props["type"] !== "hidden" && !isDisabled(props);
|
|
1659
|
+
case "button":
|
|
1660
|
+
case "select":
|
|
1661
|
+
case "textarea": return !isDisabled(props);
|
|
1662
|
+
default: return false;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
//#endregion
|
|
1666
|
+
//#region ../../lib/contract/src/aria/aria-policy-engine.ts
|
|
1667
|
+
const NO_VIOLATIONS = [{ valid: true }];
|
|
1668
|
+
const NO_VARIANT_KEYS = /* @__PURE__ */ new Set();
|
|
1669
|
+
function isIntrinsicTag(tag) {
|
|
1670
|
+
return isString(tag);
|
|
1671
|
+
}
|
|
1672
|
+
function omitProp(obj, key) {
|
|
1673
|
+
const { [key]: _, ...rest } = obj;
|
|
1674
|
+
return rest;
|
|
1675
|
+
}
|
|
1676
|
+
function strictNumeric(value) {
|
|
1677
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
1678
|
+
if (typeof value !== "string") return void 0;
|
|
1679
|
+
const trimmed = value.trim();
|
|
1680
|
+
if (trimmed === "") return void 0;
|
|
1681
|
+
const n = Number(trimmed);
|
|
1682
|
+
return Number.isFinite(n) ? n : void 0;
|
|
1683
|
+
}
|
|
1684
|
+
var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
|
|
1685
|
+
#extraRules;
|
|
1686
|
+
#variantKeys;
|
|
1687
|
+
#planCache = new LRUCache(100);
|
|
1688
|
+
static #removeAttributeFixCache = /* @__PURE__ */ new Map();
|
|
1689
|
+
constructor(diagnostics, options) {
|
|
1690
|
+
super(diagnostics);
|
|
1691
|
+
this.#extraRules = options?.rules ?? [];
|
|
1692
|
+
this.#variantKeys = options?.variantKeys ?? NO_VARIANT_KEYS;
|
|
1693
|
+
}
|
|
1694
|
+
static #normalizeEmptyRole(tag, props) {
|
|
1695
|
+
if (props.role !== "") return { normalized: false };
|
|
1696
|
+
const d = HtmlDiagnostics.emptyRole(tag);
|
|
1697
|
+
return {
|
|
1698
|
+
normalized: true,
|
|
1699
|
+
result: {
|
|
1700
|
+
props: omitProp(props, "role"),
|
|
1701
|
+
violations: [{
|
|
1702
|
+
message: d.message,
|
|
1703
|
+
diagnostic: d,
|
|
1704
|
+
tag,
|
|
1705
|
+
role: "",
|
|
1706
|
+
attribute: void 0,
|
|
1707
|
+
severity: d.severity,
|
|
1708
|
+
phase: "evaluate"
|
|
1709
|
+
}]
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
static #deriveContext(tag, props, variantKeys = NO_VARIANT_KEYS) {
|
|
1714
|
+
if (!isIntrinsicTag(tag)) return {
|
|
1715
|
+
proceed: false,
|
|
1716
|
+
result: {
|
|
1717
|
+
props,
|
|
1718
|
+
violations: []
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
const implicitRole = getImplicitRole(tag, props);
|
|
1722
|
+
const hasRole = isNonNull(implicitRole) || isString(props.role) && props.role.length > 0;
|
|
1723
|
+
const normalized = AriaPolicyEngine.#normalizeEmptyRole(tag, props);
|
|
1724
|
+
const workingProps = normalized.normalized ? normalized.result.props : props;
|
|
1725
|
+
const preExistingViolations = normalized.normalized ? normalized.result.violations : [];
|
|
1726
|
+
const effectiveRole = workingProps.role ?? implicitRole;
|
|
1727
|
+
return {
|
|
1728
|
+
proceed: true,
|
|
1729
|
+
tag,
|
|
1730
|
+
implicitRole,
|
|
1731
|
+
effectiveRole,
|
|
1732
|
+
hasRole,
|
|
1733
|
+
props: workingProps,
|
|
1734
|
+
preExistingViolations,
|
|
1735
|
+
context: {
|
|
1736
|
+
tag,
|
|
1737
|
+
props: workingProps,
|
|
1738
|
+
implicitRole,
|
|
1739
|
+
effectiveRole,
|
|
1740
|
+
variantKeys
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
static #runRules(rules, context) {
|
|
1745
|
+
const violations = [];
|
|
1746
|
+
const fixes = [];
|
|
1747
|
+
iterate.forEach(rules, (rule) => {
|
|
1748
|
+
if (isNonNull(rule.tags) && !rule.tags.includes(context.tag)) return;
|
|
1749
|
+
iterate.forEach(rule(context), (result) => {
|
|
1750
|
+
if (result.valid) return;
|
|
1751
|
+
const { tag, props: { role } } = context;
|
|
1752
|
+
const { message, attribute, severity } = result;
|
|
1753
|
+
const resolvedMessage = message ?? result.diagnostic?.message;
|
|
1754
|
+
const fallbackDiag = isNonNull(resolvedMessage) ? void 0 : AriaDiagnostics.invalidRole(role, tag);
|
|
1755
|
+
violations.push({
|
|
1756
|
+
message: resolvedMessage ?? fallbackDiag.message,
|
|
1757
|
+
tag,
|
|
1758
|
+
role,
|
|
1759
|
+
attribute,
|
|
1760
|
+
severity,
|
|
1761
|
+
phase: "evaluate",
|
|
1762
|
+
...isNonNull(result.diagnostic) && { diagnostic: result.diagnostic },
|
|
1763
|
+
...isNonNull(fallbackDiag) && { diagnostic: fallbackDiag }
|
|
1764
|
+
});
|
|
1765
|
+
if (result.fixable) fixes.push(result.fix);
|
|
1766
|
+
});
|
|
1767
|
+
});
|
|
1768
|
+
return {
|
|
1769
|
+
violations,
|
|
1770
|
+
fixes
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
static #getRules(context) {
|
|
1774
|
+
if (AriaPolicyEngine.#hasRole(context.props) || isNonNull(context.effectiveRole) && LIVE_REGION_ROLES.has(context.effectiveRole)) return AriaPolicyEngine.#pipeline;
|
|
1775
|
+
return AriaPolicyEngine.#implicitOnlyRules;
|
|
1776
|
+
}
|
|
1777
|
+
static evaluate(tag, props) {
|
|
1778
|
+
const derived = AriaPolicyEngine.#deriveContext(tag, props);
|
|
1779
|
+
if (!derived.proceed) return derived.result;
|
|
1780
|
+
if (!derived.hasRole) return {
|
|
1781
|
+
props: derived.props,
|
|
1782
|
+
violations: [...derived.preExistingViolations]
|
|
1783
|
+
};
|
|
1784
|
+
const { tag: narrowedTag, implicitRole, context, props: workingProps, preExistingViolations } = derived;
|
|
1785
|
+
const { violations, fixes } = AriaPolicyEngine.#runRules(AriaPolicyEngine.#getRules(context), context);
|
|
1786
|
+
return {
|
|
1787
|
+
props: AriaPolicyEngine.#applyFixes(narrowedTag, implicitRole, workingProps, fixes),
|
|
1788
|
+
violations: [...preExistingViolations, ...violations]
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
static #evaluateWithRules(tag, props, extraRules, extraProps, variantKeys = NO_VARIANT_KEYS) {
|
|
1792
|
+
const derived = AriaPolicyEngine.#deriveContext(tag, props, variantKeys);
|
|
1793
|
+
if (!derived.proceed) return derived.result;
|
|
1794
|
+
const { tag: narrowedTag, implicitRole, context, props: workingProps, preExistingViolations } = derived;
|
|
1795
|
+
const builtinRules = derived.hasRole ? AriaPolicyEngine.#getRules(context) : [];
|
|
1796
|
+
const builtin = AriaPolicyEngine.#runRules(builtinRules, context);
|
|
1797
|
+
const extraContext = extraProps === void 0 ? context : {
|
|
1798
|
+
...context,
|
|
1799
|
+
props: extraProps
|
|
1800
|
+
};
|
|
1801
|
+
const extra = AriaPolicyEngine.#runRules(extraRules, extraContext);
|
|
1802
|
+
const violations = [...builtin.violations, ...extra.violations];
|
|
1803
|
+
const fixes = [...builtin.fixes, ...extra.fixes];
|
|
1804
|
+
return {
|
|
1805
|
+
props: AriaPolicyEngine.#applyFixes(narrowedTag, implicitRole, workingProps, fixes, variantKeys),
|
|
1806
|
+
violations: [...preExistingViolations, ...violations]
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
report(violations) {
|
|
1810
|
+
iterate.forEach(violations, (v) => {
|
|
1811
|
+
const d = v.diagnostic ?? AriaDiagnostics.fromViolation(v);
|
|
1812
|
+
if (v.severity === "error") this.violate(d);
|
|
1813
|
+
else this.warn(d);
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
static #createPlanKey(tag, props) {
|
|
1817
|
+
if (!isIntrinsicTag(tag)) return null;
|
|
1818
|
+
const parts = [tag];
|
|
1819
|
+
if (typeof props.role === "string") parts.push(`role:${props.role}`);
|
|
1820
|
+
if (tag === "input" && typeof props.type === "string") parts.push(`type:${props.type}`);
|
|
1821
|
+
if (tag === "img") parts.push(`alt:${props.alt === "" ? "empty" : "present"}`);
|
|
1822
|
+
const ariaEntries = [];
|
|
1823
|
+
iterate.forEachEntry(props, (k, v) => {
|
|
1824
|
+
if (!k.startsWith("aria-")) return;
|
|
1825
|
+
if (!isString(v) && !isNumber(v) && typeof v !== "boolean") return;
|
|
1826
|
+
ariaEntries.push(`${k}:${String(v)}`);
|
|
1827
|
+
});
|
|
1828
|
+
if (ariaEntries.length > 0) parts.push(...ariaEntries.sort());
|
|
1829
|
+
return parts.join("|");
|
|
1830
|
+
}
|
|
1831
|
+
static #extraRulesKeySuffix(extraRules, props) {
|
|
1832
|
+
const parts = [];
|
|
1833
|
+
for (const rule of extraRules) {
|
|
1834
|
+
const readsProps = rule.readsProps;
|
|
1835
|
+
if (!isNonNull(readsProps)) return null;
|
|
1836
|
+
for (const propKey of readsProps) {
|
|
1837
|
+
const v = props[propKey];
|
|
1838
|
+
if (v !== void 0 && !isString(v) && !isNumber(v) && typeof v !== "boolean") return null;
|
|
1839
|
+
parts.push(`x:${propKey}:${String(v)}`);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
return parts.sort().join("|");
|
|
1843
|
+
}
|
|
1844
|
+
static #computePlan(inputProps, resultProps) {
|
|
1845
|
+
const removals = /* @__PURE__ */ new Set();
|
|
1846
|
+
const updates = {};
|
|
1847
|
+
iterate.forEachKey(inputProps, (key) => {
|
|
1848
|
+
if (!(key in resultProps)) removals.add(key);
|
|
1849
|
+
});
|
|
1850
|
+
iterate.forEachEntry(resultProps, (key, resultVal) => {
|
|
1851
|
+
if (inputProps[key] !== resultVal) updates[key] = resultVal;
|
|
1852
|
+
});
|
|
1853
|
+
return {
|
|
1854
|
+
removals,
|
|
1855
|
+
updates
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
static #applyPlan(props, removals, updates) {
|
|
1859
|
+
const hasRemovals = removals.size > 0;
|
|
1860
|
+
const hasUpdates = Object.keys(updates).length > 0;
|
|
1861
|
+
if (!hasRemovals && !hasUpdates) return props;
|
|
1862
|
+
const next = {};
|
|
1863
|
+
iterate.forEachEntry(props, (k, v) => {
|
|
1864
|
+
if (!removals.has(k)) next[k] = v;
|
|
1865
|
+
});
|
|
1866
|
+
Object.assign(next, updates);
|
|
1867
|
+
return next;
|
|
1868
|
+
}
|
|
1869
|
+
validate(tag, props, extraProps) {
|
|
1870
|
+
const ruleProps = extraProps ?? props;
|
|
1871
|
+
const baseKey = AriaPolicyEngine.#createPlanKey(tag, props);
|
|
1872
|
+
let key = baseKey;
|
|
1873
|
+
if (this.#extraRules.length > 0) {
|
|
1874
|
+
const suffix = AriaPolicyEngine.#extraRulesKeySuffix(this.#extraRules, ruleProps);
|
|
1875
|
+
key = isNonNull(baseKey) && isNonNull(suffix) ? `${baseKey}|${suffix}` : null;
|
|
1876
|
+
}
|
|
1877
|
+
if (!isNull(key)) {
|
|
1878
|
+
const cached = this.#planCache.get(key);
|
|
1879
|
+
if (cached !== void 0) {
|
|
1880
|
+
if (cached.violations.length > 0) this.report(cached.violations);
|
|
1881
|
+
return {
|
|
1882
|
+
props: AriaPolicyEngine.#applyPlan(props, cached.removals, cached.updates),
|
|
1883
|
+
violations: cached.violations
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
const result = this.#extraRules.length ? AriaPolicyEngine.#evaluateWithRules(tag, props, this.#extraRules, extraProps, this.#variantKeys) : AriaPolicyEngine.evaluate(tag, props);
|
|
1888
|
+
if (result.violations.length > 0) this.report(result.violations);
|
|
1889
|
+
if (!isNull(key)) {
|
|
1890
|
+
const { removals, updates } = AriaPolicyEngine.#computePlan(props, result.props);
|
|
1891
|
+
const plan = {
|
|
1892
|
+
removals,
|
|
1893
|
+
updates,
|
|
1894
|
+
violations: result.violations
|
|
1895
|
+
};
|
|
1896
|
+
this.#planCache.set(key, plan);
|
|
1897
|
+
}
|
|
1898
|
+
return result;
|
|
1899
|
+
}
|
|
1900
|
+
static #hasRole(props) {
|
|
1901
|
+
return isString(props.role) && props.role.length > 0;
|
|
1902
|
+
}
|
|
1903
|
+
static #applyFixes(tag, implicitRole, props, fixes, variantKeys = NO_VARIANT_KEYS) {
|
|
1904
|
+
if (fixes.length === 0) return props;
|
|
1905
|
+
const sorted = [...fixes].sort((a, b) => (a.priority ?? Infinity) - (b.priority ?? Infinity));
|
|
1906
|
+
let next = props;
|
|
1907
|
+
iterate.forEach(sorted, ({ apply }) => {
|
|
1908
|
+
const fixResult = apply({
|
|
1909
|
+
tag,
|
|
1910
|
+
implicitRole,
|
|
1911
|
+
effectiveRole: next.role ?? implicitRole,
|
|
1912
|
+
props: next,
|
|
1913
|
+
variantKeys
|
|
1914
|
+
});
|
|
1915
|
+
if (fixResult.applied) next = fixResult.next;
|
|
1916
|
+
});
|
|
1917
|
+
return next;
|
|
1918
|
+
}
|
|
1919
|
+
static #removeRole = {
|
|
1920
|
+
kind: "removeRole",
|
|
1921
|
+
apply: ({ props }) => {
|
|
1922
|
+
if (!("role" in props)) return {
|
|
1923
|
+
applied: false,
|
|
1924
|
+
next: props
|
|
1925
|
+
};
|
|
1926
|
+
return {
|
|
1927
|
+
applied: true,
|
|
1928
|
+
next: omitProp(props, "role"),
|
|
1929
|
+
previous: props
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
static #makeRemoveAttributeFix(attr) {
|
|
1934
|
+
const cached = AriaPolicyEngine.#removeAttributeFixCache.get(attr);
|
|
1935
|
+
if (cached) return cached;
|
|
1936
|
+
const fix = {
|
|
1937
|
+
kind: "removeAttribute",
|
|
1938
|
+
attribute: attr,
|
|
1939
|
+
apply: ({ props }) => {
|
|
1940
|
+
if (!(attr in props)) return {
|
|
1941
|
+
applied: false,
|
|
1942
|
+
next: props
|
|
1943
|
+
};
|
|
1944
|
+
return {
|
|
1945
|
+
applied: true,
|
|
1946
|
+
next: omitProp(props, attr),
|
|
1947
|
+
previous: props
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
AriaPolicyEngine.#removeAttributeFixCache.set(attr, fix);
|
|
1952
|
+
return fix;
|
|
1953
|
+
}
|
|
1954
|
+
static #pipeline = [
|
|
1955
|
+
AriaPolicyEngine.#checkInvalidRoleOverride,
|
|
1956
|
+
AriaPolicyEngine.#checkRedundantRole,
|
|
1957
|
+
AriaPolicyEngine.#checkStandaloneRegion,
|
|
1958
|
+
AriaPolicyEngine.#checkAriaAttributeValues,
|
|
1959
|
+
AriaPolicyEngine.#checkInvalidAriaAttributes,
|
|
1960
|
+
AriaPolicyEngine.#checkNameProhibitedRoles,
|
|
1961
|
+
AriaPolicyEngine.#checkRequiredAriaProperties,
|
|
1962
|
+
AriaPolicyEngine.#checkNameRequiredRoles,
|
|
1963
|
+
AriaPolicyEngine.#checkRedundantAriaLevel,
|
|
1964
|
+
AriaPolicyEngine.#checkMissingLiveRegion,
|
|
1965
|
+
AriaPolicyEngine.#checkMissingAtomic,
|
|
1966
|
+
AriaPolicyEngine.#checkInvalidAriaRelevant,
|
|
1967
|
+
AriaPolicyEngine.#checkAriaHiddenOnFocusable,
|
|
1968
|
+
AriaPolicyEngine.#checkPresentationalAriaAttributes
|
|
1969
|
+
];
|
|
1970
|
+
static #implicitOnlyRules = [
|
|
1971
|
+
AriaPolicyEngine.#checkAriaAttributeValues,
|
|
1972
|
+
AriaPolicyEngine.#checkInvalidAriaAttributes,
|
|
1973
|
+
AriaPolicyEngine.#checkNameProhibitedRoles,
|
|
1974
|
+
AriaPolicyEngine.#checkNameRequiredRoles,
|
|
1975
|
+
AriaPolicyEngine.#checkRedundantAriaLevel,
|
|
1976
|
+
AriaPolicyEngine.#checkAriaHiddenOnFocusable,
|
|
1977
|
+
AriaPolicyEngine.#checkPresentationalAriaAttributes
|
|
1978
|
+
];
|
|
1979
|
+
static #checkInvalidRoleOverride({ tag, props, implicitRole }) {
|
|
1980
|
+
const role = props.role;
|
|
1981
|
+
if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
|
|
1982
|
+
if (isStrongImplicitRole(tag) && role === "region") {
|
|
1983
|
+
const diagnostic = HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role);
|
|
1984
|
+
return [{
|
|
1985
|
+
valid: false,
|
|
1986
|
+
fixable: true,
|
|
1987
|
+
severity: diagnostic.severity,
|
|
1988
|
+
fix: AriaPolicyEngine.#removeRole,
|
|
1989
|
+
diagnostic
|
|
1990
|
+
}];
|
|
1991
|
+
}
|
|
1992
|
+
return NO_VIOLATIONS;
|
|
1993
|
+
}
|
|
1994
|
+
static #checkRedundantRole({ tag, props, implicitRole }) {
|
|
1995
|
+
const role = props.role;
|
|
1996
|
+
if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
|
|
1997
|
+
const diagnostic = HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole);
|
|
1998
|
+
return [{
|
|
1999
|
+
valid: false,
|
|
2000
|
+
fixable: true,
|
|
2001
|
+
severity: diagnostic.severity,
|
|
2002
|
+
fix: AriaPolicyEngine.#removeRole,
|
|
2003
|
+
diagnostic
|
|
2004
|
+
}];
|
|
2005
|
+
}
|
|
2006
|
+
static #checkStandaloneRegion({ tag, props, implicitRole }) {
|
|
2007
|
+
if (props.role !== "region") return NO_VIOLATIONS;
|
|
2008
|
+
if (!hasStandaloneRole(tag)) return NO_VIOLATIONS;
|
|
2009
|
+
const diagnostic = HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag);
|
|
2010
|
+
return [{
|
|
2011
|
+
valid: false,
|
|
2012
|
+
fixable: true,
|
|
2013
|
+
severity: diagnostic.severity,
|
|
2014
|
+
fix: AriaPolicyEngine.#removeRole,
|
|
2015
|
+
diagnostic
|
|
2016
|
+
}];
|
|
2017
|
+
}
|
|
2018
|
+
static #checkInvalidAriaAttributes({ tag, props, effectiveRole }) {
|
|
2019
|
+
if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
|
|
2020
|
+
const results = [];
|
|
2021
|
+
iterate.forEachEntry(props, (key) => {
|
|
2022
|
+
if (!key.startsWith("aria-")) return;
|
|
2023
|
+
if (isGlobalAriaAttribute(key)) return;
|
|
2024
|
+
if (isAriaAttributeValidForRole(key, effectiveRole)) return;
|
|
2025
|
+
results.push({
|
|
2026
|
+
valid: false,
|
|
2027
|
+
severity: "warning",
|
|
2028
|
+
fixable: true,
|
|
2029
|
+
attribute: key,
|
|
2030
|
+
diagnostic: AriaDiagnostics.attributeInvalid(key, effectiveRole ?? tag),
|
|
2031
|
+
fix: AriaPolicyEngine.#makeRemoveAttributeFix(key)
|
|
2032
|
+
});
|
|
2033
|
+
});
|
|
2034
|
+
return results;
|
|
2035
|
+
}
|
|
2036
|
+
static #isValidAriaValue(value, type) {
|
|
2037
|
+
switch (type.kind) {
|
|
2038
|
+
case "boolean": return value === "true" || value === "false" || value === true || value === false;
|
|
2039
|
+
case "tristate": return value === "true" || value === "false" || value === "mixed" || value === true || value === false;
|
|
2040
|
+
case "number": return strictNumeric(value) !== void 0;
|
|
2041
|
+
case "integer": {
|
|
2042
|
+
const n = strictNumeric(value);
|
|
2043
|
+
if (n === void 0 || !Number.isInteger(n)) return false;
|
|
2044
|
+
if (type.min !== void 0 && n < type.min) return false;
|
|
2045
|
+
if (type.max !== void 0 && n > type.max) return false;
|
|
2046
|
+
return true;
|
|
2047
|
+
}
|
|
2048
|
+
case "enum": return typeof value === "string" && type.values.has(value);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
static #describeExpected(type) {
|
|
2052
|
+
switch (type.kind) {
|
|
2053
|
+
case "boolean": return "\"true\" or \"false\"";
|
|
2054
|
+
case "tristate": return "\"true\", \"false\", or \"mixed\"";
|
|
2055
|
+
case "number": return "a finite number";
|
|
2056
|
+
case "integer": {
|
|
2057
|
+
const parts = ["an integer"];
|
|
2058
|
+
if (type.min !== void 0 && type.max !== void 0) parts.push(`between ${type.min} and ${type.max}`);
|
|
2059
|
+
else if (type.min !== void 0) parts.push(`≥ ${type.min}`);
|
|
2060
|
+
else if (type.max !== void 0) parts.push(`≤ ${type.max}`);
|
|
2061
|
+
return parts.join(" ");
|
|
2062
|
+
}
|
|
2063
|
+
case "enum": return [...type.values].map((v) => `"${v}"`).join(", ");
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
static #checkAriaAttributeValues({ props, effectiveRole }) {
|
|
2067
|
+
if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
|
|
2068
|
+
const results = [];
|
|
2069
|
+
iterate.forEachEntry(props, (key, value) => {
|
|
2070
|
+
if (!key.startsWith("aria-")) return;
|
|
2071
|
+
const type = ARIA_VALUE_TYPES.get(key);
|
|
2072
|
+
if (!isNonNull(type)) return;
|
|
2073
|
+
if (AriaPolicyEngine.#isValidAriaValue(value, type)) return;
|
|
2074
|
+
results.push({
|
|
2075
|
+
valid: false,
|
|
2076
|
+
fixable: true,
|
|
2077
|
+
severity: "warning",
|
|
2078
|
+
attribute: key,
|
|
2079
|
+
diagnostic: AriaDiagnostics.invalidAttributeValue(key, value, AriaPolicyEngine.#describeExpected(type)),
|
|
2080
|
+
fix: AriaPolicyEngine.#makeRemoveAttributeFix(key)
|
|
2081
|
+
});
|
|
2082
|
+
});
|
|
2083
|
+
return results;
|
|
2084
|
+
}
|
|
2085
|
+
static #checkRedundantAriaLevel({ tag, props, effectiveRole }) {
|
|
2086
|
+
if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
|
|
2087
|
+
const implicitLevel = HEADING_IMPLICIT_LEVELS.get(tag);
|
|
2088
|
+
if (!isNonNull(implicitLevel)) return NO_VIOLATIONS;
|
|
2089
|
+
const raw = props["aria-level"];
|
|
2090
|
+
if (!isNonNull(raw)) return NO_VIOLATIONS;
|
|
2091
|
+
const n = strictNumeric(raw);
|
|
2092
|
+
if (n === void 0 || n !== implicitLevel) return NO_VIOLATIONS;
|
|
2093
|
+
return [{
|
|
2094
|
+
valid: false,
|
|
2095
|
+
fixable: true,
|
|
2096
|
+
severity: "warning",
|
|
2097
|
+
attribute: "aria-level",
|
|
2098
|
+
diagnostic: AriaDiagnostics.redundantAriaLevel(tag, implicitLevel),
|
|
2099
|
+
fix: AriaPolicyEngine.#makeRemoveAttributeFix("aria-level")
|
|
2100
|
+
}];
|
|
2101
|
+
}
|
|
2102
|
+
static #checkNameProhibitedRoles({ props, effectiveRole }) {
|
|
2103
|
+
if (!effectiveRole || effectiveRole === "none" || effectiveRole === "presentation" || !NAME_PROHIBITED_ROLES.has(effectiveRole)) return NO_VIOLATIONS;
|
|
2104
|
+
const results = [];
|
|
2105
|
+
for (const key of NAME_PROHIBITED_ATTRIBUTES) {
|
|
2106
|
+
if (!(key in props)) continue;
|
|
2107
|
+
results.push({
|
|
2108
|
+
valid: false,
|
|
2109
|
+
fixable: true,
|
|
2110
|
+
severity: "warning",
|
|
2111
|
+
attribute: key,
|
|
2112
|
+
diagnostic: AriaDiagnostics.nameProhibited(key, effectiveRole),
|
|
2113
|
+
fix: AriaPolicyEngine.#makeRemoveAttributeFix(key)
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
return results;
|
|
2117
|
+
}
|
|
2118
|
+
static #checkNameRequiredRoles({ tag, props, effectiveRole }) {
|
|
2119
|
+
if (!effectiveRole || !NAME_REQUIRED_ROLES.has(effectiveRole)) return NO_VIOLATIONS;
|
|
2120
|
+
if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS;
|
|
2121
|
+
if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS;
|
|
2122
|
+
return [{
|
|
2123
|
+
valid: false,
|
|
2124
|
+
fixable: false,
|
|
2125
|
+
severity: "warning",
|
|
2126
|
+
diagnostic: AriaDiagnostics.missingAccessibleName(tag)
|
|
2127
|
+
}];
|
|
2128
|
+
}
|
|
2129
|
+
static #requiredAriaPropertiesRule = {
|
|
2130
|
+
attributesByRole: REQUIRED_ARIA_PROPERTIES,
|
|
2131
|
+
diagnosticFor: (attribute, role) => AriaDiagnostics.requiredProperty(attribute, role)
|
|
2132
|
+
};
|
|
2133
|
+
static #checkRequiredAriaProperties(context) {
|
|
2134
|
+
if (!isNonNull(context.props.role)) return NO_VIOLATIONS;
|
|
2135
|
+
return checkRequiredAttributes(AriaPolicyEngine.#requiredAriaPropertiesRule, context);
|
|
2136
|
+
}
|
|
2137
|
+
static #checkAriaHiddenOnFocusable({ tag, props }) {
|
|
2138
|
+
if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS;
|
|
2139
|
+
if (!isPotentiallyFocusable(tag, props)) return NO_VIOLATIONS;
|
|
2140
|
+
return [{
|
|
2141
|
+
valid: false,
|
|
2142
|
+
fixable: false,
|
|
2143
|
+
severity: "error",
|
|
2144
|
+
attribute: "aria-hidden",
|
|
2145
|
+
diagnostic: AriaDiagnostics.ariaHiddenOnFocusable(tag)
|
|
2146
|
+
}];
|
|
2147
|
+
}
|
|
2148
|
+
static #checkPresentationalAriaAttributes({ tag, props, effectiveRole }) {
|
|
2149
|
+
if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS;
|
|
2150
|
+
const results = [];
|
|
2151
|
+
iterate.forEachEntry(props, (key) => {
|
|
2152
|
+
if (!key.startsWith("aria-")) return;
|
|
2153
|
+
if (key === "aria-hidden") return;
|
|
2154
|
+
results.push({
|
|
2155
|
+
valid: false,
|
|
2156
|
+
fixable: true,
|
|
2157
|
+
severity: "warning",
|
|
2158
|
+
attribute: key,
|
|
2159
|
+
diagnostic: AriaDiagnostics.attributeOnPresentational(key, tag),
|
|
2160
|
+
fix: AriaPolicyEngine.#makeRemoveAttributeFix(key)
|
|
2161
|
+
});
|
|
2162
|
+
});
|
|
2163
|
+
return results;
|
|
2164
|
+
}
|
|
2165
|
+
static #checkMissingLiveRegion({ effectiveRole, props }) {
|
|
2166
|
+
if (!effectiveRole) return NO_VIOLATIONS;
|
|
2167
|
+
const impliedLive = LIVE_REGION_ROLES.get(effectiveRole);
|
|
2168
|
+
if (!impliedLive) return NO_VIOLATIONS;
|
|
2169
|
+
if ("aria-live" in props) return NO_VIOLATIONS;
|
|
2170
|
+
return [{
|
|
2171
|
+
valid: false,
|
|
2172
|
+
fixable: true,
|
|
2173
|
+
severity: "warning",
|
|
2174
|
+
fix: {
|
|
2175
|
+
kind: "injectLive",
|
|
2176
|
+
attribute: "aria-live",
|
|
2177
|
+
apply: (ctx) => ({
|
|
2178
|
+
applied: true,
|
|
2179
|
+
next: {
|
|
2180
|
+
...ctx.props,
|
|
2181
|
+
"aria-live": impliedLive
|
|
2182
|
+
},
|
|
2183
|
+
previous: ctx.props
|
|
2184
|
+
})
|
|
2185
|
+
},
|
|
2186
|
+
diagnostic: AriaDiagnostics.missingLiveRegion(effectiveRole, impliedLive)
|
|
2187
|
+
}];
|
|
2188
|
+
}
|
|
2189
|
+
static #missingAtomicRule = {
|
|
2190
|
+
attributesByRole: ATOMIC_REQUIREMENTS,
|
|
2191
|
+
diagnosticFor: (_attribute, role) => AriaDiagnostics.missingAtomic(role)
|
|
2192
|
+
};
|
|
2193
|
+
static #checkMissingAtomic(context) {
|
|
2194
|
+
return checkRequiredAttributes(AriaPolicyEngine.#missingAtomicRule, context);
|
|
2195
|
+
}
|
|
2196
|
+
static #normalizeRelevantAllFix = {
|
|
2197
|
+
kind: "normalizeRelevantAll",
|
|
2198
|
+
apply: ({ props: p }) => ({
|
|
2199
|
+
applied: true,
|
|
2200
|
+
next: {
|
|
2201
|
+
...p,
|
|
2202
|
+
"aria-relevant": "all"
|
|
2203
|
+
},
|
|
2204
|
+
previous: p
|
|
2205
|
+
})
|
|
2206
|
+
};
|
|
2207
|
+
static #checkInvalidAriaRelevant({ props }) {
|
|
2208
|
+
const relevant = props["aria-relevant"];
|
|
2209
|
+
if (relevant === void 0) return NO_VIOLATIONS;
|
|
2210
|
+
if (typeof relevant !== "string") return NO_VIOLATIONS;
|
|
2211
|
+
const tokens = relevant.trim().split(/\s+/);
|
|
2212
|
+
const invalid = tokens.filter((t) => !VALID_RELEVANT_TOKENS.has(t));
|
|
2213
|
+
if (invalid.length > 0) return [{
|
|
2214
|
+
valid: false,
|
|
2215
|
+
fixable: true,
|
|
2216
|
+
severity: "warning",
|
|
2217
|
+
attribute: "aria-relevant",
|
|
2218
|
+
diagnostic: AriaDiagnostics.relevantInvalidTokens(invalid),
|
|
2219
|
+
fix: AriaPolicyEngine.#makeRemoveAttributeFix("aria-relevant")
|
|
2220
|
+
}];
|
|
2221
|
+
if (tokens.includes("all") && tokens.length > 1) return [{
|
|
2222
|
+
valid: false,
|
|
2223
|
+
fixable: true,
|
|
2224
|
+
severity: "warning",
|
|
2225
|
+
attribute: "aria-relevant",
|
|
2226
|
+
diagnostic: AriaDiagnostics.relevantSuperseded(),
|
|
2227
|
+
fix: AriaPolicyEngine.#normalizeRelevantAllFix
|
|
2228
|
+
}];
|
|
2229
|
+
return NO_VIOLATIONS;
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
//#endregion
|
|
2233
|
+
//#region ../../lib/contract/src/aria/factories.ts
|
|
2234
|
+
/**
|
|
2235
|
+
* Builds a correctly-literal-typed `fixable: false` `AriaResult`. Exists so a rule author can
|
|
2236
|
+
* extract shared branch logic (severity/attribute/message computed once, reused across multiple
|
|
2237
|
+
* `return`s) without TypeScript silently widening `valid: false`/`fixable: false` to `boolean`
|
|
2238
|
+
* the moment those values leave an object-literal-in-return-position — the widening only happens
|
|
2239
|
+
* on plain object literals; a function's declared return type narrows unconditionally.
|
|
2240
|
+
*/
|
|
2241
|
+
function invalidWithoutFix(input) {
|
|
2242
|
+
return {
|
|
2243
|
+
valid: false,
|
|
2244
|
+
fixable: false,
|
|
2245
|
+
severity: input.severity,
|
|
2246
|
+
...isDefined(input.attribute) && { attribute: input.attribute },
|
|
2247
|
+
...isDefined(input.message) && { message: input.message },
|
|
2248
|
+
...isDefined(input.diagnostic) && { diagnostic: input.diagnostic }
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
/** Same as {@link invalidWithoutFix}, for the `fixable: true` branch — requires a `fix`. */
|
|
2252
|
+
function invalidWithFix(input) {
|
|
2253
|
+
return {
|
|
2254
|
+
valid: false,
|
|
2255
|
+
fixable: true,
|
|
2256
|
+
severity: input.severity,
|
|
2257
|
+
...isDefined(input.attribute) && { attribute: input.attribute },
|
|
2258
|
+
...isDefined(input.message) && { message: input.message },
|
|
2259
|
+
...isDefined(input.diagnostic) && { diagnostic: input.diagnostic },
|
|
2260
|
+
fix: input.fix
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
function removeProp(props, key) {
|
|
2264
|
+
const next = { ...props };
|
|
2265
|
+
delete next[key];
|
|
2266
|
+
return next;
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Builds an `AriaFix` that strips a single attribute — the shape `dangerousHrefRule`-style
|
|
2270
|
+
* "strip this attribute when it's dangerous/redundant" rules need. A no-op (`applied: false`) when
|
|
2271
|
+
* the attribute isn't present, so applying the fix twice (or applying it when nothing triggered it)
|
|
2272
|
+
* is always safe. Frozen — a fix is a value object; nothing should mutate `kind`/`attribute`/`apply`
|
|
2273
|
+
* after construction.
|
|
2274
|
+
*/
|
|
2275
|
+
function removeAttributeFix(attribute) {
|
|
2276
|
+
return Object.freeze({
|
|
2277
|
+
kind: "removeAttribute",
|
|
2278
|
+
attribute,
|
|
2279
|
+
apply: ({ props }) => {
|
|
2280
|
+
if (!(attribute in props)) return {
|
|
2281
|
+
applied: false,
|
|
2282
|
+
next: props
|
|
2283
|
+
};
|
|
2284
|
+
return {
|
|
2285
|
+
applied: true,
|
|
2286
|
+
next: removeProp(props, attribute),
|
|
2287
|
+
previous: props
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
function defineRuleMetadata(rule, metadata) {
|
|
2293
|
+
const descriptors = {};
|
|
2294
|
+
for (const key of Object.keys(metadata)) descriptors[key] = {
|
|
2295
|
+
value: metadata[key],
|
|
2296
|
+
enumerable: false
|
|
2297
|
+
};
|
|
2298
|
+
Object.defineProperties(rule, descriptors);
|
|
2299
|
+
return rule;
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Convenience factory for the single most common `enforcement.aria`/`enforcement.rules` shape:
|
|
2303
|
+
* "strip this attribute when some condition on the element's own props holds" — covers
|
|
2304
|
+
* security-style guards (a dangerous URL scheme on `href`) and redundant-attribute rules alike,
|
|
2305
|
+
* without hand-writing the rule function, the `AriaFix`, and the `invalidWithFix` call each time.
|
|
2306
|
+
* A rule with no fix (a warn-only advisory) still needs the raw `AriaRule` shape directly — this
|
|
2307
|
+
* factory is deliberately scoped to the strip-on-match case, not a general rule builder.
|
|
2308
|
+
*/
|
|
2309
|
+
function createRemoveAttributeRule(attribute, options) {
|
|
2310
|
+
const { when, severity = "warning", message, diagnostic, readsProps, tags } = options;
|
|
2311
|
+
const fix = removeAttributeFix(attribute);
|
|
2312
|
+
const rule = (context) => {
|
|
2313
|
+
if (!when(context)) return [];
|
|
2314
|
+
return [invalidWithFix({
|
|
2315
|
+
severity,
|
|
2316
|
+
attribute,
|
|
2317
|
+
...isDefined(message) && { message },
|
|
2318
|
+
...isDefined(diagnostic) && { diagnostic: diagnostic(context) },
|
|
2319
|
+
fix
|
|
2320
|
+
})];
|
|
2321
|
+
};
|
|
2322
|
+
return defineRuleMetadata(rule, {
|
|
2323
|
+
...isDefined(readsProps) && { readsProps },
|
|
2324
|
+
...isDefined(tags) && { tags }
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
//#endregion
|
|
2328
|
+
//#region ../../lib/contract/src/children/get-type-name.ts
|
|
2329
|
+
function getTypeName(value) {
|
|
2330
|
+
if (value === null) return "null";
|
|
2331
|
+
if (value === void 0) return "undefined";
|
|
2332
|
+
const primitive = typeof value;
|
|
2333
|
+
if (primitive !== "object") return primitive;
|
|
2334
|
+
const name = value.constructor?.name;
|
|
2335
|
+
return isString(name) && name !== "Object" ? name : "object";
|
|
2336
|
+
}
|
|
2337
|
+
//#endregion
|
|
2338
|
+
//#region ../../lib/contract/src/children/normalize-child-rule.ts
|
|
2339
|
+
function normalizeCardinality(input, impliesSingleton) {
|
|
2340
|
+
const min = input?.min ?? 0;
|
|
2341
|
+
const max = input?.max ?? (impliesSingleton ? 1 : Infinity);
|
|
2342
|
+
if (min === 0 && max === Infinity) return { kind: "unbounded" };
|
|
2343
|
+
if (min > max) throw new RangeError(`normalizeChildRule: min (${min}) cannot exceed max (${max})`);
|
|
2344
|
+
return {
|
|
2345
|
+
kind: "bounded",
|
|
2346
|
+
min,
|
|
2347
|
+
max
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
function normalizeChildRule(rule) {
|
|
2351
|
+
const position = rule.position ?? "any";
|
|
2352
|
+
const impliesSingleton = position === "first" || position === "last";
|
|
2353
|
+
const cardinality = normalizeCardinality(rule.cardinality, impliesSingleton);
|
|
2354
|
+
if (impliesSingleton && (cardinality.kind === "unbounded" || cardinality.max > 1)) throw new RangeError(`normalizeChildRule: rule "${rule.name}" sets position="${position}" with an unbounded or >1 max — position="first"|"last" implies max=1.`);
|
|
2355
|
+
return {
|
|
2356
|
+
...rule,
|
|
2357
|
+
position,
|
|
2358
|
+
cardinality
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
//#endregion
|
|
2362
|
+
//#region ../../lib/contract/src/children/rules-matcher.ts
|
|
2363
|
+
/** Reads child.type without assuming a framework — works for React elements and Vue vnodes. */
|
|
2364
|
+
function getChildType(child) {
|
|
2365
|
+
if (!isObject(child) || !("type" in child)) return void 0;
|
|
2366
|
+
return child.type;
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Separates rules into an O(1) type-dispatch index and a linear-scan remainder.
|
|
2370
|
+
*
|
|
2371
|
+
* Rules without a type field go straight to untypedIndices.
|
|
2372
|
+
* Rules whose type is shared with another rule are removed from the index and
|
|
2373
|
+
* demoted to untypedIndices — the linear path handles ambiguous multi-matches
|
|
2374
|
+
* correctly, the index cannot.
|
|
2375
|
+
*
|
|
2376
|
+
* INVARIANT: a rule with a unique `type` matches on `child.type === rule.type` alone — its
|
|
2377
|
+
* `match` predicate is **not** called on the fast path. A rule whose `match` needs to narrow
|
|
2378
|
+
* further than its `type` (e.g. also on `child.props`) must therefore either omit `type` (linear
|
|
2379
|
+
* path) or share it with another rule (demoted). See `ChildRuleInput`.
|
|
2380
|
+
*/
|
|
2381
|
+
function buildPartialIndex(rules) {
|
|
2382
|
+
const typeIndex = /* @__PURE__ */ new Map();
|
|
2383
|
+
const duplicateTypes = /* @__PURE__ */ new Set();
|
|
2384
|
+
const untypedIndices = [];
|
|
2385
|
+
iterate.forEach(rules, (rule, ri) => {
|
|
2386
|
+
const t = rule.type;
|
|
2387
|
+
if (t === void 0) untypedIndices.push(ri);
|
|
2388
|
+
else if (typeIndex.has(t)) duplicateTypes.add(t);
|
|
2389
|
+
else typeIndex.set(t, ri);
|
|
2390
|
+
});
|
|
2391
|
+
if (duplicateTypes.size > 0) {
|
|
2392
|
+
iterate.forEachSet(duplicateTypes, (t) => {
|
|
2393
|
+
typeIndex.delete(t);
|
|
2394
|
+
});
|
|
2395
|
+
iterate.forEach(rules, (rule, ri) => {
|
|
2396
|
+
if (duplicateTypes.has(rule.type)) untypedIndices.push(ri);
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
return {
|
|
2400
|
+
typeIndex,
|
|
2401
|
+
untypedIndices
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
var RuleMatcher = class {
|
|
2405
|
+
#rules;
|
|
2406
|
+
#typeIndex;
|
|
2407
|
+
#untypedIndices;
|
|
2408
|
+
constructor(rules) {
|
|
2409
|
+
this.#rules = rules;
|
|
2410
|
+
const { typeIndex, untypedIndices } = buildPartialIndex(rules);
|
|
2411
|
+
this.#typeIndex = typeIndex;
|
|
2412
|
+
this.#untypedIndices = untypedIndices;
|
|
2413
|
+
}
|
|
2414
|
+
match(children) {
|
|
2415
|
+
const forward = /* @__PURE__ */ new Map();
|
|
2416
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
2417
|
+
const unexpectedIndices = /* @__PURE__ */ new Set();
|
|
2418
|
+
const ambiguousIndices = /* @__PURE__ */ new Set();
|
|
2419
|
+
iterate.forEach(this.#rules, (_, ri) => {
|
|
2420
|
+
reverse.set(ri, /* @__PURE__ */ new Set());
|
|
2421
|
+
});
|
|
2422
|
+
iterate.forEach(children, (child, ci) => {
|
|
2423
|
+
const t = getChildType(child);
|
|
2424
|
+
if (t !== void 0) {
|
|
2425
|
+
const ri = this.#typeIndex.get(t);
|
|
2426
|
+
if (ri !== void 0) {
|
|
2427
|
+
let childEntry = forward.get(ci);
|
|
2428
|
+
if (!childEntry) {
|
|
2429
|
+
childEntry = /* @__PURE__ */ new Set();
|
|
2430
|
+
forward.set(ci, childEntry);
|
|
2431
|
+
}
|
|
2432
|
+
childEntry.add(ri);
|
|
2433
|
+
reverse.get(ri).add(ci);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
iterate.forEach(this.#untypedIndices, (ri) => {
|
|
2437
|
+
if (!this.#rules[ri].match(child)) return;
|
|
2438
|
+
let childEntry = forward.get(ci);
|
|
2439
|
+
if (!childEntry) {
|
|
2440
|
+
childEntry = /* @__PURE__ */ new Set();
|
|
2441
|
+
forward.set(ci, childEntry);
|
|
2442
|
+
}
|
|
2443
|
+
childEntry.add(ri);
|
|
2444
|
+
reverse.get(ri).add(ci);
|
|
2445
|
+
});
|
|
2446
|
+
const entry = forward.get(ci);
|
|
2447
|
+
if (!entry) unexpectedIndices.add(ci);
|
|
2448
|
+
else if (entry.size > 1) ambiguousIndices.add(ci);
|
|
2449
|
+
});
|
|
2450
|
+
return {
|
|
2451
|
+
matrix: { childToRules: {
|
|
2452
|
+
forward,
|
|
2453
|
+
reverse
|
|
2454
|
+
} },
|
|
2455
|
+
unexpectedIndices,
|
|
2456
|
+
ambiguousIndices
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
};
|
|
2460
|
+
//#endregion
|
|
2461
|
+
//#region ../../lib/contract/src/children/rule-validator.ts
|
|
2462
|
+
var RuleValidator = class RuleValidator extends InvariantBase {
|
|
2463
|
+
#context;
|
|
2464
|
+
constructor(context, diagnostics) {
|
|
2465
|
+
super(diagnostics);
|
|
2466
|
+
this.#context = context;
|
|
2467
|
+
}
|
|
2468
|
+
validate(rules, matrix, childCount) {
|
|
2469
|
+
const firstIndex = 0;
|
|
2470
|
+
const lastIndex = childCount - 1;
|
|
2471
|
+
iterate.forEach(rules, (rule, ri) => {
|
|
2472
|
+
const matches = matrix.childToRules.reverse.get(ri);
|
|
2473
|
+
const matchCount = matches.size;
|
|
2474
|
+
this.#validateCardinality(rule, matchCount);
|
|
2475
|
+
if (matchCount === 0) return;
|
|
2476
|
+
this.#validatePositions(rule, matches, firstIndex, lastIndex);
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
#validateCardinality(rule, matchCount) {
|
|
2480
|
+
const { cardinality, name } = rule;
|
|
2481
|
+
if (cardinality.kind !== "bounded") return;
|
|
2482
|
+
const { min, max } = cardinality;
|
|
2483
|
+
if (matchCount < min) {
|
|
2484
|
+
this.violate(ContractDiagnostics.cardinalityMin(name, min, this.#context));
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
if (matchCount > max) this.violate(ContractDiagnostics.cardinalityMax(name, max, this.#context));
|
|
2488
|
+
}
|
|
2489
|
+
#validatePositions(rule, matches, firstIndex, lastIndex) {
|
|
2490
|
+
const { name, position } = rule;
|
|
2491
|
+
iterate.forEachSet(matches, (index) => {
|
|
2492
|
+
if (RuleValidator.#isValidPosition(index, position, firstIndex, lastIndex)) return;
|
|
2493
|
+
this.violate(ContractDiagnostics.positionViolation(name, position, index, this.#context));
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
static #isValidPosition(matchIndex, position, firstIndex, lastIndex) {
|
|
2497
|
+
switch (position) {
|
|
2498
|
+
case "first": return matchIndex === firstIndex;
|
|
2499
|
+
case "last": return matchIndex === lastIndex;
|
|
2500
|
+
case "any": return true;
|
|
2501
|
+
default: return assertNever(position);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
};
|
|
2505
|
+
//#endregion
|
|
2506
|
+
//#region ../../lib/contract/src/children/children-evaluator.ts
|
|
2507
|
+
const isTextLike = (child) => isString(child) || isNumber(child);
|
|
2508
|
+
var ChildrenEvaluator = class extends InvariantBase {
|
|
2509
|
+
#context;
|
|
2510
|
+
/** Rules with a static cardinality — normalized once, reused on every evaluate() call. */
|
|
2511
|
+
#staticRules;
|
|
2512
|
+
/** Rules whose cardinality is `dynamic(...)` — re-resolved against a ChildRuleContext per call. */
|
|
2513
|
+
#dynamicRuleInputs;
|
|
2514
|
+
#matcher;
|
|
2515
|
+
#ruleValidator;
|
|
2516
|
+
#exclusiveChildren;
|
|
2517
|
+
#allowText;
|
|
2518
|
+
constructor(rules, diagnostics, context = "Component", options = {}) {
|
|
2519
|
+
super(diagnostics);
|
|
2520
|
+
this.#context = context;
|
|
2521
|
+
this.#exclusiveChildren = options.exclusiveChildren ?? false;
|
|
2522
|
+
this.#allowText = options.allowText ?? true;
|
|
2523
|
+
const staticRuleInputs = [];
|
|
2524
|
+
const dynamicRuleInputs = [];
|
|
2525
|
+
iterate.forEach(rules, (rule) => {
|
|
2526
|
+
if (isDynamicRule(rule.cardinality)) dynamicRuleInputs.push(rule);
|
|
2527
|
+
else staticRuleInputs.push(rule);
|
|
2528
|
+
});
|
|
2529
|
+
this.#dynamicRuleInputs = dynamicRuleInputs;
|
|
2530
|
+
this.#staticRules = staticRuleInputs.map((r) => normalizeChildRule(r));
|
|
2531
|
+
this.#ruleValidator = new RuleValidator(context, diagnostics);
|
|
2532
|
+
this.#matcher = this.#dynamicRuleInputs.length === 0 ? new RuleMatcher(this.#staticRules) : void 0;
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* @param context Required when any rule has a `dynamic(...)` cardinality —
|
|
2536
|
+
* supplies the resolved tag/props those rules are evaluated against.
|
|
2537
|
+
*/
|
|
2538
|
+
evaluate(children, context) {
|
|
2539
|
+
if (!this.warnActive) return;
|
|
2540
|
+
const { rules, matcher } = this.#resolveRules(context);
|
|
2541
|
+
const { matrix, unexpectedIndices: rawUnexpectedIndices, ambiguousIndices } = matcher.match(children);
|
|
2542
|
+
this.#ruleValidator.validate(rules, matrix, children.length);
|
|
2543
|
+
const unexpectedIndices = new Set([...rawUnexpectedIndices].filter((ci) => isTextLike(children[ci]) ? this.#allowText === false : this.#exclusiveChildren));
|
|
2544
|
+
if (unexpectedIndices.size === 0 && ambiguousIndices.size === 0) return;
|
|
2545
|
+
const violating = [...unexpectedIndices, ...ambiguousIndices].sort((a, b) => a - b);
|
|
2546
|
+
iterate.forEach(violating, (ci) => {
|
|
2547
|
+
const typeName = getTypeName(children[ci]);
|
|
2548
|
+
if (unexpectedIndices.has(ci)) this.violate(ContractDiagnostics.unexpectedChild(typeName, ci, this.#context));
|
|
2549
|
+
else {
|
|
2550
|
+
const names = [...matrix.childToRules.forward.get(ci)].map((ri) => rules[ri]?.name ?? `#${ri}`);
|
|
2551
|
+
this.violate(ContractDiagnostics.ambiguousChild(typeName, ci, names, this.#context));
|
|
2552
|
+
}
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
#resolveRules(context) {
|
|
2556
|
+
if (this.#dynamicRuleInputs.length === 0) return {
|
|
2557
|
+
rules: this.#staticRules,
|
|
2558
|
+
matcher: this.#matcher
|
|
2559
|
+
};
|
|
2560
|
+
if (context === void 0) throw new RangeError(`ChildrenEvaluator [${this.#context}]: rule(s) have a dynamic(...) cardinality — evaluate() requires a context argument to resolve them.`);
|
|
2561
|
+
const resolvedDynamicRules = this.#dynamicRuleInputs.map((r) => {
|
|
2562
|
+
const cardinality = resolveRule(r.cardinality, context);
|
|
2563
|
+
return normalizeChildRule({
|
|
2564
|
+
...r,
|
|
2565
|
+
cardinality
|
|
2566
|
+
});
|
|
2567
|
+
});
|
|
2568
|
+
const rules = [...this.#staticRules, ...resolvedDynamicRules];
|
|
2569
|
+
return {
|
|
2570
|
+
rules,
|
|
2571
|
+
matcher: new RuleMatcher(rules)
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
//#endregion
|
|
2576
|
+
//#region ../../lib/contract/src/props/make-state-normalizer.ts
|
|
2577
|
+
/**
|
|
2578
|
+
* Builds one of the eight built-in state-prop normalizers. A truthy state injects the `aria-*` /
|
|
2579
|
+
* `data-*` pair; the false state is handled per `falseState`; an explicitly supplied `aria-*` /
|
|
2580
|
+
* `data-*` value is never overwritten (the normalizer only fills when the key is `undefined`).
|
|
2581
|
+
*/
|
|
2582
|
+
function makeStateNormalizer({ state, aria, data, falseState = "omit" }) {
|
|
2583
|
+
return (props) => {
|
|
2584
|
+
const value = props[state];
|
|
2585
|
+
if (falseState === "synthesize" ? isNullish(value) : !value) return {};
|
|
2586
|
+
const out = {};
|
|
2587
|
+
if (isUndefined(props[aria])) out[aria] = value ? "true" : "false";
|
|
2588
|
+
if (value && isUndefined(props[data])) out[data] = "";
|
|
2589
|
+
return out;
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
makeStateNormalizer({
|
|
2593
|
+
state: "active",
|
|
2594
|
+
aria: "aria-current",
|
|
2595
|
+
data: "data-active"
|
|
2596
|
+
});
|
|
2597
|
+
const disabledProps = makeStateNormalizer({
|
|
2598
|
+
state: "disabled",
|
|
2599
|
+
aria: "aria-disabled",
|
|
2600
|
+
data: "data-disabled"
|
|
2601
|
+
});
|
|
2602
|
+
makeStateNormalizer({
|
|
2603
|
+
state: "expanded",
|
|
2604
|
+
aria: "aria-expanded",
|
|
2605
|
+
data: "data-expanded",
|
|
2606
|
+
falseState: "synthesize"
|
|
2607
|
+
});
|
|
2608
|
+
const invalidProps = makeStateNormalizer({
|
|
2609
|
+
state: "invalid",
|
|
2610
|
+
aria: "aria-invalid",
|
|
2611
|
+
data: "data-invalid"
|
|
2612
|
+
});
|
|
2613
|
+
makeStateNormalizer({
|
|
2614
|
+
state: "loading",
|
|
2615
|
+
aria: "aria-busy",
|
|
2616
|
+
data: "data-loading"
|
|
2617
|
+
});
|
|
2618
|
+
makeStateNormalizer({
|
|
2619
|
+
state: "pressed",
|
|
2620
|
+
aria: "aria-pressed",
|
|
2621
|
+
data: "data-pressed",
|
|
2622
|
+
falseState: "synthesize"
|
|
2623
|
+
});
|
|
2624
|
+
const readonlyProps = makeStateNormalizer({
|
|
2625
|
+
state: "readOnly",
|
|
2626
|
+
aria: "aria-readonly",
|
|
2627
|
+
data: "data-readonly"
|
|
2628
|
+
});
|
|
2629
|
+
makeStateNormalizer({
|
|
2630
|
+
state: "selected",
|
|
2631
|
+
aria: "aria-selected",
|
|
2632
|
+
data: "data-selected",
|
|
2633
|
+
falseState: "synthesize"
|
|
2634
|
+
});
|
|
2635
|
+
//#endregion
|
|
2636
|
+
//#region ../core/src/html/contracts/categories.ts
|
|
2637
|
+
/**
|
|
2638
|
+
* HTML content-model tag groups shared across multiple contracts.
|
|
2639
|
+
*
|
|
2640
|
+
* Shared here means referenced by more than one contract, or by the top-level
|
|
2641
|
+
* tag → contract map in `index.ts`. Contract-specific tag lists that only one
|
|
2642
|
+
* module needs (e.g. `pContract`'s block-level blocklist) stay local to that module.
|
|
2643
|
+
*/
|
|
2644
|
+
/**
|
|
2645
|
+
* HTML elements classified as metadata content.
|
|
2646
|
+
*
|
|
2647
|
+
* Metadata content (`<script>` and `<template>`) is permitted in almost every
|
|
2648
|
+
* HTML content model and is therefore accepted by many contracts.
|
|
2649
|
+
*/
|
|
2650
|
+
const METADATA_TAGS = ["script", "template"];
|
|
2651
|
+
/**
|
|
2652
|
+
* Elements whose content model is character data only.
|
|
2653
|
+
*
|
|
2654
|
+
* Includes raw text elements (`<script>`, `<style>`), escapable raw text
|
|
2655
|
+
* elements (`<textarea>`, `<title>`), and `<option>`.
|
|
2656
|
+
*/
|
|
2657
|
+
const TEXT_ONLY_TAGS = [
|
|
2658
|
+
"option",
|
|
2659
|
+
"script",
|
|
2660
|
+
"style",
|
|
2661
|
+
"textarea",
|
|
2662
|
+
"title"
|
|
2663
|
+
];
|
|
2664
|
+
/**
|
|
2665
|
+
* HTML elements with implicit landmark roles.
|
|
2666
|
+
*
|
|
2667
|
+
* `<section>` and `<form>` are intentionally excluded because their landmark
|
|
2668
|
+
* semantics depend on having an accessible name.
|
|
2669
|
+
*/
|
|
2670
|
+
const LANDMARK_TAGS = [
|
|
2671
|
+
"article",
|
|
2672
|
+
"aside",
|
|
2673
|
+
"footer",
|
|
2674
|
+
"header",
|
|
2675
|
+
"main",
|
|
2676
|
+
"nav"
|
|
2677
|
+
];
|
|
2678
|
+
/**
|
|
2679
|
+
* Interactive content as defined by the HTML Living Standard.
|
|
2680
|
+
*
|
|
2681
|
+
* Used to enforce restrictions such as "must not contain interactive content"
|
|
2682
|
+
* for elements like `<button>` and `<a>`.
|
|
2683
|
+
*
|
|
2684
|
+
* Only direct children are checked; descendant traversal is outside the scope
|
|
2685
|
+
* of the contract engine.
|
|
2686
|
+
*/
|
|
2687
|
+
const INTERACTIVE_CONTENT_TAGS = [
|
|
2688
|
+
"a",
|
|
2689
|
+
"button",
|
|
2690
|
+
"input",
|
|
2691
|
+
"select",
|
|
2692
|
+
"textarea",
|
|
2693
|
+
"label"
|
|
2694
|
+
];
|
|
2695
|
+
/**
|
|
2696
|
+
* HTML labelable form controls.
|
|
2697
|
+
*
|
|
2698
|
+
* Used by `<label>` to validate implicit and explicit control associations.
|
|
2699
|
+
* `input[type="hidden"]` is excluded separately because labelability depends
|
|
2700
|
+
* on element attributes rather than tag name alone.
|
|
2701
|
+
*/
|
|
2702
|
+
const LABELABLE_TAGS = [
|
|
2703
|
+
"button",
|
|
2704
|
+
"input",
|
|
2705
|
+
"meter",
|
|
2706
|
+
"output",
|
|
2707
|
+
"progress",
|
|
2708
|
+
"select",
|
|
2709
|
+
"textarea"
|
|
2710
|
+
];
|
|
2711
|
+
/**
|
|
2712
|
+
* Interactive elements that are never labelable.
|
|
2713
|
+
*
|
|
2714
|
+
* Used by `labelContract` to enforce the HTML rule that a `<label>` may contain
|
|
2715
|
+
* no interactive content other than its labeled control.
|
|
2716
|
+
*
|
|
2717
|
+
* Conditionally interactive elements (for example, `audio[controls]`) are
|
|
2718
|
+
* excluded because their interactivity depends on attributes rather than tag
|
|
2719
|
+
* name alone.
|
|
2720
|
+
*/
|
|
2721
|
+
const OTHER_INTERACTIVE_TAGS = [
|
|
2722
|
+
"a",
|
|
2723
|
+
"details",
|
|
2724
|
+
"embed",
|
|
2725
|
+
"iframe"
|
|
2726
|
+
];
|
|
2727
|
+
/**
|
|
2728
|
+
* Elements that implicitly terminate a `<p>` element.
|
|
2729
|
+
*
|
|
2730
|
+
* This contract intentionally uses a blocklist rather than attempting to model
|
|
2731
|
+
* the full phrasing-content category. Missing an obscure block-level element is
|
|
2732
|
+
* preferable to rejecting valid inline content because the allowlist is
|
|
2733
|
+
* incomplete.
|
|
2734
|
+
*/
|
|
2735
|
+
const P_BLOCKED_TAGS = [
|
|
2736
|
+
"address",
|
|
2737
|
+
"article",
|
|
2738
|
+
"aside",
|
|
2739
|
+
"blockquote",
|
|
2740
|
+
"details",
|
|
2741
|
+
"dialog",
|
|
2742
|
+
"div",
|
|
2743
|
+
"dl",
|
|
2744
|
+
"fieldset",
|
|
2745
|
+
"figure",
|
|
2746
|
+
"footer",
|
|
2747
|
+
"form",
|
|
2748
|
+
"h1",
|
|
2749
|
+
"h2",
|
|
2750
|
+
"h3",
|
|
2751
|
+
"h4",
|
|
2752
|
+
"h5",
|
|
2753
|
+
"h6",
|
|
2754
|
+
"header",
|
|
2755
|
+
"hr",
|
|
2756
|
+
"main",
|
|
2757
|
+
"nav",
|
|
2758
|
+
"ol",
|
|
2759
|
+
"p",
|
|
2760
|
+
"pre",
|
|
2761
|
+
"section",
|
|
2762
|
+
"table",
|
|
2763
|
+
"ul"
|
|
2764
|
+
];
|
|
2765
|
+
//#endregion
|
|
2766
|
+
//#region ../core/src/html/anchor-rules.ts
|
|
2767
|
+
const DANGEROUS_URL_SCHEMES = [
|
|
2768
|
+
"javascript:",
|
|
2769
|
+
"data:",
|
|
2770
|
+
"vbscript:"
|
|
2771
|
+
];
|
|
2772
|
+
function normalizeForSchemeCheck(href) {
|
|
2773
|
+
return href.replace(/[\x00-\x20]/g, "").toLowerCase();
|
|
2774
|
+
}
|
|
2775
|
+
function isDangerousUrl(href) {
|
|
2776
|
+
if (!isString(href)) return false;
|
|
2777
|
+
const normalized = normalizeForSchemeCheck(href);
|
|
2778
|
+
return DANGEROUS_URL_SCHEMES.some((scheme) => normalized.startsWith(scheme));
|
|
2779
|
+
}
|
|
2780
|
+
const ANCHOR_RULES = [
|
|
2781
|
+
createRemoveAttributeRule("href", {
|
|
2782
|
+
when: ({ props }) => isDangerousUrl(props.href),
|
|
2783
|
+
severity: "warning",
|
|
2784
|
+
diagnostic: ({ props }) => HtmlDiagnostics.anchor.dangerousHref(props.href),
|
|
2785
|
+
readsProps: ["href"],
|
|
2786
|
+
tags: ["a"]
|
|
2787
|
+
}),
|
|
2788
|
+
Object.assign(({ props }) => {
|
|
2789
|
+
if (props.role !== "button" || !isString(props.href) || props.href.length === 0) return [];
|
|
2790
|
+
const diagnostic = AnchorAccessibilityDiagnostics.roleButtonWithHref();
|
|
2791
|
+
return [invalidWithoutFix({
|
|
2792
|
+
severity: diagnostic.severity,
|
|
2793
|
+
attribute: "role",
|
|
2794
|
+
diagnostic
|
|
2795
|
+
})];
|
|
2796
|
+
}, {
|
|
2797
|
+
readsProps: ["role", "href"],
|
|
2798
|
+
tags: ["a"]
|
|
2799
|
+
}),
|
|
2800
|
+
Object.assign(({ props }) => {
|
|
2801
|
+
const ariaDisabled = props["aria-disabled"];
|
|
2802
|
+
const isDisabled = ariaDisabled === true || ariaDisabled === "true";
|
|
2803
|
+
const hasHref = isString(props.href) && props.href.length > 0;
|
|
2804
|
+
if (!isDisabled || !hasHref) return [];
|
|
2805
|
+
const diagnostic = AnchorAccessibilityDiagnostics.ariaDisabledInert();
|
|
2806
|
+
return [invalidWithoutFix({
|
|
2807
|
+
severity: diagnostic.severity,
|
|
2808
|
+
attribute: "aria-disabled",
|
|
2809
|
+
diagnostic
|
|
2810
|
+
})];
|
|
2811
|
+
}, {
|
|
2812
|
+
readsProps: ["aria-disabled", "href"],
|
|
2813
|
+
tags: ["a"]
|
|
2814
|
+
})
|
|
2815
|
+
];
|
|
2816
|
+
//#endregion
|
|
2817
|
+
//#region ../core/src/html/spec/vocabulary/input.ts
|
|
2818
|
+
const TEXT_INPUT_TYPES = [
|
|
2819
|
+
"text",
|
|
2820
|
+
"search",
|
|
2821
|
+
"url",
|
|
2822
|
+
"tel",
|
|
2823
|
+
"email",
|
|
2824
|
+
"password"
|
|
2825
|
+
];
|
|
2826
|
+
const NUMERIC_INPUT_TYPES = [
|
|
2827
|
+
"number",
|
|
2828
|
+
"range",
|
|
2829
|
+
"date",
|
|
2830
|
+
"month",
|
|
2831
|
+
"week",
|
|
2832
|
+
"time",
|
|
2833
|
+
"datetime-local"
|
|
2834
|
+
];
|
|
2835
|
+
const HTML_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
2836
|
+
...TEXT_INPUT_TYPES,
|
|
2837
|
+
...NUMERIC_INPUT_TYPES,
|
|
2838
|
+
"checkbox",
|
|
2839
|
+
"radio",
|
|
2840
|
+
"file",
|
|
2841
|
+
"color",
|
|
2842
|
+
"hidden",
|
|
2843
|
+
"button",
|
|
2844
|
+
"submit",
|
|
2845
|
+
"reset",
|
|
2846
|
+
"image"
|
|
2847
|
+
]);
|
|
2848
|
+
//#endregion
|
|
2849
|
+
//#region ../core/src/html/spec/attributes/input.ts
|
|
2850
|
+
const INPUT_ATTRIBUTE_TYPE_POLICIES = [
|
|
2851
|
+
{
|
|
2852
|
+
attribute: "checked",
|
|
2853
|
+
allowedTypes: ["checkbox", "radio"]
|
|
2854
|
+
},
|
|
2855
|
+
{
|
|
2856
|
+
attribute: "multiple",
|
|
2857
|
+
allowedTypes: ["email", "file"]
|
|
2858
|
+
},
|
|
2859
|
+
{
|
|
2860
|
+
attribute: "maxLength",
|
|
2861
|
+
allowedTypes: TEXT_INPUT_TYPES
|
|
2862
|
+
},
|
|
2863
|
+
{
|
|
2864
|
+
attribute: "minLength",
|
|
2865
|
+
allowedTypes: TEXT_INPUT_TYPES
|
|
2866
|
+
},
|
|
2867
|
+
{
|
|
2868
|
+
attribute: "pattern",
|
|
2869
|
+
allowedTypes: TEXT_INPUT_TYPES
|
|
2870
|
+
},
|
|
2871
|
+
{
|
|
2872
|
+
attribute: "min",
|
|
2873
|
+
allowedTypes: NUMERIC_INPUT_TYPES
|
|
2874
|
+
},
|
|
2875
|
+
{
|
|
2876
|
+
attribute: "max",
|
|
2877
|
+
allowedTypes: NUMERIC_INPUT_TYPES
|
|
2878
|
+
},
|
|
2879
|
+
{
|
|
2880
|
+
attribute: "step",
|
|
2881
|
+
allowedTypes: NUMERIC_INPUT_TYPES
|
|
2882
|
+
},
|
|
2883
|
+
{
|
|
2884
|
+
attribute: "accept",
|
|
2885
|
+
allowedTypes: ["file"]
|
|
2886
|
+
},
|
|
2887
|
+
{
|
|
2888
|
+
attribute: "capture",
|
|
2889
|
+
allowedTypes: ["file"]
|
|
2890
|
+
},
|
|
2891
|
+
{
|
|
2892
|
+
attribute: "size",
|
|
2893
|
+
allowedTypes: TEXT_INPUT_TYPES
|
|
2894
|
+
},
|
|
2895
|
+
{
|
|
2896
|
+
attribute: "alt",
|
|
2897
|
+
allowedTypes: ["image"]
|
|
2898
|
+
},
|
|
2899
|
+
{
|
|
2900
|
+
attribute: "height",
|
|
2901
|
+
allowedTypes: ["image"]
|
|
2902
|
+
},
|
|
2903
|
+
{
|
|
2904
|
+
attribute: "width",
|
|
2905
|
+
allowedTypes: ["image"]
|
|
2906
|
+
}
|
|
2907
|
+
];
|
|
2908
|
+
//#endregion
|
|
2909
|
+
//#region ../core/src/html/spec/constraints/input.ts
|
|
2910
|
+
const REQUIRED_READONLY_CONFLICT = {
|
|
2911
|
+
props: ["required", "readOnly"],
|
|
2912
|
+
diagnostic: () => InputAccessibilityDiagnostics.requiredReadOnlyConflict()
|
|
2913
|
+
};
|
|
2914
|
+
const INPUT_MUTUALLY_EXCLUSIVE_POLICIES = [REQUIRED_READONLY_CONFLICT];
|
|
2915
|
+
//#endregion
|
|
2916
|
+
//#region ../core/src/html/spec/validators/attribute-type-validator.ts
|
|
2917
|
+
const DEFAULT_INPUT_TYPE = "text";
|
|
2918
|
+
function createInputAttributeTypeRule({ attribute, allowedTypes }) {
|
|
2919
|
+
const rule = ({ tag, props, variantKeys }) => {
|
|
2920
|
+
if (tag !== "input" || !(attribute in props)) return [];
|
|
2921
|
+
if (variantKeys?.has(attribute)) return [];
|
|
2922
|
+
const type = typeof props.type === "string" ? props.type : DEFAULT_INPUT_TYPE;
|
|
2923
|
+
if (allowedTypes.includes(type)) return [];
|
|
2924
|
+
const diagnostic = HtmlDiagnostics.input.attributeIgnoredForType(attribute, type, allowedTypes);
|
|
2925
|
+
return [{
|
|
2926
|
+
valid: false,
|
|
2927
|
+
fixable: true,
|
|
2928
|
+
severity: diagnostic.severity,
|
|
2929
|
+
fix: removeAttributeFix(attribute),
|
|
2930
|
+
diagnostic
|
|
2931
|
+
}];
|
|
2932
|
+
};
|
|
2933
|
+
return Object.assign(rule, {
|
|
2934
|
+
readsProps: ["type", attribute],
|
|
2935
|
+
tags: ["input"]
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
//#endregion
|
|
2939
|
+
//#region ../core/src/html/spec/validators/mutually-exclusive-validator.ts
|
|
2940
|
+
function isBooleanAttrSet(value) {
|
|
2941
|
+
return value !== void 0 && value !== null && value !== false;
|
|
2942
|
+
}
|
|
2943
|
+
function createMutuallyExclusiveRule({ props: conflictingProps, diagnostic: createDiagnostic }) {
|
|
2944
|
+
const [first, second] = conflictingProps;
|
|
2945
|
+
const rule = ({ tag, props }) => {
|
|
2946
|
+
if (tag !== "input" || !isBooleanAttrSet(props[first]) || !isBooleanAttrSet(props[second])) return [];
|
|
2947
|
+
const diagnostic = createDiagnostic();
|
|
2948
|
+
return [{
|
|
2949
|
+
valid: false,
|
|
2950
|
+
fixable: false,
|
|
2951
|
+
severity: diagnostic.severity,
|
|
2952
|
+
diagnostic
|
|
2953
|
+
}];
|
|
2954
|
+
};
|
|
2955
|
+
return Object.assign(rule, {
|
|
2956
|
+
readsProps: conflictingProps,
|
|
2957
|
+
tags: ["input"]
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
//#endregion
|
|
2961
|
+
//#region ../core/src/html/input-rules.ts
|
|
2962
|
+
const policyByAttribute = Object.fromEntries(INPUT_ATTRIBUTE_TYPE_POLICIES.map((policy) => [policy.attribute, policy]));
|
|
2963
|
+
function policyFor(attribute) {
|
|
2964
|
+
return policyByAttribute[attribute];
|
|
2965
|
+
}
|
|
2966
|
+
const supportedInputTypeRule = Object.assign(({ tag, props }) => {
|
|
2967
|
+
if (tag !== "input" || typeof props.type !== "string") return [];
|
|
2968
|
+
const type = props.type;
|
|
2969
|
+
if (HTML_INPUT_TYPES.has(type)) return [];
|
|
2970
|
+
const diagnostic = HtmlDiagnostics.input.unsupportedType(type);
|
|
2971
|
+
return [{
|
|
2972
|
+
valid: false,
|
|
2973
|
+
fixable: false,
|
|
2974
|
+
severity: diagnostic.severity,
|
|
2975
|
+
diagnostic
|
|
2976
|
+
}];
|
|
2977
|
+
}, {
|
|
2978
|
+
readsProps: ["type"],
|
|
2979
|
+
tags: ["input"]
|
|
2980
|
+
});
|
|
2981
|
+
const checkedRequiresCheckableTypeRule = createInputAttributeTypeRule(policyFor("checked"));
|
|
2982
|
+
const multipleRequiresSupportedTypeRule = createInputAttributeTypeRule(policyFor("multiple"));
|
|
2983
|
+
const maxLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("maxLength"));
|
|
2984
|
+
const minLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("minLength"));
|
|
2985
|
+
const patternRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("pattern"));
|
|
2986
|
+
const minRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("min"));
|
|
2987
|
+
const maxRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("max"));
|
|
2988
|
+
const stepRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("step"));
|
|
2989
|
+
const acceptRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("accept"));
|
|
2990
|
+
const captureRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("capture"));
|
|
2991
|
+
const sizeRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("size"));
|
|
2992
|
+
const altRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("alt"));
|
|
2993
|
+
const heightRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("height"));
|
|
2994
|
+
const widthRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("width"));
|
|
2995
|
+
const inputAccessibleNameRule = Object.assign(({ tag, props }) => {
|
|
2996
|
+
if (tag !== "input" || props.type === "hidden") return [];
|
|
2997
|
+
if ("aria-label" in props || "aria-labelledby" in props) return [];
|
|
2998
|
+
const diagnostic = typeof props.placeholder === "string" && props.placeholder.length > 0 ? InputAccessibilityDiagnostics.placeholderIsNotLabel() : InputAccessibilityDiagnostics.missingAccessibleName();
|
|
2999
|
+
return [{
|
|
3000
|
+
valid: false,
|
|
3001
|
+
fixable: false,
|
|
3002
|
+
severity: diagnostic.severity,
|
|
3003
|
+
diagnostic
|
|
3004
|
+
}];
|
|
3005
|
+
}, {
|
|
3006
|
+
readsProps: [
|
|
3007
|
+
"type",
|
|
3008
|
+
"aria-label",
|
|
3009
|
+
"aria-labelledby",
|
|
3010
|
+
"placeholder"
|
|
3011
|
+
],
|
|
3012
|
+
tags: ["input"]
|
|
3013
|
+
});
|
|
3014
|
+
const PASSWORD_AUTOCOMPLETE_VALUES = ["current-password", "new-password"];
|
|
3015
|
+
const INPUT_RULES = [
|
|
3016
|
+
supportedInputTypeRule,
|
|
3017
|
+
checkedRequiresCheckableTypeRule,
|
|
3018
|
+
multipleRequiresSupportedTypeRule,
|
|
3019
|
+
maxLengthRequiresTextTypeRule,
|
|
3020
|
+
minLengthRequiresTextTypeRule,
|
|
3021
|
+
patternRequiresTextTypeRule,
|
|
3022
|
+
minRequiresNumericTypeRule,
|
|
3023
|
+
maxRequiresNumericTypeRule,
|
|
3024
|
+
stepRequiresNumericTypeRule,
|
|
3025
|
+
acceptRequiresFileTypeRule,
|
|
3026
|
+
captureRequiresFileTypeRule,
|
|
3027
|
+
sizeRequiresTextTypeRule,
|
|
3028
|
+
altRequiresImageTypeRule,
|
|
3029
|
+
heightRequiresImageTypeRule,
|
|
3030
|
+
widthRequiresImageTypeRule,
|
|
3031
|
+
inputAccessibleNameRule,
|
|
3032
|
+
Object.assign(({ tag, props }) => {
|
|
3033
|
+
if (tag !== "input" || props.type !== "password") return [];
|
|
3034
|
+
const autoComplete = props.autoComplete;
|
|
3035
|
+
const tokens = typeof autoComplete === "string" ? autoComplete.split(" ") : [];
|
|
3036
|
+
if (PASSWORD_AUTOCOMPLETE_VALUES.some((value) => tokens.includes(value))) return [];
|
|
3037
|
+
const diagnostic = InputAccessibilityDiagnostics.passwordMissingAutocomplete();
|
|
3038
|
+
return [{
|
|
3039
|
+
valid: false,
|
|
3040
|
+
fixable: false,
|
|
3041
|
+
severity: diagnostic.severity,
|
|
3042
|
+
diagnostic
|
|
3043
|
+
}];
|
|
3044
|
+
}, {
|
|
3045
|
+
readsProps: ["type", "autoComplete"],
|
|
3046
|
+
tags: ["input"]
|
|
3047
|
+
}),
|
|
3048
|
+
createMutuallyExclusiveRule(REQUIRED_READONLY_CONFLICT)
|
|
3049
|
+
];
|
|
3050
|
+
//#endregion
|
|
3051
|
+
//#region ../core/src/html/spec/types.ts
|
|
3052
|
+
function resolveAllowedRoles(spec, props) {
|
|
3053
|
+
const policy = spec.allowedRoles;
|
|
3054
|
+
if (!policy) return void 0;
|
|
3055
|
+
switch (policy.kind) {
|
|
3056
|
+
case "fixed": return policy.roles;
|
|
3057
|
+
case "byProp": {
|
|
3058
|
+
const raw = props[policy.prop];
|
|
3059
|
+
const value = typeof raw === "string" && raw in policy.map ? raw : policy.fallback;
|
|
3060
|
+
return policy.map[value];
|
|
3061
|
+
}
|
|
3062
|
+
case "dynamic": return policy.resolve({ props });
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
//#endregion
|
|
3066
|
+
//#region ../core/src/html/spec/roles/input.ts
|
|
3067
|
+
const ALLOWED_INPUT_ROLES = {
|
|
3068
|
+
checkbox: [
|
|
3069
|
+
"menuitemcheckbox",
|
|
3070
|
+
"option",
|
|
3071
|
+
"switch",
|
|
3072
|
+
"button"
|
|
3073
|
+
],
|
|
3074
|
+
radio: ["menuitemradio"],
|
|
3075
|
+
range: [],
|
|
3076
|
+
number: [],
|
|
3077
|
+
search: [],
|
|
3078
|
+
text: [
|
|
3079
|
+
"combobox",
|
|
3080
|
+
"searchbox",
|
|
3081
|
+
"spinbutton"
|
|
3082
|
+
],
|
|
3083
|
+
email: [],
|
|
3084
|
+
tel: [],
|
|
3085
|
+
url: [],
|
|
3086
|
+
button: [
|
|
3087
|
+
"checkbox",
|
|
3088
|
+
"combobox",
|
|
3089
|
+
"gridcell",
|
|
3090
|
+
"link",
|
|
3091
|
+
"menuitem",
|
|
3092
|
+
"menuitemcheckbox",
|
|
3093
|
+
"menuitemradio",
|
|
3094
|
+
"option",
|
|
3095
|
+
"radio",
|
|
3096
|
+
"separator",
|
|
3097
|
+
"slider",
|
|
3098
|
+
"switch",
|
|
3099
|
+
"tab",
|
|
3100
|
+
"treeitem"
|
|
3101
|
+
],
|
|
3102
|
+
submit: [
|
|
3103
|
+
"checkbox",
|
|
3104
|
+
"combobox",
|
|
3105
|
+
"gridcell",
|
|
3106
|
+
"link",
|
|
3107
|
+
"menuitem",
|
|
3108
|
+
"menuitemcheckbox",
|
|
3109
|
+
"menuitemradio",
|
|
3110
|
+
"option",
|
|
3111
|
+
"radio",
|
|
3112
|
+
"separator",
|
|
3113
|
+
"slider",
|
|
3114
|
+
"switch",
|
|
3115
|
+
"tab",
|
|
3116
|
+
"treeitem"
|
|
3117
|
+
],
|
|
3118
|
+
reset: [
|
|
3119
|
+
"checkbox",
|
|
3120
|
+
"combobox",
|
|
3121
|
+
"gridcell",
|
|
3122
|
+
"link",
|
|
3123
|
+
"menuitem",
|
|
3124
|
+
"menuitemcheckbox",
|
|
3125
|
+
"menuitemradio",
|
|
3126
|
+
"option",
|
|
3127
|
+
"radio",
|
|
3128
|
+
"separator",
|
|
3129
|
+
"slider",
|
|
3130
|
+
"switch",
|
|
3131
|
+
"tab",
|
|
3132
|
+
"treeitem"
|
|
3133
|
+
],
|
|
3134
|
+
image: [
|
|
3135
|
+
"checkbox",
|
|
3136
|
+
"gridcell",
|
|
3137
|
+
"link",
|
|
3138
|
+
"menuitem",
|
|
3139
|
+
"menuitemcheckbox",
|
|
3140
|
+
"menuitemradio",
|
|
3141
|
+
"option",
|
|
3142
|
+
"radio",
|
|
3143
|
+
"separator",
|
|
3144
|
+
"slider",
|
|
3145
|
+
"switch",
|
|
3146
|
+
"tab",
|
|
3147
|
+
"treeitem"
|
|
3148
|
+
],
|
|
3149
|
+
hidden: []
|
|
3150
|
+
};
|
|
3151
|
+
//#endregion
|
|
3152
|
+
//#region ../core/src/html/spec/elements/input.ts
|
|
3153
|
+
const LIST_ELIGIBLE_TYPES = /* @__PURE__ */ new Set([
|
|
3154
|
+
"text",
|
|
3155
|
+
"search",
|
|
3156
|
+
"tel",
|
|
3157
|
+
"url",
|
|
3158
|
+
"email"
|
|
3159
|
+
]);
|
|
3160
|
+
function rolesForType(type) {
|
|
3161
|
+
return ALLOWED_INPUT_ROLES[type in ALLOWED_INPUT_ROLES ? type : "text"];
|
|
3162
|
+
}
|
|
3163
|
+
const inputElementSpec = {
|
|
3164
|
+
tag: "input",
|
|
3165
|
+
allowedRoles: {
|
|
3166
|
+
kind: "dynamic",
|
|
3167
|
+
resolve: ({ props }) => {
|
|
3168
|
+
const type = isString(props.type) ? props.type : "text";
|
|
3169
|
+
if (isNonNull(props.list) && LIST_ELIGIBLE_TYPES.has(type)) return [];
|
|
3170
|
+
return rolesForType(type);
|
|
3171
|
+
}
|
|
3172
|
+
},
|
|
3173
|
+
attributes: INPUT_ATTRIBUTE_TYPE_POLICIES,
|
|
3174
|
+
mutuallyExclusive: INPUT_MUTUALLY_EXCLUSIVE_POLICIES
|
|
3175
|
+
};
|
|
3176
|
+
//#endregion
|
|
3177
|
+
//#region ../core/src/html/spec/roles/img.ts
|
|
3178
|
+
const IMG_DECORATIVE_ROLES = ["none", "presentation"];
|
|
3179
|
+
const IMG_NAMED_ROLES = [
|
|
3180
|
+
"button",
|
|
3181
|
+
"checkbox",
|
|
3182
|
+
"link",
|
|
3183
|
+
"math",
|
|
3184
|
+
"menuitem",
|
|
3185
|
+
"menuitemcheckbox",
|
|
3186
|
+
"menuitemradio",
|
|
3187
|
+
"meter",
|
|
3188
|
+
"option",
|
|
3189
|
+
"progressbar",
|
|
3190
|
+
"radio",
|
|
3191
|
+
"scrollbar",
|
|
3192
|
+
"separator",
|
|
3193
|
+
"slider",
|
|
3194
|
+
"switch",
|
|
3195
|
+
"tab",
|
|
3196
|
+
"treeitem"
|
|
3197
|
+
];
|
|
3198
|
+
//#endregion
|
|
3199
|
+
//#region ../core/src/html/spec/elements/img.ts
|
|
3200
|
+
const imgElementSpec = {
|
|
3201
|
+
tag: "img",
|
|
3202
|
+
allowedRoles: {
|
|
3203
|
+
kind: "dynamic",
|
|
3204
|
+
resolve: ({ props }) => {
|
|
3205
|
+
if (isNullish(props.alt)) return IMG_DECORATIVE_ROLES;
|
|
3206
|
+
return props.alt === "" ? [] : IMG_NAMED_ROLES;
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
};
|
|
3210
|
+
//#endregion
|
|
3211
|
+
//#region ../core/src/html/spec/roles/select.ts
|
|
3212
|
+
const ALLOWED_SELECT_ROLES = ["menu"];
|
|
3213
|
+
//#endregion
|
|
3214
|
+
//#region ../core/src/html/spec/elements/select.ts
|
|
3215
|
+
function isListBoxSelect(props) {
|
|
3216
|
+
const { multiple, size } = props;
|
|
3217
|
+
if (isNonNull(multiple) && multiple !== false && (!isString(multiple) || multiple.toLowerCase() !== "false")) return true;
|
|
3218
|
+
const parsed = isNumber(size) ? size : isString(size) ? Number(size) : NaN;
|
|
3219
|
+
return Number.isFinite(parsed) && parsed > 1;
|
|
3220
|
+
}
|
|
3221
|
+
const selectElementSpec = {
|
|
3222
|
+
tag: "select",
|
|
3223
|
+
allowedRoles: {
|
|
3224
|
+
kind: "dynamic",
|
|
3225
|
+
resolve: ({ props }) => isListBoxSelect(props) ? [] : ALLOWED_SELECT_ROLES
|
|
3226
|
+
}
|
|
3227
|
+
};
|
|
3228
|
+
//#endregion
|
|
3229
|
+
//#region ../core/src/html/spec/elements/table.ts
|
|
3230
|
+
const tableElementSpec = {
|
|
3231
|
+
tag: "table",
|
|
3232
|
+
allowedRoles: {
|
|
3233
|
+
kind: "fixed",
|
|
3234
|
+
roles: ["grid", "treegrid"]
|
|
3235
|
+
}
|
|
3236
|
+
};
|
|
3237
|
+
//#endregion
|
|
3238
|
+
//#region ../core/src/html/role-restrictions.ts
|
|
3239
|
+
const ALLOWED_ROLES = {
|
|
3240
|
+
article: [
|
|
3241
|
+
"application",
|
|
3242
|
+
"document",
|
|
3243
|
+
"feed",
|
|
3244
|
+
"main",
|
|
3245
|
+
"none",
|
|
3246
|
+
"presentation",
|
|
3247
|
+
"region"
|
|
3248
|
+
],
|
|
3249
|
+
aside: [
|
|
3250
|
+
"feed",
|
|
3251
|
+
"none",
|
|
3252
|
+
"note",
|
|
3253
|
+
"presentation",
|
|
3254
|
+
"region",
|
|
3255
|
+
"search"
|
|
3256
|
+
],
|
|
3257
|
+
footer: [
|
|
3258
|
+
"group",
|
|
3259
|
+
"none",
|
|
3260
|
+
"presentation"
|
|
3261
|
+
],
|
|
3262
|
+
header: [
|
|
3263
|
+
"group",
|
|
3264
|
+
"none",
|
|
3265
|
+
"presentation"
|
|
3266
|
+
],
|
|
3267
|
+
main: [],
|
|
3268
|
+
nav: [
|
|
3269
|
+
"menu",
|
|
3270
|
+
"menubar",
|
|
3271
|
+
"none",
|
|
3272
|
+
"presentation",
|
|
3273
|
+
"tablist"
|
|
3274
|
+
],
|
|
3275
|
+
a: [
|
|
3276
|
+
"button",
|
|
3277
|
+
"checkbox",
|
|
3278
|
+
"menuitem",
|
|
3279
|
+
"menuitemcheckbox",
|
|
3280
|
+
"menuitemradio",
|
|
3281
|
+
"option",
|
|
3282
|
+
"radio",
|
|
3283
|
+
"switch",
|
|
3284
|
+
"tab",
|
|
3285
|
+
"treeitem"
|
|
3286
|
+
],
|
|
3287
|
+
button: [
|
|
3288
|
+
"checkbox",
|
|
3289
|
+
"combobox",
|
|
3290
|
+
"gridcell",
|
|
3291
|
+
"link",
|
|
3292
|
+
"menuitem",
|
|
3293
|
+
"menuitemcheckbox",
|
|
3294
|
+
"menuitemradio",
|
|
3295
|
+
"option",
|
|
3296
|
+
"radio",
|
|
3297
|
+
"separator",
|
|
3298
|
+
"slider",
|
|
3299
|
+
"switch",
|
|
3300
|
+
"tab",
|
|
3301
|
+
"treeitem"
|
|
3302
|
+
],
|
|
3303
|
+
h1: [
|
|
3304
|
+
"tab",
|
|
3305
|
+
"presentation",
|
|
3306
|
+
"none"
|
|
3307
|
+
],
|
|
3308
|
+
h2: [
|
|
3309
|
+
"tab",
|
|
3310
|
+
"presentation",
|
|
3311
|
+
"none"
|
|
3312
|
+
],
|
|
3313
|
+
h3: [
|
|
3314
|
+
"tab",
|
|
3315
|
+
"presentation",
|
|
3316
|
+
"none"
|
|
3317
|
+
],
|
|
3318
|
+
h4: [
|
|
3319
|
+
"tab",
|
|
3320
|
+
"presentation",
|
|
3321
|
+
"none"
|
|
3322
|
+
],
|
|
3323
|
+
h5: [
|
|
3324
|
+
"tab",
|
|
3325
|
+
"presentation",
|
|
3326
|
+
"none"
|
|
3327
|
+
],
|
|
3328
|
+
h6: [
|
|
3329
|
+
"tab",
|
|
3330
|
+
"presentation",
|
|
3331
|
+
"none"
|
|
3332
|
+
],
|
|
3333
|
+
ul: [
|
|
3334
|
+
"group",
|
|
3335
|
+
"listbox",
|
|
3336
|
+
"menu",
|
|
3337
|
+
"menubar",
|
|
3338
|
+
"none",
|
|
3339
|
+
"presentation",
|
|
3340
|
+
"radiogroup",
|
|
3341
|
+
"tablist",
|
|
3342
|
+
"toolbar",
|
|
3343
|
+
"tree"
|
|
3344
|
+
],
|
|
3345
|
+
ol: [
|
|
3346
|
+
"group",
|
|
3347
|
+
"listbox",
|
|
3348
|
+
"menu",
|
|
3349
|
+
"menubar",
|
|
3350
|
+
"none",
|
|
3351
|
+
"presentation",
|
|
3352
|
+
"radiogroup",
|
|
3353
|
+
"tablist",
|
|
3354
|
+
"toolbar",
|
|
3355
|
+
"tree"
|
|
3356
|
+
],
|
|
3357
|
+
li: [
|
|
3358
|
+
"menuitem",
|
|
3359
|
+
"menuitemcheckbox",
|
|
3360
|
+
"menuitemradio",
|
|
3361
|
+
"option",
|
|
3362
|
+
"none",
|
|
3363
|
+
"presentation",
|
|
3364
|
+
"radio",
|
|
3365
|
+
"separator",
|
|
3366
|
+
"tab",
|
|
3367
|
+
"treeitem"
|
|
3368
|
+
],
|
|
3369
|
+
dialog: ["alertdialog"],
|
|
3370
|
+
fieldset: [
|
|
3371
|
+
"none",
|
|
3372
|
+
"presentation",
|
|
3373
|
+
"radiogroup"
|
|
3374
|
+
],
|
|
3375
|
+
label: []
|
|
3376
|
+
};
|
|
3377
|
+
const ELEMENT_SPECS = {
|
|
3378
|
+
input: inputElementSpec,
|
|
3379
|
+
img: imgElementSpec,
|
|
3380
|
+
select: selectElementSpec,
|
|
3381
|
+
table: tableElementSpec
|
|
3382
|
+
};
|
|
3383
|
+
function getAllowedRoles(tag, props) {
|
|
3384
|
+
const spec = ELEMENT_SPECS[tag];
|
|
3385
|
+
if (spec) return resolveAllowedRoles(spec, props);
|
|
3386
|
+
return ALLOWED_ROLES[tag];
|
|
3387
|
+
}
|
|
3388
|
+
const removeRoleFix = {
|
|
3389
|
+
kind: "removeRole",
|
|
3390
|
+
apply: ({ props }) => {
|
|
3391
|
+
if (!("role" in props)) return {
|
|
3392
|
+
applied: false,
|
|
3393
|
+
next: props
|
|
3394
|
+
};
|
|
3395
|
+
const { role: _role, ...rest } = props;
|
|
3396
|
+
return {
|
|
3397
|
+
applied: true,
|
|
3398
|
+
next: rest,
|
|
3399
|
+
previous: props
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
};
|
|
3403
|
+
const roleNotPermittedRule = Object.assign(({ tag, props, implicitRole }) => {
|
|
3404
|
+
const role = props.role;
|
|
3405
|
+
if (typeof role !== "string" || role.length === 0 || role === implicitRole) return [];
|
|
3406
|
+
const allowed = getAllowedRoles(tag, props);
|
|
3407
|
+
if (allowed === void 0 || allowed.includes(role)) return [];
|
|
3408
|
+
const diagnostic = HtmlDiagnostics.roleNotPermitted(tag, role, allowed);
|
|
3409
|
+
return [{
|
|
3410
|
+
valid: false,
|
|
3411
|
+
fixable: true,
|
|
3412
|
+
severity: diagnostic.severity,
|
|
3413
|
+
fix: removeRoleFix,
|
|
3414
|
+
diagnostic
|
|
3415
|
+
}];
|
|
3416
|
+
}, { readsProps: [
|
|
3417
|
+
"role",
|
|
3418
|
+
"type",
|
|
3419
|
+
"alt",
|
|
3420
|
+
"list",
|
|
3421
|
+
"multiple",
|
|
3422
|
+
"size"
|
|
3423
|
+
] });
|
|
3424
|
+
//#endregion
|
|
3425
|
+
//#region ../core/src/html/aria-rules.ts
|
|
3426
|
+
function defineAriaRule(tags, rule) {
|
|
3427
|
+
return Object.assign(rule, { tags });
|
|
3428
|
+
}
|
|
3429
|
+
const LANDMARK_TAG_SET = new Set(LANDMARK_TAGS);
|
|
3430
|
+
const removeLandmarkRoleOverride = {
|
|
3431
|
+
kind: "removeRole",
|
|
3432
|
+
apply: ({ props }) => {
|
|
3433
|
+
if (!("role" in props)) return {
|
|
3434
|
+
applied: false,
|
|
3435
|
+
next: props
|
|
3436
|
+
};
|
|
3437
|
+
const { role: _r, ...rest } = props;
|
|
3438
|
+
return {
|
|
3439
|
+
applied: true,
|
|
3440
|
+
next: rest,
|
|
3441
|
+
previous: props
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
};
|
|
3445
|
+
const landmarkRoleRule = defineAriaRule(LANDMARK_TAGS, ({ tag, props, implicitRole }) => {
|
|
3446
|
+
if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
|
|
3447
|
+
const { role } = props;
|
|
3448
|
+
if (!role || role === implicitRole) return [];
|
|
3449
|
+
const diagnostic = HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role);
|
|
3450
|
+
return [{
|
|
3451
|
+
valid: false,
|
|
3452
|
+
fixable: true,
|
|
3453
|
+
severity: diagnostic.severity,
|
|
3454
|
+
fix: removeLandmarkRoleOverride,
|
|
3455
|
+
diagnostic
|
|
3456
|
+
}];
|
|
3457
|
+
});
|
|
3458
|
+
function requireAccessibleName({ tag, props }) {
|
|
3459
|
+
if ("aria-label" in props || "aria-labelledby" in props) return [];
|
|
3460
|
+
return [{
|
|
3461
|
+
valid: false,
|
|
3462
|
+
fixable: false,
|
|
3463
|
+
severity: "warning",
|
|
3464
|
+
diagnostic: AriaDiagnostics.missingAccessibleName(tag)
|
|
3465
|
+
}];
|
|
3466
|
+
}
|
|
3467
|
+
const NAMED_LANDMARK_TAGS = ["nav", "aside"];
|
|
3468
|
+
const NAMED_LANDMARK_TAG_SET = new Set(NAMED_LANDMARK_TAGS);
|
|
3469
|
+
const landmarkAccessibleNameRule = defineAriaRule(NAMED_LANDMARK_TAGS, (ctx) => {
|
|
3470
|
+
if (!ctx.implicitRole || !NAMED_LANDMARK_TAG_SET.has(ctx.tag)) return [];
|
|
3471
|
+
return requireAccessibleName(ctx);
|
|
3472
|
+
});
|
|
3473
|
+
const HTML_ARIA_RULES = [
|
|
3474
|
+
landmarkRoleRule,
|
|
3475
|
+
landmarkAccessibleNameRule,
|
|
3476
|
+
roleNotPermittedRule,
|
|
3477
|
+
...INPUT_RULES,
|
|
3478
|
+
...ANCHOR_RULES
|
|
3479
|
+
];
|
|
3480
|
+
//#endregion
|
|
3481
|
+
//#region ../core/src/html/contracts/helpers.ts
|
|
3482
|
+
/** Narrows an unknown value to the minimal vnode shape used by contract helpers. */
|
|
3483
|
+
function isVNodeLike(child) {
|
|
3484
|
+
return isObject(child) && "type" in child;
|
|
3485
|
+
}
|
|
3486
|
+
/**
|
|
3487
|
+
* Creates a matcher that accepts any element except the supplied HTML tags.
|
|
3488
|
+
*
|
|
3489
|
+
* Tag resolution is semantic-aware, so Praxis components are matched by their
|
|
3490
|
+
* resolved HTML tag rather than their component type.
|
|
3491
|
+
*/
|
|
3492
|
+
function isOpenContent(...blockedTags) {
|
|
3493
|
+
const set = new Set(blockedTags);
|
|
3494
|
+
return (child) => {
|
|
3495
|
+
if (!isVNodeLike(child)) return false;
|
|
3496
|
+
const tag = getTag(child);
|
|
3497
|
+
return tag === void 0 || !set.has(tag);
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
const metadataMatch = isTag(...METADATA_TAGS);
|
|
3501
|
+
/** Creates a child rule matching HTML metadata content (`<script>` and `<template>`). */
|
|
3502
|
+
function metadata(name = "metadata") {
|
|
3503
|
+
return {
|
|
3504
|
+
name,
|
|
3505
|
+
match: metadataMatch
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
/**
|
|
3509
|
+
* Creates an optional singleton child rule (`max: 1`).
|
|
3510
|
+
*
|
|
3511
|
+
* Used as the building block for positional variants such as `firstOptional()`.
|
|
3512
|
+
*/
|
|
3513
|
+
function optional(name, tag) {
|
|
3514
|
+
return {
|
|
3515
|
+
name,
|
|
3516
|
+
match: isTag(tag),
|
|
3517
|
+
cardinality: { max: 1 }
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
function firstOptional(name, tag) {
|
|
3521
|
+
return {
|
|
3522
|
+
...optional(name, tag),
|
|
3523
|
+
position: "first"
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3526
|
+
/**
|
|
3527
|
+
* Creates an enforcement contract with the default diagnostics configuration.
|
|
3528
|
+
*/
|
|
3529
|
+
function contract(children, options) {
|
|
3530
|
+
return {
|
|
3531
|
+
diagnostics: warnDiagnostics,
|
|
3532
|
+
children,
|
|
3533
|
+
...options
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
/**
|
|
3537
|
+
* Creates a closed content model.
|
|
3538
|
+
*
|
|
3539
|
+
* Only the supplied child rules are permitted; all other children are rejected.
|
|
3540
|
+
*/
|
|
3541
|
+
function closedContract(children) {
|
|
3542
|
+
return contract(children, { exclusiveChildren: true });
|
|
3543
|
+
}
|
|
3544
|
+
/** Creates an ARIA-only enforcement contract. */
|
|
3545
|
+
function ariaContract(aria) {
|
|
3546
|
+
return {
|
|
3547
|
+
diagnostics: warnDiagnostics,
|
|
3548
|
+
aria
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
/**
|
|
3552
|
+
* Creates a contract with an optional leading child followed by open content.
|
|
3553
|
+
*
|
|
3554
|
+
* Used by elements such as `<details>` and `<fieldset>` whose first child has
|
|
3555
|
+
* special semantics.
|
|
3556
|
+
*/
|
|
3557
|
+
function firstChildContract(name, tag) {
|
|
3558
|
+
return contract([firstOptional(name, tag), {
|
|
3559
|
+
name: "content",
|
|
3560
|
+
match: isOpenContent(tag)
|
|
3561
|
+
}]);
|
|
3562
|
+
}
|
|
3563
|
+
/**
|
|
3564
|
+
* Returns a property from a vnode-like child, if present.
|
|
3565
|
+
*/
|
|
3566
|
+
function getChildProp(child, key) {
|
|
3567
|
+
if (!isVNodeLike(child)) return void 0;
|
|
3568
|
+
const { props } = child;
|
|
3569
|
+
if (!isObject(props)) return void 0;
|
|
3570
|
+
return Reflect.get(props, key);
|
|
3571
|
+
}
|
|
3572
|
+
//#endregion
|
|
3573
|
+
//#region ../core/src/html/contracts/aria/landmarks.ts
|
|
3574
|
+
/**
|
|
3575
|
+
* Elements with an unconditional landmark role (`<article>`, `<aside>`, `<footer>`,
|
|
3576
|
+
* `<header>`, `<main>`, `<nav>`).
|
|
3577
|
+
*
|
|
3578
|
+
* - `role="<implicit>"` → warning: redundant, removed (built-in engine behaviour).
|
|
3579
|
+
* - `role="<anything else>"` → error: overrides the fixed landmark, removed.
|
|
3580
|
+
* - `<nav>` and `<aside>` without an accessible name → warning (commonly multiplied).
|
|
3581
|
+
*/
|
|
3582
|
+
const landmarkContract = ariaContract([landmarkRoleRule, landmarkAccessibleNameRule]);
|
|
3583
|
+
//#endregion
|
|
3584
|
+
//#region ../core/src/html/contracts/aria/widgets.ts
|
|
3585
|
+
/**
|
|
3586
|
+
* `<dialog>` — must have an accessible name (aria-label or aria-labelledby).
|
|
3587
|
+
* APG: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
|
|
3588
|
+
* Without a name, assistive technology cannot identify the dialog in the page outline
|
|
3589
|
+
* or when focus moves into it.
|
|
3590
|
+
*/
|
|
3591
|
+
const dialogContract = ariaContract([requireAccessibleName]);
|
|
3592
|
+
//#endregion
|
|
3593
|
+
//#region ../core/src/html/contracts/html/document.ts
|
|
3594
|
+
/**
|
|
3595
|
+
* `<head>` — metadata content only.
|
|
3596
|
+
*/
|
|
3597
|
+
const headContract = closedContract([{
|
|
3598
|
+
name: "metadata",
|
|
3599
|
+
match: isTag("base", "link", "meta", "noscript", "script", "style", "template", "title")
|
|
3600
|
+
}]);
|
|
3601
|
+
/**
|
|
3602
|
+
* `<html>` — one `<head>` and one `<body>`.
|
|
3603
|
+
*/
|
|
3604
|
+
const htmlContract = closedContract([{
|
|
3605
|
+
name: "head",
|
|
3606
|
+
match: isTag("head"),
|
|
3607
|
+
cardinality: {
|
|
3608
|
+
min: 1,
|
|
3609
|
+
max: 1
|
|
3610
|
+
},
|
|
3611
|
+
position: "first"
|
|
3612
|
+
}, {
|
|
3613
|
+
name: "body",
|
|
3614
|
+
match: isTag("body"),
|
|
3615
|
+
cardinality: {
|
|
3616
|
+
min: 1,
|
|
3617
|
+
max: 1
|
|
3618
|
+
}
|
|
3619
|
+
}]);
|
|
3620
|
+
/**
|
|
3621
|
+
* Void elements — the HTML spec forbids any children, including text.
|
|
3622
|
+
*/
|
|
3623
|
+
const voidContract = contract([], {
|
|
3624
|
+
exclusiveChildren: true,
|
|
3625
|
+
allowText: false
|
|
3626
|
+
});
|
|
3627
|
+
/**
|
|
3628
|
+
* Raw text (`<script>`, `<style>`), escapable raw text (`<textarea>`, `<title>`), and
|
|
3629
|
+
* `<option>` — only text nodes (strings or numbers) are permitted as children. No
|
|
3630
|
+
* explicit rule is needed: `exclusiveChildren` rejects every element (nothing matches
|
|
3631
|
+
* the empty rule list) while `allowText` stays at its default `true`.
|
|
3632
|
+
*/
|
|
3633
|
+
const textOnlyContract = contract([], { exclusiveChildren: true });
|
|
3634
|
+
//#endregion
|
|
3635
|
+
//#region ../core/src/html/contracts/html/forms.ts
|
|
3636
|
+
/**
|
|
3637
|
+
* `<select>` — direct children must be `<option>`, `<optgroup>`, `<hr>`, `<script>`,
|
|
3638
|
+
* or `<template>`.
|
|
3639
|
+
*/
|
|
3640
|
+
const selectContract = closedContract([{
|
|
3641
|
+
name: "option",
|
|
3642
|
+
match: isTag("option", "optgroup", "hr", ...METADATA_TAGS)
|
|
3643
|
+
}]);
|
|
3644
|
+
/**
|
|
3645
|
+
* `<optgroup>` — direct children must be `<option>`, `<script>`, or `<template>`.
|
|
3646
|
+
*/
|
|
3647
|
+
const optgroupContract = closedContract([{
|
|
3648
|
+
name: "option",
|
|
3649
|
+
match: isTag("option", ...METADATA_TAGS)
|
|
3650
|
+
}]);
|
|
3651
|
+
/**
|
|
3652
|
+
* `<datalist>` — direct children must be `<option>`, `<script>`, or `<template>`.
|
|
3653
|
+
*/
|
|
3654
|
+
const datalistContract = closedContract([{
|
|
3655
|
+
name: "option",
|
|
3656
|
+
match: isTag("option", ...METADATA_TAGS)
|
|
3657
|
+
}]);
|
|
3658
|
+
/**
|
|
3659
|
+
* `<button>` — must not directly contain interactive content (`<a>`, another `<button>`,
|
|
3660
|
+
* `<input>`, `<select>`, `<textarea>`, or `<label>`) per the HTML5 spec.
|
|
3661
|
+
*/
|
|
3662
|
+
const buttonContract = closedContract([{
|
|
3663
|
+
name: "content",
|
|
3664
|
+
match: isOpenContent(...INTERACTIVE_CONTENT_TAGS)
|
|
3665
|
+
}]);
|
|
3666
|
+
/**
|
|
3667
|
+
* `<a>` — same interactive-content restriction as `<button>`: must not directly contain
|
|
3668
|
+
* `<a>`, `<button>`, `<input>`, `<select>`, `<textarea>`, or `<label>`.
|
|
3669
|
+
*/
|
|
3670
|
+
const anchorContract = closedContract([{
|
|
3671
|
+
name: "content",
|
|
3672
|
+
match: isOpenContent(...INTERACTIVE_CONTENT_TAGS)
|
|
3673
|
+
}]);
|
|
3674
|
+
const labelableTagMatch = isTag(...LABELABLE_TAGS);
|
|
3675
|
+
function isLabelableControl(child) {
|
|
3676
|
+
if (!labelableTagMatch(child)) return false;
|
|
3677
|
+
if (getTag(child) !== "input") return true;
|
|
3678
|
+
const type = getChildProp(child, "type");
|
|
3679
|
+
return !isString(type) || type !== "hidden";
|
|
3680
|
+
}
|
|
3681
|
+
function hasAccessibleNameProp(props) {
|
|
3682
|
+
return "aria-label" in props || "aria-labelledby" in props;
|
|
3683
|
+
}
|
|
3684
|
+
function isNonEmptyTextChild(child) {
|
|
3685
|
+
if (isNumber(child)) return true;
|
|
3686
|
+
return isString(child) && child.trim().length > 0;
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* `<label>` — at most one labelable form control descendant (`<button>`, `<input>`
|
|
3690
|
+
* excluding `type="hidden"`, `<meter>`, `<output>`, `<progress>`, `<select>`, `<textarea>`);
|
|
3691
|
+
* a nested `<label>` is rejected outright, as is any other interactive content (`<a>`,
|
|
3692
|
+
* `<details>`, `<embed>`, `<iframe>`) that isn't the labeled control; any other phrasing
|
|
3693
|
+
* content is permitted. Requires an accessible name: either `aria-label`/`aria-labelledby`,
|
|
3694
|
+
* or visible (non-whitespace) text among its direct children.
|
|
3695
|
+
*
|
|
3696
|
+
* The text check only looks at direct children (matching every other rule in this
|
|
3697
|
+
* contract, and the engine's own model — it doesn't walk into a descendant control to
|
|
3698
|
+
* find, say, an `<option>`'s text), so a label whose only content is a nested element
|
|
3699
|
+
* with its own accessible name (e.g. `<label><input type="image" alt="Submit" /></label>`)
|
|
3700
|
+
* still reports as missing a name. That's a known, narrower check than the full HTML AAM
|
|
3701
|
+
* accessible-name computation, not a bug.
|
|
3702
|
+
*/
|
|
3703
|
+
const labelContract = contract([
|
|
3704
|
+
{
|
|
3705
|
+
name: "control",
|
|
3706
|
+
match: isLabelableControl,
|
|
3707
|
+
cardinality: { max: 1 }
|
|
3708
|
+
},
|
|
3709
|
+
{
|
|
3710
|
+
name: "nested-label",
|
|
3711
|
+
match: isTag("label"),
|
|
3712
|
+
cardinality: { max: 0 }
|
|
3713
|
+
},
|
|
3714
|
+
{
|
|
3715
|
+
name: "other-interactive-content",
|
|
3716
|
+
match: isTag(...OTHER_INTERACTIVE_TAGS),
|
|
3717
|
+
cardinality: { max: 0 }
|
|
3718
|
+
},
|
|
3719
|
+
{
|
|
3720
|
+
name: "accessible-name",
|
|
3721
|
+
match: isNonEmptyTextChild,
|
|
3722
|
+
cardinality: dynamic((ctx) => hasAccessibleNameProp(ctx.props) ? { min: 0 } : { min: 1 })
|
|
3723
|
+
}
|
|
3724
|
+
]);
|
|
3725
|
+
//#endregion
|
|
3726
|
+
//#region ../core/src/html/contracts/html/grouping.ts
|
|
3727
|
+
/**
|
|
3728
|
+
* `<details>` — permits an optional leading `<summary>` followed by flow content.
|
|
3729
|
+
*/
|
|
3730
|
+
const detailsContract = firstChildContract("summary", "summary");
|
|
3731
|
+
/**
|
|
3732
|
+
* `<fieldset>` — permits an optional leading `<legend>` followed by flow content.
|
|
3733
|
+
*/
|
|
3734
|
+
const fieldsetContract = firstChildContract("legend", "legend");
|
|
3735
|
+
//#endregion
|
|
3736
|
+
//#region ../core/src/html/contracts/html/lists.ts
|
|
3737
|
+
/**
|
|
3738
|
+
* `<ul>`, `<ol>`, `<menu>` — direct children must be `<li>`, `<script>`, or `<template>`.
|
|
3739
|
+
*/
|
|
3740
|
+
const listContract = closedContract([{
|
|
3741
|
+
name: "list-item",
|
|
3742
|
+
match: isTag("li", ...METADATA_TAGS)
|
|
3743
|
+
}]);
|
|
3744
|
+
/**
|
|
3745
|
+
* `<dl>` — direct children must be `<dt>`, `<dd>`, `<div>` (as group wrapper),
|
|
3746
|
+
* `<script>`, or `<template>`.
|
|
3747
|
+
*/
|
|
3748
|
+
const dlContract = closedContract([
|
|
3749
|
+
{
|
|
3750
|
+
name: "term",
|
|
3751
|
+
match: isTag("dt")
|
|
3752
|
+
},
|
|
3753
|
+
{
|
|
3754
|
+
name: "description",
|
|
3755
|
+
match: isTag("dd")
|
|
3756
|
+
},
|
|
3757
|
+
{
|
|
3758
|
+
name: "group",
|
|
3759
|
+
match: isTag("div")
|
|
3760
|
+
},
|
|
3761
|
+
metadata()
|
|
3762
|
+
]);
|
|
3763
|
+
//#endregion
|
|
3764
|
+
//#region ../core/src/html/contracts/html/media.ts
|
|
3765
|
+
/**
|
|
3766
|
+
* `<picture>` — permits zero or more `<source>` elements followed by a single
|
|
3767
|
+
* `<img>` fallback.
|
|
3768
|
+
*
|
|
3769
|
+
* `<img>` must be the final child.
|
|
3770
|
+
*/
|
|
3771
|
+
const pictureContract = closedContract([{
|
|
3772
|
+
name: "source",
|
|
3773
|
+
match: isTag("source", ...METADATA_TAGS)
|
|
3774
|
+
}, {
|
|
3775
|
+
name: "image",
|
|
3776
|
+
match: isTag("img"),
|
|
3777
|
+
cardinality: {
|
|
3778
|
+
min: 1,
|
|
3779
|
+
max: 1
|
|
3780
|
+
},
|
|
3781
|
+
position: "last"
|
|
3782
|
+
}]);
|
|
3783
|
+
/**
|
|
3784
|
+
* `<figure>` — permits at most one `<figcaption>` and arbitrary flow content.
|
|
3785
|
+
*
|
|
3786
|
+
* The `<figcaption>` may appear as either the first or last child.
|
|
3787
|
+
*/
|
|
3788
|
+
const figureContract = contract([{
|
|
3789
|
+
name: "caption",
|
|
3790
|
+
match: isTag("figcaption"),
|
|
3791
|
+
cardinality: { max: 1 }
|
|
3792
|
+
}, {
|
|
3793
|
+
name: "content",
|
|
3794
|
+
match: isOpenContent("figcaption")
|
|
3795
|
+
}]);
|
|
3796
|
+
/**
|
|
3797
|
+
* `<object>` — permits `<param>` elements followed by transparent fallback
|
|
3798
|
+
* content.
|
|
3799
|
+
*
|
|
3800
|
+
* Relative ordering is not currently validated.
|
|
3801
|
+
*/
|
|
3802
|
+
const objectContract = contract([{
|
|
3803
|
+
name: "param",
|
|
3804
|
+
match: isTag("param")
|
|
3805
|
+
}, {
|
|
3806
|
+
name: "content",
|
|
3807
|
+
match: isOpenContent("param")
|
|
3808
|
+
}]);
|
|
3809
|
+
/**
|
|
3810
|
+
* `<audio>` and `<video>` — permit media source definitions, timed text tracks,
|
|
3811
|
+
* metadata, and fallback content.
|
|
3812
|
+
*/
|
|
3813
|
+
const mediaContract = contract([
|
|
3814
|
+
{
|
|
3815
|
+
name: "source",
|
|
3816
|
+
match: isTag("source")
|
|
3817
|
+
},
|
|
3818
|
+
{
|
|
3819
|
+
name: "track",
|
|
3820
|
+
match: isTag("track")
|
|
3821
|
+
},
|
|
3822
|
+
metadata(),
|
|
3823
|
+
{
|
|
3824
|
+
name: "content",
|
|
3825
|
+
match: isOpenContent("source", "track", ...METADATA_TAGS)
|
|
3826
|
+
}
|
|
3827
|
+
]);
|
|
3828
|
+
//#endregion
|
|
3829
|
+
//#region ../core/src/html/contracts/html/tables.ts
|
|
3830
|
+
/**
|
|
3831
|
+
* `<table>` — permits the HTML table sectioning elements as direct children.
|
|
3832
|
+
*
|
|
3833
|
+
* `<caption>` is optional and must be first. `<thead>` and `<tfoot>` may each
|
|
3834
|
+
* appear at most once.
|
|
3835
|
+
*
|
|
3836
|
+
* Relative ordering of the remaining sections is not currently validated.
|
|
3837
|
+
*/
|
|
3838
|
+
const tableContract = closedContract([
|
|
3839
|
+
firstOptional("caption", "caption"),
|
|
3840
|
+
{
|
|
3841
|
+
name: "colgroup",
|
|
3842
|
+
match: isTag("colgroup")
|
|
3843
|
+
},
|
|
3844
|
+
{
|
|
3845
|
+
name: "thead",
|
|
3846
|
+
match: isTag("thead"),
|
|
3847
|
+
cardinality: { max: 1 }
|
|
3848
|
+
},
|
|
3849
|
+
{
|
|
3850
|
+
name: "tbody",
|
|
3851
|
+
match: isTag("tbody")
|
|
3852
|
+
},
|
|
3853
|
+
{
|
|
3854
|
+
name: "tfoot",
|
|
3855
|
+
match: isTag("tfoot"),
|
|
3856
|
+
cardinality: { max: 1 }
|
|
3857
|
+
},
|
|
3858
|
+
{
|
|
3859
|
+
name: "table-row",
|
|
3860
|
+
match: isTag("tr", ...METADATA_TAGS)
|
|
3861
|
+
}
|
|
3862
|
+
]);
|
|
3863
|
+
/**
|
|
3864
|
+
* `<thead>`, `<tbody>`, and `<tfoot>` — permit table rows as direct children.
|
|
3865
|
+
*/
|
|
3866
|
+
const tableBodyContract = closedContract([{
|
|
3867
|
+
name: "table-row",
|
|
3868
|
+
match: isTag("tr", ...METADATA_TAGS)
|
|
3869
|
+
}]);
|
|
3870
|
+
/**
|
|
3871
|
+
* `<tr>` — permits table cells as direct children.
|
|
3872
|
+
*/
|
|
3873
|
+
const tableRowContract = closedContract([{
|
|
3874
|
+
name: "table-cell",
|
|
3875
|
+
match: isTag("td", "th", ...METADATA_TAGS)
|
|
3876
|
+
}]);
|
|
3877
|
+
/**
|
|
3878
|
+
* `<colgroup>` — permits column definitions as direct children.
|
|
3879
|
+
*/
|
|
3880
|
+
const colgroupContract = closedContract([{
|
|
3881
|
+
name: "column",
|
|
3882
|
+
match: isTag("col", "template")
|
|
3883
|
+
}]);
|
|
3884
|
+
//#endregion
|
|
3885
|
+
//#region ../core/src/html/contracts/html/text.ts
|
|
3886
|
+
/**
|
|
3887
|
+
* `<p>` — permits phrasing content only.
|
|
3888
|
+
*
|
|
3889
|
+
* Implemented as a pragmatic blocklist of elements that terminate a paragraph.
|
|
3890
|
+
*/
|
|
3891
|
+
const pContract = closedContract([{
|
|
3892
|
+
name: "content",
|
|
3893
|
+
match: isOpenContent(...P_BLOCKED_TAGS)
|
|
3894
|
+
}]);
|
|
3895
|
+
//#endregion
|
|
3896
|
+
//#region ../core/src/html/contracts/build-map.ts
|
|
3897
|
+
/**
|
|
3898
|
+
* Builds a lookup table from grouped keys.
|
|
3899
|
+
*
|
|
3900
|
+
* Each key in a group is associated with the group's shared value. Used to
|
|
3901
|
+
* expand compact declarations into a flat lookup record.
|
|
3902
|
+
*/
|
|
3903
|
+
function buildMap(groups) {
|
|
3904
|
+
return Object.fromEntries(groups.flatMap(([keys, value]) => keys.map((key) => [key, value])));
|
|
3905
|
+
}
|
|
3906
|
+
/**
|
|
3907
|
+
* Built-in HTML element contracts keyed by tag name.
|
|
3908
|
+
*
|
|
3909
|
+
* Pass a contract directly to `createContractComponent`:
|
|
3910
|
+
*
|
|
3911
|
+
* ```ts
|
|
3912
|
+
* const List = createContractComponent({
|
|
3913
|
+
* tag: 'ul',
|
|
3914
|
+
* enforcement: htmlContracts.ul,
|
|
3915
|
+
* })
|
|
3916
|
+
* ```
|
|
3917
|
+
*
|
|
3918
|
+
* Contracts default to `strict: 'warn'`. Override individual options by
|
|
3919
|
+
* spreading the contract into a new enforcement object.
|
|
3920
|
+
*/
|
|
3921
|
+
const htmlContracts = {
|
|
3922
|
+
...buildMap([
|
|
3923
|
+
[VOID_TAGS, voidContract],
|
|
3924
|
+
[TEXT_ONLY_TAGS, textOnlyContract],
|
|
3925
|
+
[LANDMARK_TAGS, landmarkContract],
|
|
3926
|
+
[[
|
|
3927
|
+
"ul",
|
|
3928
|
+
"ol",
|
|
3929
|
+
"menu"
|
|
3930
|
+
], listContract],
|
|
3931
|
+
[["audio", "video"], mediaContract],
|
|
3932
|
+
[[
|
|
3933
|
+
"thead",
|
|
3934
|
+
"tbody",
|
|
3935
|
+
"tfoot"
|
|
3936
|
+
], tableBodyContract]
|
|
3937
|
+
]),
|
|
3938
|
+
table: tableContract,
|
|
3939
|
+
tr: tableRowContract,
|
|
3940
|
+
colgroup: colgroupContract,
|
|
3941
|
+
dl: dlContract,
|
|
3942
|
+
select: selectContract,
|
|
3943
|
+
optgroup: optgroupContract,
|
|
3944
|
+
datalist: datalistContract,
|
|
3945
|
+
picture: pictureContract,
|
|
3946
|
+
figure: figureContract,
|
|
3947
|
+
details: detailsContract,
|
|
3948
|
+
fieldset: fieldsetContract,
|
|
3949
|
+
dialog: dialogContract,
|
|
3950
|
+
object: objectContract,
|
|
3951
|
+
button: buttonContract,
|
|
3952
|
+
a: anchorContract,
|
|
3953
|
+
label: labelContract,
|
|
3954
|
+
p: pContract,
|
|
3955
|
+
head: headContract,
|
|
3956
|
+
html: htmlContract
|
|
3957
|
+
};
|
|
3958
|
+
//#endregion
|
|
3959
|
+
//#region ../core/src/html/evaluators.ts
|
|
3960
|
+
const htmlDiagnostics = warnDiagnostics;
|
|
3961
|
+
function buildEvaluatorMap() {
|
|
3962
|
+
const map = /* @__PURE__ */ new Map();
|
|
3963
|
+
iterate.forEachEntry(htmlContracts, (tag, { children, exclusiveChildren, allowText }) => {
|
|
3964
|
+
if (children?.length || exclusiveChildren || allowText === false) map.set(tag, new ChildrenEvaluator(children ?? [], htmlDiagnostics, `<${tag}>`, {
|
|
3965
|
+
exclusiveChildren,
|
|
3966
|
+
allowText
|
|
3967
|
+
}));
|
|
3968
|
+
});
|
|
3969
|
+
return map;
|
|
3970
|
+
}
|
|
3971
|
+
const HTML_EVALUATORS = buildEvaluatorMap();
|
|
3972
|
+
function getHtmlChildrenEvaluator(tag) {
|
|
3973
|
+
return typeof tag === "string" ? HTML_EVALUATORS.get(tag) : void 0;
|
|
3974
|
+
}
|
|
3975
|
+
//#endregion
|
|
3976
|
+
//#region ../core/src/html/prop-normalizers.ts
|
|
3977
|
+
const HTML_FORM_NORMALIZERS = /* @__PURE__ */ new Map([
|
|
3978
|
+
["button", [disabledProps]],
|
|
3979
|
+
["input", [
|
|
3980
|
+
disabledProps,
|
|
3981
|
+
readonlyProps,
|
|
3982
|
+
invalidProps
|
|
3983
|
+
]],
|
|
3984
|
+
["select", [disabledProps]],
|
|
3985
|
+
["textarea", [disabledProps, readonlyProps]],
|
|
3986
|
+
["fieldset", [disabledProps]],
|
|
3987
|
+
["optgroup", [disabledProps]]
|
|
3988
|
+
]);
|
|
3989
|
+
function getHtmlPropNormalizers(tag) {
|
|
3990
|
+
return typeof tag === "string" ? HTML_FORM_NORMALIZERS.get(tag) : void 0;
|
|
3991
|
+
}
|
|
3992
|
+
//#endregion
|
|
3993
|
+
//#region ../../lib/styling/src/cva.ts
|
|
3994
|
+
function cva$1(base, config) {
|
|
3995
|
+
const fn = cva(base, config);
|
|
3996
|
+
return (props) => cn(fn(props));
|
|
3997
|
+
}
|
|
3998
|
+
//#endregion
|
|
3999
|
+
//#region ../../lib/styling/src/static-class-resolver.ts
|
|
4000
|
+
var StaticClassResolver = class {
|
|
4001
|
+
#baseClass;
|
|
4002
|
+
#cache = new LRUCache(200);
|
|
4003
|
+
#resolveTag;
|
|
4004
|
+
constructor(baseClass, tagMap) {
|
|
4005
|
+
this.#baseClass = Array.isArray(baseClass) ? baseClass.join(" ") : baseClass;
|
|
4006
|
+
this.#resolveTag = tagMap ? (tag) => {
|
|
4007
|
+
const extra = tagMap[tag];
|
|
4008
|
+
if (!extra) return this.#baseClass;
|
|
4009
|
+
const extraStr = Array.isArray(extra) ? extra.join(" ") : extra;
|
|
4010
|
+
return `${this.#baseClass} ${extraStr}`;
|
|
4011
|
+
} : () => this.#baseClass;
|
|
4012
|
+
}
|
|
4013
|
+
resolve(tag, skipTagMap = false) {
|
|
4014
|
+
if (typeof tag !== "string" || skipTagMap) return this.#baseClass;
|
|
4015
|
+
const cached = this.#cache.get(tag);
|
|
4016
|
+
if (cached !== void 0) return cached;
|
|
4017
|
+
const result = this.#resolveTag(tag);
|
|
4018
|
+
this.#cache.set(tag, result);
|
|
4019
|
+
return result;
|
|
4020
|
+
}
|
|
4021
|
+
};
|
|
4022
|
+
//#endregion
|
|
4023
|
+
//#region ../../lib/styling/src/variant-class-resolver.ts
|
|
4024
|
+
/**
|
|
4025
|
+
* Runtime CVA class resolution with LRU + precomputed-map caching.
|
|
4026
|
+
*
|
|
4027
|
+
* Distinct from `variant-pass/` — `createVariantPass` is a composable pipeline stage for
|
|
4028
|
+
* build-time / plugin use (a `VariantConfig` → classes function), while this is the memoized
|
|
4029
|
+
* runtime resolver `createClassPipeline` calls per render.
|
|
4030
|
+
*
|
|
4031
|
+
* The precomputed map (`compileVariantLookup`, injected by the vite plugin) covers the
|
|
4032
|
+
* **no-recipe** combinations only — its keys are variant props alone, whereas this resolver's
|
|
4033
|
+
* cache keys are `recipe | variant props`, so a recipe-active call never hits a precomputed
|
|
4034
|
+
* entry and falls through to `#compute`. Recipe resolution stays fully runtime by design.
|
|
4035
|
+
*/
|
|
4036
|
+
var VariantClassResolver = class VariantClassResolver {
|
|
4037
|
+
#cvaFn;
|
|
4038
|
+
#recipeMap;
|
|
4039
|
+
#variantKeys;
|
|
4040
|
+
#precomputedClasses;
|
|
4041
|
+
#cache = new LRUCache(1e3);
|
|
4042
|
+
constructor(cvaFn, recipeMap, variantKeys, precomputedClasses) {
|
|
4043
|
+
this.#cvaFn = cvaFn ?? null;
|
|
4044
|
+
this.#recipeMap = Object.freeze(recipeMap ?? {});
|
|
4045
|
+
this.#variantKeys = variantKeys ?? null;
|
|
4046
|
+
this.#precomputedClasses = precomputedClasses ?? null;
|
|
4047
|
+
}
|
|
4048
|
+
resolve({ props, recipe }) {
|
|
4049
|
+
const normalizedKey = recipe ?? "__none__";
|
|
4050
|
+
const cacheKey = this.#createCacheKey(props, normalizedKey);
|
|
4051
|
+
if (this.#precomputedClasses !== null) {
|
|
4052
|
+
const precomputed = this.#precomputedClasses[cacheKey];
|
|
4053
|
+
if (precomputed !== void 0) return precomputed;
|
|
4054
|
+
}
|
|
4055
|
+
const cached = this.#cache.get(cacheKey);
|
|
4056
|
+
if (cached !== void 0) return cached;
|
|
4057
|
+
const result = this.#compute(props, recipe);
|
|
4058
|
+
this.#cache.set(cacheKey, result);
|
|
4059
|
+
return result;
|
|
4060
|
+
}
|
|
4061
|
+
#compute(props, recipe) {
|
|
4062
|
+
if (!this.#cvaFn) return "";
|
|
4063
|
+
if (recipe === void 0) return this.#cvaFn(props);
|
|
4064
|
+
const preset = this.#recipeMap[recipe];
|
|
4065
|
+
if (!preset) return this.#cvaFn(props);
|
|
4066
|
+
return this.#cvaFn({
|
|
4067
|
+
...preset,
|
|
4068
|
+
...props
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
4071
|
+
#createCacheKey(props, recipe) {
|
|
4072
|
+
if (this.#variantKeys !== null) {
|
|
4073
|
+
let key = recipe;
|
|
4074
|
+
iterate.forEachSet(this.#variantKeys, (k) => {
|
|
4075
|
+
if (k in props) key += `|${k}:${VariantClassResolver.#serializeValue(props[k])}`;
|
|
4076
|
+
});
|
|
4077
|
+
return key;
|
|
4078
|
+
}
|
|
4079
|
+
let key = recipe;
|
|
4080
|
+
iterate.forEach(Object.keys(props).sort(), (k) => {
|
|
4081
|
+
key += `|${k}:${VariantClassResolver.#serializeValue(props[k])}`;
|
|
4082
|
+
});
|
|
4083
|
+
return key;
|
|
4084
|
+
}
|
|
4085
|
+
static #serializeValue(value) {
|
|
4086
|
+
if (value === void 0) return "u";
|
|
4087
|
+
if (value === null) return "n";
|
|
4088
|
+
if (typeof value === "boolean") return `b:${value}`;
|
|
4089
|
+
if (typeof value === "string") return `s:${value}`;
|
|
4090
|
+
return `x:${String(value)}`;
|
|
4091
|
+
}
|
|
4092
|
+
};
|
|
4093
|
+
//#endregion
|
|
4094
|
+
//#region ../../lib/styling/src/create-class-pipeline.ts
|
|
4095
|
+
function createClassPipeline(resolved) {
|
|
4096
|
+
const baseClass = resolved.baseClassName ?? "";
|
|
4097
|
+
const cvaFn = resolved.variants ? cva$1("", {
|
|
4098
|
+
variants: resolved.variants,
|
|
4099
|
+
defaultVariants: resolved.defaultVariants,
|
|
4100
|
+
compoundVariants: resolved.compoundVariants
|
|
4101
|
+
}) : null;
|
|
4102
|
+
const variantKeys = resolved.variants ? new Set(Object.keys(resolved.variants)) : void 0;
|
|
4103
|
+
const staticResolver = new StaticClassResolver(baseClass, resolved.tagMap);
|
|
4104
|
+
const variantResolver = new VariantClassResolver(cvaFn, resolved.recipeMap, variantKeys, resolved.precomputedClasses);
|
|
4105
|
+
return function resolveClasses(tag, props, className, recipe) {
|
|
4106
|
+
const staticClasses = staticResolver.resolve(tag, recipe !== void 0);
|
|
4107
|
+
const variantClasses = variantResolver.resolve({
|
|
4108
|
+
props,
|
|
4109
|
+
recipe
|
|
4110
|
+
});
|
|
4111
|
+
if (!className) return (staticClasses && variantClasses ? `${staticClasses} ${variantClasses}` : staticClasses || variantClasses) || void 0;
|
|
4112
|
+
return cn(staticClasses, variantClasses, className);
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
//#endregion
|
|
4116
|
+
//#region ../core/src/options/resolve-factory-options.ts
|
|
4117
|
+
const EMPTY_VARIANT_KEYS = /* @__PURE__ */ new Set();
|
|
4118
|
+
function toNormalizeFn(normalize) {
|
|
4119
|
+
if (!Array.isArray(normalize)) return normalize;
|
|
4120
|
+
const fns = normalize;
|
|
4121
|
+
if (fns.length === 0) return void 0;
|
|
4122
|
+
if (fns.length === 1) return fns[0];
|
|
4123
|
+
return ((props) => {
|
|
4124
|
+
let acc = props;
|
|
4125
|
+
for (const fn of fns) acc = fn(acc);
|
|
4126
|
+
return acc;
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
4129
|
+
function composeNormalizers(normalizers, fn) {
|
|
4130
|
+
if (!normalizers?.length) return fn;
|
|
4131
|
+
return ((props) => {
|
|
4132
|
+
const patched = { ...props };
|
|
4133
|
+
for (const normalizer of normalizers) Object.assign(patched, normalizer(patched));
|
|
4134
|
+
const resolved = patched;
|
|
4135
|
+
return fn ? fn(resolved) : resolved;
|
|
4136
|
+
});
|
|
4137
|
+
}
|
|
4138
|
+
function whenDefined(key, value) {
|
|
4139
|
+
return value === void 0 ? {} : { [key]: value };
|
|
4140
|
+
}
|
|
4141
|
+
function mergeAriaRules(aria, rules) {
|
|
4142
|
+
if (!aria?.length) return rules;
|
|
4143
|
+
if (!rules?.length) return aria;
|
|
4144
|
+
return [...aria, ...rules];
|
|
4145
|
+
}
|
|
4146
|
+
function resolveFactoryOptions(options = {}) {
|
|
4147
|
+
const { styling, enforcement } = options;
|
|
4148
|
+
const composedNormalizeFn = composeNormalizers(enforcement?.props, toNormalizeFn(options.normalize));
|
|
4149
|
+
const variantKeys = styling?.variants === void 0 ? EMPTY_VARIANT_KEYS : new Set(Object.keys(styling.variants));
|
|
4150
|
+
return Object.freeze({
|
|
4151
|
+
defaultTag: options.tag ?? "div",
|
|
4152
|
+
diagnostics: resolveDiagnostics(enforcement?.diagnostics, options.diagnostics ?? silentDiagnostics),
|
|
4153
|
+
variantKeys,
|
|
4154
|
+
...whenDefined("displayName", options.name),
|
|
4155
|
+
...whenDefined("defaultProps", options.defaults),
|
|
4156
|
+
...whenDefined("baseClassName", styling?.base),
|
|
4157
|
+
...whenDefined("tagMap", styling?.tags),
|
|
4158
|
+
...whenDefined("recipeMap", styling?.presets),
|
|
4159
|
+
...whenDefined("variants", styling?.variants),
|
|
4160
|
+
...whenDefined("defaultVariants", styling?.defaults),
|
|
4161
|
+
...whenDefined("compoundVariants", styling?.compounds),
|
|
4162
|
+
...whenDefined("normalizeFn", composedNormalizeFn),
|
|
4163
|
+
...whenDefined("ariaRules", mergeAriaRules(enforcement?.aria, enforcement?.rules)),
|
|
4164
|
+
...whenDefined("childRules", enforcement?.children),
|
|
4165
|
+
...whenDefined("exclusiveChildren", enforcement?.exclusiveChildren),
|
|
4166
|
+
...whenDefined("allowText", enforcement?.allowText),
|
|
4167
|
+
...whenDefined("allowedAs", enforcement?.allowedAs),
|
|
4168
|
+
...whenDefined("precomputedClasses", styling?.precomputedClasses)
|
|
4169
|
+
});
|
|
4170
|
+
}
|
|
4171
|
+
//#endregion
|
|
4172
|
+
//#region ../core/src/options/validate-factory-options.ts
|
|
4173
|
+
/**
|
|
4174
|
+
* Construction-time validation of the variant surface.
|
|
4175
|
+
*
|
|
4176
|
+
* A `presets` selection or `defaults` entry that references a variant key — or a
|
|
4177
|
+
* value of a key — not declared in `variants` resolves to no class at runtime,
|
|
4178
|
+
* silently. TypeScript catches this in typed usage, but untyped JS consumers and
|
|
4179
|
+
* `as`-cast escapes bypass it. This mirrors the type contract at runtime: warn
|
|
4180
|
+
* (`strict: 'warn'`) or throw (`strict: 'throw'`/`true`); a no-op when `false`.
|
|
4181
|
+
*
|
|
4182
|
+
* Runs once per factory (not per render). Render-time checks — unknown
|
|
4183
|
+
* `recipe`, undefined variant value at the call site — are a separate
|
|
4184
|
+
* follow-up (they require `strict` threaded into the class resolver).
|
|
4185
|
+
*/
|
|
4186
|
+
function validateFactoryOptions(resolved, diagnostics) {
|
|
4187
|
+
if (!diagnostics.warnActive) return;
|
|
4188
|
+
const name = resolved.displayName ?? "Component";
|
|
4189
|
+
const { variants } = resolved;
|
|
4190
|
+
const checkSelection = (label, selection) => {
|
|
4191
|
+
iterate.forEachEntry(selection, (dim, value) => {
|
|
4192
|
+
if (value === void 0 || value === null) return;
|
|
4193
|
+
if (!variants || !Object.hasOwn(variants, dim)) {
|
|
4194
|
+
diagnostics.error(ContractDiagnostics.unknownVariantDim(name, label, dim));
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
4197
|
+
const states = variants[dim];
|
|
4198
|
+
const stateKey = String(value);
|
|
4199
|
+
if (!Object.hasOwn(states, stateKey)) diagnostics.error(ContractDiagnostics.unknownVariantValue(name, label, dim, stateKey, Object.keys(states)));
|
|
4200
|
+
});
|
|
4201
|
+
};
|
|
4202
|
+
const { recipeMap } = resolved;
|
|
4203
|
+
if (recipeMap) iterate.forEachEntry(recipeMap, (recipeKey, selection) => {
|
|
4204
|
+
checkSelection(`preset "${recipeKey}"`, selection);
|
|
4205
|
+
});
|
|
4206
|
+
if (resolved.defaultVariants) checkSelection("defaults", resolved.defaultVariants);
|
|
4207
|
+
}
|
|
4208
|
+
//#endregion
|
|
4209
|
+
//#region ../core/src/options/validate-render-props.ts
|
|
4210
|
+
function label(name) {
|
|
4211
|
+
return name ? `[${name}]` : "[createContractComponent]";
|
|
4212
|
+
}
|
|
4213
|
+
function validateRenderProps(diagnostics, options, props, recipeKey) {
|
|
4214
|
+
const { recipeMap, variants, displayName } = options;
|
|
4215
|
+
const tag = label(displayName);
|
|
4216
|
+
if (recipeKey !== void 0 && (!recipeMap || !Object.hasOwn(recipeMap, recipeKey))) diagnostics.error(ContractDiagnostics.unknownRecipeKey(tag, recipeKey));
|
|
4217
|
+
if (variants) iterate.forEachKey(variants, (key) => {
|
|
4218
|
+
if (!Object.hasOwn(props, key)) return;
|
|
4219
|
+
const value = props[key];
|
|
4220
|
+
if (value === void 0 || value === null) return;
|
|
4221
|
+
const dim = variants[key];
|
|
4222
|
+
if (dim && !Object.hasOwn(dim, String(value))) diagnostics.error(ContractDiagnostics.invalidVariantValue(tag, key, String(value)));
|
|
4223
|
+
});
|
|
4224
|
+
}
|
|
4225
|
+
//#endregion
|
|
4226
|
+
//#region ../../lib/pipeline-kit/src/factory/define-pipeline.ts
|
|
4227
|
+
/** Wraps a PipelineFactory, memoizing the built Pipeline by the resolved config's object
|
|
4228
|
+
* identity. Resolved options are already frozen per-component throughout this codebase
|
|
4229
|
+
* (e.g. resolveFactoryOptions's `Object.freeze(...)`), so a WeakMap keyed on that same frozen
|
|
4230
|
+
* reference is a safe, free cache — calling a factory twice with the same resolved reference
|
|
4231
|
+
* doesn't redundantly rebuild internal resolvers. Modeled on the defineConfig-style helper
|
|
4232
|
+
* convention (Vite/Vitest) for clean generic inference at the call site.
|
|
4233
|
+
*
|
|
4234
|
+
* Note: the return type is intentionally the plain `PipelineFactory`, not wrapped in
|
|
4235
|
+
* type-fest's `Simplify` — Simplify flattens object/intersection types for cleaner hover
|
|
4236
|
+
* tooltips, but applying it to a callable type breaks the contextual typing TypeScript needs
|
|
4237
|
+
* to infer the returned function's own parameter type. */
|
|
4238
|
+
function definePipeline(factory) {
|
|
4239
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
4240
|
+
return (resolved) => {
|
|
4241
|
+
const cached = cache.get(resolved);
|
|
4242
|
+
if (cached !== void 0) return cached;
|
|
4243
|
+
const pipeline = factory(resolved);
|
|
4244
|
+
cache.set(resolved, pipeline);
|
|
4245
|
+
return pipeline;
|
|
4246
|
+
};
|
|
4247
|
+
}
|
|
4248
|
+
//#endregion
|
|
4249
|
+
//#region ../core/src/factory/pipelines/generic.ts
|
|
4250
|
+
const createTagPipeline = (resolved) => makeResolveTag(resolved.defaultTag);
|
|
4251
|
+
const createPropsPipeline = (resolved) => (props) => mergeProps(resolved.defaultProps, props);
|
|
4252
|
+
const createHtmlPropNormalizersPipeline = () => getHtmlPropNormalizers;
|
|
4253
|
+
const createHtmlChildrenEvaluatorPipeline = () => getHtmlChildrenEvaluator;
|
|
4254
|
+
const createStylingClassPipeline = (resolved) => createClassPipeline(resolved);
|
|
4255
|
+
const memoizedTagPipeline = definePipeline(createTagPipeline);
|
|
4256
|
+
const memoizedPropsPipeline = definePipeline(createPropsPipeline);
|
|
4257
|
+
const memoizedHtmlPropNormalizersPipeline = definePipeline(createHtmlPropNormalizersPipeline);
|
|
4258
|
+
const memoizedHtmlChildrenEvaluatorPipeline = definePipeline(createHtmlChildrenEvaluatorPipeline);
|
|
4259
|
+
const memoizedClassPipeline = definePipeline(createStylingClassPipeline);
|
|
4260
|
+
//#endregion
|
|
4261
|
+
//#region ../core/src/factory/pipelines/aria.ts
|
|
4262
|
+
function resolveAriaRules(resolved) {
|
|
4263
|
+
return [.../* @__PURE__ */ new Set([...HTML_ARIA_RULES, ...resolved.ariaRules ?? []])];
|
|
4264
|
+
}
|
|
4265
|
+
const createAriaPipeline = (resolved) => {
|
|
4266
|
+
const rules = resolveAriaRules(resolved);
|
|
4267
|
+
const engine = new AriaPolicyEngine(resolved.diagnostics, {
|
|
4268
|
+
...rules.length > 0 && { rules },
|
|
4269
|
+
variantKeys: resolved.variantKeys
|
|
4270
|
+
});
|
|
4271
|
+
return (tag, props, extraProps) => engine.validate(tag, props, extraProps);
|
|
4272
|
+
};
|
|
4273
|
+
const memoizedAriaPipeline = definePipeline(createAriaPipeline);
|
|
4274
|
+
function resolveAriaPassthrough(_tag, props, _extraProps) {
|
|
4275
|
+
return { props };
|
|
4276
|
+
}
|
|
4277
|
+
//#endregion
|
|
4278
|
+
//#region ../core/src/factory/plugin-diagnostics.ts
|
|
4279
|
+
const PluginDiagnostics = {
|
|
4280
|
+
invalidShape(received) {
|
|
4281
|
+
const got = isNull(received) ? "null" : typeof received;
|
|
4282
|
+
return {
|
|
4283
|
+
code: DiagnosticCode.PluginInvalidShape,
|
|
4284
|
+
category: DiagnosticCategory.Internal,
|
|
4285
|
+
message: `[praxis-kit] Plugin factory must return an object with a 'pipeline' function. Got: ${got}.`
|
|
4286
|
+
};
|
|
4287
|
+
},
|
|
4288
|
+
pipelineReturnType(received) {
|
|
4289
|
+
const got = isNull(received) ? "null" : Array.isArray(received) ? "array" : typeof received;
|
|
4290
|
+
return {
|
|
4291
|
+
code: DiagnosticCode.PluginPipelineReturnType,
|
|
4292
|
+
category: DiagnosticCategory.Internal,
|
|
4293
|
+
message: `[praxis-kit] Plugin pipeline must return a string. Got: ${got}.`
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
};
|
|
4297
|
+
//#endregion
|
|
4298
|
+
//#region ../core/src/factory/plugin-invariants.ts
|
|
4299
|
+
function assertPluginShape(result) {
|
|
4300
|
+
if (isNull(result) || !isObject(result)) throwDiagnostics.error(PluginDiagnostics.invalidShape(result));
|
|
4301
|
+
if (!isFunction(result.pipeline)) throwDiagnostics.error(PluginDiagnostics.invalidShape(result));
|
|
4302
|
+
}
|
|
4303
|
+
function guardPipeline(pipeline) {
|
|
4304
|
+
if (process.env.NODE_ENV === "production") return pipeline;
|
|
4305
|
+
return function guardedPipeline(tag, props, className, recipe) {
|
|
4306
|
+
const result = pipeline(tag, props, className, recipe);
|
|
4307
|
+
if (!isString(result)) throwDiagnostics.error(PluginDiagnostics.pipelineReturnType(result));
|
|
4308
|
+
return result;
|
|
4309
|
+
};
|
|
4310
|
+
}
|
|
4311
|
+
//#endregion
|
|
4312
|
+
//#region ../core/src/factory/pipelines/styling.ts
|
|
4313
|
+
function resolveClassPlugin(factory, resolved, diagnostics) {
|
|
4314
|
+
if (!factory) return {
|
|
4315
|
+
pluginResult: void 0,
|
|
4316
|
+
classPipeline: memoizedClassPipeline(resolved)
|
|
4317
|
+
};
|
|
4318
|
+
const pluginResult = factory(resolved, diagnostics);
|
|
4319
|
+
assertPluginShape(pluginResult);
|
|
4320
|
+
return {
|
|
4321
|
+
pluginResult,
|
|
4322
|
+
classPipeline: guardPipeline(pluginResult.pipeline)
|
|
4323
|
+
};
|
|
4324
|
+
}
|
|
4325
|
+
//#endregion
|
|
4326
|
+
//#region ../core/src/factory/create-polymorphic.ts
|
|
4327
|
+
function eraseResolvedShape(resolved) {
|
|
4328
|
+
return resolved;
|
|
4329
|
+
}
|
|
4330
|
+
function createPolymorphic(options = {}) {
|
|
4331
|
+
const baseResolved = resolveFactoryOptions(options);
|
|
4332
|
+
const anyBaseResolved = eraseResolvedShape(baseResolved);
|
|
4333
|
+
const resolved = Object.freeze({
|
|
4334
|
+
...baseResolved,
|
|
4335
|
+
htmlPropNormalizersFn: memoizedHtmlPropNormalizersPipeline(anyBaseResolved),
|
|
4336
|
+
htmlChildrenEvaluatorFn: memoizedHtmlChildrenEvaluatorPipeline(anyBaseResolved)
|
|
4337
|
+
});
|
|
4338
|
+
const anyResolved = eraseResolvedShape(resolved);
|
|
4339
|
+
if (process.env.NODE_ENV !== "production") validateFactoryOptions(resolved, resolved.diagnostics);
|
|
4340
|
+
const { pluginResult, classPipeline } = resolveClassPlugin(options.styling?.plugin, anyResolved, resolved.diagnostics);
|
|
4341
|
+
const resolveTag = memoizedTagPipeline(anyResolved);
|
|
4342
|
+
const resolveProps = memoizedPropsPipeline(anyResolved);
|
|
4343
|
+
const resolveAriaFn = options.enforcement !== void 0 ? memoizedAriaPipeline(anyResolved) : resolveAriaPassthrough;
|
|
4344
|
+
const methods = {
|
|
4345
|
+
resolveTag,
|
|
4346
|
+
resolveProps,
|
|
4347
|
+
resolveClasses(tag, props, className, recipe) {
|
|
4348
|
+
if (process.env.NODE_ENV !== "production") validateRenderProps(resolved.diagnostics, resolved, props, recipe);
|
|
4349
|
+
return classPipeline(tag, props, className, recipe) || void 0;
|
|
4350
|
+
},
|
|
4351
|
+
resolveAria(tag, props, extraProps) {
|
|
4352
|
+
return resolveAriaFn(tag, props, extraProps);
|
|
4353
|
+
}
|
|
4354
|
+
};
|
|
4355
|
+
return pluginResult ? {
|
|
4356
|
+
...methods,
|
|
4357
|
+
options: resolved,
|
|
4358
|
+
hasStyling: true,
|
|
4359
|
+
classPlugin: pluginResult
|
|
4360
|
+
} : {
|
|
4361
|
+
...methods,
|
|
4362
|
+
options: resolved
|
|
4363
|
+
};
|
|
4364
|
+
}
|
|
4365
|
+
//#endregion
|
|
4366
|
+
//#region ../core/src/resolver/resolver.ts
|
|
4367
|
+
function enforceAllowedAs(tag, allowedAs, diagnostics, displayName) {
|
|
4368
|
+
if (allowedAs.includes(tag)) return;
|
|
4369
|
+
if (!diagnostics) return;
|
|
4370
|
+
const component = displayName ?? String(tag);
|
|
4371
|
+
diagnostics.error(ContractDiagnostics.allowedAsViolation(String(tag), allowedAs, component));
|
|
4372
|
+
}
|
|
4373
|
+
//#endregion
|
|
4374
|
+
//#region ../../lib/adapter-utils/src/runtime/build-core-runtime.ts
|
|
4375
|
+
const EMPTY_SET = /* @__PURE__ */ new Set();
|
|
4376
|
+
function buildCoreRuntime(normalized) {
|
|
4377
|
+
const runtime = createPolymorphic(normalized);
|
|
4378
|
+
return {
|
|
4379
|
+
runtime,
|
|
4380
|
+
ownedKeys: "classPlugin" in runtime ? runtime.classPlugin.ownedKeys ?? EMPTY_SET : EMPTY_SET
|
|
4381
|
+
};
|
|
4382
|
+
}
|
|
4383
|
+
//#endregion
|
|
4384
|
+
//#region ../../lib/adapter-utils/src/runtime/build-engines.ts
|
|
4385
|
+
function buildEngines(diagnostics, childRules, context, childrenOptions) {
|
|
4386
|
+
const { exclusiveChildren, allowText } = childrenOptions ?? {};
|
|
4387
|
+
return childRules?.length || exclusiveChildren || allowText === false ? { childrenEvaluator: new ChildrenEvaluator(childRules ?? [], diagnostics, context, {
|
|
4388
|
+
exclusiveChildren,
|
|
4389
|
+
allowText
|
|
4390
|
+
}) } : {};
|
|
4391
|
+
}
|
|
4392
|
+
//#endregion
|
|
4393
|
+
//#region ../../lib/adapter-utils/src/runtime/resolve-adapter-common-options.ts
|
|
4394
|
+
/**
|
|
4395
|
+
* Resolves the two fields that every adapter's `normalizeOptions` must provide:
|
|
4396
|
+
* `name` (required string) and `diagnostics` (required Diagnostics instance).
|
|
4397
|
+
*
|
|
4398
|
+
* Called by each adapter's `normalizeOptions` and spread into the result:
|
|
4399
|
+
*
|
|
4400
|
+
* ```ts
|
|
4401
|
+
* function normalizeOptions(options): NormalizedOptions {
|
|
4402
|
+
* return { ...options, ...resolveAdapterCommonOptions(options) } as NormalizedOptions
|
|
4403
|
+
* }
|
|
4404
|
+
* ```
|
|
4405
|
+
*
|
|
4406
|
+
* The defaults (`'PolymorphicComponent'` and `throwDiagnostics`) match the convention used
|
|
4407
|
+
* by React, Vue, Preact, Solid, and Svelte. Adapters with different defaults
|
|
4408
|
+
* (e.g. Lit, Web: `silentDiagnostics`) pass an override argument.
|
|
4409
|
+
*/
|
|
4410
|
+
function resolveAdapterCommonOptions(options, defaultName = "PolymorphicComponent", defaultDiagnostics = throwDiagnostics$1) {
|
|
4411
|
+
return {
|
|
4412
|
+
name: options.name ?? defaultName,
|
|
4413
|
+
diagnostics: resolveDiagnostics(options.enforcement?.diagnostics, defaultDiagnostics)
|
|
4414
|
+
};
|
|
4415
|
+
}
|
|
4416
|
+
//#endregion
|
|
4417
|
+
//#region ../../lib/adapter-utils/src/props/apply-filter.ts
|
|
4418
|
+
function applyFilter(props, filterProps, variantKeys) {
|
|
4419
|
+
const out = {};
|
|
4420
|
+
iterate.forEachEntry(props, (k) => {
|
|
4421
|
+
if (!Object.hasOwn(props, k)) return;
|
|
4422
|
+
if (filterProps(k, variantKeys)) return;
|
|
4423
|
+
out[k] = props[k];
|
|
4424
|
+
});
|
|
4425
|
+
return out;
|
|
4426
|
+
}
|
|
4427
|
+
//#endregion
|
|
4428
|
+
//#region ../../lib/adapter-utils/src/props/resolve-normalized-props.ts
|
|
4429
|
+
/**
|
|
4430
|
+
* The single canonical prop-normalization step for every render path — the five
|
|
4431
|
+
* VDOM adapters (React, Preact, Vue, Solid, Svelte) and the host-state path
|
|
4432
|
+
* shared by Lit, Web, and SSR.
|
|
4433
|
+
*
|
|
4434
|
+
* Ordering is fixed and load-bearing:
|
|
4435
|
+
*
|
|
4436
|
+
* ```text
|
|
4437
|
+
* mergedProps → HTML built-in normalizers → normalizeFn
|
|
4438
|
+
* ```
|
|
4439
|
+
*
|
|
4440
|
+
* `normalizeFn` is the primitive's composed `enforcement.props` normalizers plus
|
|
4441
|
+
* the caller's `normalize` option (folded together by `composeNormalizers`).
|
|
4442
|
+
* Running it last means a caller's `normalize` always observes — and can
|
|
4443
|
+
* override — an HTML built-in's output for the same key. This must be identical
|
|
4444
|
+
* across adapters: the same component with the same props has to normalize the
|
|
4445
|
+
* same way whether it renders through React or through SSR.
|
|
4446
|
+
*
|
|
4447
|
+
* `mergedProps` must already be the result of `runtime.resolveProps(rest)`. The
|
|
4448
|
+
* input object is never mutated; a copy is taken only when an HTML normalizer
|
|
4449
|
+
* actually runs — most tags have none (`htmlPropNormalizersFn` returns
|
|
4450
|
+
* `undefined` for every non-form element).
|
|
4451
|
+
*/
|
|
4452
|
+
function resolveNormalizedProps(options, tag, mergedProps) {
|
|
4453
|
+
const htmlNormalizers = options.htmlPropNormalizersFn?.(tag);
|
|
4454
|
+
let base = mergedProps;
|
|
4455
|
+
if (htmlNormalizers?.length) {
|
|
4456
|
+
base = { ...mergedProps };
|
|
4457
|
+
for (const normalize of htmlNormalizers) Object.assign(base, normalize(base));
|
|
4458
|
+
}
|
|
4459
|
+
return typeof options.normalizeFn === "function" ? options.normalizeFn(base) : base;
|
|
4460
|
+
}
|
|
4461
|
+
//#endregion
|
|
4462
|
+
//#region ../../lib/adapter-utils/src/props/compose-filter.ts
|
|
4463
|
+
function composeFilter(ownedKeys, filterProps) {
|
|
4464
|
+
const defaultFilter = (key, variantKeys) => variantKeys.has(key) || ownedKeys.has(key);
|
|
4465
|
+
if (!filterProps) return defaultFilter;
|
|
4466
|
+
return (key, variantKeys) => defaultFilter(key, variantKeys) || filterProps(key, variantKeys);
|
|
4467
|
+
}
|
|
4468
|
+
//#endregion
|
|
4469
|
+
//#region ../../lib/adapter-utils/src/render/render-to-string.ts
|
|
4470
|
+
const SAFE_TAG_NAME = /^[a-zA-Z][a-zA-Z0-9-]*$/;
|
|
4471
|
+
function assertSerializableTag(tag) {
|
|
4472
|
+
if (!SAFE_TAG_NAME.test(tag)) throw new Error(`[renderBundleToString] refusing to serialize invalid tag name "${tag}" — an SSR tag must match /^[a-zA-Z][a-zA-Z0-9-]*\$/ (a valid HTML or custom-element tag name). It is written directly into the returned markup with no further escaping possible, so an invalid value can only be rejected, never sanitized.`);
|
|
4473
|
+
}
|
|
4474
|
+
function escapeAttr(value) {
|
|
4475
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
4476
|
+
}
|
|
4477
|
+
function buildAttrString(attributes) {
|
|
4478
|
+
const parts = [];
|
|
4479
|
+
iterate.forEachEntry(attributes, (key, value) => {
|
|
4480
|
+
if (value === false || value === null || value === void 0) return;
|
|
4481
|
+
if (value === true) parts.push(key);
|
|
4482
|
+
else parts.push(`${key}="${escapeAttr(String(value))}"`);
|
|
4483
|
+
});
|
|
4484
|
+
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
4485
|
+
}
|
|
4486
|
+
/**
|
|
4487
|
+
* Renders a praxis-kit non-VDOM component (Lit, Web) to an HTML string without
|
|
4488
|
+
* requiring a DOM. Tag polymorphism works correctly in SSR — `options.tag` and the
|
|
4489
|
+
* `as` prop are resolved directly to the HTML element tag.
|
|
4490
|
+
*
|
|
4491
|
+
* `innerHTML` is assumed to be trusted, already-escaped HTML and is inserted
|
|
4492
|
+
* verbatim — it is not escaped here. Callers are responsible for escaping any
|
|
4493
|
+
* untrusted content before passing it.
|
|
4494
|
+
*
|
|
4495
|
+
* The resolved tag, by contrast, is validated unconditionally (`assertSerializableTag`, above) —
|
|
4496
|
+
* it has no legal escaping once interpolated into `<${tag}...>`, so unlike `innerHTML` this is not
|
|
4497
|
+
* a "caller's responsibility" boundary: an `as` prop that resolves to anything other than a
|
|
4498
|
+
* syntactically valid tag name throws here rather than reaching the returned string.
|
|
4499
|
+
*/
|
|
4500
|
+
function renderBundleToString(bundle, props = {}, innerHTML = "") {
|
|
4501
|
+
const { filterProps, runtime: { options, resolveAria, resolveClasses, resolveProps, resolveTag } } = bundle;
|
|
4502
|
+
const { as, className, recipe, class: classAttr, ...rest } = props;
|
|
4503
|
+
const tag = resolveTag(as);
|
|
4504
|
+
assertSerializableTag(tag);
|
|
4505
|
+
if (options.allowedAs !== void 0) enforceAllowedAs(tag, options.allowedAs, options.diagnostics, options.displayName);
|
|
4506
|
+
const finalProps = resolveNormalizedProps(options, tag, resolveProps(rest));
|
|
4507
|
+
const resolvedClass = resolveClasses(tag, finalProps, className ?? classAttr, recipe);
|
|
4508
|
+
return `<${tag}${buildAttrString({
|
|
4509
|
+
...applyFilter(resolveAria(tag, finalProps).props, filterProps, options.variantKeys),
|
|
4510
|
+
class: resolvedClass || void 0
|
|
4511
|
+
})}>${innerHTML}</${tag}>`;
|
|
4512
|
+
}
|
|
4513
|
+
//#endregion
|
|
4514
|
+
//#region ../../lib/adapter-utils/src/render/host-state.ts
|
|
4515
|
+
/**
|
|
4516
|
+
* Runtime shape guard for the value returned by an adapter's buildRuntime() —
|
|
4517
|
+
* shared between Lit and Web, whose bundle shape (and validation needs) are
|
|
4518
|
+
* identical. childrenEvaluator is optional and adapter-owned; not part of
|
|
4519
|
+
* SsrBundle's declared type since resolveHostState below never reads it, but
|
|
4520
|
+
* still checked here since a malformed one indicates a real construction bug.
|
|
4521
|
+
*/
|
|
4522
|
+
function isLooseBundle(arg) {
|
|
4523
|
+
if (!isObject(arg)) return false;
|
|
4524
|
+
const { runtime, filterProps, childrenEvaluator } = arg;
|
|
4525
|
+
if (!isObject(runtime)) return false;
|
|
4526
|
+
if (!isObject(runtime["options"])) return false;
|
|
4527
|
+
if (typeof runtime["resolveTag"] !== "function" || typeof runtime["resolveProps"] !== "function" || typeof runtime["resolveClasses"] !== "function" || typeof runtime["resolveAria"] !== "function") return false;
|
|
4528
|
+
if (typeof filterProps !== "function") return false;
|
|
4529
|
+
if (childrenEvaluator !== void 0 && (!isObject(childrenEvaluator) || typeof childrenEvaluator["evaluate"] !== "function")) return false;
|
|
4530
|
+
return true;
|
|
4531
|
+
}
|
|
4532
|
+
function toLooseBundle(bundle) {
|
|
4533
|
+
if (!isLooseBundle(bundle)) throw new Error("[createContractComponent] buildRuntime returned an unexpected shape.");
|
|
4534
|
+
return bundle;
|
|
4535
|
+
}
|
|
4536
|
+
/**
|
|
4537
|
+
* Pure and side-effect-free (no enforceAllowedAs diagnostic): resolves the tag
|
|
4538
|
+
* and normalized props for the current attributes, nothing more.
|
|
4539
|
+
*
|
|
4540
|
+
* Exported separately from resolveHostState so a caller that only needs
|
|
4541
|
+
* tag/normalizedProps — e.g. Lit's render(), which runs before _applyPraxis()'s
|
|
4542
|
+
* full resolveHostState() call in the same update cycle and has nothing to
|
|
4543
|
+
* reuse yet — doesn't have to trigger resolveClasses/resolveAria/applyFilter,
|
|
4544
|
+
* and critically doesn't double-fire enforceAllowedAs's warning.
|
|
4545
|
+
*/
|
|
4546
|
+
function resolveTagAndNormalizedProps(bundle, props) {
|
|
4547
|
+
const { as, className: _className, recipe: _recipe, ...rest } = props;
|
|
4548
|
+
const { options, resolveProps, resolveTag } = bundle.runtime;
|
|
4549
|
+
const tag = resolveTag(as);
|
|
4550
|
+
return {
|
|
4551
|
+
tag,
|
|
4552
|
+
normalizedProps: resolveNormalizedProps(options, tag, resolveProps(rest))
|
|
4553
|
+
};
|
|
4554
|
+
}
|
|
4555
|
+
/**
|
|
4556
|
+
* Pure: resolves the class string and filtered attribute map from current props.
|
|
4557
|
+
* Shared between Lit and Web — same pipeline, only the surrounding element
|
|
4558
|
+
* lifecycle (how `props` gets built, when this gets called) differs per adapter.
|
|
4559
|
+
*
|
|
4560
|
+
* Separating resolution from DOM mutation keeps this testable without a real
|
|
4561
|
+
* DOM and keeps diffAndApplyAttributes a simple write-only function.
|
|
4562
|
+
*/
|
|
4563
|
+
function resolveHostState(bundle, props) {
|
|
4564
|
+
const { className, recipe } = props;
|
|
4565
|
+
const { options, resolveAria, resolveClasses } = bundle.runtime;
|
|
4566
|
+
const { tag, normalizedProps: finalProps } = resolveTagAndNormalizedProps(bundle, props);
|
|
4567
|
+
if (options.allowedAs !== void 0) enforceAllowedAs(tag, options.allowedAs, options.diagnostics, options.displayName);
|
|
4568
|
+
return {
|
|
4569
|
+
className: resolveClasses(tag, finalProps, className, recipe),
|
|
4570
|
+
attributes: applyFilter(resolveAria(tag, finalProps).props, bundle.filterProps, options.variantKeys),
|
|
4571
|
+
tag,
|
|
4572
|
+
normalizedProps: finalProps
|
|
4573
|
+
};
|
|
4574
|
+
}
|
|
4575
|
+
/**
|
|
4576
|
+
* Applies resolved state to a host element. Shared between Lit and Web — both
|
|
4577
|
+
* hosts are HTMLElement subclasses and the diffing algorithm is identical.
|
|
4578
|
+
*
|
|
4579
|
+
* `prevPipelineAttrs` tracks which attribute keys the pipeline set on the
|
|
4580
|
+
* previous run. Any key present there but absent from the new state is
|
|
4581
|
+
* explicitly removed, preventing stale pipeline-managed attributes from
|
|
4582
|
+
* accumulating across runs.
|
|
4583
|
+
*
|
|
4584
|
+
* Variant key attributes are intentionally never removed here — removing them
|
|
4585
|
+
* would re-trigger each adapter's own attribute-change callback (Lit's
|
|
4586
|
+
* requestUpdate, the native attributeChangedCallback), creating a feedback loop.
|
|
4587
|
+
*/
|
|
4588
|
+
function diffAndApplyAttributes(host, state, prevPipelineAttrs, incomingProps) {
|
|
4589
|
+
const hasOwn = Object.hasOwn;
|
|
4590
|
+
const attrs = state.attributes;
|
|
4591
|
+
if (state.className) host.className = state.className;
|
|
4592
|
+
else host.removeAttribute("class");
|
|
4593
|
+
for (const key of prevPipelineAttrs) if (!hasOwn(attrs, key)) host.removeAttribute(key);
|
|
4594
|
+
prevPipelineAttrs.clear();
|
|
4595
|
+
for (const key in incomingProps) {
|
|
4596
|
+
if (!hasOwn(incomingProps, key)) continue;
|
|
4597
|
+
if (!key.startsWith("aria-") && key !== "role") continue;
|
|
4598
|
+
if (!hasOwn(attrs, key)) host.removeAttribute(key);
|
|
4599
|
+
}
|
|
4600
|
+
for (const key in attrs) {
|
|
4601
|
+
if (!hasOwn(attrs, key)) continue;
|
|
4602
|
+
const value = attrs[key];
|
|
4603
|
+
if (value === void 0 || value === null || value === false) host.removeAttribute(key);
|
|
4604
|
+
else if (value === true) {
|
|
4605
|
+
host.setAttribute(key, "");
|
|
4606
|
+
prevPipelineAttrs.add(key);
|
|
4607
|
+
} else {
|
|
4608
|
+
host.setAttribute(key, String(value));
|
|
4609
|
+
prevPipelineAttrs.add(key);
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
//#endregion
|
|
4614
|
+
//#region ../../adapters/web/src/build-runtime.ts
|
|
4615
|
+
function buildRuntime(options) {
|
|
4616
|
+
const normalized = {
|
|
4617
|
+
...options,
|
|
4618
|
+
...resolveAdapterCommonOptions(options, "PolymorphicElement", silentDiagnostics)
|
|
4619
|
+
};
|
|
4620
|
+
const { filterProps: customFilter, enforcement } = normalized;
|
|
4621
|
+
const { runtime, ownedKeys } = buildCoreRuntime(normalized);
|
|
4622
|
+
const { childrenEvaluator } = buildEngines(normalized.diagnostics, enforcement?.children, normalized.name, {
|
|
4623
|
+
exclusiveChildren: enforcement?.exclusiveChildren,
|
|
4624
|
+
allowText: enforcement?.allowText
|
|
4625
|
+
});
|
|
4626
|
+
return {
|
|
4627
|
+
runtime,
|
|
4628
|
+
filterProps: composeFilter(ownedKeys, customFilter),
|
|
4629
|
+
diagnostics: normalized.diagnostics,
|
|
4630
|
+
...childrenEvaluator !== void 0 && { childrenEvaluator }
|
|
4631
|
+
};
|
|
4632
|
+
}
|
|
4633
|
+
//#endregion
|
|
4634
|
+
//#region ../../adapters/web/src/is-web-contract-component.ts
|
|
4635
|
+
/**
|
|
4636
|
+
* Type guard narrowing a generated class down to the public
|
|
4637
|
+
* `WebContractComponent<TVariants, TPluginProps>` interface the adapter
|
|
4638
|
+
* exposes.
|
|
4639
|
+
*
|
|
4640
|
+
* Like Lit's contract type, this describes an actual constructable class
|
|
4641
|
+
* (`new (): ...`) plus a required `diagnostics` static property — both real
|
|
4642
|
+
* runtime facts to check, beyond the generic-erasure boundary every guard
|
|
4643
|
+
* in this codebase hits.
|
|
4644
|
+
*
|
|
4645
|
+
* In SSR (Node) environments `HTMLElement` itself is undefined — the class
|
|
4646
|
+
* still gets created (as a stub, see `BaseElement` in
|
|
4647
|
+
* create-contract-component.ts) so `registerForSsr` can register it, but
|
|
4648
|
+
* there's no `HTMLElement` to check `instanceof` against there, so that part
|
|
4649
|
+
* of the check is skipped when `HTMLElement` doesn't exist.
|
|
4650
|
+
*
|
|
4651
|
+
* `WebContractComponent`'s third parameter, `G` (the phantom `__generics`
|
|
4652
|
+
* marker `GenericsOf`/`ContractProps` read back — `./types/contract-props`), is
|
|
4653
|
+
* left at its default here: it carries no runtime signal for this guard to
|
|
4654
|
+
* check, and `createContractComponent` supplies the real `G` itself when it
|
|
4655
|
+
* casts this function's narrowed result to its public return type.
|
|
4656
|
+
*/
|
|
4657
|
+
function isWebContractComponent(value) {
|
|
4658
|
+
if (!isFunction(value)) return false;
|
|
4659
|
+
if (typeof HTMLElement !== "undefined" && !(value.prototype instanceof HTMLElement)) return false;
|
|
4660
|
+
if (!("diagnostics" in value)) return false;
|
|
4661
|
+
return isObject(value.diagnostics);
|
|
4662
|
+
}
|
|
4663
|
+
//#endregion
|
|
4664
|
+
//#region ../../adapters/web/src/render-to-string.ts
|
|
4665
|
+
const ssrRegistry = /* @__PURE__ */ new WeakMap();
|
|
4666
|
+
/** Called by createContractComponent to enable renderContractToString for a class. */
|
|
4667
|
+
function registerForSsr(cls, bundle) {
|
|
4668
|
+
ssrRegistry.set(cls, { bundle });
|
|
4669
|
+
}
|
|
4670
|
+
/**
|
|
4671
|
+
* Serializes a praxis-kit web component's **resolved contract** to an HTML string, without
|
|
4672
|
+
* requiring a DOM — not Custom Element SSR, and not a hydration mechanism.
|
|
4673
|
+
*
|
|
4674
|
+
* This distinction matters and is easy to get wrong: the output element is `options.tag`
|
|
4675
|
+
* (`<button>…</button>`, say), never the registered custom-element tag (`<praxis-button>`) —
|
|
4676
|
+
* `createContractComponent`'s own doc comment covers why the two are different concepts ("the
|
|
4677
|
+
* element Praxis models" vs. "the element actually in the DOM"). `customElements.define()` happens
|
|
4678
|
+
* externally, after `createContractComponent()` returns, and this function has no way to know what
|
|
4679
|
+
* name (if any) a caller eventually registers the class under, so there is no tag it could emit
|
|
4680
|
+
* that's guaranteed to match. Even if it could, the browser's Custom Element upgrade mechanism only
|
|
4681
|
+
* upgrades an exact tag-name match — a server-sent `<button>` can never become the live
|
|
4682
|
+
* `<praxis-button>` no matter what the client bundle does, so this was never a viable
|
|
4683
|
+
* SSR-then-upgrade path regardless of naming.
|
|
4684
|
+
*
|
|
4685
|
+
* Use this for what it actually is: previewing/testing the resolved styling + ARIA + attribute
|
|
4686
|
+
* pipeline as plain HTML (static generation, snapshot tests, non-interactive contexts) — not for
|
|
4687
|
+
* server-rendering markup you intend the live custom element to take over.
|
|
4688
|
+
*
|
|
4689
|
+
* `innerHTML` is treated as a pre-sanitized HTML string and inserted verbatim.
|
|
4690
|
+
* Callers are responsible for escaping any untrusted content before passing it.
|
|
4691
|
+
*/
|
|
4692
|
+
function renderContractToString(component, props = {}, innerHTML = "") {
|
|
4693
|
+
const entry = ssrRegistry.get(component);
|
|
4694
|
+
if (!entry) {
|
|
4695
|
+
const name = component.name ?? "AnonymousComponent";
|
|
4696
|
+
throw new Error(`[renderContractToString] ${name} was not registered for SSR. Ensure it was created with createContractComponent from praxis-kit/web.`);
|
|
4697
|
+
}
|
|
4698
|
+
const { as: _as, ...rest } = props;
|
|
4699
|
+
return renderBundleToString(entry.bundle, rest, innerHTML);
|
|
4700
|
+
}
|
|
4701
|
+
//#endregion
|
|
4702
|
+
//#region ../../adapters/web/src/to-web-factory-options.ts
|
|
4703
|
+
/** Web-specific addition on top of `FactoryOptions`. */
|
|
4704
|
+
const WEB_FIELD_VALIDATORS = { filterProps: (v) => v === void 0 || isFunction(v) };
|
|
4705
|
+
/**
|
|
4706
|
+
* Type guard narrowing the generic `FactoryOptions` shape down to
|
|
4707
|
+
* `WebFactoryOptions` — the type `buildRuntime` is declared against. See
|
|
4708
|
+
* `isFactoryOptionsLike` for what this does and doesn't validate.
|
|
4709
|
+
*/
|
|
4710
|
+
function isWebFactoryOptions(options) {
|
|
4711
|
+
return isFactoryOptionsLike(options, WEB_FIELD_VALIDATORS);
|
|
4712
|
+
}
|
|
4713
|
+
//#endregion
|
|
4714
|
+
//#region ../../adapters/web/src/create-contract-component.ts
|
|
4715
|
+
/**
|
|
4716
|
+
* Creates a plain `HTMLElement` subclass with praxis-kit contracts applied.
|
|
4717
|
+
*
|
|
4718
|
+
* No framework dependency. Register with `customElements.define()`:
|
|
4719
|
+
*
|
|
4720
|
+
* ```ts
|
|
4721
|
+
* const Button = createContractComponent({
|
|
4722
|
+
* tag: 'button',
|
|
4723
|
+
* name: 'Button',
|
|
4724
|
+
* styling: {
|
|
4725
|
+
* base: 'btn',
|
|
4726
|
+
* variants: { intent: { primary: 'btn--primary', ghost: 'btn--ghost' } },
|
|
4727
|
+
* defaults: { intent: 'primary' },
|
|
4728
|
+
* },
|
|
4729
|
+
* enforcement: { diagnostics: warnDiagnostics },
|
|
4730
|
+
* })
|
|
4731
|
+
*
|
|
4732
|
+
* customElements.define('praxis-button', Button)
|
|
4733
|
+
* ```
|
|
4734
|
+
*
|
|
4735
|
+
* The pipeline runs synchronously on `connectedCallback` and on every
|
|
4736
|
+
* `attributeChangedCallback` for praxis-owned attributes (variant keys,
|
|
4737
|
+
* `variant-key`, `praxis-class`).
|
|
4738
|
+
*
|
|
4739
|
+
* For non-reactive attributes (`aria-*`, `role`, `data-*`) — or a praxis-owned
|
|
4740
|
+
* property set directly rather than via `setAttribute` (property assignment
|
|
4741
|
+
* alone never fires `attributeChangedCallback`) — call `element.update()` after
|
|
4742
|
+
* to trigger an explicit pipeline re-run.
|
|
4743
|
+
*
|
|
4744
|
+
* **This adapter is a Custom Element host carrying a Praxis semantic contract — not a
|
|
4745
|
+
* reimplementation of native HTML element behavior.** `options.tag` (`'button'` above) names the
|
|
4746
|
+
* _intrinsic model_ Praxis resolves ARIA roles, content-model rules, and built-in prop
|
|
4747
|
+
* normalizers (`disabledProps`, etc.) against — it is not, and was never meant to be, the tag
|
|
4748
|
+
* actually written to the DOM. The DOM tag is whatever name a caller later passes to
|
|
4749
|
+
* `customElements.define(name, Button)`, entirely separate from `options.tag` and not knowable by
|
|
4750
|
+
* this function at all (registration happens externally, after this returns, possibly under
|
|
4751
|
+
* multiple names or never). So for the example above:
|
|
4752
|
+
*
|
|
4753
|
+
* ```text
|
|
4754
|
+
* Praxis intrinsic model: button (options.tag — drives ARIA/content-model/normalizers)
|
|
4755
|
+
* DOM host: praxis-button (customElements.define()'s name — the actual element)
|
|
4756
|
+
* ```
|
|
4757
|
+
*
|
|
4758
|
+
* A concrete consequence worth internalizing: `<praxis-button disabled>` gets `aria-disabled` from
|
|
4759
|
+
* the `disabledProps` normalizer (correctly, since `disabled`'s HTML-boolean-attribute semantics
|
|
4760
|
+
* are honored in `_buildProps()` below), but the browser does **not** make the custom element
|
|
4761
|
+
* keyboard-inert, form-participating, or otherwise behave like a real `HTMLButtonElement` — that
|
|
4762
|
+
* gap isn't a bug, it's the direct consequence of `class X extends HTMLElement`, and no ARIA
|
|
4763
|
+
* attribute on any element, custom or not, ever supplies real interactive behavior. The contract
|
|
4764
|
+
* layer (styling, variants, ARIA policy, child enforcement, attribute management, lifecycle hooks,
|
|
4765
|
+
* diagnostics) and the host's actual interactive behavior are — and have to stay — conceptually
|
|
4766
|
+
* separate; a caller who needs real button/link/input behavior supplies it themselves (`onElement`
|
|
4767
|
+
* is the wiring point), the same way any `role="button"` `<div>` would require it. (Customized
|
|
4768
|
+
* built-ins — `class X extends HTMLButtonElement` + `{ extends: 'button' }` — were considered and
|
|
4769
|
+
* rejected for solving this: a real platform mechanism, but a different consumer-facing API
|
|
4770
|
+
* (`<button is="…">` instead of `<praxis-button>`) with its own platform constraints, not worth the
|
|
4771
|
+
* complexity it would add here.)
|
|
4772
|
+
*
|
|
4773
|
+
* **No `as` prop.** Like the Lit adapter (this one's closest sibling — both are fixed-identity
|
|
4774
|
+
* custom elements, sharing `resolveHostState`/`renderBundleToString` from the shared adapter runtime),
|
|
4775
|
+
* a custom element's DOM tag is fixed at `customElements.define()` time — once the model/host
|
|
4776
|
+
* distinction above is explicit, this becomes easy: there is no tag for `as` to switch. An earlier
|
|
4777
|
+
* design accepted `as` as a semantic-only override (never changing the rendered element, but
|
|
4778
|
+
* changing which ARIA/content-model rules applied, as if it really were a different tag). That was
|
|
4779
|
+
* worse than not having it: it could produce `role="link"`-shaped output with none of an anchor's
|
|
4780
|
+
* actual keyboard/click/middle-click behavior — a real accessibility footgun regardless of the
|
|
4781
|
+
* option's name — and it made `renderContractToString`'s output disagree with itself across calls
|
|
4782
|
+
* to the same component, which can only ever produce `<praxis-button>…</praxis-button>` on the live
|
|
4783
|
+
* client regardless of what tag `as` named. `as` is filtered out unconditionally in `_buildProps()`
|
|
4784
|
+
* below (including as a raw, undeclared HTML attribute, not just a declared property) so
|
|
4785
|
+
* `resolveHostState`/`renderBundleToString` — shared, unmodified, cross-adapter code — always
|
|
4786
|
+
* resolve to `options.tag` here, on both the client and SSR paths. Need different semantics for one
|
|
4787
|
+
* instance? Register a second component with a different `tag`, or set `role` directly — both
|
|
4788
|
+
* already work today, unaffected by this.
|
|
4789
|
+
*/
|
|
4790
|
+
function createContractComponent(options) {
|
|
4791
|
+
invariant(isWebFactoryOptions(options), "options is not a valid WebFactoryOptions object");
|
|
4792
|
+
const bundle = buildRuntime(options);
|
|
4793
|
+
const looseBundle = toLooseBundle(bundle);
|
|
4794
|
+
const variantKeys = options.styling?.variants ? Object.keys(options.styling.variants) : [];
|
|
4795
|
+
const pluginKeys = "classPlugin" in bundle.runtime ? [...bundle.runtime.classPlugin.ownedKeys ?? []] : [];
|
|
4796
|
+
const observedAttrNames = [
|
|
4797
|
+
"variant-key",
|
|
4798
|
+
"praxis-class",
|
|
4799
|
+
...variantKeys,
|
|
4800
|
+
...pluginKeys
|
|
4801
|
+
];
|
|
4802
|
+
const BaseElement = typeof HTMLElement !== "undefined" ? HTMLElement : class {};
|
|
4803
|
+
class PolymorphicWebElement extends BaseElement {
|
|
4804
|
+
_pipelineAttrs = /* @__PURE__ */ new Set();
|
|
4805
|
+
static get observedAttributes() {
|
|
4806
|
+
return observedAttrNames;
|
|
4807
|
+
}
|
|
4808
|
+
_onElementCleanup;
|
|
4809
|
+
connectedCallback() {
|
|
4810
|
+
this._applyPraxis();
|
|
4811
|
+
if (options.onElement) this._onElementCleanup = options.onElement(this, () => this._buildProps()) ?? void 0;
|
|
4812
|
+
}
|
|
4813
|
+
disconnectedCallback() {
|
|
4814
|
+
this._onElementCleanup?.();
|
|
4815
|
+
this._onElementCleanup = void 0;
|
|
4816
|
+
}
|
|
4817
|
+
attributeChangedCallback(_name, _old, _next) {
|
|
4818
|
+
if (this.isConnected) this._applyPraxis();
|
|
4819
|
+
}
|
|
4820
|
+
/** Re-runs the pipeline. Call after setting non-reactive attributes (aria-*, role, data-*)
|
|
4821
|
+
* or a praxis-owned property directly (see this module's own doc comment). */
|
|
4822
|
+
update() {
|
|
4823
|
+
this._applyPraxis();
|
|
4824
|
+
}
|
|
4825
|
+
get _self() {
|
|
4826
|
+
return this;
|
|
4827
|
+
}
|
|
4828
|
+
_buildProps() {
|
|
4829
|
+
const self = this._self;
|
|
4830
|
+
const observedSet = new Set(this.constructor.observedAttributes ?? []);
|
|
4831
|
+
const props = {};
|
|
4832
|
+
for (const attr of Array.from(this.attributes)) {
|
|
4833
|
+
if (attr.name === "class" || attr.name === "as" || observedSet.has(attr.name)) continue;
|
|
4834
|
+
props[attr.name] = attr.name === "disabled" ? true : attr.value;
|
|
4835
|
+
}
|
|
4836
|
+
props["recipe"] = self.recipe ?? this.getAttribute("variant-key") ?? void 0;
|
|
4837
|
+
props["className"] = self.praxisClass ?? this.getAttribute("praxis-class") ?? void 0;
|
|
4838
|
+
for (const key of variantKeys) {
|
|
4839
|
+
const val = self[key] ?? this.getAttribute(key);
|
|
4840
|
+
if (val != null) props[key] = val;
|
|
4841
|
+
}
|
|
4842
|
+
return props;
|
|
4843
|
+
}
|
|
4844
|
+
_applyPraxis() {
|
|
4845
|
+
const { childrenEvaluator, runtime: { options } } = bundle;
|
|
4846
|
+
const props = this._buildProps();
|
|
4847
|
+
const hostState = resolveHostState(looseBundle, props);
|
|
4848
|
+
const children = Array.from(this.childNodes);
|
|
4849
|
+
if (childrenEvaluator) childrenEvaluator.evaluate(children, {
|
|
4850
|
+
tag: hostState.tag,
|
|
4851
|
+
props: hostState.normalizedProps
|
|
4852
|
+
});
|
|
4853
|
+
options.htmlChildrenEvaluatorFn?.(hostState.tag)?.evaluate(children, {
|
|
4854
|
+
tag: hostState.tag,
|
|
4855
|
+
props: hostState.normalizedProps
|
|
4856
|
+
});
|
|
4857
|
+
diffAndApplyAttributes(this, hostState, this._pipelineAttrs, props);
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
if (options.name) Object.defineProperty(PolymorphicWebElement, "name", { value: options.name });
|
|
4861
|
+
Object.defineProperty(PolymorphicWebElement, "diagnostics", { value: bundle.diagnostics });
|
|
4862
|
+
const contractClass = PolymorphicWebElement;
|
|
4863
|
+
invariant(isWebContractComponent(contractClass), "Generated class failed to satisfy the WebContractComponent shape");
|
|
4864
|
+
registerForSsr(contractClass, looseBundle);
|
|
4865
|
+
return assembleCompoundComponent(contractClass, options.subComponents);
|
|
4866
|
+
}
|
|
4867
|
+
//#endregion
|
|
4868
|
+
export { createContractComponent, defineContractComponent, renderContractToString };
|