@markuplint/ml-spec 3.0.0-alpha.82 → 3.0.0-dev.24

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.
Files changed (91) hide show
  1. package/README.md +8 -3
  2. package/lib/dom-traverse/get-computed-aria-props.d.ts +12 -0
  3. package/lib/dom-traverse/get-computed-aria-props.js +106 -0
  4. package/lib/dom-traverse/get-computed-role.js +13 -25
  5. package/lib/dom-traverse/get-content-model.d.ts +3 -0
  6. package/lib/dom-traverse/get-content-model.js +28 -0
  7. package/lib/dom-traverse/get-explicit-role.js +1 -1
  8. package/lib/dom-traverse/get-spec.d.ts +2 -2
  9. package/lib/dom-traverse/get-spec.js +2 -2
  10. package/lib/dom-traverse/has-required-owned-elements.js +12 -0
  11. package/lib/dom-traverse/is-exposed.d.ts +11 -0
  12. package/lib/dom-traverse/is-exposed.js +181 -0
  13. package/lib/dom-traverse/may-be-focusable.d.ts +2 -0
  14. package/lib/dom-traverse/may-be-focusable.js +22 -0
  15. package/lib/index.d.ts +8 -1
  16. package/lib/index.js +8 -1
  17. package/lib/specs/content-model-category-to-tag-names.d.ts +2 -2
  18. package/lib/specs/content-model-category-to-tag-names.js +2 -2
  19. package/lib/specs/get-aria.js +3 -3
  20. package/lib/specs/get-attr-specs.d.ts +1 -1
  21. package/lib/specs/get-selectors-by-content-model-category.d.ts +1 -1
  22. package/lib/specs/get-spec-by-tag-name.d.ts +2 -2
  23. package/lib/specs/get-spec-by-tag-name.js +1 -1
  24. package/lib/specs/is-nothing-content-model.d.ts +5 -0
  25. package/lib/specs/is-nothing-content-model.js +12 -0
  26. package/lib/specs/is-palpable-elements.d.ts +5 -0
  27. package/lib/specs/is-palpable-elements.js +47 -0
  28. package/lib/specs/is-void-element.d.ts +1 -0
  29. package/lib/specs/is-void-element.js +25 -0
  30. package/lib/specs/schema-to-spec.d.ts +1 -1
  31. package/lib/specs/schema-to-spec.js +1 -1
  32. package/lib/types/aria.d.ts +10 -4
  33. package/lib/types/attributes.d.ts +94 -39
  34. package/lib/types/index.d.ts +27 -27
  35. package/lib/types/permitted-structres.d.ts +22 -28
  36. package/lib/types/permitted-structures.d.ts +54 -0
  37. package/lib/types/permitted-structures.js +8 -0
  38. package/lib/utils/get-ns.d.ts +2 -1
  39. package/lib/utils/get-ns.js +3 -0
  40. package/lib/utils/merge-array.d.ts +1 -1
  41. package/lib/utils/resolve-namespace.d.ts +4 -4
  42. package/lib/utils/resolve-namespace.js +18 -13
  43. package/package.json +7 -7
  44. package/schema.json +6 -6
  45. package/schemas/aria.schema.json +1 -1
  46. package/schemas/content-models.schema.json +33 -49
  47. package/src/dom-traverse/accname-computation.spec.ts +6 -6
  48. package/src/dom-traverse/get-computed-aria-props.spec.ts +368 -0
  49. package/src/dom-traverse/get-computed-aria-props.ts +130 -0
  50. package/src/dom-traverse/get-computed-role.spec.ts +1 -1
  51. package/src/dom-traverse/get-computed-role.ts +13 -25
  52. package/src/dom-traverse/get-content-model.ts +32 -0
  53. package/src/dom-traverse/get-explicit-role.ts +1 -1
  54. package/src/dom-traverse/get-spec.ts +3 -3
  55. package/src/dom-traverse/has-required-owned-elements.ts +13 -0
  56. package/src/dom-traverse/is-exposed.spec.ts +50 -0
  57. package/src/dom-traverse/is-exposed.ts +196 -0
  58. package/src/dom-traverse/may-be-focusable.ts +21 -0
  59. package/src/index.ts +8 -1
  60. package/src/specs/content-model-category-to-tag-names.ts +3 -3
  61. package/src/specs/get-aria.ts +3 -3
  62. package/src/specs/get-selectors-by-content-model-category.ts +1 -1
  63. package/src/specs/get-spec-by-tag-name.spec.ts +10 -10
  64. package/src/specs/get-spec-by-tag-name.ts +9 -5
  65. package/src/specs/is-nothing-content-model.ts +9 -0
  66. package/src/specs/is-palpable-elements.ts +55 -0
  67. package/src/specs/is-void-element.ts +22 -0
  68. package/src/specs/schema-to-spec.ts +1 -1
  69. package/src/types/aria.ts +7 -1
  70. package/src/types/attributes.ts +227 -121
  71. package/src/types/index.ts +8 -8
  72. package/src/types/permitted-structres.ts +24 -32
  73. package/src/types/permitted-structures.ts +96 -0
  74. package/src/utils/get-ns.ts +6 -1
  75. package/src/utils/resolve-namespace.spec.ts +37 -0
  76. package/src/utils/resolve-namespace.ts +22 -18
  77. package/tsconfig.tsbuildinfo +1 -1
  78. package/lib/dom-traverse/focusability-computation.d.ts +0 -1
  79. package/lib/dom-traverse/focusability-computation.js +0 -8
  80. package/lib/dom-traverse/get-aria.d.ts +0 -9
  81. package/lib/dom-traverse/get-aria.js +0 -54
  82. package/lib/specs/aria-spec.d.ts +0 -5
  83. package/lib/specs/aria-spec.js +0 -12
  84. package/lib/specs/get-spec.d.ts +0 -13
  85. package/lib/specs/get-spec.js +0 -85
  86. package/lib/specs/html-spec.d.ts +0 -6
  87. package/lib/specs/html-spec.js +0 -16
  88. package/lib/specs/resolve-namespace.d.ts +0 -9
  89. package/lib/specs/resolve-namespace.js +0 -26
  90. package/lib/utils.d.ts +0 -8
  91. package/lib/utils.js +0 -50
@@ -7,7 +7,7 @@ test('Get accessible name', () => {
7
7
  expect(getAccname(c('<div>text</div>'))).toBe('');
8
8
  expect(getAccname(c('<div aria-label="label">text</div>'))).toBe('label');
9
9
  expect(getAccname(c('<span aria-label="label">text</span>'))).toBe('label');
10
- expect(getAccname(c('<img alt="alterative-text" />'))).toBe('alterative-text');
10
+ expect(getAccname(c('<img alt="alternative-text" />'))).toBe('alternative-text');
11
11
  expect(getAccname(c('<img title="title" />'))).toBe('title');
12
12
  expect(getAccname(c('<div><label for="a">label</label><input id="a" /></div>').children[1])).toBe('label');
13
13
  });
@@ -23,7 +23,7 @@ test('Invisible element', () => {
23
23
  /**
24
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
25
  */
26
- test('accname-1.1 Expample 1', () => {
26
+ test('accname-1.1 Example 1', () => {
27
27
  const complex = c(`<div>
28
28
  <input id="el1" aria-labelledby="el3" />
29
29
  <input id="el2" aria-labelledby="el1" />
@@ -35,9 +35,9 @@ test('accname-1.1 Expample 1', () => {
35
35
  });
36
36
 
37
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
38
+ * @see 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
39
  */
40
- test('accname-1.1 Expample 2', () => {
40
+ test('accname-1.1 Example 2', () => {
41
41
  const complex = c(`<ul>
42
42
  <li>
43
43
  <a id="file_row1" href="./files/Documentation.pdf">Documentation.pdf</a>
@@ -54,9 +54,9 @@ test('accname-1.1 Expample 2', () => {
54
54
  });
55
55
 
56
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
57
+ * @see 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
58
  */
59
- test('accname-1.1 Expample 3', () => {
59
+ test('accname-1.1 Example 3', () => {
60
60
  const complex = c(
61
61
  '<div role="checkbox" aria-checked="false">Flash the screen <span role="textbox" aria-multiline="false"> 5 </span> times</div>',
62
62
  );
@@ -0,0 +1,368 @@
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 { getComputedAriaProps } from './get-computed-aria-props';
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 getComputedAriaProps(specs, _(html, selector), version);
18
+ }
19
+
20
+ describe('getComputedAriaProps', () => {
21
+ test('the link role', () => {
22
+ expect(c('<a href></a>', '1.2')).toStrictEqual({
23
+ 'aria-atomic': {
24
+ name: 'aria-atomic',
25
+ value: 'false',
26
+ deprecated: false,
27
+ required: false,
28
+ from: 'default',
29
+ },
30
+ 'aria-busy': {
31
+ name: 'aria-busy',
32
+ value: 'false',
33
+ deprecated: false,
34
+ required: false,
35
+ from: 'default',
36
+ },
37
+ 'aria-controls': {
38
+ name: 'aria-controls',
39
+ value: undefined,
40
+ deprecated: false,
41
+ required: false,
42
+ from: 'default',
43
+ },
44
+ 'aria-current': {
45
+ name: 'aria-current',
46
+ value: 'false',
47
+ deprecated: false,
48
+ required: false,
49
+ from: 'default',
50
+ },
51
+ 'aria-describedby': {
52
+ name: 'aria-describedby',
53
+ value: undefined,
54
+ deprecated: false,
55
+ required: false,
56
+ from: 'default',
57
+ },
58
+ 'aria-details': {
59
+ name: 'aria-details',
60
+ value: undefined,
61
+ deprecated: false,
62
+ required: false,
63
+ from: 'default',
64
+ },
65
+ 'aria-disabled': {
66
+ name: 'aria-disabled',
67
+ value: 'false',
68
+ deprecated: false,
69
+ required: false,
70
+ from: 'default',
71
+ },
72
+ 'aria-dropeffect': {
73
+ name: 'aria-dropeffect',
74
+ value: 'none',
75
+ deprecated: true,
76
+ required: false,
77
+ from: 'default',
78
+ },
79
+ 'aria-errormessage': {
80
+ name: 'aria-errormessage',
81
+ value: undefined,
82
+ deprecated: false,
83
+ required: false,
84
+ from: 'default',
85
+ },
86
+ 'aria-expanded': {
87
+ name: 'aria-expanded',
88
+ value: 'undefined',
89
+ deprecated: false,
90
+ required: false,
91
+ from: 'default',
92
+ },
93
+ 'aria-flowto': {
94
+ name: 'aria-flowto',
95
+ value: undefined,
96
+ deprecated: false,
97
+ required: false,
98
+ from: 'default',
99
+ },
100
+ 'aria-grabbed': {
101
+ name: 'aria-grabbed',
102
+ value: 'undefined',
103
+ deprecated: true,
104
+ required: false,
105
+ from: 'default',
106
+ },
107
+ 'aria-haspopup': {
108
+ name: 'aria-haspopup',
109
+ value: 'false',
110
+ deprecated: false,
111
+ required: false,
112
+ from: 'default',
113
+ },
114
+ 'aria-hidden': {
115
+ name: 'aria-hidden',
116
+ value: 'undefined',
117
+ deprecated: false,
118
+ required: false,
119
+ from: 'default',
120
+ },
121
+ 'aria-invalid': {
122
+ name: 'aria-invalid',
123
+ value: 'false',
124
+ deprecated: false,
125
+ required: false,
126
+ from: 'default',
127
+ },
128
+ 'aria-keyshortcuts': {
129
+ name: 'aria-keyshortcuts',
130
+ value: undefined,
131
+ deprecated: false,
132
+ required: false,
133
+ from: 'default',
134
+ },
135
+ 'aria-label': {
136
+ name: 'aria-label',
137
+ value: undefined,
138
+ deprecated: false,
139
+ required: false,
140
+ from: 'default',
141
+ },
142
+ 'aria-labelledby': {
143
+ name: 'aria-labelledby',
144
+ value: undefined,
145
+ deprecated: false,
146
+ required: false,
147
+ from: 'default',
148
+ },
149
+ 'aria-live': {
150
+ name: 'aria-live',
151
+ value: 'off',
152
+ deprecated: false,
153
+ required: false,
154
+ from: 'default',
155
+ },
156
+ 'aria-owns': {
157
+ name: 'aria-owns',
158
+ value: undefined,
159
+ deprecated: false,
160
+ required: false,
161
+ from: 'default',
162
+ },
163
+ 'aria-relevant': {
164
+ name: 'aria-relevant',
165
+ value: 'additions text',
166
+ deprecated: false,
167
+ required: false,
168
+ from: 'default',
169
+ },
170
+ 'aria-roledescription': {
171
+ name: 'aria-roledescription',
172
+ value: undefined,
173
+ deprecated: false,
174
+ required: false,
175
+ from: 'default',
176
+ },
177
+ });
178
+ });
179
+
180
+ test('the heading role', () => {
181
+ expect(c('<h1></h1>', '1.2')).toStrictEqual({
182
+ 'aria-atomic': {
183
+ name: 'aria-atomic',
184
+ value: 'false',
185
+ deprecated: false,
186
+ required: false,
187
+ from: 'default',
188
+ },
189
+ 'aria-busy': {
190
+ name: 'aria-busy',
191
+ value: 'false',
192
+ deprecated: false,
193
+ required: false,
194
+ from: 'default',
195
+ },
196
+ 'aria-controls': {
197
+ name: 'aria-controls',
198
+ value: undefined,
199
+ deprecated: false,
200
+ required: false,
201
+ from: 'default',
202
+ },
203
+ 'aria-current': {
204
+ name: 'aria-current',
205
+ value: 'false',
206
+ deprecated: false,
207
+ required: false,
208
+ from: 'default',
209
+ },
210
+ 'aria-describedby': {
211
+ name: 'aria-describedby',
212
+ value: undefined,
213
+ deprecated: false,
214
+ required: false,
215
+ from: 'default',
216
+ },
217
+ 'aria-details': {
218
+ name: 'aria-details',
219
+ value: undefined,
220
+ deprecated: false,
221
+ required: false,
222
+ from: 'default',
223
+ },
224
+ 'aria-disabled': {
225
+ name: 'aria-disabled',
226
+ value: 'false',
227
+ deprecated: false,
228
+ required: false,
229
+ from: 'default',
230
+ },
231
+ 'aria-dropeffect': {
232
+ name: 'aria-dropeffect',
233
+ value: 'none',
234
+ deprecated: true,
235
+ required: false,
236
+ from: 'default',
237
+ },
238
+ 'aria-errormessage': {
239
+ name: 'aria-errormessage',
240
+ value: undefined,
241
+ deprecated: false,
242
+ required: false,
243
+ from: 'default',
244
+ },
245
+ 'aria-flowto': {
246
+ name: 'aria-flowto',
247
+ value: undefined,
248
+ deprecated: false,
249
+ required: false,
250
+ from: 'default',
251
+ },
252
+ 'aria-grabbed': {
253
+ name: 'aria-grabbed',
254
+ value: 'undefined',
255
+ deprecated: true,
256
+ required: false,
257
+ from: 'default',
258
+ },
259
+ 'aria-haspopup': {
260
+ name: 'aria-haspopup',
261
+ value: 'false',
262
+ deprecated: false,
263
+ required: false,
264
+ from: 'default',
265
+ },
266
+ 'aria-hidden': {
267
+ name: 'aria-hidden',
268
+ value: 'undefined',
269
+ deprecated: false,
270
+ required: false,
271
+ from: 'default',
272
+ },
273
+ 'aria-invalid': {
274
+ name: 'aria-invalid',
275
+ value: 'false',
276
+ deprecated: false,
277
+ required: false,
278
+ from: 'default',
279
+ },
280
+ 'aria-keyshortcuts': {
281
+ name: 'aria-keyshortcuts',
282
+ value: undefined,
283
+ deprecated: false,
284
+ required: false,
285
+ from: 'default',
286
+ },
287
+ 'aria-label': {
288
+ name: 'aria-label',
289
+ value: undefined,
290
+ deprecated: false,
291
+ required: false,
292
+ from: 'default',
293
+ },
294
+ 'aria-labelledby': {
295
+ name: 'aria-labelledby',
296
+ value: undefined,
297
+ deprecated: false,
298
+ required: false,
299
+ from: 'default',
300
+ },
301
+ 'aria-level': {
302
+ name: 'aria-level',
303
+ value: '1',
304
+ deprecated: false,
305
+ required: true,
306
+ from: 'default',
307
+ },
308
+ 'aria-live': {
309
+ name: 'aria-live',
310
+ value: 'off',
311
+ deprecated: false,
312
+ required: false,
313
+ from: 'default',
314
+ },
315
+ 'aria-owns': {
316
+ name: 'aria-owns',
317
+ value: undefined,
318
+ deprecated: false,
319
+ required: false,
320
+ from: 'default',
321
+ },
322
+ 'aria-relevant': {
323
+ name: 'aria-relevant',
324
+ value: 'additions text',
325
+ deprecated: false,
326
+ required: false,
327
+ from: 'default',
328
+ },
329
+ 'aria-roledescription': {
330
+ name: 'aria-roledescription',
331
+ value: undefined,
332
+ deprecated: false,
333
+ required: false,
334
+ from: 'default',
335
+ },
336
+ });
337
+ });
338
+
339
+ test('from html-attr', () => {
340
+ expect(c('<input type="checkbox" checked>', '1.2')['aria-checked']).toStrictEqual({
341
+ name: 'aria-checked',
342
+ value: 'true',
343
+ deprecated: false,
344
+ required: true,
345
+ from: 'html-attr',
346
+ });
347
+ });
348
+
349
+ test('overwrite aria-attr', () => {
350
+ expect(c('<input type="checkbox" checked aria-checked="false">', '1.2')['aria-checked']).toStrictEqual({
351
+ name: 'aria-checked',
352
+ value: 'false',
353
+ deprecated: false,
354
+ required: true,
355
+ from: 'aria-attr',
356
+ });
357
+ });
358
+
359
+ test('overwrite aria-attr', () => {
360
+ expect(c('<input type="checkbox" checked aria-checked="mixed">', '1.2')['aria-checked']).toStrictEqual({
361
+ name: 'aria-checked',
362
+ value: 'mixed',
363
+ deprecated: false,
364
+ required: true,
365
+ from: 'aria-attr',
366
+ });
367
+ });
368
+ });
@@ -0,0 +1,130 @@
1
+ import type { ARIAAttributeValue, ARIAProperty, ARIAVersion, MLMLSpec } from '../types';
2
+
3
+ import { ariaSpecs as _ariaSpecs } from '../specs/aria-specs';
4
+
5
+ import { getComputedRole } from './get-computed-role';
6
+
7
+ type ARIAProps = Record<string, ARIAProp>;
8
+
9
+ type ARIAProp = {
10
+ name: string;
11
+ value: string | undefined;
12
+ required: boolean;
13
+ deprecated: boolean;
14
+ from: ARIAPropReferenceType;
15
+ };
16
+
17
+ type ARIAPropReferenceType = 'default' | 'html-attr' | 'aria-attr';
18
+
19
+ export function getComputedAriaProps(specs: Readonly<MLMLSpec>, el: Element, version: ARIAVersion): ARIAProps {
20
+ const ariaSpecs = _ariaSpecs(specs, version);
21
+
22
+ const { role } = getComputedRole(specs, el, version);
23
+
24
+ if (!role) {
25
+ return {};
26
+ }
27
+
28
+ const props: ARIAProps = {};
29
+
30
+ role.ownedProperties.forEach(ownedProp => {
31
+ const spec = ariaSpecs.props.find(propSpec => propSpec.name === ownedProp.name);
32
+ if (!spec) {
33
+ return;
34
+ }
35
+
36
+ if (el.hasAttribute(spec.name)) {
37
+ const attrValue = el.getAttribute(spec.name);
38
+ if (attrValue && isValidAriaValue(spec, role.name, attrValue, spec.enum)) {
39
+ props[ownedProp.name] = {
40
+ name: ownedProp.name,
41
+ value: attrValue,
42
+ deprecated: !!spec.deprecated,
43
+ required: !!ownedProp.required,
44
+ from: 'aria-attr',
45
+ };
46
+ return;
47
+ }
48
+ }
49
+
50
+ const equivalentHtmlAttr = spec.equivalentHtmlAttrs?.[0];
51
+ if (equivalentHtmlAttr && el.hasAttribute(equivalentHtmlAttr.htmlAttrName)) {
52
+ const value =
53
+ equivalentHtmlAttr.value === null
54
+ ? el.getAttribute(equivalentHtmlAttr.htmlAttrName)
55
+ : equivalentHtmlAttr.value;
56
+ if (value != null) {
57
+ props[ownedProp.name] = {
58
+ name: ownedProp.name,
59
+ value,
60
+ deprecated: !!spec.deprecated,
61
+ required: !!ownedProp.required,
62
+ from: 'html-attr',
63
+ };
64
+ return;
65
+ }
66
+ }
67
+
68
+ let defaultValue = spec.defaultValue;
69
+
70
+ /**
71
+ * @see https://www.w3.org/TR/html-aria/#el-h1-h6
72
+ */
73
+ if (ownedProp.name === 'aria-level' && /^H[1-6]$/.test(el.nodeName)) {
74
+ defaultValue = el.nodeName.replace('H', '');
75
+ }
76
+
77
+ props[ownedProp.name] = {
78
+ name: ownedProp.name,
79
+ value: defaultValue,
80
+ deprecated: !!spec.deprecated,
81
+ required: !!ownedProp.required,
82
+ from: 'default',
83
+ };
84
+ });
85
+
86
+ return props;
87
+ }
88
+
89
+ function isValidAriaValue(spec: ARIAProperty, role: string, value: string | undefined, enumList: string[]) {
90
+ let type: ARIAAttributeValue = spec.value;
91
+ if (spec.conditionalValue) {
92
+ for (const cond of spec.conditionalValue) {
93
+ if (cond.role.includes(role)) {
94
+ type = cond.value;
95
+ }
96
+ }
97
+ }
98
+
99
+ value = (value ?? '').trim();
100
+ switch (type) {
101
+ case 'string': {
102
+ return true;
103
+ }
104
+ case 'ID reference':
105
+ case 'ID reference list':
106
+ case 'URI': {
107
+ return !!value;
108
+ }
109
+ case 'integer':
110
+ case 'number': {
111
+ return !isNaN(parseFloat(value));
112
+ }
113
+ case 'token':
114
+ case 'token list': {
115
+ if (!enumList.length) {
116
+ throw new Error('Need an enum list in token and token list types');
117
+ }
118
+ return enumList.includes(value.toLowerCase());
119
+ }
120
+ case 'tristate': {
121
+ return ['true', 'false', 'mixed'].includes(value.toLowerCase());
122
+ }
123
+ case 'true/false': {
124
+ return ['true', 'false'].includes(value.toLowerCase());
125
+ }
126
+ case 'true/false/undefined': {
127
+ return ['true', 'false', 'undefined'].includes(value.toLowerCase());
128
+ }
129
+ }
130
+ }
@@ -93,7 +93,7 @@ describe('getComputedRole', () => {
93
93
  expect(c('<div hidden><span tabindex="0" role="none"></span></div>', '1.2', 'span').role?.name).toBe('none');
94
94
  });
95
95
 
96
- test('Presentational Roles Conflict Resolution (2-1) Required Owend Elements', () => {
96
+ test('Presentational Roles Conflict Resolution (2-1) Required Owned Elements', () => {
97
97
  expect(c('<table><tr><td>foo</td></tr></table>', '1.2', 'td').role?.name).toBe('cell');
98
98
  expect(c('<table><tr><td role="none">foo</td></tr></table>', '1.2', 'td').role?.name).toBe('cell');
99
99
  expect(c('<table><tbody role="none"><tr><td>foo</td></tr></tbody></table>', '1.2', 'tbody').role?.name).toBe(
@@ -1,7 +1,6 @@
1
1
  import type { ARIAVersion, ComputedRole, MLMLSpec } from '../types';
2
2
 
3
3
  import { ariaSpecs } from '../specs/aria-specs';
4
- import { getSelectorsByContentModelCategory } from '../specs/get-selectors-by-content-model-category';
5
4
  import { isPresentational } from '../specs/is-presentational';
6
5
 
7
6
  import { getAttrSpecs } from './get-attr-specs';
@@ -9,6 +8,7 @@ import { getExplicitRole } from './get-explicit-role';
9
8
  import { getImplicitRole } from './get-implicit-role';
10
9
  import { getNonPresentationalAncestor } from './get-non-presentational-ancestor';
11
10
  import { isRequiredOwnedElement } from './has-required-owned-elements';
11
+ import { mayBeFocusable } from './may-be-focusable';
12
12
 
13
13
  export function getComputedRole(specs: Readonly<MLMLSpec>, el: Element, version: ARIAVersion): ComputedRole {
14
14
  const implicitRole = getImplicitRole(specs, el, version);
@@ -76,38 +76,26 @@ export function getComputedRole(specs: Readonly<MLMLSpec>, el: Element, version:
76
76
  */
77
77
  if (
78
78
  /**
79
- * Interactive element
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`).
80
87
  */
81
- [
82
- /**
83
- * Interactive content
84
- *
85
- * THIS CONDITION(SELECTORS) IS ALMOST MEANINGLESS.
86
- * Because it has already been determined that
87
- * the interactive elements can not specify the presentational role
88
- * in the previous processing that computes the permitted role (`getExplicitRole`).
89
- *
90
- * @see https://html.spec.whatwg.org/multipage/dom.html#interactive-content
91
- */
92
- ...getSelectorsByContentModelCategory(specs, '#interactive'),
93
- /**
94
- * Interaction
95
- *
96
- * @see https://html.spec.whatwg.org/multipage/interaction.html
97
- */
98
- '[tabindex]',
99
- '[contenteditable]:not([contenteditable="false" i])',
100
- ].some(selector => el.matches(selector)) &&
88
+ mayBeFocusable(el, specs) &&
101
89
  /**
102
- * No disabled
90
+ * 2. No disabled
103
91
  */
104
92
  !someAncestors(el, p => isEnabledAttr(p, specs, 'disabled')) &&
105
93
  /**
106
- * No inert
94
+ * 3. No inert
107
95
  */
108
96
  !someAncestors(el, p => isEnabledAttr(p, specs, 'inert')) &&
109
97
  /**
110
- * No hidden
98
+ * 4. No hidden
111
99
  */
112
100
  !someAncestors(el, p => isEnabledAttr(p, specs, 'hidden'))
113
101
  ) {
@@ -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
+ }
@@ -16,7 +16,7 @@ export function getExplicitRole(specs: MLMLSpec, el: Element, version: ARIAVersi
16
16
  /**
17
17
  * Resolve from values and **Handling Author Errors**
18
18
  *
19
- * @see https://w3c.github.io/aria/#document-handling_author-errors_roles
19
+ * @see https://w3c.github.io/aria/#document-handling_author-errors
20
20
  */
21
21
  for (const roleName of roleNames) {
22
22
  const spec = getRoleSpec(specs, roleName, namespaceURI, version);
@@ -1,7 +1,7 @@
1
- import type { MLMLSpec } from '../types';
1
+ import type { ElementSpec } from '../types';
2
2
 
3
3
  import { getSpecByTagName } from '../specs/get-spec-by-tag-name';
4
4
 
5
- export function getSpec(el: Element, schema: MLMLSpec) {
6
- return getSpecByTagName(schema, el.localName, el.namespaceURI);
5
+ export function getSpec<K extends keyof ElementSpec>(el: Element, specs: Pick<ElementSpec, 'name' | K>[]) {
6
+ return getSpecByTagName(specs, el.localName, el.namespaceURI);
7
7
  }
@@ -7,6 +7,19 @@ import { getExplicitRole } from './get-explicit-role';
7
7
  import { getImplicitRole } from './get-implicit-role';
8
8
 
9
9
  export function hasRequiredOwnedElement(el: Element, specs: Readonly<MLMLSpec>, version: ARIAVersion): boolean {
10
+ /**
11
+ * The element has aria-owns which means it may have owned elements.
12
+ *
13
+ * THIS CONDITION IS PARTIAL SUPPORT.
14
+ */
15
+ if (el.hasAttribute('aria-owns')) {
16
+ // FIXME
17
+ return true;
18
+ }
19
+
20
+ /**
21
+ * Otherwise, traverses descendants to find owned elements.
22
+ */
10
23
  const computed = getComputedRole(specs, el, version);
11
24
  if (!computed.role || computed.role.requiredOwnedElements.length === 0) {
12
25
  return true;