@markuplint/ml-spec 3.0.0-dev.24 → 3.0.0-dev.38

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 (58) hide show
  1. package/lib/dom-traverse/is-exposed.js +8 -0
  2. package/package.json +4 -4
  3. package/schemas/aria.schema.json +0 -258
  4. package/schemas/attributes.schema.json +0 -204
  5. package/schemas/content-models.schema.json +0 -199
  6. package/schemas/element.schema.json +0 -11
  7. package/schemas/global-attributes.schema.json +0 -761
  8. package/src/dom-traverse/accname-computation.spec.ts +0 -69
  9. package/src/dom-traverse/accname-computation.ts +0 -5
  10. package/src/dom-traverse/get-attr-specs.ts +0 -8
  11. package/src/dom-traverse/get-computed-aria-props.spec.ts +0 -368
  12. package/src/dom-traverse/get-computed-aria-props.ts +0 -130
  13. package/src/dom-traverse/get-computed-role.spec.ts +0 -134
  14. package/src/dom-traverse/get-computed-role.ts +0 -181
  15. package/src/dom-traverse/get-content-model.ts +0 -32
  16. package/src/dom-traverse/get-explicit-role.ts +0 -112
  17. package/src/dom-traverse/get-implicit-role.ts +0 -36
  18. package/src/dom-traverse/get-non-presentational-ancestor.ts +0 -20
  19. package/src/dom-traverse/get-permitted-roles.ts +0 -7
  20. package/src/dom-traverse/get-spec.ts +0 -7
  21. package/src/dom-traverse/has-required-owned-elements.spec.ts +0 -36
  22. package/src/dom-traverse/has-required-owned-elements.ts +0 -85
  23. package/src/dom-traverse/is-exposed.spec.ts +0 -50
  24. package/src/dom-traverse/is-exposed.ts +0 -196
  25. package/src/dom-traverse/may-be-focusable.ts +0 -21
  26. package/src/index.ts +0 -24
  27. package/src/specs/aria-specs.ts +0 -6
  28. package/src/specs/content-model-category-to-tag-names.ts +0 -15
  29. package/src/specs/get-aria.ts +0 -85
  30. package/src/specs/get-attr-specs.spec.ts +0 -47
  31. package/src/specs/get-attr-specs.ts +0 -116
  32. package/src/specs/get-implicit-role.spec.ts +0 -81
  33. package/src/specs/get-implicit-role.ts +0 -17
  34. package/src/specs/get-permitted-roles.spec.ts +0 -420
  35. package/src/specs/get-permitted-roles.ts +0 -87
  36. package/src/specs/get-role-spec.spec.ts +0 -67
  37. package/src/specs/get-role-spec.ts +0 -72
  38. package/src/specs/get-selectors-by-content-model-category.ts +0 -10
  39. package/src/specs/get-spec-by-tag-name.spec.ts +0 -68
  40. package/src/specs/get-spec-by-tag-name.ts +0 -20
  41. package/src/specs/is-nothing-content-model.ts +0 -9
  42. package/src/specs/is-palpable-elements.ts +0 -55
  43. package/src/specs/is-presentational.ts +0 -6
  44. package/src/specs/is-void-element.ts +0 -22
  45. package/src/specs/resolve-version.ts +0 -20
  46. package/src/specs/schema-to-spec.spec.ts +0 -39
  47. package/src/specs/schema-to-spec.ts +0 -103
  48. package/src/types/aria.ts +0 -153
  49. package/src/types/attributes.ts +0 -1524
  50. package/src/types/index.ts +0 -255
  51. package/src/types/permitted-structres.ts +0 -100
  52. package/src/types/permitted-structures.ts +0 -96
  53. package/src/utils/get-ns.ts +0 -18
  54. package/src/utils/merge-array.ts +0 -35
  55. package/src/utils/resolve-namespace.spec.ts +0 -37
  56. package/src/utils/resolve-namespace.ts +0 -40
  57. package/tsconfig.test.json +0 -3
  58. package/tsconfig.tsbuildinfo +0 -1
@@ -1,134 +0,0 @@
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
- });
@@ -1,181 +0,0 @@
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
- }
@@ -1,32 +0,0 @@
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
- }
@@ -1,112 +0,0 @@
1
- import type { ARIAVersion, ComputedRole, MLMLSpec, RoleComputationError } from '../types';
2
-
3
- import { getRoleSpec } from '../specs/get-role-spec';
4
- import { resolveNamespace } from '../utils/resolve-namespace';
5
-
6
- import { getPermittedRoles } from './get-permitted-roles';
7
-
8
- export function getExplicitRole(specs: MLMLSpec, el: Element, version: ARIAVersion): ComputedRole {
9
- const roleValue = el.getAttribute('role');
10
- const roleNames = roleValue?.toLowerCase().trim().split(/\s+/g) || [];
11
- const permittedRoles = getPermittedRoles(el, version, specs);
12
- const { namespaceURI } = resolveNamespace(el.localName, el.namespaceURI);
13
-
14
- let error: RoleComputationError = 'NO_EXPLICIT';
15
-
16
- /**
17
- * Resolve from values and **Handling Author Errors**
18
- *
19
- * @see https://w3c.github.io/aria/#document-handling_author-errors
20
- */
21
- for (const roleName of roleNames) {
22
- const spec = getRoleSpec(specs, roleName, namespaceURI, version);
23
-
24
- /**
25
- * If `spec` is null, the role DOES NOT EXIST.
26
- *
27
- * > If the role attribute contains no tokens matching the name
28
- * > of a non-abstract WAI-ARIA role,
29
- * > the user agent MUST treat the element as
30
- * > if no role had been provided. For example,
31
- * > <table role="foo"> should be exposed
32
- * > in the same way as <table> and <input type="text" role="structure">
33
- * > in the same way as <input type="text">.
34
- */
35
- if (!spec) {
36
- error = 'ROLE_NO_EXISTS';
37
- continue;
38
- }
39
-
40
- /**
41
- * > As stated in the Definition of Roles section,
42
- * > it is considered an authoring error to use abstract roles in content.
43
- * > User agents MUST NOT map abstract roles
44
- * > via the standard role mechanism of the accessibility API.
45
- */
46
- if (spec.isAbstract) {
47
- error = 'ABSTRACT';
48
- continue;
49
- }
50
-
51
- /**
52
- * Whether the role is the permitted role according to ARIA in HTML.
53
- */
54
- if (!permittedRoles.some(r => r.name === roleName)) {
55
- error = 'NO_PERMITTED';
56
- continue;
57
- }
58
-
59
- /**
60
- * > Certain landmark roles require names from authors.
61
- * > In situations where an author has not specified names for these landmarks,
62
- * > it is considered an authoring error.
63
- * > The user agent MUST treat such elements as if no role had been provided.
64
- * > If a valid fallback role had been specified,
65
- * > or if the element had an implicit ARIA role,
66
- * > then user agents would continue to expose that role, instead.
67
- */
68
- if (isLandmarkRole(spec) && !isValidLandmarkRole(el, spec)) {
69
- error = 'INVALID_LANDMARK';
70
- continue;
71
- }
72
-
73
- /**
74
- * Otherwise
75
- */
76
- return {
77
- el,
78
- role: {
79
- ...spec,
80
- isImplicit: false,
81
- },
82
- };
83
- }
84
-
85
- return {
86
- el,
87
- role: null,
88
- errorType: error,
89
- };
90
- }
91
-
92
- function isLandmarkRole(role: NonNullable<ReturnType<typeof getRoleSpec>>) {
93
- return role?.superClassRoles.some(su => su.name === 'landmark');
94
- }
95
-
96
- function isValidLandmarkRole(el: Element, role: NonNullable<ReturnType<typeof getRoleSpec>>) {
97
- if (!role.accessibleNameRequired) {
98
- return true;
99
- }
100
- if (role.accessibleNameFromAuthor) {
101
- if (el.getAttribute('aria-label')) {
102
- return true;
103
- }
104
- const id = el.getAttribute('aria-labelledby');
105
- if (id && el.ownerDocument.getElementById(id)) {
106
- return true;
107
- }
108
- }
109
- // The landmark role does not require names from the content
110
- // if (role.accessibleNameFromContent) {}
111
- return false;
112
- }
@@ -1,36 +0,0 @@
1
- import type { ARIAVersion, ComputedRole, MLMLSpec } from '../types';
2
-
3
- import { getImplicitRole as _getImplicitRole } from '../specs/get-implicit-role';
4
- import { getRoleSpec } from '../specs/get-role-spec';
5
- import { resolveNamespace } from '../utils/resolve-namespace';
6
-
7
- export function getImplicitRole(specs: Readonly<MLMLSpec>, el: Element, version: ARIAVersion): ComputedRole {
8
- const implicitRole = getImplicitRoleName(el, version, specs);
9
- if (implicitRole === false) {
10
- // No Corresponding Role
11
- return {
12
- el,
13
- role: null,
14
- };
15
- }
16
- const { namespaceURI } = resolveNamespace(el.localName, el.namespaceURI);
17
- const spec = getRoleSpec(specs, implicitRole, namespaceURI, version);
18
- if (!spec) {
19
- return {
20
- el,
21
- role: null,
22
- errorType: 'IMPLICIT_ROLE_NAMESPACE_ERROR',
23
- };
24
- }
25
- return {
26
- el,
27
- role: {
28
- ...spec,
29
- isImplicit: true,
30
- },
31
- };
32
- }
33
-
34
- export function getImplicitRoleName(el: Element, version: ARIAVersion, specs: Readonly<MLMLSpec>) {
35
- return _getImplicitRole(specs, el.localName, el.namespaceURI, version, el.matches.bind(el));
36
- }
@@ -1,20 +0,0 @@
1
- import type { ARIAVersion, MLMLSpec } from '../types';
2
-
3
- import { isPresentational } from '../specs/is-presentational';
4
-
5
- import { getComputedRole } from './get-computed-role';
6
-
7
- export function getNonPresentationalAncestor(el: Element, specs: MLMLSpec, version: ARIAVersion) {
8
- let ancestor: Element | null = el.parentElement;
9
- while (ancestor) {
10
- const ancestorRole = getComputedRole(specs, ancestor, version);
11
- if (!isPresentational(ancestorRole.role?.name)) {
12
- return ancestorRole;
13
- }
14
- ancestor = ancestor.parentElement;
15
- }
16
- return {
17
- el: ancestor,
18
- role: null,
19
- };
20
- }
@@ -1,7 +0,0 @@
1
- import type { ARIAVersion, MLMLSpec } from '../types';
2
-
3
- import { getPermittedRoles as _getPermittedRoles } from '../specs/get-permitted-roles';
4
-
5
- export function getPermittedRoles(el: Element, version: ARIAVersion, specs: Readonly<MLMLSpec>) {
6
- return _getPermittedRoles(specs, el.localName, el.namespaceURI, version, el.matches.bind(el));
7
- }
@@ -1,7 +0,0 @@
1
- import type { ElementSpec } from '../types';
2
-
3
- import { getSpecByTagName } from '../specs/get-spec-by-tag-name';
4
-
5
- export function getSpec<K extends keyof ElementSpec>(el: Element, specs: Pick<ElementSpec, 'name' | K>[]) {
6
- return getSpecByTagName(specs, el.localName, el.namespaceURI);
7
- }
@@ -1,36 +0,0 @@
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 { hasRequiredOwnedElement } from './has-required-owned-elements';
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 m(html: string, version: ARIAVersion, selector?: string) {
17
- const el = _(html, selector);
18
- return hasRequiredOwnedElement(el, specs, version);
19
- }
20
-
21
- describe('matchesOwnedRole', () => {
22
- test('rowgroup > row', () => {
23
- expect(m('<table><tbody><tr><td></td></tr></tbody></table>', '1.2')).toBe(true);
24
- });
25
- test('row', () => {
26
- expect(m('<table><tr><td></td></tr></table>', '1.2', 'tr')).toBe(true);
27
- });
28
- test('list > listitem', () => {
29
- expect(m('<ul><li></li></ul>', '1.2')).toBe(true);
30
- expect(m('<ul><div><li></li></div></ul>', '1.2')).toBe(false);
31
- expect(m('<ul><div role="none"><li></li></div></ul>', '1.2')).toBe(true);
32
- expect(
33
- m('<ul><div role="none"><div role="none"><div role="none"><li></li></div></div></div></ul>', '1.2'),
34
- ).toBe(true);
35
- });
36
- });
@@ -1,85 +0,0 @@
1
- import type { ARIAVersion, ComputedRole, MLMLSpec } from '../types';
2
-
3
- import { isPresentational } from '../specs/is-presentational';
4
-
5
- import { getComputedRole } from './get-computed-role';
6
- import { getExplicitRole } from './get-explicit-role';
7
- import { getImplicitRole } from './get-implicit-role';
8
-
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
- */
23
- const computed = getComputedRole(specs, el, version);
24
- if (!computed.role || computed.role.requiredOwnedElements.length === 0) {
25
- return true;
26
- }
27
- for (const expectRole of computed.role.requiredOwnedElements) {
28
- for (const owned of getClosestNonPresentationalDescendants(el, specs, version)) {
29
- if (isRequiredOwnedElement(owned, expectRole, specs, version)) {
30
- return true;
31
- }
32
- }
33
- }
34
-
35
- return false;
36
- }
37
-
38
- export function isRequiredOwnedElement(owned: ComputedRole, query: string, specs: MLMLSpec, version: ARIAVersion) {
39
- const [baseRole, owningRole] = query.split(' > ') as [string, string | undefined];
40
- if (owned.role?.name !== baseRole) {
41
- return false;
42
- }
43
-
44
- if (!owningRole) {
45
- return true;
46
- }
47
-
48
- for (const owning of getClosestNonPresentationalDescendants(owned.el, specs, version)) {
49
- if (owning.role?.name === owningRole) {
50
- return true;
51
- }
52
- }
53
-
54
- return false;
55
- }
56
-
57
- /**
58
- * Gets the list of closest non-presentational descendants.
59
- * ⚠ THE SPECIFICATION HAS AN ISSUE
60
- * that has not decided whether the owned element is a child or a descendant.
61
- *
62
- * @see https://github.com/w3c/aria/issues/1033
63
- * @see https://github.com/w3c/aria/issues/748
64
- * @see https://github.com/w3c/aria/pull/1162
65
- * @see https://github.com/w3c/aria/pull/1213
66
- *
67
- * Currently, this process interprets that as A CHILD
68
- * because it wants to be near to HTML semantics.
69
- * However, the presentational role behaves transparently
70
- * according to the sample code in WAI-ARIA specification.
71
- */
72
- function getClosestNonPresentationalDescendants(el: Element, specs: MLMLSpec, version: ARIAVersion): ComputedRole[] {
73
- const owned: ComputedRole[] = [];
74
- for (const child of Array.from(el.children)) {
75
- const implicitRole = getImplicitRole(specs, child, version);
76
- const explicitRole = getExplicitRole(specs, child, version);
77
- const computed = explicitRole.role ? explicitRole : implicitRole;
78
- if (isPresentational(computed.role?.name)) {
79
- owned.push(...getClosestNonPresentationalDescendants(child, specs, version));
80
- continue;
81
- }
82
- owned.push({ ...computed, el: child });
83
- }
84
- return owned;
85
- }