praxis-kit 7.4.0 → 7.8.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/dist/_shared/diagnostics.d.ts +3 -0
- package/dist/_shared/diagnostics.js +3 -0
- package/dist/{chunk-EIKPSL26.js → chunk-QYP7KLRE.js} +316 -154
- package/dist/contract/index.d.ts +27 -4
- package/dist/eslint/index.js +227 -187
- package/dist/html/index.d.ts +6 -1
- package/dist/html/index.js +234 -68
- package/dist/lit/index.d.ts +27 -4
- package/dist/lit/index.js +305 -141
- package/dist/preact/index.d.ts +114 -6
- package/dist/preact/index.js +321 -153
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +7 -2
- package/dist/react/legacy.d.ts +8 -5
- package/dist/react/legacy.js +13 -9
- package/dist/{react-options-BnjZpdVh.d.ts → react-options-DU3EdFOv.d.ts} +126 -7
- package/dist/solid/index.d.ts +127 -8
- package/dist/solid/index.js +320 -153
- package/dist/svelte/Polymorphic.svelte +8 -0
- package/dist/svelte/index.d.ts +224 -13
- package/dist/svelte/index.js +319 -152
- package/dist/tailwind/index.d.ts +1 -1
- package/dist/vite-plugin/index.d.ts +6 -3
- package/dist/vue/index.d.ts +94 -6
- package/dist/vue/index.js +320 -153
- package/dist/web/index.d.ts +27 -4
- package/dist/web/index.js +305 -141
- package/package.json +9 -9
package/dist/html/index.js
CHANGED
|
@@ -1,18 +1,72 @@
|
|
|
1
|
-
// ../../lib/
|
|
1
|
+
// ../../lib/primitive/src/guards/foundational/is-defined.ts
|
|
2
|
+
function isDefined(value) {
|
|
3
|
+
return value !== void 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// ../../lib/primitive/src/utils/type-guards.ts
|
|
7
|
+
function isString(value) {
|
|
8
|
+
return typeof value === "string";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ../../lib/contract/src/diagnostics/anchor-accessibility.ts
|
|
2
12
|
import { DiagnosticCategory, DiagnosticCode } from "../_shared/diagnostics.js";
|
|
13
|
+
function createDiagnostic(input) {
|
|
14
|
+
return { category: DiagnosticCategory.Accessibility, ...input };
|
|
15
|
+
}
|
|
16
|
+
var AnchorAccessibilityDiagnostics = {
|
|
17
|
+
roleButtonWithHref() {
|
|
18
|
+
return createDiagnostic({
|
|
19
|
+
code: DiagnosticCode.A11yAnchorRoleButtonWithHref,
|
|
20
|
+
severity: "warning",
|
|
21
|
+
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.',
|
|
22
|
+
rationale: 'Assistive technology announces role="button" as a button, not a link \u2014 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.',
|
|
23
|
+
suggestions: [
|
|
24
|
+
{
|
|
25
|
+
title: "Use a real <button>",
|
|
26
|
+
description: "If this element should not navigate, render a <button> instead of an <a>."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
title: 'Remove role="button"',
|
|
30
|
+
description: "If this element should navigate, keep the default link role."
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
ariaDisabledInert() {
|
|
36
|
+
return createDiagnostic({
|
|
37
|
+
code: DiagnosticCode.A11yAnchorAriaDisabledInert,
|
|
38
|
+
severity: "warning",
|
|
39
|
+
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.",
|
|
40
|
+
rationale: "Unlike a native form control, an <a> has no disabled state the browser enforces \u2014 aria-disabled only changes what assistive technology announces, not what actually happens when the link is activated by mouse or keyboard.",
|
|
41
|
+
suggestions: [
|
|
42
|
+
{
|
|
43
|
+
title: "Remove href",
|
|
44
|
+
description: "If this element is temporarily unavailable, remove href until it can be activated."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
title: "Prevent navigation when disabled",
|
|
48
|
+
description: "Prevent the click and keyboard activation when the link is disabled so it behaves consistently for all users."
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ../../lib/contract/src/diagnostics/aria.ts
|
|
56
|
+
import { DiagnosticCategory as DiagnosticCategory2, DiagnosticCode as DiagnosticCode2 } from "../_shared/diagnostics.js";
|
|
3
57
|
var AriaDiagnostics = {
|
|
4
58
|
/** Generic bridge for violations produced by external AriaRule functions. */
|
|
5
59
|
fromViolation(v) {
|
|
6
60
|
return {
|
|
7
|
-
code:
|
|
8
|
-
category:
|
|
61
|
+
code: DiagnosticCode2.AriaViolation,
|
|
62
|
+
category: DiagnosticCategory2.ARIA,
|
|
9
63
|
message: v.message
|
|
10
64
|
};
|
|
11
65
|
},
|
|
12
66
|
attributeInvalid(key, role) {
|
|
13
67
|
return {
|
|
14
|
-
code:
|
|
15
|
-
category:
|
|
68
|
+
code: DiagnosticCode2.AriaAttributeInvalid,
|
|
69
|
+
category: DiagnosticCategory2.ARIA,
|
|
16
70
|
message: `"${key}" is not valid on role="${role}". It will be removed.`,
|
|
17
71
|
rationale: "Invalid ARIA attributes are ignored by assistive technology and may trigger accessibility-tree warnings in browser devtools.",
|
|
18
72
|
suggestions: [
|
|
@@ -25,8 +79,8 @@ var AriaDiagnostics = {
|
|
|
25
79
|
},
|
|
26
80
|
missingLiveRegion(role, impliedLive) {
|
|
27
81
|
return {
|
|
28
|
-
code:
|
|
29
|
-
category:
|
|
82
|
+
code: DiagnosticCode2.AriaMissingLiveRegion,
|
|
83
|
+
category: DiagnosticCategory2.ARIA,
|
|
30
84
|
message: `role="${role}" implies aria-live="${impliedLive}" but it is missing. It has been injected.`,
|
|
31
85
|
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
86
|
suggestions: [
|
|
@@ -40,8 +94,8 @@ var AriaDiagnostics = {
|
|
|
40
94
|
},
|
|
41
95
|
missingAtomic(role) {
|
|
42
96
|
return {
|
|
43
|
-
code:
|
|
44
|
-
category:
|
|
97
|
+
code: DiagnosticCode2.AriaMissingAtomic,
|
|
98
|
+
category: DiagnosticCategory2.ARIA,
|
|
45
99
|
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
100
|
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
101
|
};
|
|
@@ -49,8 +103,8 @@ var AriaDiagnostics = {
|
|
|
49
103
|
relevantInvalidTokens(invalid) {
|
|
50
104
|
const quoted = invalid.map((t) => `"${t}"`).join(", ");
|
|
51
105
|
return {
|
|
52
|
-
code:
|
|
53
|
-
category:
|
|
106
|
+
code: DiagnosticCode2.AriaRelevantInvalidToken,
|
|
107
|
+
category: DiagnosticCategory2.ARIA,
|
|
54
108
|
message: `aria-relevant contains invalid token(s): ${quoted}. Valid tokens are: additions, removals, text, all.`,
|
|
55
109
|
rationale: "aria-relevant accepts a space-separated list of change types. Unrecognised tokens are silently ignored by assistive technology, making the attribute ineffective.",
|
|
56
110
|
suggestions: [
|
|
@@ -63,8 +117,8 @@ var AriaDiagnostics = {
|
|
|
63
117
|
},
|
|
64
118
|
relevantSuperseded() {
|
|
65
119
|
return {
|
|
66
|
-
code:
|
|
67
|
-
category:
|
|
120
|
+
code: DiagnosticCode2.AriaRelevantSuperseded,
|
|
121
|
+
category: DiagnosticCategory2.ARIA,
|
|
68
122
|
message: 'aria-relevant includes "all" alongside other tokens. "all" supersedes additions, removals, and text \u2014 use aria-relevant="all" alone.',
|
|
69
123
|
rationale: '"all" is equivalent to "additions removals text". Combining it with other tokens is redundant and may confuse readers of the markup.',
|
|
70
124
|
suggestions: [
|
|
@@ -77,8 +131,8 @@ var AriaDiagnostics = {
|
|
|
77
131
|
},
|
|
78
132
|
missingAccessibleName(tag) {
|
|
79
133
|
return {
|
|
80
|
-
code:
|
|
81
|
-
category:
|
|
134
|
+
code: DiagnosticCode2.AriaMissingAccessibleName,
|
|
135
|
+
category: DiagnosticCategory2.ARIA,
|
|
82
136
|
message: `<${tag}> has no accessible name. Add aria-label or aria-labelledby.`,
|
|
83
137
|
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
138
|
suggestions: [
|
|
@@ -95,8 +149,8 @@ var AriaDiagnostics = {
|
|
|
95
149
|
},
|
|
96
150
|
attributeOnPresentational(attr, tag) {
|
|
97
151
|
return {
|
|
98
|
-
code:
|
|
99
|
-
category:
|
|
152
|
+
code: DiagnosticCode2.AriaAttributeOnPresentational,
|
|
153
|
+
category: DiagnosticCategory2.ARIA,
|
|
100
154
|
message: `"${attr}" is not allowed on a presentational <${tag}>. Presentational elements are invisible to assistive technology.`,
|
|
101
155
|
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
156
|
suggestions: [
|
|
@@ -109,8 +163,8 @@ var AriaDiagnostics = {
|
|
|
109
163
|
},
|
|
110
164
|
ariaHiddenOnFocusable(tag) {
|
|
111
165
|
return {
|
|
112
|
-
code:
|
|
113
|
-
category:
|
|
166
|
+
code: DiagnosticCode2.AriaHiddenOnFocusable,
|
|
167
|
+
category: DiagnosticCategory2.ARIA,
|
|
114
168
|
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
169
|
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
170
|
suggestions: [
|
|
@@ -128,8 +182,8 @@ var AriaDiagnostics = {
|
|
|
128
182
|
invalidAttributeValue(attr, value, expected) {
|
|
129
183
|
const got = value === null ? "null" : value === void 0 ? "undefined" : typeof value === "string" ? `"${value}"` : String(value);
|
|
130
184
|
return {
|
|
131
|
-
code:
|
|
132
|
-
category:
|
|
185
|
+
code: DiagnosticCode2.AriaInvalidAttributeValue,
|
|
186
|
+
category: DiagnosticCategory2.ARIA,
|
|
133
187
|
message: `"${attr}" has an invalid value (${got}). Expected: ${expected}.`,
|
|
134
188
|
rationale: "ARIA attributes with invalid values are silently ignored by assistive technology, making the markup semantically inert.",
|
|
135
189
|
suggestions: [
|
|
@@ -142,8 +196,8 @@ var AriaDiagnostics = {
|
|
|
142
196
|
},
|
|
143
197
|
redundantAriaLevel(tag, level) {
|
|
144
198
|
return {
|
|
145
|
-
code:
|
|
146
|
-
category:
|
|
199
|
+
code: DiagnosticCode2.AriaRedundantLevelAttribute,
|
|
200
|
+
category: DiagnosticCategory2.ARIA,
|
|
147
201
|
message: `aria-level="${level}" is redundant on <${tag}>: the element already has an implicit heading level of ${level}. Remove the attribute.`,
|
|
148
202
|
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
203
|
suggestions: [
|
|
@@ -156,8 +210,8 @@ var AriaDiagnostics = {
|
|
|
156
210
|
},
|
|
157
211
|
requiredProperty(attr, role) {
|
|
158
212
|
return {
|
|
159
|
-
code:
|
|
160
|
-
category:
|
|
213
|
+
code: DiagnosticCode2.AriaRequiredProperty,
|
|
214
|
+
category: DiagnosticCategory2.ARIA,
|
|
161
215
|
message: `"${attr}" is required for role="${role}" but is missing.`,
|
|
162
216
|
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
217
|
suggestions: [
|
|
@@ -170,8 +224,8 @@ var AriaDiagnostics = {
|
|
|
170
224
|
},
|
|
171
225
|
invalidRole(role, tag) {
|
|
172
226
|
return {
|
|
173
|
-
code:
|
|
174
|
-
category:
|
|
227
|
+
code: DiagnosticCode2.AriaInvalidRole,
|
|
228
|
+
category: DiagnosticCategory2.ARIA,
|
|
175
229
|
message: `Invalid role "${role ?? ""}" on <${tag}>.`,
|
|
176
230
|
rationale: "An unrecognised or misapplied ARIA role is ignored by assistive technology and may degrade the accessibility of the element."
|
|
177
231
|
};
|
|
@@ -179,68 +233,68 @@ var AriaDiagnostics = {
|
|
|
179
233
|
};
|
|
180
234
|
|
|
181
235
|
// ../../lib/contract/src/diagnostics/html.ts
|
|
182
|
-
import { DiagnosticCategory as
|
|
236
|
+
import { DiagnosticCategory as DiagnosticCategory3, DiagnosticCode as DiagnosticCode3 } from "../_shared/diagnostics.js";
|
|
183
237
|
var ATTRIBUTE_IGNORED_CODES = {
|
|
184
|
-
checked:
|
|
185
|
-
multiple:
|
|
186
|
-
maxLength:
|
|
187
|
-
minLength:
|
|
188
|
-
pattern:
|
|
189
|
-
min:
|
|
190
|
-
max:
|
|
191
|
-
step:
|
|
192
|
-
accept:
|
|
193
|
-
capture:
|
|
194
|
-
size:
|
|
195
|
-
alt:
|
|
196
|
-
height:
|
|
197
|
-
width:
|
|
238
|
+
checked: DiagnosticCode3.HtmlInputCheckedIgnoredForType,
|
|
239
|
+
multiple: DiagnosticCode3.HtmlInputMultipleIgnoredForType,
|
|
240
|
+
maxLength: DiagnosticCode3.HtmlInputMaxLengthIgnoredForType,
|
|
241
|
+
minLength: DiagnosticCode3.HtmlInputMinLengthIgnoredForType,
|
|
242
|
+
pattern: DiagnosticCode3.HtmlInputPatternIgnoredForType,
|
|
243
|
+
min: DiagnosticCode3.HtmlInputMinIgnoredForType,
|
|
244
|
+
max: DiagnosticCode3.HtmlInputMaxIgnoredForType,
|
|
245
|
+
step: DiagnosticCode3.HtmlInputStepIgnoredForType,
|
|
246
|
+
accept: DiagnosticCode3.HtmlInputAcceptIgnoredForType,
|
|
247
|
+
capture: DiagnosticCode3.HtmlInputCaptureIgnoredForType,
|
|
248
|
+
size: DiagnosticCode3.HtmlInputSizeIgnoredForType,
|
|
249
|
+
alt: DiagnosticCode3.HtmlInputAltIgnoredForType,
|
|
250
|
+
height: DiagnosticCode3.HtmlInputHeightIgnoredForType,
|
|
251
|
+
width: DiagnosticCode3.HtmlInputWidthIgnoredForType
|
|
198
252
|
};
|
|
199
253
|
var HtmlDiagnostics = {
|
|
200
254
|
emptyRole(tag) {
|
|
201
255
|
return {
|
|
202
|
-
code:
|
|
203
|
-
category:
|
|
256
|
+
code: DiagnosticCode3.HtmlEmptyRole,
|
|
257
|
+
category: DiagnosticCategory3.HTML,
|
|
204
258
|
severity: "warning",
|
|
205
259
|
message: `<${tag}> has an explicit empty role="". Omit the attribute instead.`
|
|
206
260
|
};
|
|
207
261
|
},
|
|
208
262
|
implicitRoleRedundant(tag, implicitRole) {
|
|
209
263
|
return {
|
|
210
|
-
code:
|
|
211
|
-
category:
|
|
264
|
+
code: DiagnosticCode3.HtmlImplicitRoleRedundant,
|
|
265
|
+
category: DiagnosticCategory3.HTML,
|
|
212
266
|
severity: "warning",
|
|
213
267
|
message: `<${tag}> already has implicit role="${implicitRole}". Avoid redundant role assignment.`
|
|
214
268
|
};
|
|
215
269
|
},
|
|
216
270
|
implicitRoleOverride(tag, implicitRole, role) {
|
|
217
271
|
return {
|
|
218
|
-
code:
|
|
219
|
-
category:
|
|
272
|
+
code: DiagnosticCode3.HtmlImplicitRoleOverride,
|
|
273
|
+
category: DiagnosticCategory3.HTML,
|
|
220
274
|
severity: "error",
|
|
221
275
|
message: `<${tag}> should not override its implicit role="${implicitRole}" with role="${role}".`
|
|
222
276
|
};
|
|
223
277
|
},
|
|
224
278
|
standaloneRegionOverride(tag, implicitRole) {
|
|
225
279
|
return {
|
|
226
|
-
code:
|
|
227
|
-
category:
|
|
280
|
+
code: DiagnosticCode3.HtmlStandaloneRegionOverride,
|
|
281
|
+
category: DiagnosticCategory3.HTML,
|
|
228
282
|
severity: "error",
|
|
229
283
|
message: `<${tag}> is a self-contained element with implicit role="${implicitRole}". Assigning role="region" has been removed.`
|
|
230
284
|
};
|
|
231
285
|
},
|
|
232
286
|
landmarkRoleOverride(tag, implicitRole, role) {
|
|
233
287
|
return {
|
|
234
|
-
code:
|
|
235
|
-
category:
|
|
288
|
+
code: DiagnosticCode3.HtmlLandmarkRoleOverride,
|
|
289
|
+
category: DiagnosticCategory3.HTML,
|
|
236
290
|
severity: "error",
|
|
237
291
|
message: `<${tag}> has a fixed landmark role="${implicitRole}". role="${role}" overrides it and confuses assistive technology. The override has been removed.`
|
|
238
292
|
};
|
|
239
293
|
},
|
|
240
294
|
invalidChild(child, parent, allowed) {
|
|
241
295
|
return {
|
|
242
|
-
code:
|
|
243
|
-
category:
|
|
296
|
+
code: DiagnosticCode3.HtmlInvalidChild,
|
|
297
|
+
category: DiagnosticCategory3.HTML,
|
|
244
298
|
severity: "error",
|
|
245
299
|
message: `<${child}> is not a valid direct child of <${parent}>. Allowed: ${allowed}.`
|
|
246
300
|
};
|
|
@@ -248,8 +302,8 @@ var HtmlDiagnostics = {
|
|
|
248
302
|
roleNotPermitted(tag, role, allowedRoles) {
|
|
249
303
|
const allowed = allowedRoles.length > 0 ? allowedRoles.map((r) => `"${r}"`).join(", ") : "none \u2014 no explicit role is permitted on this element";
|
|
250
304
|
return {
|
|
251
|
-
code:
|
|
252
|
-
category:
|
|
305
|
+
code: DiagnosticCode3.HtmlRoleNotPermitted,
|
|
306
|
+
category: DiagnosticCategory3.HTML,
|
|
253
307
|
severity: "error",
|
|
254
308
|
message: `role="${role}" is not permitted on <${tag}>. Allowed alternate role(s): ${allowed}.`,
|
|
255
309
|
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.'
|
|
@@ -260,8 +314,8 @@ var HtmlDiagnostics = {
|
|
|
260
314
|
input: {
|
|
261
315
|
unsupportedType(type) {
|
|
262
316
|
return {
|
|
263
|
-
code:
|
|
264
|
-
category:
|
|
317
|
+
code: DiagnosticCode3.HtmlInputUnsupportedType,
|
|
318
|
+
category: DiagnosticCategory3.HTML,
|
|
265
319
|
severity: "warning",
|
|
266
320
|
message: `type="${type}" is not a value defined by the HTML specification. Browsers silently fall back to type="text".`,
|
|
267
321
|
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).',
|
|
@@ -281,7 +335,7 @@ var HtmlDiagnostics = {
|
|
|
281
335
|
if (!code) throw new Error(`No DiagnosticCode registered for input attribute "${attribute}"`);
|
|
282
336
|
return {
|
|
283
337
|
code,
|
|
284
|
-
category:
|
|
338
|
+
category: DiagnosticCategory3.HTML,
|
|
285
339
|
severity: "warning",
|
|
286
340
|
message: `"${attribute}" is ignored on <input type="${type}">.`,
|
|
287
341
|
rationale: `"${attribute}" only has an effect when type is one of: ${allowed}. Browsers silently ignore it on other input types.`,
|
|
@@ -293,18 +347,30 @@ var HtmlDiagnostics = {
|
|
|
293
347
|
]
|
|
294
348
|
};
|
|
295
349
|
}
|
|
350
|
+
},
|
|
351
|
+
// Reserved for <a>-specific facts (HTML3201–3299, see codes.ts).
|
|
352
|
+
anchor: {
|
|
353
|
+
dangerousHref(href) {
|
|
354
|
+
return {
|
|
355
|
+
code: DiagnosticCode3.HtmlAnchorDangerousHref,
|
|
356
|
+
category: DiagnosticCategory3.HTML,
|
|
357
|
+
severity: "warning",
|
|
358
|
+
message: `href="${href}" uses a scheme that executes attacker-controlled content when navigated to. It has been removed.`,
|
|
359
|
+
rationale: '"javascript:", "data:", and "vbscript:" URLs are a common XSS vector when href comes from untrusted input \u2014 none of the three are needed for legitimate navigation.'
|
|
360
|
+
};
|
|
361
|
+
}
|
|
296
362
|
}
|
|
297
363
|
};
|
|
298
364
|
|
|
299
365
|
// ../../lib/contract/src/diagnostics/input-accessibility.ts
|
|
300
|
-
import { DiagnosticCategory as
|
|
366
|
+
import { DiagnosticCategory as DiagnosticCategory4, DiagnosticCode as DiagnosticCode4 } from "../_shared/diagnostics.js";
|
|
301
367
|
function accessibilityFact(input) {
|
|
302
|
-
return { category:
|
|
368
|
+
return { category: DiagnosticCategory4.Accessibility, ...input };
|
|
303
369
|
}
|
|
304
370
|
var InputAccessibilityDiagnostics = {
|
|
305
371
|
missingAccessibleName() {
|
|
306
372
|
return accessibilityFact({
|
|
307
|
-
code:
|
|
373
|
+
code: DiagnosticCode4.A11yInputMissingAccessibleName,
|
|
308
374
|
severity: "warning",
|
|
309
375
|
message: "This input has no accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
|
|
310
376
|
rationale: "Assistive technology announces a form field by its accessible name; without one, users of screen readers cannot tell what the field is for.",
|
|
@@ -319,7 +385,7 @@ var InputAccessibilityDiagnostics = {
|
|
|
319
385
|
},
|
|
320
386
|
placeholderIsNotLabel() {
|
|
321
387
|
return accessibilityFact({
|
|
322
|
-
code:
|
|
388
|
+
code: DiagnosticCode4.A11yInputPlaceholderNotLabel,
|
|
323
389
|
severity: "warning",
|
|
324
390
|
message: "Placeholder text does not provide an accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
|
|
325
391
|
rationale: "Placeholder text disappears as users interact with the field and is not treated as the control's accessible name by many assistive technologies.",
|
|
@@ -334,7 +400,7 @@ var InputAccessibilityDiagnostics = {
|
|
|
334
400
|
},
|
|
335
401
|
passwordMissingAutocomplete() {
|
|
336
402
|
return accessibilityFact({
|
|
337
|
-
code:
|
|
403
|
+
code: DiagnosticCode4.A11yInputPasswordAutocomplete,
|
|
338
404
|
severity: "warning",
|
|
339
405
|
message: "Password inputs should specify an autoComplete value.",
|
|
340
406
|
rationale: "Without an explicit autocomplete hint, password managers and browsers cannot reliably tell a sign-in field apart from a password-creation field.",
|
|
@@ -352,7 +418,7 @@ var InputAccessibilityDiagnostics = {
|
|
|
352
418
|
},
|
|
353
419
|
requiredReadOnlyConflict() {
|
|
354
420
|
return accessibilityFact({
|
|
355
|
-
code:
|
|
421
|
+
code: DiagnosticCode4.A11yInputRequiredReadOnlyConflict,
|
|
356
422
|
severity: "warning",
|
|
357
423
|
message: "The required and readOnly attributes are both present. A read-only field cannot satisfy required validation through user interaction.",
|
|
358
424
|
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."
|
|
@@ -361,6 +427,27 @@ var InputAccessibilityDiagnostics = {
|
|
|
361
427
|
};
|
|
362
428
|
|
|
363
429
|
// ../../lib/contract/src/aria/factories.ts
|
|
430
|
+
function invalidWithoutFix(input) {
|
|
431
|
+
return {
|
|
432
|
+
valid: false,
|
|
433
|
+
fixable: false,
|
|
434
|
+
severity: input.severity,
|
|
435
|
+
...isDefined(input.attribute) && { attribute: input.attribute },
|
|
436
|
+
...isDefined(input.message) && { message: input.message },
|
|
437
|
+
...isDefined(input.diagnostic) && { diagnostic: input.diagnostic }
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function invalidWithFix(input) {
|
|
441
|
+
return {
|
|
442
|
+
valid: false,
|
|
443
|
+
fixable: true,
|
|
444
|
+
severity: input.severity,
|
|
445
|
+
...isDefined(input.attribute) && { attribute: input.attribute },
|
|
446
|
+
...isDefined(input.message) && { message: input.message },
|
|
447
|
+
...isDefined(input.diagnostic) && { diagnostic: input.diagnostic },
|
|
448
|
+
fix: input.fix
|
|
449
|
+
};
|
|
450
|
+
}
|
|
364
451
|
function removeProp(props, key) {
|
|
365
452
|
const next = { ...props };
|
|
366
453
|
delete next[key];
|
|
@@ -376,6 +463,34 @@ function removeAttributeFix(attribute) {
|
|
|
376
463
|
}
|
|
377
464
|
});
|
|
378
465
|
}
|
|
466
|
+
function defineRuleMetadata(rule, metadata) {
|
|
467
|
+
const descriptors = {};
|
|
468
|
+
for (const key of Object.keys(metadata)) {
|
|
469
|
+
descriptors[key] = { value: metadata[key], enumerable: false };
|
|
470
|
+
}
|
|
471
|
+
Object.defineProperties(rule, descriptors);
|
|
472
|
+
return rule;
|
|
473
|
+
}
|
|
474
|
+
function createRemoveAttributeRule(attribute, options) {
|
|
475
|
+
const { when, severity = "warning", message, diagnostic, readsProps, tags } = options;
|
|
476
|
+
const fix = removeAttributeFix(attribute);
|
|
477
|
+
const rule = (context) => {
|
|
478
|
+
if (!when(context)) return [];
|
|
479
|
+
return [
|
|
480
|
+
invalidWithFix({
|
|
481
|
+
severity,
|
|
482
|
+
attribute,
|
|
483
|
+
...isDefined(message) && { message },
|
|
484
|
+
...isDefined(diagnostic) && { diagnostic: diagnostic(context) },
|
|
485
|
+
fix
|
|
486
|
+
})
|
|
487
|
+
];
|
|
488
|
+
};
|
|
489
|
+
return defineRuleMetadata(rule, {
|
|
490
|
+
...isDefined(readsProps) && { readsProps },
|
|
491
|
+
...isDefined(tags) && { tags }
|
|
492
|
+
});
|
|
493
|
+
}
|
|
379
494
|
|
|
380
495
|
// ../core/src/html/contracts/categories.ts
|
|
381
496
|
var LANDMARK_TAGS = [
|
|
@@ -387,6 +502,52 @@ var LANDMARK_TAGS = [
|
|
|
387
502
|
"nav"
|
|
388
503
|
];
|
|
389
504
|
|
|
505
|
+
// ../core/src/html/anchor-rules.ts
|
|
506
|
+
var DANGEROUS_URL_SCHEMES = ["javascript:", "data:", "vbscript:"];
|
|
507
|
+
function normalizeForSchemeCheck(href) {
|
|
508
|
+
return href.replace(/[\x00-\x20]/g, "").toLowerCase();
|
|
509
|
+
}
|
|
510
|
+
function isDangerousUrl(href) {
|
|
511
|
+
if (!isString(href)) return false;
|
|
512
|
+
const normalized = normalizeForSchemeCheck(href);
|
|
513
|
+
return DANGEROUS_URL_SCHEMES.some((scheme) => normalized.startsWith(scheme));
|
|
514
|
+
}
|
|
515
|
+
var dangerousHrefRule = createRemoveAttributeRule("href", {
|
|
516
|
+
when: ({ props }) => isDangerousUrl(props.href),
|
|
517
|
+
severity: "warning",
|
|
518
|
+
diagnostic: ({ props }) => HtmlDiagnostics.anchor.dangerousHref(props.href),
|
|
519
|
+
readsProps: ["href"],
|
|
520
|
+
tags: ["a"]
|
|
521
|
+
});
|
|
522
|
+
var roleButtonWithHrefRule = Object.assign(
|
|
523
|
+
({ props }) => {
|
|
524
|
+
if (props.role !== "button" || !isString(props.href) || props.href.length === 0) {
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
const diagnostic = AnchorAccessibilityDiagnostics.roleButtonWithHref();
|
|
528
|
+
return [invalidWithoutFix({ severity: diagnostic.severity, attribute: "role", diagnostic })];
|
|
529
|
+
},
|
|
530
|
+
{ readsProps: ["role", "href"], tags: ["a"] }
|
|
531
|
+
);
|
|
532
|
+
var ariaDisabledInertRule = Object.assign(
|
|
533
|
+
({ props }) => {
|
|
534
|
+
const ariaDisabled = props["aria-disabled"];
|
|
535
|
+
const isDisabled = ariaDisabled === true || ariaDisabled === "true";
|
|
536
|
+
const hasHref = isString(props.href) && props.href.length > 0;
|
|
537
|
+
if (!isDisabled || !hasHref) return [];
|
|
538
|
+
const diagnostic = AnchorAccessibilityDiagnostics.ariaDisabledInert();
|
|
539
|
+
return [
|
|
540
|
+
invalidWithoutFix({ severity: diagnostic.severity, attribute: "aria-disabled", diagnostic })
|
|
541
|
+
];
|
|
542
|
+
},
|
|
543
|
+
{ readsProps: ["aria-disabled", "href"], tags: ["a"] }
|
|
544
|
+
);
|
|
545
|
+
var ANCHOR_RULES = [
|
|
546
|
+
dangerousHrefRule,
|
|
547
|
+
roleButtonWithHrefRule,
|
|
548
|
+
ariaDisabledInertRule
|
|
549
|
+
];
|
|
550
|
+
|
|
390
551
|
// ../core/src/html/spec/vocabulary/input.ts
|
|
391
552
|
var TEXT_INPUT_TYPES = ["text", "search", "url", "tel", "email", "password"];
|
|
392
553
|
var NUMERIC_INPUT_TYPES = [
|
|
@@ -466,12 +627,12 @@ function createInputAttributeTypeRule({
|
|
|
466
627
|
// ../core/src/html/spec/validators/mutually-exclusive-validator.ts
|
|
467
628
|
function createMutuallyExclusiveRule({
|
|
468
629
|
props: conflictingProps,
|
|
469
|
-
diagnostic:
|
|
630
|
+
diagnostic: createDiagnostic2
|
|
470
631
|
}) {
|
|
471
632
|
const [first, second] = conflictingProps;
|
|
472
633
|
const rule = ({ tag, props }) => {
|
|
473
634
|
if (tag !== "input" || !props[first] || !props[second]) return [];
|
|
474
|
-
const diagnostic =
|
|
635
|
+
const diagnostic = createDiagnostic2();
|
|
475
636
|
return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
|
|
476
637
|
};
|
|
477
638
|
return Object.assign(rule, { readsProps: conflictingProps, tags: ["input"] });
|
|
@@ -851,15 +1012,19 @@ var HTML_ARIA_RULES = [
|
|
|
851
1012
|
landmarkRoleRule,
|
|
852
1013
|
landmarkAccessibleNameRule,
|
|
853
1014
|
roleNotPermittedRule,
|
|
854
|
-
...INPUT_RULES
|
|
1015
|
+
...INPUT_RULES,
|
|
1016
|
+
...ANCHOR_RULES
|
|
855
1017
|
];
|
|
856
1018
|
export {
|
|
1019
|
+
ANCHOR_RULES,
|
|
857
1020
|
HTML_ARIA_RULES,
|
|
858
1021
|
INPUT_RULES,
|
|
859
1022
|
acceptRequiresFileTypeRule,
|
|
860
1023
|
altRequiresImageTypeRule,
|
|
1024
|
+
ariaDisabledInertRule,
|
|
861
1025
|
captureRequiresFileTypeRule,
|
|
862
1026
|
checkedRequiresCheckableTypeRule,
|
|
1027
|
+
dangerousHrefRule,
|
|
863
1028
|
heightRequiresImageTypeRule,
|
|
864
1029
|
inputAccessibleNameRule,
|
|
865
1030
|
landmarkAccessibleNameRule,
|
|
@@ -873,6 +1038,7 @@ export {
|
|
|
873
1038
|
patternRequiresTextTypeRule,
|
|
874
1039
|
requireAccessibleName,
|
|
875
1040
|
requiredReadOnlyConflictRule,
|
|
1041
|
+
roleButtonWithHrefRule,
|
|
876
1042
|
roleNotPermittedRule,
|
|
877
1043
|
sizeRequiresTextTypeRule,
|
|
878
1044
|
stepRequiresNumericTypeRule,
|
package/dist/lit/index.d.ts
CHANGED
|
@@ -72,6 +72,19 @@ type MergeRecords<A extends object, B extends object> = IsEmptyRecord<A> extends
|
|
|
72
72
|
type IntrinsicTag = keyof HTMLElementTagNameMap;
|
|
73
73
|
|
|
74
74
|
type ElementType = IntrinsicTag | (string & {});
|
|
75
|
+
/**
|
|
76
|
+
* Resolves a component's default tag to its real DOM interface — `HTMLDialogElement` for
|
|
77
|
+
* `'dialog'`, `HTMLDetailsElement` for `'details'`, and so on — falling back to `HTMLElement`
|
|
78
|
+
* for custom-element tags or anything not in `HTMLElementTagNameMap`. Used to type
|
|
79
|
+
* `FactoryOptions.onElement`'s `element` param so component authors get direct, correctly-typed
|
|
80
|
+
* access to tag-specific native members (`dialogEl.showModal()`) without an unsafe cast.
|
|
81
|
+
*
|
|
82
|
+
* The fallback is `HTMLElement`, not the more generic `Element` — every tag reachable through
|
|
83
|
+
* `IntrinsicTag` extends it, and so does every custom element per spec, so members `HTMLElement`
|
|
84
|
+
* itself declares (`showPopover()`/`hidePopover()`/`togglePopover()`, the `popover` attribute)
|
|
85
|
+
* stay directly accessible even for tags with no dedicated entry in `HTMLElementTagNameMap`.
|
|
86
|
+
*/
|
|
87
|
+
type ElementForTag<TDefault extends ElementType> = TDefault extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TDefault] : HTMLElement;
|
|
75
88
|
|
|
76
89
|
declare const KNOWN_ARIA_ROLES: readonly ["alert", "alertdialog", "application", "article", "banner", "blockquote", "button", "caption", "cell", "checkbox", "code", "columnheader", "combobox", "complementary", "contentinfo", "definition", "deletion", "dialog", "document", "emphasis", "feed", "figure", "form", "generic", "grid", "gridcell", "group", "heading", "img", "insertion", "link", "list", "listbox", "listitem", "log", "main", "marquee", "math", "menu", "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", "meter", "navigation", "none", "note", "option", "paragraph", "presentation", "progressbar", "radio", "radiogroup", "region", "row", "rowgroup", "rowheader", "scrollbar", "search", "searchbox", "separator", "slider", "spinbutton", "status", "strong", "subscript", "superscript", "switch", "tab", "table", "tablist", "tabpanel", "term", "textbox", "time", "timer", "toolbar", "tooltip", "tree", "treegrid", "treeitem"];
|
|
77
90
|
type KnownAriaRole = (typeof KNOWN_ARIA_ROLES)[number];
|
|
@@ -171,7 +184,7 @@ type DefaultVariants<V extends VariantMap> = {
|
|
|
171
184
|
* Presets are named bundles of variant props that callers activate by key,
|
|
172
185
|
* avoiding the need to repeat variant combinations at each call site.
|
|
173
186
|
*/
|
|
174
|
-
type RecipeMap<V extends VariantMap = VariantMap> = Readonly<
|
|
187
|
+
type RecipeMap<V extends VariantMap = VariantMap> = Readonly<StringMap<VariantSelection<V>>>;
|
|
175
188
|
|
|
176
189
|
type RecipeTarget<TVariants extends VariantMap = VariantMap> = VariantSelection<TVariants>;
|
|
177
190
|
|
|
@@ -205,7 +218,7 @@ interface BaseClassOptions {
|
|
|
205
218
|
type ClassPipelineFn = (tag: unknown, props: AnyRecord, className?: ClassName, recipe?: string) => string | undefined;
|
|
206
219
|
|
|
207
220
|
interface RecipeOptions<TVariants extends VariantMap = VariantMap> {
|
|
208
|
-
recipeMap?:
|
|
221
|
+
recipeMap?: StringMap<RecipeTarget<TVariants>>;
|
|
209
222
|
}
|
|
210
223
|
|
|
211
224
|
interface TagMapOptions {
|
|
@@ -374,7 +387,7 @@ type StylingOptions<V extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPre
|
|
|
374
387
|
* combination, skipping runtime class computation entirely when a match is found. Normally
|
|
375
388
|
* generated by a build-time class-extraction plugin rather than hand-authored.
|
|
376
389
|
*/
|
|
377
|
-
readonly precomputedClasses?: Readonly<
|
|
390
|
+
readonly precomputedClasses?: Readonly<StringMap<string>>;
|
|
378
391
|
};
|
|
379
392
|
|
|
380
393
|
type NormalizeFn<Props extends AnyRecord = AnyRecord> = {
|
|
@@ -420,13 +433,23 @@ type FactoryOptions<TDefault extends ElementType = ElementType, Props extends An
|
|
|
420
433
|
* no prop-based equivalent), not for anything expressible as a plain
|
|
421
434
|
* prop.
|
|
422
435
|
*
|
|
436
|
+
* `element` is typed to the real DOM interface of every tag the rendered
|
|
437
|
+
* element could actually be — `TDefault` plus whatever `enforcement.allowed`
|
|
438
|
+
* permits via `as` (`HTMLDialogElement` for `tag: 'dialog'`,
|
|
439
|
+
* `HTMLDetailsElement` for `tag: 'details'`, and so on) — no cast needed to
|
|
440
|
+
* reach tag-specific members. A component that leaves `allowed`
|
|
441
|
+
* unconstrained (any tag reachable via `as`) falls back to `HTMLElement`,
|
|
442
|
+
* which still covers members every element shares (`showPopover()` and
|
|
443
|
+
* friends); restrict `enforcement.allowed` to the tags `onElement`
|
|
444
|
+
* actually knows how to handle to get real narrowing.
|
|
445
|
+
*
|
|
423
446
|
* `getProps` returns the instance's *current* resolved props at call
|
|
424
447
|
* time — read it from inside a listener registered once at mount, rather
|
|
425
448
|
* than re-subscribing on every prop change.
|
|
426
449
|
*
|
|
427
450
|
* Return a cleanup function to run when the instance unmounts.
|
|
428
451
|
*/
|
|
429
|
-
readonly onElement?: (element:
|
|
452
|
+
readonly onElement?: (element: ElementForTag<TDefault | TAllowed>, getProps: () => Readonly<Props>) => void | (() => void);
|
|
430
453
|
};
|
|
431
454
|
|
|
432
455
|
/**
|