@markuplint/ml-spec 2.1.1 → 3.0.0-alpha.1
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/lib/dom-traverse/accname-computation.d.ts +1 -0
- package/lib/dom-traverse/accname-computation.js +8 -0
- package/lib/dom-traverse/get-attr-specs.d.ts +2 -0
- package/lib/dom-traverse/get-attr-specs.js +8 -0
- package/lib/dom-traverse/get-computed-role.d.ts +2 -0
- package/lib/dom-traverse/get-computed-role.js +170 -0
- package/lib/dom-traverse/get-content-model.d.ts +3 -0
- package/lib/dom-traverse/get-content-model.js +28 -0
- package/lib/dom-traverse/get-explicit-role.d.ts +2 -0
- package/lib/dom-traverse/get-explicit-role.js +102 -0
- package/lib/dom-traverse/get-implicit-role.d.ts +3 -0
- package/lib/dom-traverse/get-implicit-role.js +37 -0
- package/lib/dom-traverse/get-non-presentational-ancestor.d.ts +5 -0
- package/lib/dom-traverse/get-non-presentational-ancestor.js +21 -0
- package/lib/dom-traverse/get-permitted-roles.d.ts +5 -0
- package/lib/dom-traverse/get-permitted-roles.js +8 -0
- package/lib/dom-traverse/get-spec.d.ts +2 -0
- package/lib/dom-traverse/get-spec.js +8 -0
- package/lib/dom-traverse/has-required-owned-elements.d.ts +3 -0
- package/lib/dom-traverse/has-required-owned-elements.js +81 -0
- package/lib/dom-traverse/may-be-focusable.d.ts +2 -0
- package/lib/dom-traverse/may-be-focusable.js +22 -0
- package/lib/index.d.ts +18 -6
- package/lib/index.js +18 -6
- package/lib/specs/aria-specs.d.ts +6 -0
- package/lib/specs/aria-specs.js +8 -0
- package/lib/specs/content-model-category-to-tag-names.d.ts +3 -0
- package/lib/specs/content-model-category-to-tag-names.js +15 -0
- package/lib/specs/get-aria.d.ts +3 -0
- package/lib/specs/get-aria.js +71 -0
- package/lib/specs/get-attr-specs.d.ts +8 -0
- package/lib/{get-attr-specs.js → specs/get-attr-specs.js} +8 -6
- package/lib/specs/get-implicit-role.d.ts +2 -0
- package/lib/specs/get-implicit-role.js +12 -0
- package/lib/specs/get-permitted-roles.d.ts +12 -0
- package/lib/specs/get-permitted-roles.js +61 -0
- package/lib/specs/get-role-spec.d.ts +5 -0
- package/lib/specs/get-role-spec.js +52 -0
- package/lib/specs/get-selectors-by-content-model-category.d.ts +3 -0
- package/lib/specs/get-selectors-by-content-model-category.js +8 -0
- package/lib/specs/get-spec-by-tag-name.d.ts +2 -0
- package/lib/specs/get-spec-by-tag-name.js +16 -0
- package/lib/specs/is-presentational.d.ts +1 -0
- package/lib/specs/is-presentational.js +10 -0
- package/lib/specs/resolve-version.d.ts +3 -0
- package/lib/specs/resolve-version.js +21 -0
- package/lib/specs/schema-to-spec.d.ts +13 -0
- package/lib/{get-spec.js → specs/schema-to-spec.js} +19 -11
- package/lib/types/aria.d.ts +136 -0
- package/lib/{attributes.js → types/aria.js} +0 -0
- package/lib/{attributes.d.ts → types/attributes.d.ts} +95 -48
- package/lib/{permitted-structres.js → types/attributes.js} +0 -0
- package/lib/{types.d.ts → types/index.d.ts} +55 -70
- package/lib/{types.js → types/index.js} +0 -0
- package/lib/types/permitted-structres.d.ts +58 -0
- package/lib/types/permitted-structres.js +8 -0
- package/lib/types/permitted-structures.d.ts +54 -0
- package/lib/types/permitted-structures.js +8 -0
- package/lib/utils/get-ns.d.ts +2 -0
- package/lib/{get-ns.js → utils/get-ns.js} +3 -0
- package/lib/{utils.d.ts → utils/merge-array.d.ts} +0 -0
- package/lib/{utils.js → utils/merge-array.js} +0 -0
- package/lib/utils/resolve-namespace.d.ts +9 -0
- package/lib/utils/resolve-namespace.js +31 -0
- package/package.json +12 -8
- package/schema.json +6 -6
- package/schemas/aria.schema.json +258 -0
- package/schemas/attributes.schema.json +3 -22
- package/schemas/content-models.schema.json +200 -0
- package/schemas/element.schema.json +11 -0
- package/src/dom-traverse/accname-computation.spec.ts +69 -0
- package/src/dom-traverse/accname-computation.ts +5 -0
- package/src/dom-traverse/get-attr-specs.ts +8 -0
- package/src/dom-traverse/get-computed-role.spec.ts +134 -0
- package/src/dom-traverse/get-computed-role.ts +181 -0
- package/src/dom-traverse/get-content-model.ts +32 -0
- package/src/dom-traverse/get-explicit-role.ts +112 -0
- package/src/dom-traverse/get-implicit-role.ts +36 -0
- package/src/dom-traverse/get-non-presentational-ancestor.ts +20 -0
- package/src/dom-traverse/get-permitted-roles.ts +7 -0
- package/src/dom-traverse/get-spec.ts +7 -0
- package/src/dom-traverse/has-required-owned-elements.spec.ts +36 -0
- package/src/dom-traverse/has-required-owned-elements.ts +85 -0
- package/src/dom-traverse/may-be-focusable.ts +21 -0
- package/src/index.ts +18 -6
- package/src/specs/aria-specs.ts +6 -0
- package/src/specs/content-model-category-to-tag-names.ts +15 -0
- package/src/specs/get-aria.ts +85 -0
- package/src/{get-attr-specs.spec.ts → specs/get-attr-specs.spec.ts} +4 -7
- package/src/{get-attr-specs.ts → specs/get-attr-specs.ts} +12 -7
- package/src/specs/get-implicit-role.spec.ts +81 -0
- package/src/specs/get-implicit-role.ts +17 -0
- package/src/specs/get-permitted-roles.spec.ts +420 -0
- package/src/specs/get-permitted-roles.ts +87 -0
- package/src/specs/get-role-spec.spec.ts +67 -0
- package/src/specs/get-role-spec.ts +72 -0
- package/src/specs/get-selectors-by-content-model-category.ts +10 -0
- package/src/specs/get-spec-by-tag-name.spec.ts +68 -0
- package/src/specs/get-spec-by-tag-name.ts +20 -0
- package/src/specs/is-presentational.ts +6 -0
- package/src/specs/resolve-version.ts +20 -0
- package/src/{get-spec.spec.ts → specs/schema-to-spec.spec.ts} +2 -2
- package/src/{get-spec.ts → specs/schema-to-spec.ts} +23 -9
- package/src/types/aria.ts +153 -0
- package/src/{attributes.ts → types/attributes.ts} +162 -117
- package/src/{types.ts → types/index.ts} +75 -74
- package/src/types/permitted-structres.ts +101 -0
- package/src/types/permitted-structures.ts +97 -0
- package/src/{get-ns.ts → utils/get-ns.ts} +6 -1
- package/src/{utils.ts → utils/merge-array.ts} +0 -0
- package/src/utils/resolve-namespace.spec.ts +37 -0
- package/src/utils/resolve-namespace.ts +40 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/lib/get-attr-specs.d.ts +0 -7
- package/lib/get-ns.d.ts +0 -1
- package/lib/get-spec-by-tag-name.d.ts +0 -2
- package/lib/get-spec-by-tag-name.js +0 -31
- package/lib/get-spec.d.ts +0 -13
- package/lib/permitted-structres.d.ts +0 -73
- package/schemas/permitted-structures.schema.json +0 -244
- package/schemas/wai-aria.schema.json +0 -87
- package/src/get-spec-by-tag-name.ts +0 -32
- package/src/permitted-structres.ts +0 -118
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
{
|
|
2
|
+
"definitions": {
|
|
3
|
+
"ContentModel": {
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": ["contents"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"contents": {
|
|
9
|
+
"oneOf": [
|
|
10
|
+
{
|
|
11
|
+
"type": "array",
|
|
12
|
+
"items": { "$ref": "#/definitions/PermittedContentPattern" }
|
|
13
|
+
},
|
|
14
|
+
{ "type": "boolean" }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"descendantOf": { "type": "string" },
|
|
18
|
+
"conditional": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"additionalProperties": false,
|
|
23
|
+
"required": ["condition", "contents"],
|
|
24
|
+
"properties": {
|
|
25
|
+
"condition": {
|
|
26
|
+
"type": "string"
|
|
27
|
+
},
|
|
28
|
+
"contents": {
|
|
29
|
+
"oneOf": [
|
|
30
|
+
{
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": { "$ref": "#/definitions/PermittedContentPattern" }
|
|
33
|
+
},
|
|
34
|
+
{ "type": "boolean" }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"ContentType": {
|
|
43
|
+
"oneOf": [
|
|
44
|
+
{
|
|
45
|
+
"description": "Element",
|
|
46
|
+
"type": "string",
|
|
47
|
+
"pattern": "^[^#]+$"
|
|
48
|
+
},
|
|
49
|
+
{ "$ref": "#/definitions/Category" }
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"Category": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"enum": [
|
|
55
|
+
"#text",
|
|
56
|
+
"#phrasing",
|
|
57
|
+
"#flow",
|
|
58
|
+
"#interactive",
|
|
59
|
+
"#heading",
|
|
60
|
+
"#sectioning",
|
|
61
|
+
"#metadata",
|
|
62
|
+
"#embedded",
|
|
63
|
+
"#palpable",
|
|
64
|
+
"#script-supporting",
|
|
65
|
+
"#SVGAnimation",
|
|
66
|
+
"#SVGBasicShapes",
|
|
67
|
+
"#SVGContainer",
|
|
68
|
+
"#SVGDescriptive",
|
|
69
|
+
"#SVGFilterPrimitive",
|
|
70
|
+
"#SVGFont",
|
|
71
|
+
"#SVGGradient",
|
|
72
|
+
"#SVGGraphics",
|
|
73
|
+
"#SVGGraphicsReferencing",
|
|
74
|
+
"#SVGLightSource",
|
|
75
|
+
"#SVGNeverRendered",
|
|
76
|
+
"#SVGNone",
|
|
77
|
+
"#SVGPaintServer",
|
|
78
|
+
"#SVGRenderable",
|
|
79
|
+
"#SVGShape",
|
|
80
|
+
"#SVGStructural",
|
|
81
|
+
"#SVGStructurallyExternal",
|
|
82
|
+
"#SVGTextContent",
|
|
83
|
+
"#SVGTextContentChild",
|
|
84
|
+
"#SVGOtherXMLNamespace"
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
"Model": {
|
|
88
|
+
"oneOf": [
|
|
89
|
+
{ "$ref": "#/definitions/ContentType" },
|
|
90
|
+
{
|
|
91
|
+
"type": "array",
|
|
92
|
+
"items": { "$ref": "#/definitions/ContentType" },
|
|
93
|
+
"uniqueItems": true
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
"PermittedContentPattern": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"oneOf": [
|
|
100
|
+
{ "$ref": "#/definitions/PermittedContentRequire" },
|
|
101
|
+
{ "$ref": "#/definitions/PermittedContentOptional" },
|
|
102
|
+
{ "$ref": "#/definitions/PermittedContentOneOrMore" },
|
|
103
|
+
{ "$ref": "#/definitions/PermittedContentZeroOrMore" },
|
|
104
|
+
{ "$ref": "#/definitions/PermittedContentChoice" },
|
|
105
|
+
{ "$ref": "#/definitions/PermittedContentTransparent" }
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"PermittedContentRequire": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"additionalProperties": false,
|
|
111
|
+
"required": ["require"],
|
|
112
|
+
"properties": {
|
|
113
|
+
"require": { "$ref": "#/definitions/Model" },
|
|
114
|
+
"max": { "type": "number" },
|
|
115
|
+
"min": { "type": "number" },
|
|
116
|
+
"_TODO_": { "type": "string" }
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"PermittedContentOptional": {
|
|
120
|
+
"type": "object",
|
|
121
|
+
"additionalProperties": false,
|
|
122
|
+
"required": ["optional"],
|
|
123
|
+
"properties": {
|
|
124
|
+
"optional": { "$ref": "#/definitions/Model" },
|
|
125
|
+
"max": { "type": "number" },
|
|
126
|
+
"_TODO_": { "type": "string" }
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"PermittedContentOneOrMore": {
|
|
130
|
+
"type": "object",
|
|
131
|
+
"additionalProperties": false,
|
|
132
|
+
"required": ["oneOrMore"],
|
|
133
|
+
"properties": {
|
|
134
|
+
"oneOrMore": {
|
|
135
|
+
"oneOf": [
|
|
136
|
+
{ "$ref": "#/definitions/Model" },
|
|
137
|
+
{
|
|
138
|
+
"type": "array",
|
|
139
|
+
"items": { "$ref": "#/definitions/PermittedContentPattern" }
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
"max": { "type": "number" },
|
|
144
|
+
"_TODO_": { "type": "string" }
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"PermittedContentZeroOrMore": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"additionalProperties": false,
|
|
150
|
+
"required": ["zeroOrMore"],
|
|
151
|
+
"properties": {
|
|
152
|
+
"zeroOrMore": {
|
|
153
|
+
"oneOf": [
|
|
154
|
+
{ "$ref": "#/definitions/Model" },
|
|
155
|
+
{
|
|
156
|
+
"type": "array",
|
|
157
|
+
"items": { "$ref": "#/definitions/PermittedContentPattern" }
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
"max": { "type": "number" },
|
|
162
|
+
"_TODO_": { "type": "string" }
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
"PermittedContentChoice": {
|
|
166
|
+
"type": "object",
|
|
167
|
+
"additionalProperties": false,
|
|
168
|
+
"required": ["choice"],
|
|
169
|
+
"properties": {
|
|
170
|
+
"choice": {
|
|
171
|
+
"type": "array",
|
|
172
|
+
"items": {
|
|
173
|
+
"type": "array",
|
|
174
|
+
"items": { "$ref": "#/definitions/PermittedContentPattern" }
|
|
175
|
+
},
|
|
176
|
+
"minItems": 2,
|
|
177
|
+
"maxItems": 5
|
|
178
|
+
},
|
|
179
|
+
"_TODO_": { "type": "string" }
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"PermittedContentTransparent": {
|
|
183
|
+
"type": "object",
|
|
184
|
+
"additionalProperties": false,
|
|
185
|
+
"required": ["transparent"],
|
|
186
|
+
"properties": {
|
|
187
|
+
"transparent": {
|
|
188
|
+
"type": "string"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"type": "object",
|
|
194
|
+
"additionalProperties": false,
|
|
195
|
+
"properties": {
|
|
196
|
+
"__contentModel": {
|
|
197
|
+
"$ref": "#/definitions/ContentModel"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"required": ["contentModel", "globalAttrs", "attributes", "aria"],
|
|
5
|
+
"properties": {
|
|
6
|
+
"contentModel": { "$ref": "./content-models.schema.json#/definitions/ContentModel" },
|
|
7
|
+
"globalAttrs": { "$ref": "./attributes.schema.json#/definitions/GlobalAttributes" },
|
|
8
|
+
"attributes": { "$ref": "./attributes.schema.json#/definitions/Attributes" },
|
|
9
|
+
"aria": { "$ref": "./aria.schema.json#/definitions/ARIA" }
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createJSDOMElement as c } from '@markuplint/test-tools';
|
|
2
|
+
|
|
3
|
+
import { getAccname } from './accname-computation';
|
|
4
|
+
|
|
5
|
+
test('Get accessible name', () => {
|
|
6
|
+
expect(getAccname(c('<button>label</button>'))).toBe('label');
|
|
7
|
+
expect(getAccname(c('<div>text</div>'))).toBe('');
|
|
8
|
+
expect(getAccname(c('<div aria-label="label">text</div>'))).toBe('label');
|
|
9
|
+
expect(getAccname(c('<span aria-label="label">text</span>'))).toBe('label');
|
|
10
|
+
expect(getAccname(c('<img alt="alterative-text" />'))).toBe('alterative-text');
|
|
11
|
+
expect(getAccname(c('<img title="title" />'))).toBe('title');
|
|
12
|
+
expect(getAccname(c('<div><label for="a">label</label><input id="a" /></div>').children[1])).toBe('label');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('Invisible element', () => {
|
|
16
|
+
expect(getAccname(c('<button>label</button>'))).toBe('label');
|
|
17
|
+
expect(getAccname(c('<button aria-disabled="true">label</button>'))).toBe('label');
|
|
18
|
+
expect(getAccname(c('<button aria-hidden="true">label</button>'))).toBe('');
|
|
19
|
+
expect(getAccname(c('<button hidden>label</button>'))).toBe('');
|
|
20
|
+
expect(getAccname(c('<button style="display: none">label</button>'))).toBe('');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @see https://www.w3.org/TR/accname-1.1/#ex-1-example-1-element1-id-el1-aria-labelledby-el3-element2-id-el2-aria-labelledby-el1-element3-id-el3-hello-element3
|
|
25
|
+
*/
|
|
26
|
+
test('accname-1.1 Example 1', () => {
|
|
27
|
+
const complex = c(`<div>
|
|
28
|
+
<input id="el1" aria-labelledby="el3" />
|
|
29
|
+
<input id="el2" aria-labelledby="el1" />
|
|
30
|
+
<h1 id="el3"> hello </h1>
|
|
31
|
+
</div>`);
|
|
32
|
+
expect(getAccname(complex.children[0])).toBe('hello');
|
|
33
|
+
expect(getAccname(complex.children[1])).toBe('');
|
|
34
|
+
expect(getAccname(complex.children[2])).toBe('hello');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* https://www.w3.org/TR/accname-1.1/#ex-2-example-2-h1-files-h1-ul-li-a-id-file_row1-href-files-documentation-pdf-documentation-pdf-a-span-role-button-tabindex-0-id-del_row1-aria-label-delete-aria-labelledby-del_row1-file_row1-span-li-li-a-id-file_row2-href-files-holidayletter-pdf-holidayletter-pdf-a-span-role-button-tabindex-0-id-del_row2-aria-label-delete-aria-labelledby-del_row2-file_row2-span-li-ul
|
|
39
|
+
*/
|
|
40
|
+
test('accname-1.1 Example 2', () => {
|
|
41
|
+
const complex = c(`<ul>
|
|
42
|
+
<li>
|
|
43
|
+
<a id="file_row1" href="./files/Documentation.pdf">Documentation.pdf</a>
|
|
44
|
+
<span role="button" tabindex="0" id="del_row1" aria-label="Delete" aria-labelledby="del_row1 file_row1"></span>
|
|
45
|
+
</li>
|
|
46
|
+
<li>
|
|
47
|
+
<a id="file_row2" href="./files/HolidayLetter.pdf">HolidayLetter.pdf</a>
|
|
48
|
+
<span role="button" tabindex="0" id="del_row2" aria-label="Delete" aria-labelledby="del_row2 file_row2"></span>
|
|
49
|
+
</li>
|
|
50
|
+
</ul>`);
|
|
51
|
+
const spans = complex.querySelectorAll('span');
|
|
52
|
+
expect(getAccname(spans[0])).toBe('Delete Documentation.pdf');
|
|
53
|
+
expect(getAccname(spans[1])).toBe('Delete HolidayLetter.pdf');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* https://www.w3.org/TR/accname-1.1/#ex-3-example-3-div-role-checkbox-aria-checked-false-flash-the-screen-span-role-textbox-aria-multiline-false-5-span-times-div
|
|
58
|
+
*/
|
|
59
|
+
test('accname-1.1 Example 3', () => {
|
|
60
|
+
const complex = c(
|
|
61
|
+
'<div role="checkbox" aria-checked="false">Flash the screen <span role="textbox" aria-multiline="false"> 5 </span> times</div>',
|
|
62
|
+
);
|
|
63
|
+
expect(getAccname(complex)).toBe('Flash the screen 5 times');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('with comment', () => {
|
|
67
|
+
expect(getAccname(c('<button>label</button>'))).toBe('label');
|
|
68
|
+
expect(getAccname(c('<button>label<!-- comment --></button>'))).toBe('label');
|
|
69
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MLMLSpec } from '../types';
|
|
2
|
+
import type { NamespaceURI } from '@markuplint/ml-ast';
|
|
3
|
+
|
|
4
|
+
import { getAttrSpecs as _getAttrSpecs } from '../specs/get-attr-specs';
|
|
5
|
+
|
|
6
|
+
export function getAttrSpecs(el: Element, schema: MLMLSpec) {
|
|
7
|
+
return _getAttrSpecs(el.localName, el.namespaceURI as NamespaceURI, schema);
|
|
8
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ARIAVersion } from '../types';
|
|
2
|
+
|
|
3
|
+
import specs from '@markuplint/html-spec';
|
|
4
|
+
import { createSelector } from '@markuplint/selector';
|
|
5
|
+
import { createJSDOMElement } from '@markuplint/test-tools';
|
|
6
|
+
|
|
7
|
+
import { getComputedRole } from './get-computed-role';
|
|
8
|
+
|
|
9
|
+
function _(html: string, selector?: string) {
|
|
10
|
+
return createJSDOMElement(html, selector, function (selector) {
|
|
11
|
+
// JSDOM supports no level 4 selectors yet.
|
|
12
|
+
return !!createSelector(selector, specs).match(this);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function c(html: string, version: ARIAVersion, selector?: string) {
|
|
17
|
+
return getComputedRole(specs, _(html, selector), version);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function tree(html: string, version: ARIAVersion) {
|
|
21
|
+
const el = _(html);
|
|
22
|
+
const tree: [string, string | null][] = [];
|
|
23
|
+
let current: Element | null = el;
|
|
24
|
+
while (current) {
|
|
25
|
+
tree.push([current.localName, getComputedRole(specs, current, version).role?.name || null]);
|
|
26
|
+
current = current.children.item(0);
|
|
27
|
+
}
|
|
28
|
+
return tree;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('getComputedRole', () => {
|
|
32
|
+
test('the a element', () => {
|
|
33
|
+
expect(c('<a></a>', '1.2').role?.name).toBe(undefined);
|
|
34
|
+
expect(c('<a href></a>', '1.2').role?.name).toBe('link');
|
|
35
|
+
expect(c('<a role="button"></a>', '1.2').role?.name).toBe('button');
|
|
36
|
+
expect(c('<a role="button" href></a>', '1.2').role?.name).toBe('button');
|
|
37
|
+
expect(c('<a role="foo" href></a>', '1.2').role?.name).toBe('link');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('the heading role', () => {
|
|
41
|
+
expect(c('<h1></h1>', '1.2').role?.name).toBe('heading');
|
|
42
|
+
expect(c('<h1></h1>', '1.2').role?.isImplicit).toBe(true);
|
|
43
|
+
expect(c('<div role="heading"></div>', '1.2').role?.name).toBe('heading');
|
|
44
|
+
expect(c('<div role="heading"></div>', '1.2').role?.isImplicit).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('multiple', () => {
|
|
48
|
+
expect(c('<div role="presentation heading"></div>', '1.2').role?.name).toBe('presentation');
|
|
49
|
+
expect(c('<div role="roletype heading"></div>', '1.2').role?.name).toBe('heading');
|
|
50
|
+
expect(c('<img alt="alt" role="banner button"/>', '1.2').role?.name).toBe('button');
|
|
51
|
+
expect(c('<img alt="alt" role="foo button"/>', '1.2').role?.name).toBe('button');
|
|
52
|
+
expect(c('<img alt="alt" role="graphics-symbol button"/>', '1.2').role?.name).toBe('button');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('svg', () => {
|
|
56
|
+
expect(c('<svg><rect role="graphics-symbol img"></rect></svg>', '1.2', 'rect').role?.name).toBe(
|
|
57
|
+
'graphics-symbol',
|
|
58
|
+
);
|
|
59
|
+
expect(c('<svg><rect role="roletype img"></rect></svg>', '1.2', 'rect').role?.name).toBe('img');
|
|
60
|
+
expect(c('<svg><rect role="roletype"></rect></svg>', '1.2', 'rect').role?.name).toBe('graphics-symbol');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('landmark', () => {
|
|
64
|
+
expect(c('<div role="region"></div>', '1.2').role?.name).toBe('generic');
|
|
65
|
+
expect(c('<div role="region" aria-label="foo"></div>', '1.2').role?.name).toBe('region');
|
|
66
|
+
expect(c('<div role="form"></div>', '1.2').role?.name).toBe('generic');
|
|
67
|
+
expect(c('<div role="form" aria-label="foo"></div>', '1.2').role?.name).toBe('form');
|
|
68
|
+
expect(c('<div role="navigation"></div>', '1.2').role?.name).toBe('navigation');
|
|
69
|
+
expect(c('<div role="navigation" aria-label="foo"></div>', '1.2').role?.name).toBe('navigation');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('Presentational Roles Conflict Resolution (1) Interactive Elements', () => {
|
|
73
|
+
expect(c('<a href="path/to"></a>', '1.2').role?.name).toBe('link');
|
|
74
|
+
expect(c('<a role="none" href="path/to"></a>', '1.2').role?.name).toBe('link'); // No permitted role
|
|
75
|
+
expect(c('<a></a>', '1.2').role?.name).toBe(undefined);
|
|
76
|
+
expect(c('<a role="none"></a>', '1.2').role?.name).toBe('none');
|
|
77
|
+
expect(c('<a role="none" href="path/to" disabled></a>', '1.2').role?.name).toBe('link'); // No permitted role
|
|
78
|
+
expect(c('<button></button>', '1.2').role?.name).toBe('button');
|
|
79
|
+
expect(c('<button disabled></button>', '1.2').role?.name).toBe('button');
|
|
80
|
+
expect(c('<button role="none"></button>', '1.2').role?.name).toBe('button'); // No permitted role
|
|
81
|
+
expect(c('<button role="none" disabled></button>', '1.2').role?.name).toBe('button'); // No permitted role
|
|
82
|
+
expect(c('<div></div>', '1.2').role?.name).toBe('generic');
|
|
83
|
+
expect(c('<div role="none"></div>', '1.2').role?.name).toBe('none');
|
|
84
|
+
expect(c('<div tabindex="0"></div>', '1.2').role?.name).toBe('generic');
|
|
85
|
+
expect(c('<div tabindex="0" role="none"></div>', '1.2').role?.name).toBe('generic');
|
|
86
|
+
expect(c('<div><span></span></div>', '1.2', 'span').role?.name).toBe(undefined);
|
|
87
|
+
expect(c('<div><span role="none"></span></div>', '1.2', 'span').role?.name).toBe('none');
|
|
88
|
+
expect(c('<div><span tabindex="0"></span></div>', '1.2', 'span').role?.name).toBe(undefined);
|
|
89
|
+
expect(c('<div><span tabindex="0" role="none"></span></div>', '1.2', 'span').role?.name).toBe(undefined);
|
|
90
|
+
expect(c('<div hidden><span></span></div>', '1.2', 'span').role?.name).toBe(undefined);
|
|
91
|
+
expect(c('<div hidden><span role="none"></span></div>', '1.2', 'span').role?.name).toBe('none');
|
|
92
|
+
expect(c('<div hidden><span tabindex="0"></span></div>', '1.2', 'span').role?.name).toBe(undefined);
|
|
93
|
+
expect(c('<div hidden><span tabindex="0" role="none"></span></div>', '1.2', 'span').role?.name).toBe('none');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('Presentational Roles Conflict Resolution (2-1) Required Owned Elements', () => {
|
|
97
|
+
expect(c('<table><tr><td>foo</td></tr></table>', '1.2', 'td').role?.name).toBe('cell');
|
|
98
|
+
expect(c('<table><tr><td role="none">foo</td></tr></table>', '1.2', 'td').role?.name).toBe('cell');
|
|
99
|
+
expect(c('<table><tbody role="none"><tr><td>foo</td></tr></tbody></table>', '1.2', 'tbody').role?.name).toBe(
|
|
100
|
+
'rowgroup',
|
|
101
|
+
);
|
|
102
|
+
expect(c('<ul><li></li></ul>', '1.2', 'li').role?.name).toBe('listitem');
|
|
103
|
+
expect(c('<ul><li role="presentation"></li></ul>', '1.2', 'li').role?.name).toBe('listitem');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('Presentational Roles Conflict Resolution (2-2) the accessibility tree to be malformed', () => {
|
|
107
|
+
expect(c('<table><tr><td>foo</td></tr></table>', '1.2', 'td').role?.name).toBe('cell');
|
|
108
|
+
expect(c('<table role="none"><tr><td>foo</td></tr></table>', '1.2', 'td').role?.name).toBe(undefined);
|
|
109
|
+
expect(tree('<table><tr><td>foo</td></tr></table>', '1.2')).toStrictEqual([
|
|
110
|
+
['table', 'table'],
|
|
111
|
+
['tbody', 'rowgroup'],
|
|
112
|
+
['tr', 'row'],
|
|
113
|
+
['td', 'cell'],
|
|
114
|
+
]);
|
|
115
|
+
expect(tree('<table role="none"><tr><td>foo</td></tr></table>', '1.2')).toStrictEqual([
|
|
116
|
+
['table', 'none'],
|
|
117
|
+
['tbody', null],
|
|
118
|
+
['tr', null],
|
|
119
|
+
['td', null],
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('Presentational Roles Conflict Resolution (3) Global Props', () => {
|
|
124
|
+
/**
|
|
125
|
+
* @see https://w3c.github.io/aria/#example-41
|
|
126
|
+
*/
|
|
127
|
+
expect(c('<h1 role="presentation" aria-describedby="comment-1"> Sample Content </h1>', '1.2').role?.name).toBe(
|
|
128
|
+
'heading',
|
|
129
|
+
);
|
|
130
|
+
expect(c('<h1 role="presentation" aria-level="2"> Sample Content </h1>', '1.2').role?.name).toBe(
|
|
131
|
+
'presentation',
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { ARIAVersion, ComputedRole, MLMLSpec } from '../types';
|
|
2
|
+
|
|
3
|
+
import { ariaSpecs } from '../specs/aria-specs';
|
|
4
|
+
import { isPresentational } from '../specs/is-presentational';
|
|
5
|
+
|
|
6
|
+
import { getAttrSpecs } from './get-attr-specs';
|
|
7
|
+
import { getExplicitRole } from './get-explicit-role';
|
|
8
|
+
import { getImplicitRole } from './get-implicit-role';
|
|
9
|
+
import { getNonPresentationalAncestor } from './get-non-presentational-ancestor';
|
|
10
|
+
import { isRequiredOwnedElement } from './has-required-owned-elements';
|
|
11
|
+
import { mayBeFocusable } from './may-be-focusable';
|
|
12
|
+
|
|
13
|
+
export function getComputedRole(specs: Readonly<MLMLSpec>, el: Element, version: ARIAVersion): ComputedRole {
|
|
14
|
+
const implicitRole = getImplicitRole(specs, el, version);
|
|
15
|
+
const explicitRole = getExplicitRole(specs, el, version);
|
|
16
|
+
const computedRole = explicitRole.role ? explicitRole : implicitRole;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Presentational Roles Conflict Resolution
|
|
20
|
+
*
|
|
21
|
+
* @see https://www.w3.org/TR/wai-aria/#conflict_resolution_presentation_none
|
|
22
|
+
* @see https://w3c.github.io/aria/#conflict_resolution_presentation_none
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* > If the action of exposing the explicit role
|
|
27
|
+
* > causes the accessibility tree to be malformed,
|
|
28
|
+
* > the expected results are undefined.
|
|
29
|
+
*
|
|
30
|
+
* Determines whether the context is valid.
|
|
31
|
+
* ⚠ THE SPECIFICATION HAS AN ISSUE
|
|
32
|
+
* that has not decided whether the context is a parent or an ancestor.
|
|
33
|
+
*
|
|
34
|
+
* @see https://github.com/w3c/aria/issues/1033
|
|
35
|
+
* @see https://github.com/w3c/aria/issues/748
|
|
36
|
+
* @see https://github.com/w3c/aria/pull/1162
|
|
37
|
+
* @see https://github.com/w3c/aria/pull/1213
|
|
38
|
+
*
|
|
39
|
+
* Currently, this process interprets that as A PARENT
|
|
40
|
+
* because it wants to be near to HTML semantics.
|
|
41
|
+
* However, the presentational role behaves transparently
|
|
42
|
+
* according to the sample code in WAI-ARIA specification.
|
|
43
|
+
*/
|
|
44
|
+
if (computedRole.role?.requiredContextRole.length) {
|
|
45
|
+
const nonPresentationalAncestor = getNonPresentationalAncestor(el, specs, version);
|
|
46
|
+
if (!nonPresentationalAncestor.role) {
|
|
47
|
+
return {
|
|
48
|
+
el,
|
|
49
|
+
role: null,
|
|
50
|
+
errorType: 'NO_OWNER',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (!computedRole.role?.requiredContextRole.includes(nonPresentationalAncestor.role.name)) {
|
|
54
|
+
return {
|
|
55
|
+
el,
|
|
56
|
+
role: null,
|
|
57
|
+
errorType: 'INVALID_REQUIRED_CONTEXT_ROLE',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (computedRole.role && !isPresentational(computedRole.role.name)) {
|
|
63
|
+
return computedRole;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* > If an element is focusable,
|
|
68
|
+
* > user agents MUST ignore the presentation/none role
|
|
69
|
+
* > and expose the element with its implicit role,
|
|
70
|
+
* > in order to ensure that the element is operable.
|
|
71
|
+
*
|
|
72
|
+
* With the issue: What does focusable or otherwise interactive mean](https://github.com/w3c/aria/issues/1192)
|
|
73
|
+
*
|
|
74
|
+
* - Focus: https://html.spec.whatwg.org/multipage/interaction.html#focus
|
|
75
|
+
* - Interactive content: https://html.spec.whatwg.org/multipage/dom.html#interactive-content
|
|
76
|
+
*/
|
|
77
|
+
if (
|
|
78
|
+
/**
|
|
79
|
+
* If interactive element
|
|
80
|
+
*
|
|
81
|
+
* 1. It may be focusable
|
|
82
|
+
*
|
|
83
|
+
* THIS CONDITION IS ALMOST MEANINGLESS.
|
|
84
|
+
* Because it has already been determined that
|
|
85
|
+
* the interactive elements can not specify the presentational role
|
|
86
|
+
* in the previous processing that computes the permitted role (`getExplicitRole`).
|
|
87
|
+
*/
|
|
88
|
+
mayBeFocusable(el, specs) &&
|
|
89
|
+
/**
|
|
90
|
+
* 2. No disabled
|
|
91
|
+
*/
|
|
92
|
+
!someAncestors(el, p => isEnabledAttr(p, specs, 'disabled')) &&
|
|
93
|
+
/**
|
|
94
|
+
* 3. No inert
|
|
95
|
+
*/
|
|
96
|
+
!someAncestors(el, p => isEnabledAttr(p, specs, 'inert')) &&
|
|
97
|
+
/**
|
|
98
|
+
* 4. No hidden
|
|
99
|
+
*/
|
|
100
|
+
!someAncestors(el, p => isEnabledAttr(p, specs, 'hidden'))
|
|
101
|
+
) {
|
|
102
|
+
return {
|
|
103
|
+
...implicitRole,
|
|
104
|
+
errorType: 'INTERACTIVE_ELEMENT_MUST_NOT_BE_PRESENTATIONAL',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* > If a required owned element has an explicit non-presentational role,
|
|
110
|
+
* > user agents MUST ignore an inherited presentational role
|
|
111
|
+
* > and expose the element with its explicit role.
|
|
112
|
+
*
|
|
113
|
+
* In other words,
|
|
114
|
+
* if the element has `role=presentation` and
|
|
115
|
+
* the role of the parent element has a required owned element,
|
|
116
|
+
* `role=presentation` is to be ignored absolutely.
|
|
117
|
+
*/
|
|
118
|
+
if (explicitRole.role) {
|
|
119
|
+
const nonPresentationalAncestor = getNonPresentationalAncestor(el, specs, version);
|
|
120
|
+
if (nonPresentationalAncestor.role?.requiredOwnedElements.length) {
|
|
121
|
+
if (
|
|
122
|
+
nonPresentationalAncestor.role.requiredOwnedElements.some(expected => {
|
|
123
|
+
// const ancestor = nonPresentationalAncestor.el;
|
|
124
|
+
// const ancestorImplicitRole = getImplicitRole(specs, ancestor, version);
|
|
125
|
+
// console.log({ nonPresentationalAncestor, ancestorImplicitRole });
|
|
126
|
+
return isRequiredOwnedElement(implicitRole, expected, specs, version);
|
|
127
|
+
})
|
|
128
|
+
) {
|
|
129
|
+
return {
|
|
130
|
+
...implicitRole,
|
|
131
|
+
errorType: 'REQUIRED_OWNED_ELEMENT_MUST_NOT_BE_PRESENTATIONAL',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* > If an element has global WAI-ARIA states or properties,
|
|
139
|
+
* > user agents MUST ignore the presentation role
|
|
140
|
+
* > and instead expose the element's implicit role.
|
|
141
|
+
* > However, if an element has only non-global,
|
|
142
|
+
* > role-specific WAI-ARIA states or properties,
|
|
143
|
+
* > the element MUST NOT be exposed
|
|
144
|
+
* > unless the presentational role is inherited
|
|
145
|
+
* > and an explicit non-presentational role is applied.
|
|
146
|
+
*/
|
|
147
|
+
const { props } = ariaSpecs(specs, version);
|
|
148
|
+
for (const attr of Array.from(el.attributes)) {
|
|
149
|
+
if (props.find(p => p.name === attr.name)?.isGlobal) {
|
|
150
|
+
return {
|
|
151
|
+
...implicitRole,
|
|
152
|
+
errorType: 'GLOBAL_PROP_MUST_NOT_BE_PRESENTATIONAL',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return computedRole;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The attribute is available in its owner element,
|
|
162
|
+
* it has the attribute.
|
|
163
|
+
*/
|
|
164
|
+
function isEnabledAttr(el: Element, specs: MLMLSpec, attrName: string) {
|
|
165
|
+
const attrs = getAttrSpecs(el, specs);
|
|
166
|
+
const attr = attrs?.find(attr => attr.name === attrName);
|
|
167
|
+
return !!attr && el.hasAttribute(attrName);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Determines whether some ancestors match the condition that the specified callback function.
|
|
172
|
+
*/
|
|
173
|
+
function someAncestors(el: Element, predicate: (ancestor: Element) => boolean) {
|
|
174
|
+
const list: Element[] = [];
|
|
175
|
+
let current: Element | null = el;
|
|
176
|
+
while (current) {
|
|
177
|
+
list.push(current);
|
|
178
|
+
current = current.parentElement;
|
|
179
|
+
}
|
|
180
|
+
return list.some(predicate);
|
|
181
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ElementSpec } from '../types';
|
|
2
|
+
import type { PermittedContentPattern } from '../types/permitted-structures';
|
|
3
|
+
|
|
4
|
+
import { getSpec } from './get-spec';
|
|
5
|
+
|
|
6
|
+
type Specs = Pick<ElementSpec, 'name' | 'contentModel'>[];
|
|
7
|
+
|
|
8
|
+
const cachesBySpecs = new Map<Specs, Map<Element, PermittedContentPattern[] | boolean | null>>();
|
|
9
|
+
export function getContentModel(el: Element, specs: Pick<ElementSpec, 'name' | 'contentModel'>[]) {
|
|
10
|
+
const cacheByEl = cachesBySpecs.get(specs) ?? new Map<Element, PermittedContentPattern[] | boolean>();
|
|
11
|
+
const cached = cacheByEl.get(el);
|
|
12
|
+
if (cached) {
|
|
13
|
+
return cached;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const spec = getSpec<'contentModel'>(el, specs);
|
|
17
|
+
if (!spec) {
|
|
18
|
+
cacheByEl.set(el, null);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const conditions = spec.contentModel.conditional ?? [];
|
|
23
|
+
for (const cond of conditions) {
|
|
24
|
+
if (el.matches(cond.condition)) {
|
|
25
|
+
cacheByEl.set(el, cond.contents);
|
|
26
|
+
return cond.contents;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
cacheByEl.set(el, spec.contentModel.contents);
|
|
31
|
+
return spec.contentModel.contents;
|
|
32
|
+
}
|