@markuplint/rules 5.2.0 → 5.4.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 +48 -0
- package/CLAUDE.md +19 -19
- package/lib/aria-prop-requires-role/index.js +2 -1
- package/lib/attr-check.js +6 -12
- package/lib/attr-eligibility.d.ts +15 -8
- package/lib/attr-eligibility.js +19 -10
- package/lib/attr-order/index.js +24 -18
- package/lib/case-sensitive-attr-name/index.js +24 -5
- package/lib/class-naming/index.js +6 -0
- package/lib/debug.d.ts +8 -1
- package/lib/debug.js +8 -1
- package/lib/element-supports-aria-prop/index.js +2 -1
- package/lib/form-attr-references-form/index.d.ts +6 -3
- package/lib/form-attr-references-form/index.js +6 -3
- package/lib/helpers.d.ts +111 -1
- package/lib/helpers.js +212 -2
- package/lib/input-list-references-datalist/index.d.ts +8 -6
- package/lib/input-list-references-datalist/index.js +8 -6
- package/lib/label-for-references-labelable/index.d.ts +8 -5
- package/lib/label-for-references-labelable/index.js +8 -5
- package/lib/landmark-roles/roles.js +14 -2
- package/lib/map-id-name-match/index.d.ts +3 -3
- package/lib/map-id-name-match/index.js +3 -3
- package/lib/no-ambiguous-navigable-target-names/index.js +2 -1
- package/lib/no-aria-hidden-on-hidden-until-found/index.d.ts +9 -6
- package/lib/no-aria-hidden-on-hidden-until-found/index.js +9 -6
- package/lib/no-aria-hidden-on-hidden-until-found/meta.d.ts +1 -1
- package/lib/no-aria-hidden-on-hidden-until-found/meta.js +1 -1
- package/lib/no-aria-on-presentational-children/index.js +3 -1
- package/lib/no-aria-on-unsupported-element/index.js +2 -2
- package/lib/no-aria-on-unsupported-element/meta.d.ts +1 -1
- package/lib/no-aria-on-unsupported-element/meta.js +1 -1
- package/lib/no-boolean-attr-value/index.js +2 -2
- package/lib/no-content-after-body/index.d.ts +2 -2
- package/lib/no-content-after-body/index.js +2 -2
- package/lib/no-contradictory-aria-prop/index.js +2 -1
- package/lib/no-default-aria-value/index.js +2 -1
- package/lib/no-default-value/index.js +2 -2
- package/lib/no-deprecated-aria-prop/index.js +2 -1
- package/lib/no-deprecated-attr/index.js +2 -1
- package/lib/no-disallowed-attr/index.d.ts +7 -1
- package/lib/no-disallowed-attr/index.js +13 -20
- package/lib/no-duplicate-attr/index.js +1 -2
- package/lib/no-duplicate-autofocus/index.js +2 -2
- package/lib/no-duplicate-h1/index.d.ts +1 -1
- package/lib/no-duplicate-h1/index.js +1 -1
- package/lib/no-empty-table-track/index.d.ts +6 -2
- package/lib/no-empty-table-track/index.js +6 -2
- package/lib/no-experimental-features/index.d.ts +5 -1
- package/lib/no-experimental-features/index.js +6 -8
- package/lib/no-hardcoded-id/index.js +1 -1
- package/lib/no-ineffective-attr/index.js +2 -2
- package/lib/no-input-file-value/index.d.ts +10 -4
- package/lib/no-input-file-value/index.js +10 -4
- package/lib/no-invalid-aria-prop-value/index.js +2 -1
- package/lib/no-invalid-attr-value/index.d.ts +3 -0
- package/lib/no-invalid-attr-value/index.js +9 -24
- package/lib/no-nonstandard-features/index.d.ts +5 -1
- package/lib/no-nonstandard-features/index.js +6 -8
- package/lib/no-obsolete-attr/index.js +3 -3
- package/lib/no-prohibited-naming/index.js +2 -1
- package/lib/no-redundant-accessible-name/index.js +17 -1
- package/lib/no-redundant-aria-prop/index.js +2 -1
- package/lib/no-refer-to-non-existent-id/index.js +4 -4
- package/lib/no-restricted-attr/index.d.ts +4 -0
- package/lib/no-restricted-attr/index.js +5 -22
- package/lib/no-stray-head-or-body-tag/index.d.ts +6 -4
- package/lib/no-stray-head-or-body-tag/index.js +11 -6
- package/lib/no-unknown-attr/index.d.ts +7 -1
- package/lib/no-unknown-attr/index.js +11 -25
- package/lib/no-unpaired-srcset-sizes/index.d.ts +3 -2
- package/lib/no-unpaired-srcset-sizes/index.js +23 -17
- package/lib/no-unsupported-browser-features/compat-data.d.ts +15 -2
- package/lib/no-unsupported-browser-features/compat-data.js +48 -3
- package/lib/no-unsupported-browser-features/index.d.ts +5 -1
- package/lib/no-unsupported-browser-features/index.js +6 -7
- package/lib/permitted-contents/choice.js +19 -8
- package/lib/permitted-contents/content-model.d.ts +24 -1
- package/lib/permitted-contents/content-model.js +56 -9
- package/lib/permitted-contents/count-pattern.js +36 -13
- package/lib/permitted-contents/debug.browser.d.ts +18 -0
- package/lib/permitted-contents/debug.browser.js +27 -0
- package/lib/permitted-contents/debug.d.ts +18 -0
- package/lib/permitted-contents/debug.js +27 -0
- package/lib/permitted-contents/index.js +2 -2
- package/lib/permitted-contents/matches-selector.js +40 -18
- package/lib/permitted-contents/order.js +23 -11
- package/lib/permitted-contents/represent-transparent-nodes.js +3 -1
- package/lib/permitted-contents/transparent.js +6 -2
- package/lib/placeholder-label-option/index.d.ts +3 -3
- package/lib/placeholder-label-option/index.js +163 -37
- package/lib/progress-value-bounds/index.d.ts +1 -1
- package/lib/progress-value-bounds/index.js +1 -1
- package/lib/require-attr/index.d.ts +6 -1
- package/lib/require-attr/index.js +13 -5
- package/lib/require-h1/index.d.ts +1 -1
- package/lib/require-h1/index.js +1 -1
- package/lib/role-supports-aria-prop/index.js +2 -1
- package/lib/sizes-auto-requires-lazy-loading/index.d.ts +3 -1
- package/lib/sizes-auto-requires-lazy-loading/index.js +30 -16
- package/lib/srcset-sizes-constraint/parse-srcset.d.ts +6 -5
- package/lib/srcset-sizes-constraint/parse-srcset.js +8 -7
- package/lib/srcset-sizes-constraint/sibling-helpers.d.ts +31 -0
- package/lib/srcset-sizes-constraint/sibling-helpers.js +58 -0
- package/lib/valid-speculation-rules/index.js +1 -1
- package/lib/wai-aria/checkings/contradictory-aria-prop.js +3 -5
- package/lib/wai-aria/checkings/default-value.js +1 -1
- package/lib/wai-aria/checkings/deprecated-props.js +2 -2
- package/lib/wai-aria/checkings/element-supports-aria-prop.js +5 -4
- package/lib/wai-aria/checkings/interaction-in-hidden.d.ts +1 -1
- package/lib/wai-aria/checkings/interaction-in-hidden.js +1 -1
- package/lib/wai-aria/checkings/no-global-prop.js +1 -1
- package/lib/wai-aria/checkings/presentational-children.d.ts +21 -10
- package/lib/wai-aria/checkings/presentational-children.js +240 -26
- package/lib/wai-aria/checkings/prohibited-naming.js +5 -4
- package/lib/wai-aria/checkings/redundant-aria-prop.js +3 -5
- package/lib/wai-aria/checkings/required-prop.js +1 -4
- package/lib/wai-aria/checkings/role-supports-aria-prop.js +4 -3
- package/lib/wai-aria/checkings/tab-requires-tabpanel.d.ts +3 -2
- package/lib/wai-aria/checkings/tab-requires-tabpanel.js +3 -2
- package/lib/wai-aria/checkings/value.d.ts +1 -1
- package/lib/wai-aria/checkings/value.js +2 -2
- package/package.json +15 -15
|
@@ -13,7 +13,7 @@ export const checkingDefaultValue = ({ attr, propSpecs }) => t => {
|
|
|
13
13
|
if (attr.isDynamicValue) {
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
const propSpec = propSpecs.find(p => p.name === attr.
|
|
16
|
+
const propSpec = propSpecs.find(p => p.name === attr.nameKey);
|
|
17
17
|
const value = attr.value.trim().toLowerCase();
|
|
18
18
|
if (propSpec?.defaultValue != null && propSpec.defaultValue === value) {
|
|
19
19
|
return {
|
|
@@ -13,9 +13,9 @@ export const checkingDeprecatedProps = ({ attr, role, propSpecs }) => t => {
|
|
|
13
13
|
if (!role) {
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
const props = role.ownedProperties.find(p => p.name === attr.
|
|
16
|
+
const props = role.ownedProperties.find(p => p.name === attr.nameKey);
|
|
17
17
|
if (props?.deprecated) {
|
|
18
|
-
const propSpec = propSpecs.find(p => p.name === attr.
|
|
18
|
+
const propSpec = propSpecs.find(p => p.name === attr.nameKey);
|
|
19
19
|
return {
|
|
20
20
|
scope: attr,
|
|
21
21
|
message: t('{0:c} on {1}', t('{0} is {1:c}', t('the "{0*}" {1}', attr.name, `ARIA ${propSpec?.type ?? 'property'}`), 'deprecated'), t('the "{0*}" {1}', role.name, 'role')),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAriaAttr } from '../../helpers.js';
|
|
1
2
|
import { ARIA_RECOMMENDED_VERSION, getARIA } from '@markuplint/ml-spec';
|
|
2
3
|
/**
|
|
3
4
|
* Checks the ARIA in HTML element-specific `aria-*` restrictions: spec data
|
|
@@ -13,13 +14,13 @@ import { ARIA_RECOMMENDED_VERSION, getARIA } from '@markuplint/ml-spec';
|
|
|
13
14
|
* @returns A violation if the element-specific restriction disallows the property.
|
|
14
15
|
*/
|
|
15
16
|
export const checkingElementSupportsAriaProp = ({ attr, role, propSpecs }) => t => {
|
|
16
|
-
if (
|
|
17
|
+
if (!isAriaAttr(attr)) {
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
const ariaVersion = attr.rule.options?.version ??
|
|
20
21
|
attr.ownerMLDocument.ruleCommonSettings?.ariaVersion ??
|
|
21
22
|
ARIA_RECOMMENDED_VERSION;
|
|
22
|
-
const propSpec = propSpecs.find(p => p.name === attr.
|
|
23
|
+
const propSpec = propSpecs.find(p => p.name === attr.nameKey);
|
|
23
24
|
const elAriaSpec = getARIA(attr.ownerMLDocument.specs, attr.ownerElement.localName, attr.ownerElement.namespaceURI, ariaVersion, attr.ownerElement.matches.bind(attr.ownerElement));
|
|
24
25
|
// Spec data may set `properties: false` to forbid every aria-* attribute on
|
|
25
26
|
// elements that have no implicit role and accept no explicit role
|
|
@@ -47,7 +48,7 @@ export const checkingElementSupportsAriaProp = ({ attr, role, propSpecs }) => t
|
|
|
47
48
|
// already implemented in the `properties.without` branch below.
|
|
48
49
|
if (elAriaSpec?.properties !== false && elAriaSpec?.properties?.only) {
|
|
49
50
|
const onlyNames = elAriaSpec.properties.only.map(o => (typeof o === 'string' ? o : o.name));
|
|
50
|
-
if (!onlyNames.includes(attr.
|
|
51
|
+
if (!onlyNames.includes(attr.nameKey)) {
|
|
51
52
|
return {
|
|
52
53
|
scope: attr,
|
|
53
54
|
message: t('{0:c} on {1}', t('{0} is {1:c}', t('the "{0*}" {1}', attr.name, `ARIA ${propSpec?.type ?? 'property'}`), 'disallowed'), t('the "{0*}" {1}', attr.ownerElement.localName, 'element')),
|
|
@@ -56,7 +57,7 @@ export const checkingElementSupportsAriaProp = ({ attr, role, propSpecs }) => t
|
|
|
56
57
|
}
|
|
57
58
|
if (elAriaSpec?.properties !== false && elAriaSpec?.properties?.without) {
|
|
58
59
|
for (const ignore of elAriaSpec.properties.without) {
|
|
59
|
-
if (ignore.name === attr.
|
|
60
|
+
if (ignore.name === attr.nameKey) {
|
|
60
61
|
const hasNativeAttr = ignore.alt?.method === 'set-attr' && attr.ownerElement.hasAttribute(ignore.alt.target);
|
|
61
62
|
return {
|
|
62
63
|
scope: attr,
|
|
@@ -6,7 +6,7 @@ import type { ElementChecker } from '@markuplint/ml-core';
|
|
|
6
6
|
* Including Elements in the Accessibility Tree
|
|
7
7
|
*
|
|
8
8
|
* @see https://w3c.github.io/aria/#tree_inclusion
|
|
9
|
-
* > Elements that are not hidden and
|
|
9
|
+
* > Elements that are not hidden and can fire an accessibility API event, including:
|
|
10
10
|
* > - Elements that are currently focused, even if the element or one of its ancestor elements has its aria-hidden attribute set to true.
|
|
11
11
|
* > - Elements that are a valid target of an aria-activedescendant attribute.
|
|
12
12
|
*
|
|
@@ -5,7 +5,7 @@ import { mayBeFocusable } from '@markuplint/ml-spec';
|
|
|
5
5
|
* Including Elements in the Accessibility Tree
|
|
6
6
|
*
|
|
7
7
|
* @see https://w3c.github.io/aria/#tree_inclusion
|
|
8
|
-
* > Elements that are not hidden and
|
|
8
|
+
* > Elements that are not hidden and can fire an accessibility API event, including:
|
|
9
9
|
* > - Elements that are currently focused, even if the element or one of its ancestor elements has its aria-hidden attribute set to true.
|
|
10
10
|
* > - Elements that are a valid target of an aria-activedescendant attribute.
|
|
11
11
|
*
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @returns A violation if the property is not a global ARIA property.
|
|
10
10
|
*/
|
|
11
11
|
export const checkingNoGlobalProp = ({ attr, propSpecs }) => t => {
|
|
12
|
-
const propSpec = propSpecs.find(prop => prop.name === attr.
|
|
12
|
+
const propSpec = propSpecs.find(prop => prop.name === attr.nameKey);
|
|
13
13
|
if (propSpec && !propSpec.isGlobal) {
|
|
14
14
|
return {
|
|
15
15
|
scope: attr,
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import type { Options } from '../types.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { AttrChecker, Element } from '@markuplint/ml-core';
|
|
3
3
|
/**
|
|
4
|
-
* Checks whether ARIA
|
|
5
|
-
* role has the `childrenArePresentational` characteristic.
|
|
4
|
+
* Checks whether an ARIA attribute or `role` is applied to a descendant of an
|
|
5
|
+
* element whose role has the `childrenArePresentational` characteristic.
|
|
6
6
|
*
|
|
7
7
|
* Presentational Children
|
|
8
8
|
*
|
|
9
|
-
* @see https://
|
|
9
|
+
* @see https://w3c.github.io/aria/#childrenArePresentational
|
|
10
10
|
* @see https://w3c.github.io/aria/#childrenArePresentational
|
|
11
11
|
*
|
|
12
|
-
* >
|
|
12
|
+
* > Indicates whether DOM descendants are presentational.
|
|
13
13
|
* > user agents SHOULD NOT expose descendants of
|
|
14
14
|
* > this element through the platform accessibility API.
|
|
15
15
|
* > If user agents do not hide the descendant nodes,
|
|
16
|
-
* > some information
|
|
16
|
+
* > some information might be read twice.
|
|
17
17
|
*
|
|
18
18
|
* @see https://w3c.github.io/aria/#tree_exclusion
|
|
19
19
|
*
|
|
@@ -26,10 +26,21 @@ import type { ElementChecker } from '@markuplint/ml-core';
|
|
|
26
26
|
* @see https://w3c.github.io/aria/#tree_inclusion
|
|
27
27
|
*
|
|
28
28
|
* > Text equivalents for hidden referenced objects
|
|
29
|
-
* >
|
|
29
|
+
* > can still be used in the name and description computation
|
|
30
30
|
* > even when not included in the accessibility tree.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* Because the text of the excluded descendants is still used, an attribute that
|
|
33
|
+
* changes what a descendant contributes to a text alternative is not
|
|
34
|
+
* ineffective, and is not reported (see {@link takesEffect}). The computations
|
|
35
|
+
* that can read the descendant are listed by {@link getVisits};
|
|
36
|
+
* `MLElement#getAccessibleNameVisits` tells which elements each of them reaches
|
|
37
|
+
* and how.
|
|
38
|
+
*
|
|
39
|
+
* @param attr - The attribute to inspect.
|
|
40
|
+
* @param el - The element the attribute is read on. For a pretended component it is
|
|
41
|
+
* the component, while the attribute belongs to the element it pretends to be.
|
|
42
|
+
* @returns A violation if the attribute is an ARIA attribute or `role` that takes no effect under an ancestor with presentational children.
|
|
34
43
|
*/
|
|
35
|
-
export declare const checkingPresentationalChildren:
|
|
44
|
+
export declare const checkingPresentationalChildren: AttrChecker<boolean, Options, {
|
|
45
|
+
readonly el: Element<boolean, Options>;
|
|
46
|
+
}>;
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import {
|
|
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';
|
|
2
3
|
/**
|
|
3
|
-
* Checks whether ARIA
|
|
4
|
-
* role has the `childrenArePresentational` characteristic.
|
|
4
|
+
* Checks whether an ARIA attribute or `role` is applied to a descendant of an
|
|
5
|
+
* element whose role has the `childrenArePresentational` characteristic.
|
|
5
6
|
*
|
|
6
7
|
* Presentational Children
|
|
7
8
|
*
|
|
8
|
-
* @see https://
|
|
9
|
+
* @see https://w3c.github.io/aria/#childrenArePresentational
|
|
9
10
|
* @see https://w3c.github.io/aria/#childrenArePresentational
|
|
10
11
|
*
|
|
11
|
-
* >
|
|
12
|
+
* > Indicates whether DOM descendants are presentational.
|
|
12
13
|
* > user agents SHOULD NOT expose descendants of
|
|
13
14
|
* > this element through the platform accessibility API.
|
|
14
15
|
* > If user agents do not hide the descendant nodes,
|
|
15
|
-
* > some information
|
|
16
|
+
* > some information might be read twice.
|
|
16
17
|
*
|
|
17
18
|
* @see https://w3c.github.io/aria/#tree_exclusion
|
|
18
19
|
*
|
|
@@ -25,47 +26,260 @@ import { ARIA_RECOMMENDED_VERSION, getComputedRole } from '@markuplint/ml-spec';
|
|
|
25
26
|
* @see https://w3c.github.io/aria/#tree_inclusion
|
|
26
27
|
*
|
|
27
28
|
* > Text equivalents for hidden referenced objects
|
|
28
|
-
* >
|
|
29
|
+
* > can still be used in the name and description computation
|
|
29
30
|
* > even when not included in the accessibility tree.
|
|
30
31
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
32
|
+
* Because the text of the excluded descendants is still used, an attribute that
|
|
33
|
+
* changes what a descendant contributes to a text alternative is not
|
|
34
|
+
* ineffective, and is not reported (see {@link takesEffect}). The computations
|
|
35
|
+
* that can read the descendant are listed by {@link getVisits};
|
|
36
|
+
* `MLElement#getAccessibleNameVisits` tells which elements each of them reaches
|
|
37
|
+
* and how.
|
|
38
|
+
*
|
|
39
|
+
* @param attr - The attribute to inspect.
|
|
40
|
+
* @param el - The element the attribute is read on. For a pretended component it is
|
|
41
|
+
* the component, while the attribute belongs to the element it pretends to be.
|
|
42
|
+
* @returns A violation if the attribute is an ARIA attribute or `role` that takes no effect under an ancestor with presentational children.
|
|
33
43
|
*/
|
|
34
|
-
export const checkingPresentationalChildren = ({ el }) => t => {
|
|
35
|
-
|
|
36
|
-
if (!ancestor) {
|
|
44
|
+
export const checkingPresentationalChildren = ({ attr, el }) => t => {
|
|
45
|
+
if (!isAriaAttr(attr) && attr.nameKey !== 'role') {
|
|
37
46
|
return;
|
|
38
47
|
}
|
|
39
|
-
|
|
48
|
+
const context = getContext(el);
|
|
49
|
+
if (!context) {
|
|
40
50
|
return;
|
|
41
51
|
}
|
|
42
|
-
|
|
43
|
-
if (!hasAriaAttr) {
|
|
52
|
+
if (takesEffect(attr, el, context)) {
|
|
44
53
|
return;
|
|
45
54
|
}
|
|
46
55
|
return {
|
|
47
|
-
scope:
|
|
48
|
-
message: t('it may be ineffective because {0}', t("it has {0} as an ancestor that doesn't expose its descendants to the accessibility tree", t('the "{0*}" {1}',
|
|
56
|
+
scope: attr,
|
|
57
|
+
message: t('it may be ineffective because {0}', t("it has {0} as an ancestor that doesn't expose its descendants to the accessibility tree", t('the "{0*}" {1}', context.role.name, 'role'))),
|
|
49
58
|
};
|
|
50
59
|
};
|
|
51
60
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
61
|
+
* The closest ancestor role with presentational children, and the visits of the
|
|
62
|
+
* element by the computations that can read it, once per element.
|
|
63
|
+
*/
|
|
64
|
+
const getContext = memoizeByObject(
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
66
|
+
(el) => {
|
|
67
|
+
const ariaVersion = el.rule.options?.version ?? el.ownerMLDocument.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
68
|
+
const role = getClosestPresentationalChildrenRole(el, ariaVersion);
|
|
69
|
+
if (!role) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return { ariaVersion, role, visits: getVisits(el, ariaVersion) };
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Finds the closest ancestor whose computed role has the
|
|
76
|
+
* `childrenArePresentational` characteristic.
|
|
54
77
|
*
|
|
55
78
|
* @param el - The element to start searching from (exclusive, starts from parent).
|
|
56
|
-
* @
|
|
79
|
+
* @param ariaVersion - The ARIA version to compute the roles with.
|
|
80
|
+
* @returns The role of that ancestor, or `null` if none is found.
|
|
57
81
|
*/
|
|
58
|
-
function
|
|
82
|
+
function getClosestPresentationalChildrenRole(
|
|
59
83
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
60
|
-
el) {
|
|
61
|
-
const ariaVersion = el.rule.options?.version ?? el.ownerMLDocument.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
84
|
+
el, ariaVersion) {
|
|
62
85
|
let current = el.parentElement;
|
|
63
86
|
while (current) {
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
return
|
|
87
|
+
const role = getComputedRole(el.ownerMLDocument.specs, current, ariaVersion).role;
|
|
88
|
+
if (role?.childrenPresentational) {
|
|
89
|
+
return role;
|
|
67
90
|
}
|
|
68
91
|
current = current.parentElement;
|
|
69
92
|
}
|
|
70
93
|
return null;
|
|
71
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* The visits of the element by every computation that can read it. Each reader
|
|
97
|
+
* is taken whether or not its computation reaches the element; one that does
|
|
98
|
+
* not contributes no visit.
|
|
99
|
+
*
|
|
100
|
+
* - the accessible name of every ancestor, as any of them can collect the text
|
|
101
|
+
* of its subtree (Step 2F and the recursion into descendants)
|
|
102
|
+
* - the text alternative of the element or of an ancestor as a node that
|
|
103
|
+
* `aria-labelledby` or `aria-describedby` refers to
|
|
104
|
+
* - the accessible name of every control that a `label` ancestor can name: the
|
|
105
|
+
* element its `for` refers to, and its labelable descendants (HTML-AAM)
|
|
106
|
+
*
|
|
107
|
+
* @see https://w3c.github.io/accname/#comp_name_from_content
|
|
108
|
+
* @see https://w3c.github.io/accname/#comp_labelledby_traversal
|
|
109
|
+
* @see https://w3c.github.io/html-aam/#accessible-name-and-description-computation
|
|
110
|
+
*
|
|
111
|
+
* @param el - The descendant
|
|
112
|
+
* @param ariaVersion - The ARIA version to compute the names with
|
|
113
|
+
* @returns The visits
|
|
114
|
+
*/
|
|
115
|
+
function getVisits(
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
117
|
+
el, ariaVersion) {
|
|
118
|
+
const visits = [];
|
|
119
|
+
const add = (
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
121
|
+
reader, asReferenced = false) => {
|
|
122
|
+
visits.push(...(reader.getAccessibleNameVisits(ariaVersion, asReferenced).get(el) ?? []));
|
|
123
|
+
};
|
|
124
|
+
const document = el.ownerMLDocument;
|
|
125
|
+
const referenced = getReferencedIds(document);
|
|
126
|
+
let current = el;
|
|
127
|
+
while (current) {
|
|
128
|
+
if (current !== el) {
|
|
129
|
+
add(current);
|
|
130
|
+
}
|
|
131
|
+
if (current.id && referenced.has(current.id) && document.getElementById(current.id) === current) {
|
|
132
|
+
add(current, true);
|
|
133
|
+
}
|
|
134
|
+
for (const control of getLabeledControls(current)) {
|
|
135
|
+
add(control);
|
|
136
|
+
}
|
|
137
|
+
current = current.parentElement;
|
|
138
|
+
}
|
|
139
|
+
return visits;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The controls that the element can name if it is a `label`, as the accessible
|
|
143
|
+
* name computation finds labels: by `for` or as the closest `label` ancestor.
|
|
144
|
+
* Once per label.
|
|
145
|
+
*
|
|
146
|
+
* @see https://html.spec.whatwg.org/multipage/forms.html#labeled-control
|
|
147
|
+
*/
|
|
148
|
+
const getLabeledControls = memoizeByObject(
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
150
|
+
(el) => {
|
|
151
|
+
if (el.localName !== 'label' || el.namespaceURI !== 'http://www.w3.org/1999/xhtml') {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
const htmlFor = el.getAttribute('for');
|
|
155
|
+
const target = htmlFor == null ? null : el.ownerMLDocument.getElementById(htmlFor);
|
|
156
|
+
return [...(target ? [target] : []), ...el.querySelectorAll(labelable.join(', '))];
|
|
157
|
+
});
|
|
158
|
+
/**
|
|
159
|
+
* The IDs that an `aria-labelledby` or `aria-describedby` of the document refers to.
|
|
160
|
+
*/
|
|
161
|
+
const getReferencedIds = memoizeByObject(
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
163
|
+
(document) => {
|
|
164
|
+
const ids = new Set();
|
|
165
|
+
for (const referrer of document.querySelectorAll('[aria-labelledby], [aria-describedby]')) {
|
|
166
|
+
for (const name of ['aria-labelledby', 'aria-describedby']) {
|
|
167
|
+
for (const id of referrer.getAttribute(name)?.split(/\s+/) ?? []) {
|
|
168
|
+
if (id !== '') {
|
|
169
|
+
ids.add(id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return ids;
|
|
175
|
+
});
|
|
176
|
+
/**
|
|
177
|
+
* Whether the attribute changes what its element contributes to a text
|
|
178
|
+
* alternative that reads it, given how the computations reach the element.
|
|
179
|
+
*
|
|
180
|
+
* - `aria-hidden="true"` makes the element hidden, so it and its subtree
|
|
181
|
+
* contribute nothing (Step 2A), unless the `hidden` attribute hides it already
|
|
182
|
+
* (the other way `@markuplint/ml-core` tells an element hidden).
|
|
183
|
+
* - An `aria-labelledby` replaces the element's subtree (Step 2B) when the
|
|
184
|
+
* elements it refers to have any text, except inside an ongoing
|
|
185
|
+
* `aria-labelledby` traversal.
|
|
186
|
+
* - A non-empty `aria-label` replaces it the same way (Step 2D), unless the
|
|
187
|
+
* element is hidden, an `aria-labelledby` of it takes effect first (one that
|
|
188
|
+
* refers to the element itself reads the `aria-label` again), or it is an
|
|
189
|
+
* embedded control, whose `aria-label` the recursion ignores.
|
|
190
|
+
* - `role="none"` or `role="presentation"` drops the host language text
|
|
191
|
+
* alternative of the element, such as the `alt` of an image (Step 2E), when the
|
|
192
|
+
* element has one and the Presentational Roles Conflict Resolution does not
|
|
193
|
+
* ignore the role.
|
|
194
|
+
* - A role that makes the element an embedded control, or keeps a native one
|
|
195
|
+
* from being it, changes what it contributes between its value and its text
|
|
196
|
+
* (Step 2C).
|
|
197
|
+
* - The `aria-valuetext`, or else the `aria-valuenow`, of a range embedded
|
|
198
|
+
* control is its value (Step 2C).
|
|
199
|
+
*
|
|
200
|
+
* @see https://w3c.github.io/accname/#comp_embedded_control
|
|
201
|
+
* @see https://w3c.github.io/accname/#comp_hidden_not_referenced
|
|
202
|
+
* @see https://w3c.github.io/accname/#comp_labelledby
|
|
203
|
+
* @see https://w3c.github.io/accname/#comp_label
|
|
204
|
+
* @see https://w3c.github.io/accname/#comp_host_language_label
|
|
205
|
+
*
|
|
206
|
+
* > Otherwise, if the current node's native markup provides an attribute (e.g. alt) or element (e.g. HTML label or SVG title) that defines a text alternative, return that alternative in the form of a flat string as defined by the host language […], unless the current node is exposed as presentational (role="presentation" or role="none", alt="").
|
|
207
|
+
*
|
|
208
|
+
* @param attr - The attribute on a descendant of a presentational ancestor
|
|
209
|
+
* @param el - The descendant
|
|
210
|
+
* @param context - The ARIA version and the visits of the descendant
|
|
211
|
+
* @returns `true` if the attribute takes effect on a text alternative
|
|
212
|
+
*/
|
|
213
|
+
function takesEffect(
|
|
214
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
215
|
+
attr,
|
|
216
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
217
|
+
el, { ariaVersion, visits }) {
|
|
218
|
+
const shown = visits.filter(visit => !visit.hidden);
|
|
219
|
+
switch (namespaceKeyOf(attr)) {
|
|
220
|
+
case 'aria-hidden': {
|
|
221
|
+
return attr.value === 'true' && visits.some(visit => visit.hidden) && !el.hasAttribute('hidden');
|
|
222
|
+
}
|
|
223
|
+
case 'aria-labelledby': {
|
|
224
|
+
return labelledbyGivesText(el, ariaVersion) && shown.some(visit => !visit.inLabelledbyTraversal);
|
|
225
|
+
}
|
|
226
|
+
case 'aria-label': {
|
|
227
|
+
const overridden = labelledbyGivesText(el, ariaVersion) &&
|
|
228
|
+
!(el.id !== '' && el.getAttribute('aria-labelledby')?.split(/\s+/).includes(el.id));
|
|
229
|
+
return (attr.value.trim() !== '' &&
|
|
230
|
+
shown.some(visit => !visit.embeddedControl && (visit.inLabelledbyTraversal || !overridden)));
|
|
231
|
+
}
|
|
232
|
+
case 'aria-valuetext':
|
|
233
|
+
case 'aria-valuenow': {
|
|
234
|
+
// The value of a range embedded control: `aria-valuetext`, else `aria-valuenow`
|
|
235
|
+
const isRange = ['slider', 'spinbutton'].includes(el.getAttribute('role')?.trim().split(/\s+/)[0] ?? '') ||
|
|
236
|
+
(el.localName === 'input' && el.getAttribute('type')?.trim().toLowerCase() === 'range');
|
|
237
|
+
const valuetext = el.getAttribute('aria-valuetext')?.trim() ?? '';
|
|
238
|
+
const shadowed = namespaceKeyOf(attr) === 'aria-valuenow' && valuetext !== '';
|
|
239
|
+
// Step 2B comes first outside an ongoing aria-labelledby traversal
|
|
240
|
+
const labelledby = labelledbyGivesText(el, ariaVersion);
|
|
241
|
+
return (isRange &&
|
|
242
|
+
attr.value.trim() !== '' &&
|
|
243
|
+
!shadowed &&
|
|
244
|
+
shown.some(visit => visit.embeddedControl && (visit.inLabelledbyTraversal || !labelledby)));
|
|
245
|
+
}
|
|
246
|
+
case 'role': {
|
|
247
|
+
if (shown.length === 0) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
const { specs } = el.ownerMLDocument;
|
|
251
|
+
const computed = getComputedRole(specs, el, ariaVersion);
|
|
252
|
+
if (isEmbeddedControl(el, computed) !== isEmbeddedControl(el, getImplicitRole(specs, el, ariaVersion))) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
return (shown.some(visit => !visit.embeddedControl) &&
|
|
256
|
+
isExposedAsPresentational(computed) &&
|
|
257
|
+
el.getHostLanguageLabelSource(ariaVersion) !== null);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Whether the element is an embedded control with the role, as the resolver of
|
|
264
|
+
* `@markuplint/ml-core` tells it: by the role, or as a native control without one.
|
|
265
|
+
*/
|
|
266
|
+
function isEmbeddedControl(
|
|
267
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
268
|
+
el, computed) {
|
|
269
|
+
return computed.role ? EMBEDDED_CONTROL_ROLES.has(computed.role.name) : isNativeEmbeddedControl(el);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Whether the `aria-labelledby` of the element gives a text: Step 2B returns the
|
|
273
|
+
* accumulated text of the elements it refers to only when that text is not empty.
|
|
274
|
+
*/
|
|
275
|
+
function labelledbyGivesText(
|
|
276
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
277
|
+
el, ariaVersion) {
|
|
278
|
+
return (el.getAttribute('aria-labelledby')?.split(/\s+/) ?? []).some(id => {
|
|
279
|
+
if (id === '') {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
const referenced = el.ownerMLDocument.getElementById(id);
|
|
283
|
+
return referenced != null && referenced.getReferencedTextAlternative(ariaVersion) !== '';
|
|
284
|
+
});
|
|
285
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { isAriaAttr } from '../../helpers.js';
|
|
1
2
|
import { ARIA_RECOMMENDED_VERSION, getARIA } from '@markuplint/ml-spec';
|
|
2
3
|
/**
|
|
3
4
|
* ARIA naming attributes subject to the "naming prohibition" constraint
|
|
4
5
|
* defined by ARIA in HTML.
|
|
5
6
|
*
|
|
6
|
-
* @see https://
|
|
7
|
+
* @see https://www.w3.org/TR/html-aria/#dfn-naming-prohibited
|
|
7
8
|
*/
|
|
8
9
|
const NAMING_ATTRS = new Set(['aria-label', 'aria-labelledby', 'aria-braillelabel']);
|
|
9
10
|
/**
|
|
@@ -35,10 +36,10 @@ const NAMING_ATTRS = new Set(['aria-label', 'aria-labelledby', 'aria-braillelabe
|
|
|
35
36
|
* @returns A violation if a naming attribute is used on a naming-prohibited element.
|
|
36
37
|
*/
|
|
37
38
|
export const checkingProhibitedNaming = ({ attr, role, propSpecs }) => t => {
|
|
38
|
-
if (
|
|
39
|
+
if (!isAriaAttr(attr)) {
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
|
-
if (!NAMING_ATTRS.has(attr.
|
|
42
|
+
if (!NAMING_ATTRS.has(attr.nameKey)) {
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
if (role) {
|
|
@@ -56,7 +57,7 @@ export const checkingProhibitedNaming = ({ attr, role, propSpecs }) => t => {
|
|
|
56
57
|
if (elAriaSpec?.namingProhibited !== true && !isAutonomousCustomElement) {
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
|
-
const propSpec = propSpecs.find(p => p.name === attr.
|
|
60
|
+
const propSpec = propSpecs.find(p => p.name === attr.nameKey);
|
|
60
61
|
return {
|
|
61
62
|
scope: attr,
|
|
62
63
|
message: t('{0:c} on {1}', t('{0} is {1:c}', t('the "{0*}" {1}', attr.name, `ARIA ${propSpec?.type ?? 'property'}`), 'prohibited'), t('the "{0*}" {1}', attr.ownerElement.localName, 'element')),
|
|
@@ -22,20 +22,18 @@ export const checkingRedundantAriaProp = ({ attr, propSpecs, attrSpecs }) => t =
|
|
|
22
22
|
if (attr.isDynamicValue) {
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
-
const propSpec = propSpecs.find(p => p.name === attr.
|
|
25
|
+
const propSpec = propSpecs.find(p => p.name === attr.nameKey);
|
|
26
26
|
if (!propSpec?.equivalentHtmlAttrs) {
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
for (const equivalentHtmlAttr of propSpec.equivalentHtmlAttrs) {
|
|
30
30
|
const eligibility = resolveAttrEligibility(equivalentHtmlAttr.htmlAttrName, attr.ownerElement, attrSpecs);
|
|
31
31
|
// Skip only when the native attribute doesn't apply to this element at all
|
|
32
|
-
// (unknown name
|
|
32
|
+
// (unknown name or an unmet condition) — mirrors the old
|
|
33
33
|
// `invalidType === 'non-existent'` check. `noUse` still proceeds: an
|
|
34
34
|
// explicitly-forbidden native attribute can still be "implicit" for
|
|
35
35
|
// equivalence purposes.
|
|
36
|
-
if (eligibility.status === 'unknown' ||
|
|
37
|
-
eligibility.status === 'case-mismatch' ||
|
|
38
|
-
eligibility.status === 'condition-not-met') {
|
|
36
|
+
if (eligibility.status === 'unknown' || eligibility.status === 'condition-not-met') {
|
|
39
37
|
continue;
|
|
40
38
|
}
|
|
41
39
|
if (!attr.ownerElement.hasAttribute(equivalentHtmlAttr.htmlAttrName)) {
|
|
@@ -34,10 +34,7 @@ export const checkingRequiredProp = ({ el, role, propSpecs }) => t => {
|
|
|
34
34
|
return true;
|
|
35
35
|
});
|
|
36
36
|
for (const { name: requiredProp } of requiredProps) {
|
|
37
|
-
const has = el.attributes.some(attr =>
|
|
38
|
-
const attrName = attr.name.toLowerCase();
|
|
39
|
-
return attrName === requiredProp;
|
|
40
|
-
});
|
|
37
|
+
const has = el.attributes.some(attr => attr.nameKey === requiredProp);
|
|
41
38
|
if (!has) {
|
|
42
39
|
const propSpec = propSpecs.find(p => p.name === requiredProp);
|
|
43
40
|
const ariaVersion = el.rule.options?.version ??
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAriaAttr } from '../../helpers.js';
|
|
1
2
|
/**
|
|
2
3
|
* Checks whether an ARIA property or state is supported by the element's
|
|
3
4
|
* computed role, per the role's WAI-ARIA definition (`ownedProperties`).
|
|
@@ -12,17 +13,17 @@
|
|
|
12
13
|
* @returns A violation if the property is not supported by the given role.
|
|
13
14
|
*/
|
|
14
15
|
export const checkingRoleSupportsAriaProp = ({ attr, role, propSpecs }) => t => {
|
|
15
|
-
if (
|
|
16
|
+
if (!isAriaAttr(attr)) {
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
18
19
|
if (!role) {
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
|
-
const statesAndProp = role.ownedProperties.find(p => p.name === attr.
|
|
22
|
+
const statesAndProp = role.ownedProperties.find(p => p.name === attr.nameKey);
|
|
22
23
|
if (statesAndProp) {
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
|
-
const propSpec = propSpecs.find(p => p.name === attr.
|
|
26
|
+
const propSpec = propSpecs.find(p => p.name === attr.nameKey);
|
|
26
27
|
return {
|
|
27
28
|
scope: attr,
|
|
28
29
|
message: t('{0:c} on {1}', t('{0} is {1:c}', t('the "{0*}" {1}', attr.name, `ARIA ${propSpec?.type ?? 'property'}`), 'disallowed'), t('the "{0*}" {1}', role.name, 'role')),
|
|
@@ -8,8 +8,9 @@ import type { ARIARole } from '@markuplint/ml-spec';
|
|
|
8
8
|
* express it: the tab's `aria-controls` idref list points to a `tabpanel`, or some
|
|
9
9
|
* `tabpanel` element's `aria-labelledby` idref list points back at the tab's own `id`.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* > Authors MUST ensure that if a tab is active, a corresponding tabpanel that represents the active tab is rendered.
|
|
12
|
+
*
|
|
13
|
+
* @see https://w3c.github.io/aria/#tab
|
|
13
14
|
* @param el - The element node to inspect.
|
|
14
15
|
* @param role - The computed ARIA role of the element.
|
|
15
16
|
* @returns A violation if the active tab has no corresponding tabpanel.
|
|
@@ -6,8 +6,9 @@ import { ARIA_RECOMMENDED_VERSION, getComputedRole } from '@markuplint/ml-spec';
|
|
|
6
6
|
* express it: the tab's `aria-controls` idref list points to a `tabpanel`, or some
|
|
7
7
|
* `tabpanel` element's `aria-labelledby` idref list points back at the tab's own `id`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* > Authors MUST ensure that if a tab is active, a corresponding tabpanel that represents the active tab is rendered.
|
|
10
|
+
*
|
|
11
|
+
* @see https://w3c.github.io/aria/#tab
|
|
11
12
|
* @param el - The element node to inspect.
|
|
12
13
|
* @param role - The computed ARIA role of the element.
|
|
13
14
|
* @returns A violation if the active tab has no corresponding tabpanel.
|
|
@@ -26,7 +26,7 @@ export declare const checkingValue: AttrChecker<boolean, Options, {
|
|
|
26
26
|
* Supports token, token list, string, ID reference, true/false, tristate,
|
|
27
27
|
* true/false/undefined, integer, and number value types.
|
|
28
28
|
*
|
|
29
|
-
* @see https://
|
|
29
|
+
* @see https://w3c.github.io/aria/#propcharacteristic_value
|
|
30
30
|
* @param type - The ARIA value type (e.g., `"token"`, `"true/false"`, `"integer"`).
|
|
31
31
|
* @param value - The raw attribute value to validate.
|
|
32
32
|
* @param tokenEnum - The list of allowed token values for token-based types.
|
|
@@ -16,7 +16,7 @@ export const checkingValue = ({ attr, role, propSpecs, booleanish }) => t => {
|
|
|
16
16
|
if (attr.isDynamicValue) {
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
const propSpec = propSpecs.find(p => p.name === attr.
|
|
19
|
+
const propSpec = propSpecs.find(p => p.name === attr.nameKey);
|
|
20
20
|
const result = checkAria(propSpec, attr.value, role?.name, booleanish);
|
|
21
21
|
if (result.isValid) {
|
|
22
22
|
return;
|
|
@@ -68,7 +68,7 @@ function checkAria(propSpec, currentValue, role, booleanish) {
|
|
|
68
68
|
* Supports token, token list, string, ID reference, true/false, tristate,
|
|
69
69
|
* true/false/undefined, integer, and number value types.
|
|
70
70
|
*
|
|
71
|
-
* @see https://
|
|
71
|
+
* @see https://w3c.github.io/aria/#propcharacteristic_value
|
|
72
72
|
* @param type - The ARIA value type (e.g., `"token"`, `"true/false"`, `"integer"`).
|
|
73
73
|
* @param value - The raw attribute value to validate.
|
|
74
74
|
* @param tokenEnum - The list of allowed token values for token-based types.
|