@markuplint/rules 5.4.0 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -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 +2 -2
- package/lib/consistent-table-row-length/index.js +1 -1
- package/lib/create-message.js +6 -5
- package/lib/element-supports-aria-prop/index.js +2 -2
- package/lib/form-attr-references-form/index.js +3 -3
- package/lib/helpers.d.ts +96 -5
- package/lib/helpers.js +187 -29
- package/lib/input-list-references-datalist/index.js +3 -3
- package/lib/itemprop-requires-itemscope/index.js +28 -11
- package/lib/label-for-references-labelable/index.js +3 -3
- 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 +8 -8
- package/lib/landmark-roles/roles.d.ts +13 -1
- package/lib/landmark-roles/roles.js +22 -0
- package/lib/link-types/index.js +8 -8
- package/lib/map-id-name-match/index.js +1 -1
- package/lib/meter-value-bounds/index.js +3 -3
- package/lib/no-ambiguous-navigable-target-names/index.js +5 -1
- package/lib/no-aria-hidden-on-hidden-until-found/index.js +1 -1
- 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-content-after-body/index.js +2 -2
- 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 +68 -5
- package/lib/no-disallowed-attr/index.js +5 -1
- package/lib/no-duplicate-autofocus/index.js +1 -1
- 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-duplicate-sibling-attr/index.js +1 -1
- package/lib/no-duplicate-visible-main/index.js +1 -1
- package/lib/no-empty-table-track/index.js +1 -1
- package/lib/no-extra-selected-options/index.js +1 -1
- package/lib/no-ineffective-attr/index.js +4 -0
- package/lib/no-input-file-value/index.js +1 -1
- package/lib/no-invalid-aria-prop-value/index.js +3 -3
- package/lib/no-invalid-attr-value/index.js +12 -0
- package/lib/no-malformed-character-reference/index.js +1 -1
- 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 +15 -8
- 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-attr/index.js +2 -1
- 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 +4 -3
- package/lib/no-table-cell-overlap/index.js +3 -2
- package/lib/no-table-span-overflow/index.js +1 -1
- package/lib/no-translate.d.ts +17 -0
- package/lib/no-translate.js +29 -0
- package/lib/no-unclosed-element-at-eof/index.js +1 -1
- package/lib/no-unescaped-char/index.js +1 -1
- package/lib/no-unsupported-browser-features/index.d.ts +9 -0
- package/lib/no-unsupported-browser-features/index.js +10 -7
- package/lib/permitted-contents/content-model.d.ts +14 -0
- package/lib/permitted-contents/content-model.js +21 -1
- package/lib/permitted-contents/index.js +6 -8
- package/lib/permitted-contents/matches-selector.js +32 -5
- package/lib/permitted-contents/represent-transparent-nodes.js +100 -68
- package/lib/permitted-contents/slot-content.d.ts +18 -1
- package/lib/permitted-contents/slot-content.js +49 -2
- package/lib/permitted-contents/start.d.ts +8 -1
- package/lib/permitted-contents/start.js +39 -6
- 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/progress-value-bounds/index.js +2 -2
- 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 +37 -1
- package/lib/require-ancestor/index.js +82 -9
- package/lib/require-ancestor/meta.d.ts +5 -0
- package/lib/require-ancestor/meta.js +5 -0
- package/lib/require-attr/index.js +4 -1
- package/lib/require-datetime/index.js +3 -2
- package/lib/require-dialog-autofocus/index.js +8 -6
- package/lib/require-element/index.js +8 -2
- 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 +12 -3
- package/lib/valid-speculation-rules/index.js +1 -1
- package/lib/wai-aria/checkings/contradictory-aria-prop.js +2 -2
- 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/redundant-aria-prop.js +1 -1
- package/lib/wai-aria/checkings/required-accessibility-parent-role.js +4 -2
- package/lib/wai-aria/checkings/required-owned-elements.d.ts +2 -0
- package/lib/wai-aria/checkings/required-owned-elements.js +175 -21
- 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 +8 -19
- package/package.json +14 -11
|
@@ -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
|
}
|
|
@@ -44,7 +44,7 @@ export const checkingRedundantAriaProp = ({ attr, propSpecs, attrSpecs }) => t =
|
|
|
44
44
|
if ((equivalentHtmlAttr.value == null && targetAttrValue === value) || equivalentHtmlAttr.value === value) {
|
|
45
45
|
return {
|
|
46
46
|
scope: attr,
|
|
47
|
-
message: t('{0} has {1}', t('the "{0*}" {1}', attr.name, `ARIA ${propSpec.type}`), t('the same {0} as {1}', 'semantics', t('{0} or {1}', t('the current "{0}" {1}', equivalentHtmlAttr.htmlAttrName, 'attribute'), t('the implicit "{0}" {1}', equivalentHtmlAttr.htmlAttrName, 'attribute')))),
|
|
47
|
+
message: t('{0} has {1}', t('the "{0*}" {1}', attr.name, `ARIA ${propSpec.type}`), t('the same {0} as {1}', 'semantics', t('{0} or {1}', t('the current "{0*}" {1}', equivalentHtmlAttr.htmlAttrName, 'attribute'), t('the implicit "{0*}" {1}', equivalentHtmlAttr.htmlAttrName, 'attribute')))),
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ARIA_RECOMMENDED_VERSION, ariaSpecs } from '@markuplint/ml-spec';
|
|
2
|
+
import { noTranslate } from '../../no-translate.js';
|
|
2
3
|
/**
|
|
3
4
|
* Checks whether an element with an explicit role satisfies its
|
|
4
5
|
* "Required Accessibility Parent Role" (called "Required Context Role" in ARIA 1.2).
|
|
@@ -23,7 +24,8 @@ export const checkingRequiredAccessibilityParentRole = ({ el, computed }) => t =
|
|
|
23
24
|
(computed.errorType !== 'INVALID_REQUIRED_CONTEXT_ROLE' &&
|
|
24
25
|
// NO_OWNER with role: null means all ancestors were transparent
|
|
25
26
|
// and no valid context could be found. NO_OWNER with role kept
|
|
26
|
-
// (
|
|
27
|
+
// (no parent in the accessibility tree) is a root fragment, or the top
|
|
28
|
+
// of the content of an ordinary `template` element — skip.
|
|
27
29
|
!(computed.errorType === 'NO_OWNER' && !computed.role))) {
|
|
28
30
|
return;
|
|
29
31
|
}
|
|
@@ -49,7 +51,7 @@ export const checkingRequiredAccessibilityParentRole = ({ el, computed }) => t =
|
|
|
49
51
|
message: t('{0} requires {1}', t('the "{0*}" {1}', roleSpec.name, 'role'), parentRoles.length === 1 && parentRoles[0]
|
|
50
52
|
? t('an accessibility parent with the "{0*}" {1}', parentRoles[0], 'role')
|
|
51
53
|
: t('an accessibility parent with one of the {0}', 'roles') +
|
|
52
|
-
`: ${t(parentRoles)}`),
|
|
54
|
+
`: ${t(parentRoles.map(noTranslate))}`),
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -11,6 +11,8 @@ import type { ARIARole } from '@markuplint/ml-spec';
|
|
|
11
11
|
* preprocessor blocks, and mutable children from template engines.
|
|
12
12
|
* For a pretended component, the owned elements are the ones the component renders
|
|
13
13
|
* itself (`contents`) together with the children given to it, not the usage site alone.
|
|
14
|
+
* For a shadow host, they are the elements of its declarative shadow root, which is
|
|
15
|
+
* rendered in place of its light children (see `getOwnedChildNodes`).
|
|
14
16
|
*
|
|
15
17
|
* @see https://w3c.github.io/aria/#mustContain
|
|
16
18
|
* @param el - The element node to inspect for allowed accessibility child roles.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ARIA_RECOMMENDED_VERSION, getComputedRole, isRequiredOwnedElement, isTransparentForOwnership, } from '@markuplint/ml-spec';
|
|
1
|
+
import { ARIA_RECOMMENDED_VERSION, getComposedParentElement, getComputedRole, isRequiredOwnedElement, isSlotElement, isTransparentForOwnership, } from '@markuplint/ml-spec';
|
|
2
2
|
import { expandChildren, isContentMutable } from '../../permitted-contents/slot-content.js';
|
|
3
|
+
import { noTranslate } from '../../no-translate.js';
|
|
3
4
|
/**
|
|
4
5
|
* Checks whether an element with a role that has "Allowed Accessibility Child Roles"
|
|
5
6
|
* (called "Required Owned Elements" in ARIA 1.2) actually contains children
|
|
@@ -10,6 +11,8 @@ import { expandChildren, isContentMutable } from '../../permitted-contents/slot-
|
|
|
10
11
|
* preprocessor blocks, and mutable children from template engines.
|
|
11
12
|
* For a pretended component, the owned elements are the ones the component renders
|
|
12
13
|
* itself (`contents`) together with the children given to it, not the usage site alone.
|
|
14
|
+
* For a shadow host, they are the elements of its declarative shadow root, which is
|
|
15
|
+
* rendered in place of its light children (see `getOwnedChildNodes`).
|
|
13
16
|
*
|
|
14
17
|
* @see https://w3c.github.io/aria/#mustContain
|
|
15
18
|
* @param el - The element node to inspect for allowed accessibility child roles.
|
|
@@ -37,10 +40,12 @@ export const checkingRequiredOwnedElements = ({ el, role }) => t => {
|
|
|
37
40
|
return;
|
|
38
41
|
}
|
|
39
42
|
// TODO: Needs to resolve `aria-owns` references to include virtually owned elements.
|
|
40
|
-
// Currently, only
|
|
41
|
-
// should also be considered as owned elements per the
|
|
43
|
+
// Currently, only the child nodes (those of the shadow root for a shadow host) are checked.
|
|
44
|
+
// Elements referenced via `aria-owns` should also be considered as owned elements per the
|
|
45
|
+
// ARIA specification.
|
|
42
46
|
const ariaVersion = el.rule.options?.version ?? el.ownerMLDocument.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
43
|
-
const
|
|
47
|
+
const ownedChildNodes = getOwnedChildNodes(el);
|
|
48
|
+
const children = classifyChildren(el, ownedChildNodes, role, ariaVersion);
|
|
44
49
|
if (children.some(([, type]) => type === 'BUSY' || type === 'UNKNOWN')) {
|
|
45
50
|
return;
|
|
46
51
|
}
|
|
@@ -71,30 +76,30 @@ export const checkingRequiredOwnedElements = ({ el, role }) => t => {
|
|
|
71
76
|
*/
|
|
72
77
|
// A virtual slot wrapper stands for the component, whose nodes are the ones given to it.
|
|
73
78
|
const component = el.pretenderContext?.type === 'origin' ? el.pretenderContext.origin : el;
|
|
74
|
-
if (
|
|
79
|
+
if (isOwnedContentMutable(component)) {
|
|
75
80
|
return;
|
|
76
81
|
}
|
|
77
|
-
if (mayBeBeforeCreated(
|
|
82
|
+
if (mayBeBeforeCreated(ownedChildNodes ?? [])) {
|
|
78
83
|
return {
|
|
79
84
|
scope: component,
|
|
80
85
|
message: t('{0}. Or, {1}', t('{0} requires {1}', t('the {0}', 'child element'), role.allowedAccessibilityChildRoles.length === 1 && role.allowedAccessibilityChildRoles[0]
|
|
81
86
|
? t('the "{0*}" {1}', role.allowedAccessibilityChildRoles[0], 'role')
|
|
82
|
-
: t('the {0}', 'roles') + `: ${t(role.allowedAccessibilityChildRoles)}`), t('require {0}', 'aria-busy="true"')),
|
|
87
|
+
: t('the {0}', 'roles') + `: ${t(role.allowedAccessibilityChildRoles.map(noTranslate))}`), t('require {0*}', 'aria-busy="true"')),
|
|
83
88
|
};
|
|
84
89
|
}
|
|
85
90
|
return {
|
|
86
91
|
scope: component,
|
|
87
92
|
message: t('{0} expects {1}', t('the "{0*}" {1}', role.name, 'role'), role.allowedAccessibilityChildRoles.length === 1 && role.allowedAccessibilityChildRoles[0]
|
|
88
93
|
? t('the "{0*}" {1}', role.allowedAccessibilityChildRoles[0], 'role')
|
|
89
|
-
: t('the {0}', 'roles') + `: ${t(role.allowedAccessibilityChildRoles)}`),
|
|
94
|
+
: t('the {0}', 'roles') + `: ${t(role.allowedAccessibilityChildRoles.map(noTranslate))}`),
|
|
90
95
|
};
|
|
91
96
|
};
|
|
92
97
|
/**
|
|
93
|
-
* Determines whether the
|
|
98
|
+
* Determines whether the owned child nodes may not yet exist (e.g., empty or
|
|
94
99
|
* containing only `<script>` / `<template>` elements that could dynamically create content).
|
|
95
100
|
*
|
|
96
|
-
* @param
|
|
97
|
-
* @returns `true` if the
|
|
101
|
+
* @param childNodes - The child nodes the element owns (see `getOwnedChildNodes`).
|
|
102
|
+
* @returns `true` if the nodes are empty or only contain script/template elements.
|
|
98
103
|
*/
|
|
99
104
|
function mayBeBeforeCreated(childNodes) {
|
|
100
105
|
if (childNodes.every(child => child.is(child.TEXT_NODE) && child.textContent?.trim() === '')) {
|
|
@@ -112,14 +117,14 @@ function mayBeBeforeCreated(childNodes) {
|
|
|
112
117
|
* are evaluated as if they were direct children of the owning element.
|
|
113
118
|
*
|
|
114
119
|
* @param el - The parent element whose children to classify.
|
|
120
|
+
* @param childNodes - The child nodes `el` owns, from `getOwnedChildNodes`.
|
|
115
121
|
* @param role - The ARIA role of the parent, which defines allowed child roles.
|
|
116
122
|
* @param version - The WAI-ARIA version for version-gated transparency behavior.
|
|
117
123
|
* @returns An array of classified child nodes.
|
|
118
124
|
*/
|
|
119
125
|
function classifyChildren(
|
|
120
126
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
121
|
-
el, role, version) {
|
|
122
|
-
const childNodes = getOwnedChildNodes(el);
|
|
127
|
+
el, childNodes, role, version) {
|
|
123
128
|
if (!childNodes) {
|
|
124
129
|
return [[el, 'UNKNOWN']];
|
|
125
130
|
}
|
|
@@ -130,24 +135,30 @@ el, role, version) {
|
|
|
130
135
|
result.push([child, 'BUSY']);
|
|
131
136
|
continue;
|
|
132
137
|
}
|
|
138
|
+
if (isSlotElement(child)) {
|
|
139
|
+
result.push(...classifySlot(child, role, version));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
133
142
|
/**
|
|
134
|
-
* The role of a child is resolved against its
|
|
135
|
-
*
|
|
136
|
-
* that
|
|
137
|
-
*
|
|
138
|
-
*
|
|
143
|
+
* The role of a child is resolved against its parent in the accessibility tree,
|
|
144
|
+
* which is the host for a child at the top of a declarative shadow root. A child
|
|
145
|
+
* that a pretender places somewhere other than under that parent (a node given at
|
|
146
|
+
* the usage site that the component renders inside its slot wrapper, or an element
|
|
147
|
+
* a fragment component renders in place) would be judged against the wrong parent
|
|
148
|
+
* and lose its role, so it is resolved on its own.
|
|
139
149
|
*/
|
|
140
|
-
const computedChild = getComputedRole(child.ownerMLDocument.specs, child, version, child
|
|
150
|
+
const computedChild = getComputedRole(child.ownerMLDocument.specs, child, version, getComposedParentElement(child) !== el);
|
|
141
151
|
if (role.allowedAccessibilityChildRoles.some(ownedRole => isRequiredOwnedElement(computedChild.el, computedChild.role, ownedRole, child.ownerMLDocument.specs, version))) {
|
|
142
152
|
result.push([child, 'REQUIRED']);
|
|
143
153
|
continue;
|
|
144
154
|
}
|
|
145
155
|
if (isTransparentForOwnership(computedChild.role?.name, version)) {
|
|
146
|
-
|
|
156
|
+
const shadowRootTemplate = getShadowRootTemplate(child);
|
|
157
|
+
if (shadowRootTemplate !== 'unknown' && isContentMutable(shadowRootTemplate ?? child, 'pretended')) {
|
|
147
158
|
result.push([child, 'UNKNOWN']);
|
|
148
159
|
continue;
|
|
149
160
|
}
|
|
150
|
-
result.push(...classifyChildren(child, role, version));
|
|
161
|
+
result.push(...classifyChildren(child, getOwnedChildNodes(child), role, version));
|
|
151
162
|
continue;
|
|
152
163
|
}
|
|
153
164
|
result.push([child, 'OTHER']);
|
|
@@ -161,6 +172,45 @@ el, role, version) {
|
|
|
161
172
|
}
|
|
162
173
|
return result;
|
|
163
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Classifies what a `slot` that the owned elements are looked for through renders: the nodes assigned to
|
|
177
|
+
* it, or its own children when none is, in its place. A slot is not an element of the accessibility tree.
|
|
178
|
+
*
|
|
179
|
+
* The slot is reached only through the elements that are transparent for ownership, so what it renders
|
|
180
|
+
* is owned by the element as if it were written in place of the slot. A slot that is not reached (one
|
|
181
|
+
* in a `p`, or in the content of a nested `template`) places nothing as an owned element, and is not
|
|
182
|
+
* looked at. Where the assignment is not known (a slot that is not in a shadow root, a framework's, manual
|
|
183
|
+
* assignment, template syntax among the light children), what it renders is not known: `UNKNOWN`.
|
|
184
|
+
*
|
|
185
|
+
* The light children assigned to the slot are not in the shadow root that the mutability gate reads
|
|
186
|
+
* (`isOwnedContentMutable`), so the template syntax and the mutable attributes in them are looked at
|
|
187
|
+
* here: a node that may not be what it is written as keeps the rule from deciding.
|
|
188
|
+
*
|
|
189
|
+
* > If slottables is the empty list, then append each slottable child of slot, in tree order, to slottables.
|
|
190
|
+
*
|
|
191
|
+
* @see https://dom.spec.whatwg.org/#find-flattened-slotables
|
|
192
|
+
* @param slot - The `slot` element
|
|
193
|
+
* @param role - The ARIA role of the owner, which defines allowed child roles.
|
|
194
|
+
* @param version - The WAI-ARIA version for version-gated transparency behavior.
|
|
195
|
+
* @returns The classified nodes the slot renders.
|
|
196
|
+
*/
|
|
197
|
+
function classifySlot(
|
|
198
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
199
|
+
slot, role, version) {
|
|
200
|
+
const assigned = slot.getAssignedNodes();
|
|
201
|
+
if (!assigned) {
|
|
202
|
+
return [[slot, 'UNKNOWN']];
|
|
203
|
+
}
|
|
204
|
+
if (assigned.length === 0) {
|
|
205
|
+
return classifyChildren(slot, getOwnedChildNodes(slot), role, version);
|
|
206
|
+
}
|
|
207
|
+
const mutable = assigned.some(node => node.is(node.ELEMENT_NODE) &&
|
|
208
|
+
(node.hasMutableAttributes() || node.hasMutableChildren(true, isFlattenedSlot)));
|
|
209
|
+
if (mutable) {
|
|
210
|
+
return [[slot, 'UNKNOWN']];
|
|
211
|
+
}
|
|
212
|
+
return classifyChildren(slot, assigned, role, version);
|
|
213
|
+
}
|
|
164
214
|
/**
|
|
165
215
|
* The child nodes that `el` owns as the page renders them.
|
|
166
216
|
*
|
|
@@ -174,12 +224,37 @@ el, role, version) {
|
|
|
174
224
|
* - Several slot wrappers leave the owner of each node unknown, so `null` is returned
|
|
175
225
|
* and nothing is claimed about the element.
|
|
176
226
|
*
|
|
227
|
+
* A shadow host owns the child nodes of its declarative shadow root, of either mode, and
|
|
228
|
+
* nothing else: the shadow root is rendered in place of the light children, which include
|
|
229
|
+
* the `template` that declares it and what a pretended component renders itself. This
|
|
230
|
+
* comes first, as ml-core attaches the root to a pretended host that renders the children
|
|
231
|
+
* written in it in itself. A `template` written in a block or a `template` with a directive
|
|
232
|
+
* may not be rendered, so `null` is returned for it (`getShadowRootTemplate`). The light children
|
|
233
|
+
* are rendered only where a `slot` in the shadow root places them: the nodes assigned to it
|
|
234
|
+
* (`MLElement#getAssignedNodes`) are classified in its place, and the light children that
|
|
235
|
+
* no slot takes are not owned (`classifySlot`).
|
|
236
|
+
*
|
|
237
|
+
* `getRenderedChildNodes` of `@markuplint/ml-core` (the accessible name computation)
|
|
238
|
+
* resolves the rendered children in the same order, but does not expand fragment
|
|
239
|
+
* components.
|
|
240
|
+
*
|
|
241
|
+
* > […] the flat tree is the top-level DOM tree, but shadow hosts are filled with their shadow tree children instead of their light tree children […]
|
|
242
|
+
*
|
|
243
|
+
* @see https://drafts.csswg.org/css-shadow-1/#flattening
|
|
177
244
|
* @param el - The element, or the virtual slot wrapper of a pretended component.
|
|
178
245
|
* @returns The child nodes, or `null` when they cannot be known.
|
|
179
246
|
*/
|
|
180
247
|
function getOwnedChildNodes(
|
|
181
248
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
182
249
|
el) {
|
|
250
|
+
// The content of the `template` is the shadow root, and holds the same nodes as its children.
|
|
251
|
+
const shadowRootTemplate = getShadowRootTemplate(el);
|
|
252
|
+
if (shadowRootTemplate === 'unknown') {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
if (shadowRootTemplate) {
|
|
256
|
+
return expandChildren(shadowRootTemplate, [...shadowRootTemplate.childNodes], 'pretended');
|
|
257
|
+
}
|
|
183
258
|
const context = el.pretenderContext;
|
|
184
259
|
if (context?.type === 'pretender') {
|
|
185
260
|
const wrapper = context.slotContent?.wrapper;
|
|
@@ -201,3 +276,82 @@ el) {
|
|
|
201
276
|
}
|
|
202
277
|
return expandChildren(el, [...el.childNodes], 'pretended');
|
|
203
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Whether a `slot` is one that `classifyChildren` cannot see: a parser that makes the `slot` of its
|
|
281
|
+
* framework a fragment (Vue, Astro) has it flattened out of the child nodes, and what it renders is
|
|
282
|
+
* not known. Any other `slot` is classified where the owned elements reach it (`classifySlot`), and
|
|
283
|
+
* one that is not reached places nothing as an owned element.
|
|
284
|
+
*
|
|
285
|
+
* @param slot - A `slot` element
|
|
286
|
+
* @returns `true` if the slot is flattened out of the child nodes
|
|
287
|
+
*/
|
|
288
|
+
function isFlattenedSlot(
|
|
289
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
290
|
+
slot) {
|
|
291
|
+
return slot.isFragment;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Whether the nodes `el` owns may be rendered by something the markup does not show:
|
|
295
|
+
* template syntax, a mutable attribute or a component that no pretender resolved among them
|
|
296
|
+
* (`hasMutableChildren`), or a pretender whose `contents` are unknown (`isContentMutable`).
|
|
297
|
+
* For a shadow host they are read in the shadow root, as that is what it owns: a light
|
|
298
|
+
* child that no `slot` places is not rendered, mutable or not.
|
|
299
|
+
*
|
|
300
|
+
* A `slot` is not counted here unless it is flattened out of the child nodes (`isFlattenedSlot`):
|
|
301
|
+
* `classifySlot` reads what it renders where the owned elements are looked for through it, and a slot
|
|
302
|
+
* that is not reached places nothing as an owned element.
|
|
303
|
+
*
|
|
304
|
+
* @param el - The element, or the component a virtual slot wrapper stands for.
|
|
305
|
+
* @returns `true` when the owned nodes may differ from the markup.
|
|
306
|
+
*/
|
|
307
|
+
function isOwnedContentMutable(
|
|
308
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
309
|
+
el) {
|
|
310
|
+
const template = getShadowRootTemplate(el);
|
|
311
|
+
if (template === 'unknown') {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
const scope = template ?? el;
|
|
315
|
+
return scope.hasMutableChildren(true, isFlattenedSlot) || isContentMutable(scope, 'pretended');
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The `template` element whose content is the declarative shadow root of `host`, which ml-core
|
|
319
|
+
* exposes without the element.
|
|
320
|
+
*
|
|
321
|
+
* ml-core finds the `template` through what a template engine renders away (a block, a
|
|
322
|
+
* `template` with a directive such as `v-if`), as the id lookups need a tree either way. Such
|
|
323
|
+
* a wrapper, or a conditional branch that leaves the `template` out, may render it or not,
|
|
324
|
+
* and the host owns the shadow root or its light children accordingly. So what the host owns
|
|
325
|
+
* is unknown unless the `template` is written right in the host and in every branch of its
|
|
326
|
+
* children (`conditionalChildNodes`), and no block among them has a range that the syntax
|
|
327
|
+
* tree does not give.
|
|
328
|
+
*
|
|
329
|
+
* @param host - An element.
|
|
330
|
+
* @returns The `template` element; `'unknown'` when it is written in such a wrapper; `null`
|
|
331
|
+
* when no shadow root is attached to `host`.
|
|
332
|
+
*/
|
|
333
|
+
function getShadowRootTemplate(
|
|
334
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
335
|
+
host) {
|
|
336
|
+
const shadowRoot = host.declarativeShadowRoot;
|
|
337
|
+
if (!shadowRoot) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
// A block without a behavior may be a marker of a language whose blocks the syntax tree does
|
|
341
|
+
// not nest (`{% if %}` … `{% endif %}`), so whether it encloses the `template` is not known.
|
|
342
|
+
for (const child of host.getPureChildNodes()) {
|
|
343
|
+
if (child.is(child.MARKUPLINT_PREPROCESSOR_BLOCK) && !child.blockBehavior) {
|
|
344
|
+
return 'unknown';
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (const child of host.childNodes) {
|
|
348
|
+
if (child.is(child.ELEMENT_NODE) && child.content === shadowRoot) {
|
|
349
|
+
if (child.syntacticalParentNode !== host) {
|
|
350
|
+
return 'unknown';
|
|
351
|
+
}
|
|
352
|
+
const branches = host.conditionalChildNodes();
|
|
353
|
+
return branches.every(branch => [...branch].includes(child)) ? child : 'unknown';
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return 'unknown';
|
|
357
|
+
}
|
|
@@ -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;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { noTranslate } from '../../no-translate.js';
|
|
1
2
|
/**
|
|
2
3
|
* Checks whether the value of an ARIA property or state conforms to its expected type.
|
|
3
4
|
*
|
|
@@ -9,23 +10,22 @@
|
|
|
9
10
|
* @param attr - The ARIA attribute node to inspect.
|
|
10
11
|
* @param role - The computed ARIA role, used to resolve conditional value types.
|
|
11
12
|
* @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
13
|
* @returns A violation if the attribute value does not match the expected type.
|
|
14
14
|
*/
|
|
15
|
-
export const checkingValue = ({ attr, role, propSpecs
|
|
15
|
+
export const checkingValue = ({ attr, role, propSpecs }) => t => {
|
|
16
16
|
if (attr.isDynamicValue) {
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
const propSpec = propSpecs.find(p => p.name === attr.nameKey);
|
|
20
|
-
const result = checkAria(propSpec, attr.value, role?.name
|
|
20
|
+
const result = checkAria(propSpec, attr.value, role?.name);
|
|
21
21
|
if (result.isValid) {
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
return {
|
|
25
25
|
scope: attr,
|
|
26
|
-
message: t('{0:c} on {1}', t('{0} is {1:c}', t('the "{0}"', attr.value), 'disallowed'), t('the "{0*}" {1}', attr.name, `ARIA ${propSpec?.type ?? 'property'}`)) +
|
|
26
|
+
message: t('{0:c} on {1}', t('{0} is {1:c}', t('the "{0*}"', attr.value), 'disallowed'), t('the "{0*}" {1}', attr.name, `ARIA ${propSpec?.type ?? 'property'}`)) +
|
|
27
27
|
('enum' in result && result.enum.length > 0
|
|
28
|
-
? t('. ') + t('Allowed values are: {0}', t(result.enum))
|
|
28
|
+
? t('. ') + t('Allowed values are: {0}', t(result.enum.map(noTranslate)))
|
|
29
29
|
: ''),
|
|
30
30
|
};
|
|
31
31
|
};
|
|
@@ -35,10 +35,9 @@ export const checkingValue = ({ attr, role, propSpecs, booleanish }) => t => {
|
|
|
35
35
|
* @param propSpec - The ARIA property specification, or `undefined` if the property is unknown.
|
|
36
36
|
* @param currentValue - The current attribute value to validate.
|
|
37
37
|
* @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
38
|
* @returns An object containing the validation result and, for enum types, the allowed values.
|
|
40
39
|
*/
|
|
41
|
-
function checkAria(propSpec, currentValue, role
|
|
40
|
+
function checkAria(propSpec, currentValue, role) {
|
|
42
41
|
if (!propSpec) {
|
|
43
42
|
return {
|
|
44
43
|
currentValue,
|
|
@@ -55,7 +54,7 @@ function checkAria(propSpec, currentValue, role, booleanish) {
|
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
|
-
const isValid = checkAriaValue(valueType, currentValue, propSpec.enum
|
|
57
|
+
const isValid = checkAriaValue(valueType, currentValue, propSpec.enum);
|
|
59
58
|
return {
|
|
60
59
|
...propSpec,
|
|
61
60
|
currentValue,
|
|
@@ -72,10 +71,9 @@ function checkAria(propSpec, currentValue, role, booleanish) {
|
|
|
72
71
|
* @param type - The ARIA value type (e.g., `"token"`, `"true/false"`, `"integer"`).
|
|
73
72
|
* @param value - The raw attribute value to validate.
|
|
74
73
|
* @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
74
|
* @returns `true` if the value is valid for the given type, `false` otherwise.
|
|
77
75
|
*/
|
|
78
|
-
export function checkAriaValue(type, value, tokenEnum
|
|
76
|
+
export function checkAriaValue(type, value, tokenEnum) {
|
|
79
77
|
switch (type) {
|
|
80
78
|
case 'token': {
|
|
81
79
|
return tokenEnum.includes(value);
|
|
@@ -90,21 +88,12 @@ export function checkAriaValue(type, value, tokenEnum, booleanish) {
|
|
|
90
88
|
return true;
|
|
91
89
|
}
|
|
92
90
|
case 'true/false': {
|
|
93
|
-
if (booleanish && value === '') {
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
91
|
return ['true', 'false'].includes(value);
|
|
97
92
|
}
|
|
98
93
|
case 'tristate': {
|
|
99
|
-
if (booleanish && value === '') {
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
94
|
return ['mixed', 'true', 'false', 'undefined'].includes(value);
|
|
103
95
|
}
|
|
104
96
|
case 'true/false/undefined': {
|
|
105
|
-
if (booleanish && value === '') {
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
97
|
return ['true', 'false', 'undefined'].includes(value);
|
|
109
98
|
}
|
|
110
99
|
case 'integer': {
|