praxis-kit 6.5.0 → 6.6.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.
@@ -0,0 +1,860 @@
1
+ // ../../lib/contract/src/diagnostics/aria.ts
2
+ import { DiagnosticCategory, DiagnosticCode } from "../_shared/diagnostics.js";
3
+ var AriaDiagnostics = {
4
+ /** Generic bridge for violations produced by external AriaRule functions. */
5
+ fromViolation(v) {
6
+ return {
7
+ code: DiagnosticCode.AriaViolation,
8
+ category: DiagnosticCategory.ARIA,
9
+ message: v.message
10
+ };
11
+ },
12
+ attributeInvalid(key, role) {
13
+ return {
14
+ code: DiagnosticCode.AriaAttributeInvalid,
15
+ category: DiagnosticCategory.ARIA,
16
+ message: `"${key}" is not valid on role="${role}". It will be removed.`,
17
+ rationale: "Invalid ARIA attributes are ignored by assistive technology and may trigger accessibility-tree warnings in browser devtools.",
18
+ suggestions: [
19
+ {
20
+ title: "Remove the attribute",
21
+ description: `"${key}" is not in the allowed attribute set for role="${role}".`
22
+ }
23
+ ]
24
+ };
25
+ },
26
+ missingLiveRegion(role, impliedLive) {
27
+ return {
28
+ code: DiagnosticCode.AriaMissingLiveRegion,
29
+ category: DiagnosticCategory.ARIA,
30
+ message: `role="${role}" implies aria-live="${impliedLive}" but it is missing. It has been injected.`,
31
+ rationale: "Live-region roles announce dynamic content changes to screen readers. Without aria-live the politeness level is unspecified and announcements may be silent.",
32
+ suggestions: [
33
+ {
34
+ title: `Add aria-live="${impliedLive}"`,
35
+ description: `role="${role}" conventionally implies aria-live="${impliedLive}".`,
36
+ fix: `aria-live="${impliedLive}"`
37
+ }
38
+ ]
39
+ };
40
+ },
41
+ missingAtomic(role) {
42
+ return {
43
+ code: DiagnosticCode.AriaMissingAtomic,
44
+ category: DiagnosticCategory.ARIA,
45
+ 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.`,
46
+ rationale: "aria-atomic controls whether assistive technology announces the entire live region or only the changed nodes. Omitting it leaves the behaviour browser-defined."
47
+ };
48
+ },
49
+ relevantInvalidTokens(invalid) {
50
+ const quoted = invalid.map((t) => `"${t}"`).join(", ");
51
+ return {
52
+ code: DiagnosticCode.AriaRelevantInvalidToken,
53
+ category: DiagnosticCategory.ARIA,
54
+ message: `aria-relevant contains invalid token(s): ${quoted}. Valid tokens are: additions, removals, text, all.`,
55
+ rationale: "aria-relevant accepts a space-separated list of change types. Unrecognised tokens are silently ignored by assistive technology, making the attribute ineffective.",
56
+ suggestions: [
57
+ {
58
+ title: "Use only valid tokens",
59
+ description: "Valid values are: additions, removals, text, all (or a space-separated combination)."
60
+ }
61
+ ]
62
+ };
63
+ },
64
+ relevantSuperseded() {
65
+ return {
66
+ code: DiagnosticCode.AriaRelevantSuperseded,
67
+ category: DiagnosticCategory.ARIA,
68
+ message: 'aria-relevant includes "all" alongside other tokens. "all" supersedes additions, removals, and text \u2014 use aria-relevant="all" alone.',
69
+ rationale: '"all" is equivalent to "additions removals text". Combining it with other tokens is redundant and may confuse readers of the markup.',
70
+ suggestions: [
71
+ {
72
+ title: 'Use aria-relevant="all"',
73
+ fix: 'aria-relevant="all"'
74
+ }
75
+ ]
76
+ };
77
+ },
78
+ missingAccessibleName(tag) {
79
+ return {
80
+ code: DiagnosticCode.AriaMissingAccessibleName,
81
+ category: DiagnosticCategory.ARIA,
82
+ message: `<${tag}> has no accessible name. Add aria-label or aria-labelledby.`,
83
+ 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.",
84
+ suggestions: [
85
+ {
86
+ title: "Add aria-label",
87
+ description: `Add aria-label="\u2026" directly to the <${tag}> element.`
88
+ },
89
+ {
90
+ title: "Add aria-labelledby",
91
+ description: "Point aria-labelledby at the id of an existing heading or label element."
92
+ }
93
+ ]
94
+ };
95
+ },
96
+ attributeOnPresentational(attr, tag) {
97
+ return {
98
+ code: DiagnosticCode.AriaAttributeOnPresentational,
99
+ category: DiagnosticCategory.ARIA,
100
+ message: `"${attr}" is not allowed on a presentational <${tag}>. Presentational elements are invisible to assistive technology.`,
101
+ 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.',
102
+ suggestions: [
103
+ {
104
+ title: "Remove the attribute",
105
+ description: `"${attr}" has no effect when the element has role="none" or role="presentation".`
106
+ }
107
+ ]
108
+ };
109
+ },
110
+ ariaHiddenOnFocusable(tag) {
111
+ return {
112
+ code: DiagnosticCode.AriaHiddenOnFocusable,
113
+ category: DiagnosticCategory.ARIA,
114
+ 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.`,
115
+ rationale: 'aria-hidden removes an element from the accessibility tree while leaving it keyboard-reachable. This creates a "ghost" \u2014 a focusable element assistive technology cannot describe.',
116
+ suggestions: [
117
+ {
118
+ title: "Remove aria-hidden",
119
+ description: "If the element should be hidden from all users, use the HTML hidden attribute or CSS display:none instead."
120
+ },
121
+ {
122
+ title: "Make the element non-focusable",
123
+ description: 'If the element is intentionally decorative, add tabindex="-1" and disable it so it is not reachable by keyboard.'
124
+ }
125
+ ]
126
+ };
127
+ },
128
+ invalidAttributeValue(attr, value, expected) {
129
+ const got = value === null ? "null" : value === void 0 ? "undefined" : typeof value === "string" ? `"${value}"` : String(value);
130
+ return {
131
+ code: DiagnosticCode.AriaInvalidAttributeValue,
132
+ category: DiagnosticCategory.ARIA,
133
+ message: `"${attr}" has an invalid value (${got}). Expected: ${expected}.`,
134
+ rationale: "ARIA attributes with invalid values are silently ignored by assistive technology, making the markup semantically inert.",
135
+ suggestions: [
136
+ {
137
+ title: `Use a valid value for ${attr}`,
138
+ description: `Valid values are: ${expected}.`
139
+ }
140
+ ]
141
+ };
142
+ },
143
+ redundantAriaLevel(tag, level) {
144
+ return {
145
+ code: DiagnosticCode.AriaRedundantLevelAttribute,
146
+ category: DiagnosticCategory.ARIA,
147
+ message: `aria-level="${level}" is redundant on <${tag}>: the element already has an implicit heading level of ${level}. Remove the attribute.`,
148
+ 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>).',
149
+ suggestions: [
150
+ {
151
+ title: "Remove aria-level",
152
+ description: `<${tag}> already implies aria-level="${level}".`
153
+ }
154
+ ]
155
+ };
156
+ },
157
+ requiredProperty(attr, role) {
158
+ return {
159
+ code: DiagnosticCode.AriaRequiredProperty,
160
+ category: DiagnosticCategory.ARIA,
161
+ message: `"${attr}" is required for role="${role}" but is missing.`,
162
+ 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.`,
163
+ suggestions: [
164
+ {
165
+ title: `Add ${attr}`,
166
+ description: `role="${role}" requires "${attr}" to be present.`
167
+ }
168
+ ]
169
+ };
170
+ },
171
+ invalidRole(role, tag) {
172
+ return {
173
+ code: DiagnosticCode.AriaInvalidRole,
174
+ category: DiagnosticCategory.ARIA,
175
+ message: `Invalid role "${role ?? ""}" on <${tag}>.`,
176
+ rationale: "An unrecognised or misapplied ARIA role is ignored by assistive technology and may degrade the accessibility of the element."
177
+ };
178
+ }
179
+ };
180
+
181
+ // ../../lib/contract/src/diagnostics/html.ts
182
+ import { DiagnosticCategory as DiagnosticCategory2, DiagnosticCode as DiagnosticCode2 } from "../_shared/diagnostics.js";
183
+ var ATTRIBUTE_IGNORED_CODES = {
184
+ checked: DiagnosticCode2.HtmlInputCheckedIgnoredForType,
185
+ multiple: DiagnosticCode2.HtmlInputMultipleIgnoredForType,
186
+ maxLength: DiagnosticCode2.HtmlInputMaxLengthIgnoredForType,
187
+ minLength: DiagnosticCode2.HtmlInputMinLengthIgnoredForType,
188
+ pattern: DiagnosticCode2.HtmlInputPatternIgnoredForType,
189
+ min: DiagnosticCode2.HtmlInputMinIgnoredForType,
190
+ max: DiagnosticCode2.HtmlInputMaxIgnoredForType,
191
+ step: DiagnosticCode2.HtmlInputStepIgnoredForType,
192
+ accept: DiagnosticCode2.HtmlInputAcceptIgnoredForType,
193
+ capture: DiagnosticCode2.HtmlInputCaptureIgnoredForType,
194
+ size: DiagnosticCode2.HtmlInputSizeIgnoredForType,
195
+ alt: DiagnosticCode2.HtmlInputAltIgnoredForType,
196
+ height: DiagnosticCode2.HtmlInputHeightIgnoredForType,
197
+ width: DiagnosticCode2.HtmlInputWidthIgnoredForType
198
+ };
199
+ var HtmlDiagnostics = {
200
+ emptyRole(tag) {
201
+ return {
202
+ code: DiagnosticCode2.HtmlEmptyRole,
203
+ category: DiagnosticCategory2.HTML,
204
+ severity: "warning",
205
+ message: `<${tag}> has an explicit empty role="". Omit the attribute instead.`
206
+ };
207
+ },
208
+ implicitRoleRedundant(tag, implicitRole) {
209
+ return {
210
+ code: DiagnosticCode2.HtmlImplicitRoleRedundant,
211
+ category: DiagnosticCategory2.HTML,
212
+ severity: "warning",
213
+ message: `<${tag}> already has implicit role="${implicitRole}". Avoid redundant role assignment.`
214
+ };
215
+ },
216
+ implicitRoleOverride(tag, implicitRole, role) {
217
+ return {
218
+ code: DiagnosticCode2.HtmlImplicitRoleOverride,
219
+ category: DiagnosticCategory2.HTML,
220
+ severity: "error",
221
+ message: `<${tag}> should not override its implicit role="${implicitRole}" with role="${role}".`
222
+ };
223
+ },
224
+ standaloneRegionOverride(tag, implicitRole) {
225
+ return {
226
+ code: DiagnosticCode2.HtmlStandaloneRegionOverride,
227
+ category: DiagnosticCategory2.HTML,
228
+ severity: "error",
229
+ message: `<${tag}> is a self-contained element with implicit role="${implicitRole}". Assigning role="region" has been removed.`
230
+ };
231
+ },
232
+ landmarkRoleOverride(tag, implicitRole, role) {
233
+ return {
234
+ code: DiagnosticCode2.HtmlLandmarkRoleOverride,
235
+ category: DiagnosticCategory2.HTML,
236
+ severity: "error",
237
+ message: `<${tag}> has a fixed landmark role="${implicitRole}". role="${role}" overrides it and confuses assistive technology. The override has been removed.`
238
+ };
239
+ },
240
+ invalidChild(child, parent, allowed) {
241
+ return {
242
+ code: DiagnosticCode2.HtmlInvalidChild,
243
+ category: DiagnosticCategory2.HTML,
244
+ severity: "error",
245
+ message: `<${child}> is not a valid direct child of <${parent}>. Allowed: ${allowed}.`
246
+ };
247
+ },
248
+ roleNotPermitted(tag, role, allowedRoles) {
249
+ const allowed = allowedRoles.length > 0 ? allowedRoles.map((r) => `"${r}"`).join(", ") : "none \u2014 no explicit role is permitted on this element";
250
+ return {
251
+ code: DiagnosticCode2.HtmlRoleNotPermitted,
252
+ category: DiagnosticCategory2.HTML,
253
+ severity: "error",
254
+ message: `role="${role}" is not permitted on <${tag}>. Allowed alternate role(s): ${allowed}.`,
255
+ 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.'
256
+ };
257
+ },
258
+ // Reserved for <input>-specific facts (HTML3101–3199, see codes.ts) — later element families
259
+ // (button, img, table, ...) get their own reserved block and their own namespace here.
260
+ input: {
261
+ unsupportedType(type) {
262
+ return {
263
+ code: DiagnosticCode2.HtmlInputUnsupportedType,
264
+ category: DiagnosticCategory2.HTML,
265
+ severity: "warning",
266
+ message: `type="${type}" is not a value defined by the HTML specification. Browsers silently fall back to type="text".`,
267
+ rationale: 'An unrecognized input type is not invalid markup \u2014 the spec requires the "text" fallback \u2014 but it usually means a typo, since the input keeps working while silently losing the intended type-specific behavior (validation, virtual keyboard, picker UI).',
268
+ suggestions: [
269
+ {
270
+ title: "Check for a typo in the type value",
271
+ description: `"${type}" does not match any HTML5 input type.`
272
+ }
273
+ ]
274
+ };
275
+ },
276
+ // The user-visible problem is that the attribute is ignored — not that it "requires" a type;
277
+ // that's the rule's internal framing, not what the browser actually does.
278
+ attributeIgnoredForType(attribute, type, allowedTypes) {
279
+ const allowed = allowedTypes.map((t) => `"${t}"`).join(", ");
280
+ const code = ATTRIBUTE_IGNORED_CODES[attribute];
281
+ if (!code) throw new Error(`No DiagnosticCode registered for input attribute "${attribute}"`);
282
+ return {
283
+ code,
284
+ category: DiagnosticCategory2.HTML,
285
+ severity: "warning",
286
+ message: `"${attribute}" is ignored on <input type="${type}">.`,
287
+ rationale: `"${attribute}" only has an effect when type is one of: ${allowed}. Browsers silently ignore it on other input types.`,
288
+ suggestions: [
289
+ {
290
+ title: `Remove "${attribute}"`,
291
+ description: `"${attribute}" only affects <input> when type is one of: ${allowed}.`
292
+ }
293
+ ]
294
+ };
295
+ }
296
+ }
297
+ };
298
+
299
+ // ../../lib/contract/src/diagnostics/input-accessibility.ts
300
+ import { DiagnosticCategory as DiagnosticCategory3, DiagnosticCode as DiagnosticCode3 } from "../_shared/diagnostics.js";
301
+ function accessibilityFact(input) {
302
+ return { category: DiagnosticCategory3.Accessibility, ...input };
303
+ }
304
+ var InputAccessibilityDiagnostics = {
305
+ missingAccessibleName() {
306
+ return accessibilityFact({
307
+ code: DiagnosticCode3.A11yInputMissingAccessibleName,
308
+ severity: "warning",
309
+ message: "This input has no accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
310
+ rationale: "Assistive technology announces a form field by its accessible name; without one, users of screen readers cannot tell what the field is for.",
311
+ suggestions: [
312
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
313
+ {
314
+ title: "Add an associated <label>",
315
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
316
+ }
317
+ ]
318
+ });
319
+ },
320
+ placeholderIsNotLabel() {
321
+ return accessibilityFact({
322
+ code: DiagnosticCode3.A11yInputPlaceholderNotLabel,
323
+ severity: "warning",
324
+ message: "Placeholder text does not provide an accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
325
+ rationale: "Placeholder text disappears as users interact with the field and is not treated as the control's accessible name by many assistive technologies.",
326
+ suggestions: [
327
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
328
+ {
329
+ title: "Add an associated <label>",
330
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
331
+ }
332
+ ]
333
+ });
334
+ },
335
+ passwordMissingAutocomplete() {
336
+ return accessibilityFact({
337
+ code: DiagnosticCode3.A11yInputPasswordAutocomplete,
338
+ severity: "warning",
339
+ message: "Password inputs should specify an autoComplete value.",
340
+ rationale: "Without an explicit autocomplete hint, password managers and browsers cannot reliably tell a sign-in field apart from a password-creation field.",
341
+ suggestions: [
342
+ {
343
+ title: 'Set autoComplete="current-password"',
344
+ description: "Use this for sign-in forms."
345
+ },
346
+ {
347
+ title: 'Set autoComplete="new-password"',
348
+ description: "Use this for sign-up / change-password forms."
349
+ }
350
+ ]
351
+ });
352
+ },
353
+ requiredReadOnlyConflict() {
354
+ return accessibilityFact({
355
+ code: DiagnosticCode3.A11yInputRequiredReadOnlyConflict,
356
+ severity: "warning",
357
+ message: "The required and readOnly attributes are both present. A read-only field cannot satisfy required validation through user interaction.",
358
+ rationale: "This combination is valid HTML but usually indicates an unintended state. Consider using disabled instead of readOnly, or only applying required when the field is editable."
359
+ });
360
+ }
361
+ };
362
+
363
+ // ../core/src/html/spec/vocabulary/input.ts
364
+ var TEXT_INPUT_TYPES = ["text", "search", "url", "tel", "email", "password"];
365
+ var NUMERIC_INPUT_TYPES = [
366
+ "number",
367
+ "range",
368
+ "date",
369
+ "month",
370
+ "week",
371
+ "time",
372
+ "datetime-local"
373
+ ];
374
+ var HTML_INPUT_TYPES = /* @__PURE__ */ new Set([
375
+ ...TEXT_INPUT_TYPES,
376
+ ...NUMERIC_INPUT_TYPES,
377
+ "checkbox",
378
+ "radio",
379
+ "file",
380
+ "color",
381
+ "hidden",
382
+ "button",
383
+ "submit",
384
+ "reset",
385
+ "image"
386
+ ]);
387
+
388
+ // ../core/src/html/spec/attributes/input.ts
389
+ var INPUT_ATTRIBUTE_TYPE_POLICIES = [
390
+ { attribute: "checked", allowedTypes: ["checkbox", "radio"] },
391
+ { attribute: "multiple", allowedTypes: ["email", "file"] },
392
+ { attribute: "maxLength", allowedTypes: TEXT_INPUT_TYPES },
393
+ { attribute: "minLength", allowedTypes: TEXT_INPUT_TYPES },
394
+ { attribute: "pattern", allowedTypes: TEXT_INPUT_TYPES },
395
+ { attribute: "min", allowedTypes: NUMERIC_INPUT_TYPES },
396
+ { attribute: "max", allowedTypes: NUMERIC_INPUT_TYPES },
397
+ { attribute: "step", allowedTypes: NUMERIC_INPUT_TYPES },
398
+ { attribute: "accept", allowedTypes: ["file"] },
399
+ { attribute: "capture", allowedTypes: ["file"] },
400
+ { attribute: "size", allowedTypes: TEXT_INPUT_TYPES },
401
+ { attribute: "alt", allowedTypes: ["image"] },
402
+ { attribute: "height", allowedTypes: ["image"] },
403
+ { attribute: "width", allowedTypes: ["image"] }
404
+ ];
405
+
406
+ // ../core/src/html/spec/constraints/input.ts
407
+ var REQUIRED_READONLY_CONFLICT = {
408
+ props: ["required", "readOnly"],
409
+ diagnostic: () => InputAccessibilityDiagnostics.requiredReadOnlyConflict()
410
+ };
411
+ var INPUT_MUTUALLY_EXCLUSIVE_POLICIES = [
412
+ REQUIRED_READONLY_CONFLICT
413
+ ];
414
+
415
+ // ../core/src/html/spec/validators/attribute-type-validator.ts
416
+ var DEFAULT_INPUT_TYPE = "text";
417
+ function omit(props, key) {
418
+ const next = { ...props };
419
+ delete next[key];
420
+ return next;
421
+ }
422
+ function removeAttributeFix(attribute) {
423
+ return {
424
+ kind: `removeAttribute:${attribute}`,
425
+ apply: ({ props }) => {
426
+ if (!(attribute in props)) return { applied: false, next: props };
427
+ return { applied: true, next: omit(props, attribute), previous: props };
428
+ }
429
+ };
430
+ }
431
+ function createInputAttributeTypeRule({
432
+ attribute,
433
+ allowedTypes
434
+ }) {
435
+ const rule = ({ tag, props }) => {
436
+ if (tag !== "input" || !(attribute in props)) return [];
437
+ const type = typeof props.type === "string" ? props.type : DEFAULT_INPUT_TYPE;
438
+ if (allowedTypes.includes(type)) return [];
439
+ const diagnostic = HtmlDiagnostics.input.attributeIgnoredForType(attribute, type, allowedTypes);
440
+ return [
441
+ {
442
+ valid: false,
443
+ fixable: true,
444
+ severity: diagnostic.severity,
445
+ fix: removeAttributeFix(attribute),
446
+ diagnostic
447
+ }
448
+ ];
449
+ };
450
+ return Object.assign(rule, { readsProps: ["type", attribute], tags: ["input"] });
451
+ }
452
+
453
+ // ../core/src/html/spec/validators/mutually-exclusive-validator.ts
454
+ function createMutuallyExclusiveRule({
455
+ props: conflictingProps,
456
+ diagnostic: createDiagnostic
457
+ }) {
458
+ const [first, second] = conflictingProps;
459
+ const rule = ({ tag, props }) => {
460
+ if (tag !== "input" || !props[first] || !props[second]) return [];
461
+ const diagnostic = createDiagnostic();
462
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
463
+ };
464
+ return Object.assign(rule, { readsProps: conflictingProps, tags: ["input"] });
465
+ }
466
+
467
+ // ../core/src/html/input-rules.ts
468
+ var policyByAttribute = Object.fromEntries(
469
+ INPUT_ATTRIBUTE_TYPE_POLICIES.map((policy) => [policy.attribute, policy])
470
+ );
471
+ function policyFor(attribute) {
472
+ return policyByAttribute[attribute];
473
+ }
474
+ var supportedInputTypeRule = Object.assign(
475
+ ({ tag, props }) => {
476
+ if (tag !== "input" || typeof props.type !== "string") return [];
477
+ const type = props.type;
478
+ if (HTML_INPUT_TYPES.has(type)) return [];
479
+ const diagnostic = HtmlDiagnostics.input.unsupportedType(type);
480
+ return [
481
+ {
482
+ valid: false,
483
+ fixable: false,
484
+ severity: diagnostic.severity,
485
+ diagnostic
486
+ }
487
+ ];
488
+ },
489
+ { readsProps: ["type"], tags: ["input"] }
490
+ );
491
+ var checkedRequiresCheckableTypeRule = createInputAttributeTypeRule(policyFor("checked"));
492
+ var multipleRequiresSupportedTypeRule = createInputAttributeTypeRule(policyFor("multiple"));
493
+ var maxLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("maxLength"));
494
+ var minLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("minLength"));
495
+ var patternRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("pattern"));
496
+ var minRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("min"));
497
+ var maxRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("max"));
498
+ var stepRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("step"));
499
+ var acceptRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("accept"));
500
+ var captureRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("capture"));
501
+ var sizeRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("size"));
502
+ var altRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("alt"));
503
+ var heightRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("height"));
504
+ var widthRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("width"));
505
+ var inputAccessibleNameRule = Object.assign(
506
+ ({ tag, props }) => {
507
+ if (tag !== "input" || props.type === "hidden") return [];
508
+ if ("aria-label" in props || "aria-labelledby" in props) return [];
509
+ const hasPlaceholder = typeof props.placeholder === "string" && props.placeholder.length > 0;
510
+ const diagnostic = hasPlaceholder ? InputAccessibilityDiagnostics.placeholderIsNotLabel() : InputAccessibilityDiagnostics.missingAccessibleName();
511
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
512
+ },
513
+ {
514
+ readsProps: ["type", "aria-label", "aria-labelledby", "placeholder"],
515
+ tags: ["input"]
516
+ }
517
+ );
518
+ var PASSWORD_AUTOCOMPLETE_VALUES = ["current-password", "new-password"];
519
+ var passwordAutocompleteRule = Object.assign(
520
+ ({ tag, props }) => {
521
+ if (tag !== "input" || props.type !== "password") return [];
522
+ const autoComplete = props.autoComplete;
523
+ const tokens = typeof autoComplete === "string" ? autoComplete.split(" ") : [];
524
+ if (PASSWORD_AUTOCOMPLETE_VALUES.some((value) => tokens.includes(value))) return [];
525
+ const diagnostic = InputAccessibilityDiagnostics.passwordMissingAutocomplete();
526
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
527
+ },
528
+ { readsProps: ["type", "autoComplete"], tags: ["input"] }
529
+ );
530
+ var requiredReadOnlyConflictRule = createMutuallyExclusiveRule(REQUIRED_READONLY_CONFLICT);
531
+ var INPUT_RULES = [
532
+ supportedInputTypeRule,
533
+ checkedRequiresCheckableTypeRule,
534
+ multipleRequiresSupportedTypeRule,
535
+ maxLengthRequiresTextTypeRule,
536
+ minLengthRequiresTextTypeRule,
537
+ patternRequiresTextTypeRule,
538
+ minRequiresNumericTypeRule,
539
+ maxRequiresNumericTypeRule,
540
+ stepRequiresNumericTypeRule,
541
+ acceptRequiresFileTypeRule,
542
+ captureRequiresFileTypeRule,
543
+ sizeRequiresTextTypeRule,
544
+ altRequiresImageTypeRule,
545
+ heightRequiresImageTypeRule,
546
+ widthRequiresImageTypeRule,
547
+ inputAccessibleNameRule,
548
+ passwordAutocompleteRule,
549
+ requiredReadOnlyConflictRule
550
+ ];
551
+
552
+ // ../core/src/html/spec/types.ts
553
+ function definePropRolePolicy(prop, map, fallback) {
554
+ return { kind: "byProp", prop, map, fallback };
555
+ }
556
+ function resolveAllowedRoles(spec, props) {
557
+ const policy = spec.allowedRoles;
558
+ if (!policy) return void 0;
559
+ switch (policy.kind) {
560
+ case "fixed":
561
+ return policy.roles;
562
+ case "byProp": {
563
+ const value = typeof props[policy.prop] === "string" ? props[policy.prop] : policy.fallback;
564
+ return policy.map[value];
565
+ }
566
+ case "dynamic":
567
+ return policy.resolve({ props });
568
+ }
569
+ }
570
+
571
+ // ../core/src/html/spec/roles/input.ts
572
+ var ALLOWED_INPUT_ROLES = {
573
+ checkbox: ["menuitemcheckbox", "option", "switch", "button"],
574
+ radio: ["menuitemradio"],
575
+ range: [],
576
+ number: [],
577
+ search: ["combobox"],
578
+ text: ["combobox", "searchbox", "spinbutton"],
579
+ email: ["combobox"],
580
+ tel: ["combobox"],
581
+ url: ["combobox"],
582
+ button: [
583
+ "link",
584
+ "menuitem",
585
+ "menuitemcheckbox",
586
+ "menuitemradio",
587
+ "option",
588
+ "radio",
589
+ "switch",
590
+ "tab"
591
+ ],
592
+ submit: [
593
+ "link",
594
+ "menuitem",
595
+ "menuitemcheckbox",
596
+ "menuitemradio",
597
+ "option",
598
+ "radio",
599
+ "switch",
600
+ "tab"
601
+ ],
602
+ reset: [
603
+ "link",
604
+ "menuitem",
605
+ "menuitemcheckbox",
606
+ "menuitemradio",
607
+ "option",
608
+ "radio",
609
+ "switch",
610
+ "tab"
611
+ ],
612
+ image: [
613
+ "link",
614
+ "menuitem",
615
+ "menuitemcheckbox",
616
+ "menuitemradio",
617
+ "option",
618
+ "radio",
619
+ "switch",
620
+ "tab"
621
+ ],
622
+ hidden: []
623
+ };
624
+
625
+ // ../core/src/html/spec/elements/input.ts
626
+ var inputElementSpec = {
627
+ tag: "input",
628
+ allowedRoles: definePropRolePolicy("type", ALLOWED_INPUT_ROLES, "text"),
629
+ attributes: INPUT_ATTRIBUTE_TYPE_POLICIES,
630
+ mutuallyExclusive: INPUT_MUTUALLY_EXCLUSIVE_POLICIES
631
+ };
632
+
633
+ // ../core/src/html/spec/roles/img.ts
634
+ var IMG_NAMED_ROLES = [
635
+ "button",
636
+ "checkbox",
637
+ "link",
638
+ "menuitem",
639
+ "menuitemcheckbox",
640
+ "menuitemradio",
641
+ "option",
642
+ "progressbar",
643
+ "scrollbar",
644
+ "separator",
645
+ "slider",
646
+ "switch",
647
+ "tab",
648
+ "treeitem"
649
+ ];
650
+
651
+ // ../core/src/html/spec/elements/img.ts
652
+ var imgElementSpec = {
653
+ tag: "img",
654
+ allowedRoles: {
655
+ kind: "dynamic",
656
+ resolve: ({ props }) => props.alt === "" ? [] : IMG_NAMED_ROLES
657
+ }
658
+ };
659
+
660
+ // ../core/src/html/spec/roles/table.ts
661
+ var ALLOWED_TABLE_ROLES = ["grid", "treegrid"];
662
+
663
+ // ../core/src/html/spec/elements/table.ts
664
+ var tableElementSpec = {
665
+ tag: "table",
666
+ allowedRoles: { kind: "fixed", roles: ALLOWED_TABLE_ROLES }
667
+ };
668
+
669
+ // ../core/src/html/role-restrictions.ts
670
+ var ALLOWED_ROLES = {
671
+ article: ["application", "document", "feed", "main", "none", "presentation", "region"],
672
+ aside: ["feed", "none", "presentation", "region", "search"],
673
+ footer: ["group", "none", "presentation"],
674
+ header: ["group", "none", "presentation"],
675
+ main: [],
676
+ nav: [],
677
+ a: [
678
+ "button",
679
+ "checkbox",
680
+ "menuitem",
681
+ "menuitemcheckbox",
682
+ "menuitemradio",
683
+ "option",
684
+ "radio",
685
+ "switch",
686
+ "tab",
687
+ "treeitem"
688
+ ],
689
+ button: [
690
+ "checkbox",
691
+ "link",
692
+ "menuitem",
693
+ "menuitemcheckbox",
694
+ "menuitemradio",
695
+ "option",
696
+ "radio",
697
+ "switch",
698
+ "tab"
699
+ ],
700
+ select: ["menu"],
701
+ h1: ["tab", "presentation", "none"],
702
+ h2: ["tab", "presentation", "none"],
703
+ h3: ["tab", "presentation", "none"],
704
+ h4: ["tab", "presentation", "none"],
705
+ h5: ["tab", "presentation", "none"],
706
+ h6: ["tab", "presentation", "none"],
707
+ ul: [
708
+ "directory",
709
+ "group",
710
+ "listbox",
711
+ "menu",
712
+ "menubar",
713
+ "radiogroup",
714
+ "tablist",
715
+ "toolbar",
716
+ "tree"
717
+ ],
718
+ ol: [
719
+ "directory",
720
+ "group",
721
+ "listbox",
722
+ "menu",
723
+ "menubar",
724
+ "radiogroup",
725
+ "tablist",
726
+ "toolbar",
727
+ "tree"
728
+ ],
729
+ li: [
730
+ "menuitem",
731
+ "menuitemcheckbox",
732
+ "menuitemradio",
733
+ "option",
734
+ "none",
735
+ "presentation",
736
+ "radio",
737
+ "separator",
738
+ "tab",
739
+ "treeitem"
740
+ ],
741
+ dialog: ["alertdialog"],
742
+ fieldset: ["none", "presentation", "radiogroup"]
743
+ };
744
+ var ELEMENT_SPECS = {
745
+ input: inputElementSpec,
746
+ img: imgElementSpec,
747
+ table: tableElementSpec
748
+ };
749
+ function getAllowedRoles(tag, props) {
750
+ const spec = ELEMENT_SPECS[tag];
751
+ if (spec) return resolveAllowedRoles(spec, props);
752
+ return ALLOWED_ROLES[tag];
753
+ }
754
+ var removeRoleFix = {
755
+ kind: "removeRole",
756
+ apply: ({ props }) => {
757
+ if (!("role" in props)) return { applied: false, next: props };
758
+ const { role: _role, ...rest } = props;
759
+ return { applied: true, next: rest, previous: props };
760
+ }
761
+ };
762
+ var roleNotPermittedRule = Object.assign(
763
+ ({ tag, props, implicitRole }) => {
764
+ const role = props.role;
765
+ if (typeof role !== "string" || role.length === 0 || role === implicitRole) return [];
766
+ const allowed = getAllowedRoles(tag, props);
767
+ if (allowed === void 0 || allowed.includes(role)) return [];
768
+ const diagnostic = HtmlDiagnostics.roleNotPermitted(tag, role, allowed);
769
+ return [
770
+ {
771
+ valid: false,
772
+ fixable: true,
773
+ severity: diagnostic.severity,
774
+ fix: removeRoleFix,
775
+ diagnostic
776
+ }
777
+ ];
778
+ },
779
+ { readsProps: ["role", "type", "alt"] }
780
+ );
781
+
782
+ // ../core/src/html/aria-rules.ts
783
+ var LANDMARK_TAG_SET = /* @__PURE__ */ new Set(["article", "aside", "footer", "header", "main", "nav"]);
784
+ var removeLandmarkRoleOverride = {
785
+ kind: "removeRole",
786
+ apply: ({ props }) => {
787
+ if (!("role" in props)) return { applied: false, next: props };
788
+ const { role: _r, ...rest } = props;
789
+ return { applied: true, next: rest, previous: props };
790
+ }
791
+ };
792
+ var landmarkRoleRule = Object.assign(
793
+ ({ tag, props, implicitRole }) => {
794
+ if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
795
+ const role = props.role;
796
+ if (!role || role === implicitRole) return [];
797
+ const diagnostic = HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role);
798
+ return [
799
+ {
800
+ valid: false,
801
+ fixable: true,
802
+ severity: diagnostic.severity,
803
+ fix: removeLandmarkRoleOverride,
804
+ diagnostic
805
+ }
806
+ ];
807
+ },
808
+ { tags: [...LANDMARK_TAG_SET] }
809
+ );
810
+ function requireAccessibleName({ tag, props }) {
811
+ if ("aria-label" in props || "aria-labelledby" in props) return [];
812
+ return [
813
+ {
814
+ valid: false,
815
+ fixable: false,
816
+ severity: "warning",
817
+ diagnostic: AriaDiagnostics.missingAccessibleName(tag)
818
+ }
819
+ ];
820
+ }
821
+ var NAMED_LANDMARK_TAGS = /* @__PURE__ */ new Set(["nav", "aside"]);
822
+ var landmarkNameAdvisory = Object.assign(
823
+ (ctx) => {
824
+ if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
825
+ return requireAccessibleName(ctx);
826
+ },
827
+ { tags: [...NAMED_LANDMARK_TAGS] }
828
+ );
829
+ var HTML_ARIA_RULES = [
830
+ landmarkRoleRule,
831
+ landmarkNameAdvisory,
832
+ roleNotPermittedRule,
833
+ ...INPUT_RULES
834
+ ];
835
+ export {
836
+ HTML_ARIA_RULES,
837
+ INPUT_RULES,
838
+ acceptRequiresFileTypeRule,
839
+ altRequiresImageTypeRule,
840
+ captureRequiresFileTypeRule,
841
+ checkedRequiresCheckableTypeRule,
842
+ heightRequiresImageTypeRule,
843
+ inputAccessibleNameRule,
844
+ landmarkNameAdvisory,
845
+ landmarkRoleRule,
846
+ maxLengthRequiresTextTypeRule,
847
+ maxRequiresNumericTypeRule,
848
+ minLengthRequiresTextTypeRule,
849
+ minRequiresNumericTypeRule,
850
+ multipleRequiresSupportedTypeRule,
851
+ passwordAutocompleteRule,
852
+ patternRequiresTextTypeRule,
853
+ requireAccessibleName,
854
+ requiredReadOnlyConflictRule,
855
+ roleNotPermittedRule,
856
+ sizeRequiresTextTypeRule,
857
+ stepRequiresNumericTypeRule,
858
+ supportedInputTypeRule,
859
+ widthRequiresImageTypeRule
860
+ };