@markuplint/rules 5.4.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/CLAUDE.md +4 -3
- package/lib/aria-prop-requires-role/index.js +2 -2
- package/lib/attr-eligibility.d.ts +21 -0
- package/lib/attr-eligibility.js +86 -6
- package/lib/attr-order/index.js +2 -1
- package/lib/attr-value-quotes/index.js +1 -1
- package/lib/consistent-table-row-length/index.js +1 -1
- package/lib/element-supports-aria-prop/index.js +2 -2
- package/lib/form-attr-references-form/index.js +2 -2
- package/lib/helpers.d.ts +43 -5
- package/lib/helpers.js +88 -26
- package/lib/input-list-references-datalist/index.js +2 -2
- package/lib/itemprop-requires-itemscope/index.js +27 -10
- package/lib/label-for-references-labelable/index.js +2 -2
- package/lib/label-has-control/index.d.ts +17 -0
- package/lib/label-has-control/index.js +28 -1
- package/lib/label-no-multiple-controls/index.d.ts +4 -5
- package/lib/label-no-multiple-controls/index.js +6 -6
- package/lib/landmark-roles/roles.d.ts +13 -1
- package/lib/landmark-roles/roles.js +22 -0
- package/lib/no-ambiguous-navigable-target-names/index.js +4 -0
- package/lib/no-aria-on-presentational-children/index.js +4 -0
- package/lib/no-aria-on-unsupported-element/index.js +2 -2
- package/lib/no-boolean-attr-value/index.js +2 -1
- package/lib/no-broken-fragment-link/index.d.ts +5 -0
- package/lib/no-broken-fragment-link/index.js +8 -2
- package/lib/no-consecutive-br/index.js +1 -1
- package/lib/no-contradictory-aria-prop/index.js +2 -2
- package/lib/no-default-aria-value/index.js +2 -2
- package/lib/no-default-value/index.js +4 -0
- package/lib/no-deprecated-aria-prop/index.js +2 -2
- package/lib/no-disallowed-ancestor/index.d.ts +16 -1
- package/lib/no-disallowed-ancestor/index.js +78 -4
- package/lib/no-disallowed-attr/index.js +4 -0
- package/lib/no-duplicate-dt/index.js +1 -1
- package/lib/no-duplicate-h1/index.js +18 -14
- package/lib/no-duplicate-id/index.d.ts +9 -0
- package/lib/no-duplicate-id/index.js +23 -4
- package/lib/no-empty-table-track/index.js +1 -1
- package/lib/no-ineffective-attr/index.js +4 -0
- package/lib/no-invalid-aria-prop-value/index.js +3 -3
- package/lib/no-invalid-attr-value/index.js +12 -0
- package/lib/no-nested-top-level-landmark/index.js +20 -17
- package/lib/no-prohibited-naming/index.js +2 -2
- package/lib/no-redundant-accessible-name/index.js +8 -7
- package/lib/no-redundant-aria-prop/index.js +2 -2
- package/lib/no-refer-to-non-existent-id/index.d.ts +15 -0
- package/lib/no-refer-to-non-existent-id/index.js +47 -16
- package/lib/no-restricted-element/index.js +4 -2
- package/lib/no-skipped-heading-level/index.js +9 -4
- package/lib/no-stray-head-or-body-tag/index.d.ts +2 -1
- package/lib/no-stray-head-or-body-tag/index.js +2 -1
- package/lib/no-table-cell-overlap/index.js +1 -1
- package/lib/no-table-span-overflow/index.js +1 -1
- package/lib/no-unescaped-char/index.js +1 -1
- package/lib/permitted-contents/index.js +2 -4
- package/lib/permitted-contents/matches-selector.js +28 -5
- package/lib/permitted-contents/represent-transparent-nodes.js +100 -68
- package/lib/permitted-contents/slot-content.d.ts +2 -1
- package/lib/permitted-contents/slot-content.js +28 -2
- package/lib/permitted-contents/start.js +4 -2
- package/lib/permitted-contents/utils.d.ts +11 -0
- package/lib/permitted-contents/utils.js +15 -0
- package/lib/placeholder-label-option/index.js +4 -26
- package/lib/require-accessible-name/index.js +2 -2
- package/lib/require-adjacent-popover/index.js +4 -2
- package/lib/require-ancestor/index.d.ts +20 -0
- package/lib/require-ancestor/index.js +33 -7
- package/lib/require-datetime/index.js +1 -1
- package/lib/require-dialog-autofocus/index.js +8 -6
- package/lib/require-h1/index.js +4 -8
- package/lib/require-landmark-label/index.js +17 -15
- package/lib/role-supports-aria-prop/index.js +2 -2
- package/lib/sizes-auto-requires-lazy-loading/index.d.ts +4 -2
- package/lib/sizes-auto-requires-lazy-loading/index.js +4 -2
- package/lib/usemap-references-map/index.js +11 -2
- package/lib/valid-speculation-rules/index.js +1 -1
- package/lib/wai-aria/checkings/interaction-in-hidden.js +6 -2
- package/lib/wai-aria/checkings/presentational-children.js +38 -17
- package/lib/wai-aria/checkings/required-accessibility-parent-role.js +2 -1
- package/lib/wai-aria/checkings/required-prop.js +2 -1
- package/lib/wai-aria/checkings/tab-requires-tabpanel.js +10 -6
- package/lib/wai-aria/checkings/value.d.ts +1 -4
- package/lib/wai-aria/checkings/value.js +5 -17
- package/package.json +11 -11
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* `auto` (`allowsAutoSizes`), not `loading="lazy"` alone.
|
|
10
10
|
*
|
|
11
11
|
* Gated on `srcset` being present, matching the source rule this was split
|
|
12
|
-
* from
|
|
13
|
-
*
|
|
12
|
+
* from. An `<img>` without `srcset` is left to the `@markuplint/html-spec`
|
|
13
|
+
* data of its `sizes`: the condition allows it only on a lazily loaded image
|
|
14
|
+
* (`no-disallowed-attr`) and the type only as the value `auto`
|
|
15
|
+
* (`no-invalid-attr-value`).
|
|
14
16
|
*/
|
|
15
17
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, undefined>>;
|
|
16
18
|
export default _default;
|
|
@@ -13,8 +13,10 @@ import meta from './meta.js';
|
|
|
13
13
|
* `auto` (`allowsAutoSizes`), not `loading="lazy"` alone.
|
|
14
14
|
*
|
|
15
15
|
* Gated on `srcset` being present, matching the source rule this was split
|
|
16
|
-
* from
|
|
17
|
-
*
|
|
16
|
+
* from. An `<img>` without `srcset` is left to the `@markuplint/html-spec`
|
|
17
|
+
* data of its `sizes`: the condition allows it only on a lazily loaded image
|
|
18
|
+
* (`no-disallowed-attr`) and the type only as the value `auto`
|
|
19
|
+
* (`no-invalid-attr-value`).
|
|
18
20
|
*/
|
|
19
21
|
export default createRule({
|
|
20
22
|
meta,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { getDOMValue, querySelectorAllInTree } from '../helpers.js';
|
|
2
3
|
import meta from './meta.js';
|
|
3
4
|
/**
|
|
4
5
|
* Require the `usemap` attribute on an `<img>` element to be a valid
|
|
@@ -27,11 +28,19 @@ export default createRule({
|
|
|
27
28
|
return;
|
|
28
29
|
if (usemapAttr.isDynamicValue)
|
|
29
30
|
return;
|
|
30
|
-
|
|
31
|
+
// The character references are resolved on both sides, as the DOM has the strings
|
|
32
|
+
const value = getDOMValue(usemapAttr) ?? usemapAttr.value;
|
|
31
33
|
if (!value.startsWith('#') || value.length <= 1)
|
|
32
34
|
return;
|
|
33
35
|
const targetName = value.slice(1);
|
|
34
|
-
const target =
|
|
36
|
+
const target = querySelectorAllInTree(el, 'map').find(candidate => {
|
|
37
|
+
const name = candidate.getAttributeNode('name');
|
|
38
|
+
if (!name) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
// A name that is not known until it is rendered may be the one that the reference means
|
|
42
|
+
return name.isDynamicValue || getDOMValue(name) === targetName;
|
|
43
|
+
});
|
|
35
44
|
if (!target) {
|
|
36
45
|
report({
|
|
37
46
|
scope: usemapAttr,
|
|
@@ -216,7 +216,7 @@ function validateUrls(urls, label, reportAt, t) {
|
|
|
216
216
|
* Validate a document-rule `where` predicate. A predicate object must contain
|
|
217
217
|
* exactly one of `and` / `or` / `not` / `href_matches` / `selector_matches`.
|
|
218
218
|
*
|
|
219
|
-
* @see https://
|
|
219
|
+
* @see https://html.spec.whatwg.org/multipage/speculative-loading.html#parse-a-document-rule-predicate
|
|
220
220
|
*/
|
|
221
221
|
function validatePredicate(predicate, label, reportAt, t) {
|
|
222
222
|
if (!isPlainObject(predicate)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mayBeFocusable } from '@markuplint/ml-spec';
|
|
1
|
+
import { getComposedParentElement, mayBeFocusable } from '@markuplint/ml-spec';
|
|
2
2
|
/**
|
|
3
3
|
* Checks whether a focusable interactive element is inside an `aria-hidden=true` subtree.
|
|
4
4
|
*
|
|
@@ -38,6 +38,10 @@ export const checkingInteractionInHidden = ({ el }) => t => {
|
|
|
38
38
|
/**
|
|
39
39
|
* Traverses the element's ancestor chain to find the closest element with `aria-hidden="true"`.
|
|
40
40
|
*
|
|
41
|
+
* The chain is the one the accessibility tree is built from ({@link getComposedParentElement}): it ends at the
|
|
42
|
+
* top of the content of an ordinary `template` element, which is not rendered and whose placement is not known,
|
|
43
|
+
* and it goes on from a declarative shadow root to its host.
|
|
44
|
+
*
|
|
41
45
|
* @param el - The element to start searching from (inclusive).
|
|
42
46
|
* @returns The `aria-hidden` attribute node if found, or `null` if no ancestor is hidden.
|
|
43
47
|
*/
|
|
@@ -50,7 +54,7 @@ el) {
|
|
|
50
54
|
if (ariaHidden?.value === 'true') {
|
|
51
55
|
return ariaHidden;
|
|
52
56
|
}
|
|
53
|
-
current = current
|
|
57
|
+
current = getComposedParentElement(current);
|
|
54
58
|
}
|
|
55
59
|
return null;
|
|
56
60
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isAriaAttr, labelable, memoizeByObject, namespaceKeyOf } from '../../helpers.js';
|
|
2
|
-
import { ARIA_RECOMMENDED_VERSION, EMBEDDED_CONTROL_ROLES, getComputedRole, getImplicitRole, isExposedAsPresentational, isNativeEmbeddedControl, } from '@markuplint/ml-spec';
|
|
1
|
+
import { getDOMValue, getElementByIdInTree, isAriaAttr, labelable, memoizeByObject, namespaceKeyOf, } from '../../helpers.js';
|
|
2
|
+
import { ARIA_RECOMMENDED_VERSION, EMBEDDED_CONTROL_ROLES, getComposedParentElement, getComputedRole, getImplicitRole, isExposedAsPresentational, isNativeEmbeddedControl, } from '@markuplint/ml-spec';
|
|
3
3
|
/**
|
|
4
4
|
* Checks whether an ARIA attribute or `role` is applied to a descendant of an
|
|
5
5
|
* element whose role has the `childrenArePresentational` characteristic.
|
|
@@ -75,6 +75,9 @@ const getContext = memoizeByObject(
|
|
|
75
75
|
* Finds the closest ancestor whose computed role has the
|
|
76
76
|
* `childrenArePresentational` characteristic.
|
|
77
77
|
*
|
|
78
|
+
* The ancestors are those of the accessibility tree ({@link getComposedParentElement}): the search ends at the
|
|
79
|
+
* top of the content of an ordinary `template` element, and it goes on from a declarative shadow root to its host.
|
|
80
|
+
*
|
|
78
81
|
* @param el - The element to start searching from (exclusive, starts from parent).
|
|
79
82
|
* @param ariaVersion - The ARIA version to compute the roles with.
|
|
80
83
|
* @returns The role of that ancestor, or `null` if none is found.
|
|
@@ -82,13 +85,13 @@ const getContext = memoizeByObject(
|
|
|
82
85
|
function getClosestPresentationalChildrenRole(
|
|
83
86
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
84
87
|
el, ariaVersion) {
|
|
85
|
-
let current = el
|
|
88
|
+
let current = getComposedParentElement(el);
|
|
86
89
|
while (current) {
|
|
87
90
|
const role = getComputedRole(el.ownerMLDocument.specs, current, ariaVersion).role;
|
|
88
91
|
if (role?.childrenPresentational) {
|
|
89
92
|
return role;
|
|
90
93
|
}
|
|
91
|
-
current = current
|
|
94
|
+
current = getComposedParentElement(current);
|
|
92
95
|
}
|
|
93
96
|
return null;
|
|
94
97
|
}
|
|
@@ -104,6 +107,10 @@ el, ariaVersion) {
|
|
|
104
107
|
* - the accessible name of every control that a `label` ancestor can name: the
|
|
105
108
|
* element its `for` refers to, and its labelable descendants (HTML-AAM)
|
|
106
109
|
*
|
|
110
|
+
* The ancestors are those of the accessibility tree ({@link getComposedParentElement}): an ancestor
|
|
111
|
+
* outside of the content of an ordinary `template` element reads nothing in it, as the content is not
|
|
112
|
+
* rendered there.
|
|
113
|
+
*
|
|
107
114
|
* @see https://w3c.github.io/accname/#comp_name_from_content
|
|
108
115
|
* @see https://w3c.github.io/accname/#comp_labelledby_traversal
|
|
109
116
|
* @see https://w3c.github.io/html-aam/#accessible-name-and-description-computation
|
|
@@ -128,13 +135,17 @@ el, ariaVersion) {
|
|
|
128
135
|
if (current !== el) {
|
|
129
136
|
add(current);
|
|
130
137
|
}
|
|
131
|
-
|
|
138
|
+
// An `aria-labelledby` refers to an element of its own tree
|
|
139
|
+
const idValue = getDOMValue(current.getAttributeNode('id'));
|
|
140
|
+
if (idValue &&
|
|
141
|
+
referenced.get(current.getRootNode())?.has(idValue) &&
|
|
142
|
+
getElementByIdInTree(current, idValue, true) === current) {
|
|
132
143
|
add(current, true);
|
|
133
144
|
}
|
|
134
145
|
for (const control of getLabeledControls(current)) {
|
|
135
146
|
add(control);
|
|
136
147
|
}
|
|
137
|
-
current = current
|
|
148
|
+
current = getComposedParentElement(current);
|
|
138
149
|
}
|
|
139
150
|
return visits;
|
|
140
151
|
}
|
|
@@ -151,27 +162,36 @@ const getLabeledControls = memoizeByObject(
|
|
|
151
162
|
if (el.localName !== 'label' || el.namespaceURI !== 'http://www.w3.org/1999/xhtml') {
|
|
152
163
|
return [];
|
|
153
164
|
}
|
|
154
|
-
const
|
|
155
|
-
const target =
|
|
165
|
+
const forAttr = el.getAttributeNode('for');
|
|
166
|
+
const target = forAttr == null ? null : getElementByIdInTree(el, forAttr.value, forAttr.isExpressionValue);
|
|
156
167
|
return [...(target ? [target] : []), ...el.querySelectorAll(labelable.join(', '))];
|
|
157
168
|
});
|
|
158
169
|
/**
|
|
159
|
-
* The IDs that an `aria-labelledby` or `aria-describedby` of the
|
|
170
|
+
* The IDs that an `aria-labelledby` or `aria-describedby` refers to, by the tree of the referrer: an
|
|
171
|
+
* id is resolved in the tree of the element that refers to it.
|
|
160
172
|
*/
|
|
161
173
|
const getReferencedIds = memoizeByObject(
|
|
162
174
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
163
175
|
(document) => {
|
|
164
|
-
const
|
|
165
|
-
for (const referrer of document.
|
|
176
|
+
const idsOfTree = new Map();
|
|
177
|
+
for (const referrer of document.deepQuerySelectorAll('[aria-labelledby], [aria-describedby]')) {
|
|
178
|
+
const root = referrer.getRootNode();
|
|
179
|
+
let ids = idsOfTree.get(root);
|
|
180
|
+
if (!ids) {
|
|
181
|
+
ids = new Set();
|
|
182
|
+
idsOfTree.set(root, ids);
|
|
183
|
+
}
|
|
166
184
|
for (const name of ['aria-labelledby', 'aria-describedby']) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
const attr = referrer.getAttributeNode(name);
|
|
186
|
+
for (const id of attr?.value.split(/\s+/) ?? []) {
|
|
187
|
+
const domId = id === '' ? null : getDOMValue(attr, id);
|
|
188
|
+
if (domId != null) {
|
|
189
|
+
ids.add(domId);
|
|
170
190
|
}
|
|
171
191
|
}
|
|
172
192
|
}
|
|
173
193
|
}
|
|
174
|
-
return
|
|
194
|
+
return idsOfTree;
|
|
175
195
|
});
|
|
176
196
|
/**
|
|
177
197
|
* Whether the attribute changes what its element contributes to a text
|
|
@@ -275,11 +295,12 @@ el, computed) {
|
|
|
275
295
|
function labelledbyGivesText(
|
|
276
296
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
277
297
|
el, ariaVersion) {
|
|
278
|
-
|
|
298
|
+
const labelledbyAttr = el.getAttributeNode('aria-labelledby');
|
|
299
|
+
return (labelledbyAttr?.value.split(/\s+/) ?? []).some(id => {
|
|
279
300
|
if (id === '') {
|
|
280
301
|
return false;
|
|
281
302
|
}
|
|
282
|
-
const referenced = el
|
|
303
|
+
const referenced = getElementByIdInTree(el, id, labelledbyAttr?.isExpressionValue);
|
|
283
304
|
return referenced != null && referenced.getReferencedTextAlternative(ariaVersion) !== '';
|
|
284
305
|
});
|
|
285
306
|
}
|
|
@@ -23,7 +23,8 @@ export const checkingRequiredAccessibilityParentRole = ({ el, computed }) => t =
|
|
|
23
23
|
(computed.errorType !== 'INVALID_REQUIRED_CONTEXT_ROLE' &&
|
|
24
24
|
// NO_OWNER with role: null means all ancestors were transparent
|
|
25
25
|
// and no valid context could be found. NO_OWNER with role kept
|
|
26
|
-
// (
|
|
26
|
+
// (no parent in the accessibility tree) is a root fragment, or the top
|
|
27
|
+
// of the content of an ordinary `template` element — skip.
|
|
27
28
|
!(computed.errorType === 'NO_OWNER' && !computed.role))) {
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
@@ -34,7 +34,8 @@ export const checkingRequiredProp = ({ el, role, propSpecs }) => t => {
|
|
|
34
34
|
return true;
|
|
35
35
|
});
|
|
36
36
|
for (const { name: requiredProp } of requiredProps) {
|
|
37
|
-
|
|
37
|
+
// An attribute that the framework does not output (`aria-checked={null}`) is not there
|
|
38
|
+
const has = el.attributes.some(attr => !attr.isAbsent && attr.nameKey === requiredProp);
|
|
38
39
|
if (!has) {
|
|
39
40
|
const propSpec = propSpecs.find(p => p.name === requiredProp);
|
|
40
41
|
const ariaVersion = el.rule.options?.version ??
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ARIA_RECOMMENDED_VERSION, getComputedRole } from '@markuplint/ml-spec';
|
|
2
|
+
import { getDOMValue, getElementByIdInTree, querySelectorAllInTree } from '../../helpers.js';
|
|
2
3
|
/**
|
|
3
4
|
* Checks that an active `tab` role element has a corresponding `tabpanel` role element.
|
|
4
5
|
*
|
|
@@ -30,21 +31,24 @@ export const checkingTabRequiresTabpanel = ({ el, role }) => t => {
|
|
|
30
31
|
.split(/\s+/)
|
|
31
32
|
.filter(Boolean)
|
|
32
33
|
.some(id => {
|
|
33
|
-
const target =
|
|
34
|
+
const target = getElementByIdInTree(el, id, controlsAttr.isExpressionValue);
|
|
34
35
|
return !!target && isTabpanel(target);
|
|
35
36
|
});
|
|
36
37
|
if (hasControlledTabpanel) {
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const hasLabellingTabpanel =
|
|
41
|
+
// The tabpanel refers to the tab in the tree of the tab, and the ids are compared as the DOM has them
|
|
42
|
+
const tabId = getDOMValue(el.getAttributeNode('id'));
|
|
43
|
+
if (tabId != null) {
|
|
44
|
+
const hasLabellingTabpanel = querySelectorAllInTree(el, '[aria-labelledby]').some(candidate => {
|
|
44
45
|
const labelledbyAttr = candidate.getAttributeNode('aria-labelledby');
|
|
45
46
|
if (!labelledbyAttr || labelledbyAttr.isDynamicValue)
|
|
46
47
|
return false;
|
|
47
|
-
return labelledbyAttr.value
|
|
48
|
+
return (labelledbyAttr.value
|
|
49
|
+
.split(/\s+/)
|
|
50
|
+
.map(id => getDOMValue(labelledbyAttr, id))
|
|
51
|
+
.includes(tabId) && isTabpanel(candidate));
|
|
48
52
|
});
|
|
49
53
|
if (hasLabellingTabpanel) {
|
|
50
54
|
return;
|
|
@@ -12,13 +12,11 @@ import type { ARIAProperty, ARIARole } from '@markuplint/ml-spec';
|
|
|
12
12
|
* @param attr - The ARIA attribute node to inspect.
|
|
13
13
|
* @param role - The computed ARIA role, used to resolve conditional value types.
|
|
14
14
|
* @param propSpecs - The list of ARIA property specifications for value type lookup.
|
|
15
|
-
* @param booleanish - Whether the document supports booleanish attribute values (e.g., JSX).
|
|
16
15
|
* @returns A violation if the attribute value does not match the expected type.
|
|
17
16
|
*/
|
|
18
17
|
export declare const checkingValue: AttrChecker<boolean, Options, {
|
|
19
18
|
role?: ARIARole | null;
|
|
20
19
|
propSpecs: readonly ARIAProperty[];
|
|
21
|
-
booleanish?: boolean;
|
|
22
20
|
}>;
|
|
23
21
|
/**
|
|
24
22
|
* Validates a raw ARIA value against the expected value type defined in the specification.
|
|
@@ -30,7 +28,6 @@ export declare const checkingValue: AttrChecker<boolean, Options, {
|
|
|
30
28
|
* @param type - The ARIA value type (e.g., `"token"`, `"true/false"`, `"integer"`).
|
|
31
29
|
* @param value - The raw attribute value to validate.
|
|
32
30
|
* @param tokenEnum - The list of allowed token values for token-based types.
|
|
33
|
-
* @param booleanish - Whether empty string is accepted as a boolean `true` value.
|
|
34
31
|
* @returns `true` if the value is valid for the given type, `false` otherwise.
|
|
35
32
|
*/
|
|
36
|
-
export declare function checkAriaValue(type: string, value: string, tokenEnum: readonly string[]
|
|
33
|
+
export declare function checkAriaValue(type: string, value: string, tokenEnum: readonly string[]): boolean;
|
|
@@ -9,15 +9,14 @@
|
|
|
9
9
|
* @param attr - The ARIA attribute node to inspect.
|
|
10
10
|
* @param role - The computed ARIA role, used to resolve conditional value types.
|
|
11
11
|
* @param propSpecs - The list of ARIA property specifications for value type lookup.
|
|
12
|
-
* @param booleanish - Whether the document supports booleanish attribute values (e.g., JSX).
|
|
13
12
|
* @returns A violation if the attribute value does not match the expected type.
|
|
14
13
|
*/
|
|
15
|
-
export const checkingValue = ({ attr, role, propSpecs
|
|
14
|
+
export const checkingValue = ({ attr, role, propSpecs }) => t => {
|
|
16
15
|
if (attr.isDynamicValue) {
|
|
17
16
|
return;
|
|
18
17
|
}
|
|
19
18
|
const propSpec = propSpecs.find(p => p.name === attr.nameKey);
|
|
20
|
-
const result = checkAria(propSpec, attr.value, role?.name
|
|
19
|
+
const result = checkAria(propSpec, attr.value, role?.name);
|
|
21
20
|
if (result.isValid) {
|
|
22
21
|
return;
|
|
23
22
|
}
|
|
@@ -35,10 +34,9 @@ export const checkingValue = ({ attr, role, propSpecs, booleanish }) => t => {
|
|
|
35
34
|
* @param propSpec - The ARIA property specification, or `undefined` if the property is unknown.
|
|
36
35
|
* @param currentValue - The current attribute value to validate.
|
|
37
36
|
* @param role - The name of the computed role, used to resolve conditional value types.
|
|
38
|
-
* @param booleanish - Whether booleanish values (empty string as `true`) are accepted.
|
|
39
37
|
* @returns An object containing the validation result and, for enum types, the allowed values.
|
|
40
38
|
*/
|
|
41
|
-
function checkAria(propSpec, currentValue, role
|
|
39
|
+
function checkAria(propSpec, currentValue, role) {
|
|
42
40
|
if (!propSpec) {
|
|
43
41
|
return {
|
|
44
42
|
currentValue,
|
|
@@ -55,7 +53,7 @@ function checkAria(propSpec, currentValue, role, booleanish) {
|
|
|
55
53
|
}
|
|
56
54
|
}
|
|
57
55
|
}
|
|
58
|
-
const isValid = checkAriaValue(valueType, currentValue, propSpec.enum
|
|
56
|
+
const isValid = checkAriaValue(valueType, currentValue, propSpec.enum);
|
|
59
57
|
return {
|
|
60
58
|
...propSpec,
|
|
61
59
|
currentValue,
|
|
@@ -72,10 +70,9 @@ function checkAria(propSpec, currentValue, role, booleanish) {
|
|
|
72
70
|
* @param type - The ARIA value type (e.g., `"token"`, `"true/false"`, `"integer"`).
|
|
73
71
|
* @param value - The raw attribute value to validate.
|
|
74
72
|
* @param tokenEnum - The list of allowed token values for token-based types.
|
|
75
|
-
* @param booleanish - Whether empty string is accepted as a boolean `true` value.
|
|
76
73
|
* @returns `true` if the value is valid for the given type, `false` otherwise.
|
|
77
74
|
*/
|
|
78
|
-
export function checkAriaValue(type, value, tokenEnum
|
|
75
|
+
export function checkAriaValue(type, value, tokenEnum) {
|
|
79
76
|
switch (type) {
|
|
80
77
|
case 'token': {
|
|
81
78
|
return tokenEnum.includes(value);
|
|
@@ -90,21 +87,12 @@ export function checkAriaValue(type, value, tokenEnum, booleanish) {
|
|
|
90
87
|
return true;
|
|
91
88
|
}
|
|
92
89
|
case 'true/false': {
|
|
93
|
-
if (booleanish && value === '') {
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
90
|
return ['true', 'false'].includes(value);
|
|
97
91
|
}
|
|
98
92
|
case 'tristate': {
|
|
99
|
-
if (booleanish && value === '') {
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
93
|
return ['mixed', 'true', 'false', 'undefined'].includes(value);
|
|
103
94
|
}
|
|
104
95
|
case 'true/false/undefined': {
|
|
105
|
-
if (booleanish && value === '') {
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
96
|
return ['true', 'false', 'undefined'].includes(value);
|
|
109
97
|
}
|
|
110
98
|
case 'integer': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/rules",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"description": "Built-in rules of markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,21 +31,21 @@
|
|
|
31
31
|
"./lib/permitted-contents/debug.js": "./lib/permitted-contents/debug.browser.js"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@markuplint/html-spec": "5.
|
|
35
|
-
"@markuplint/ml-config": "5.
|
|
36
|
-
"@markuplint/ml-core": "5.
|
|
37
|
-
"@markuplint/ml-spec": "5.
|
|
38
|
-
"@markuplint/selector": "5.
|
|
39
|
-
"@markuplint/shared": "5.
|
|
40
|
-
"@markuplint/types": "5.
|
|
41
|
-
"@mdn/browser-compat-data": "^8.1.
|
|
34
|
+
"@markuplint/html-spec": "5.5.0",
|
|
35
|
+
"@markuplint/ml-config": "5.5.0",
|
|
36
|
+
"@markuplint/ml-core": "5.5.0",
|
|
37
|
+
"@markuplint/ml-spec": "5.5.0",
|
|
38
|
+
"@markuplint/selector": "5.5.0",
|
|
39
|
+
"@markuplint/shared": "5.5.0",
|
|
40
|
+
"@markuplint/types": "5.5.0",
|
|
41
|
+
"@mdn/browser-compat-data": "^8.1.8",
|
|
42
42
|
"@types/debug": "^4.1.13",
|
|
43
43
|
"ansi-colors": "^4.1.3",
|
|
44
|
-
"browserslist": "^4.29.
|
|
44
|
+
"browserslist": "^4.29.5",
|
|
45
45
|
"chrono-node": "^2.10.3",
|
|
46
46
|
"debug": "^4.4.3",
|
|
47
47
|
"image-size": "^2.0.4",
|
|
48
48
|
"type-fest": "^5.10.0"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "fedebcfd56eaec4642e68643b2f9e10750d810e1"
|
|
51
51
|
}
|