UNPKG

11.2 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.computeAriaBusy = computeAriaBusy;
7exports.computeAriaChecked = computeAriaChecked;
8exports.computeAriaCurrent = computeAriaCurrent;
9exports.computeAriaExpanded = computeAriaExpanded;
10exports.computeAriaPressed = computeAriaPressed;
11exports.computeAriaSelected = computeAriaSelected;
12exports.computeAriaValueMax = computeAriaValueMax;
13exports.computeAriaValueMin = computeAriaValueMin;
14exports.computeAriaValueNow = computeAriaValueNow;
15exports.computeAriaValueText = computeAriaValueText;
16exports.computeHeadingLevel = computeHeadingLevel;
17exports.getImplicitAriaRoles = getImplicitAriaRoles;
18exports.getRoles = getRoles;
19exports.isInaccessible = isInaccessible;
20exports.isSubtreeInaccessible = isSubtreeInaccessible;
21exports.logRoles = void 0;
22exports.prettyRoles = prettyRoles;
23var _ariaQuery = require("aria-query");
24var _domAccessibilityApi = require("dom-accessibility-api");
25var _prettyDom = require("./pretty-dom");
26var _config = require("./config");
27const elementRoleList = buildElementRoleList(_ariaQuery.elementRoles);
28
29/**
30 * @param {Element} element -
31 * @returns {boolean} - `true` if `element` and its subtree are inaccessible
32 */
33function isSubtreeInaccessible(element) {
34 if (element.hidden === true) {
35 return true;
36 }
37 if (element.getAttribute('aria-hidden') === 'true') {
38 return true;
39 }
40 const window = element.ownerDocument.defaultView;
41 if (window.getComputedStyle(element).display === 'none') {
42 return true;
43 }
44 return false;
45}
46
47/**
48 * Partial implementation https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
49 * which should only be used for elements with a non-presentational role i.e.
50 * `role="none"` and `role="presentation"` will not be excluded.
51 *
52 * Implements aria-hidden semantics (i.e. parent overrides child)
53 * Ignores "Child Presentational: True" characteristics
54 *
55 * @param {Element} element -
56 * @param {object} [options] -
57 * @param {function (element: Element): boolean} options.isSubtreeInaccessible -
58 * can be used to return cached results from previous isSubtreeInaccessible calls
59 * @returns {boolean} true if excluded, otherwise false
60 */
61function isInaccessible(element, options = {}) {
62 const {
63 isSubtreeInaccessible: isSubtreeInaccessibleImpl = isSubtreeInaccessible
64 } = options;
65 const window = element.ownerDocument.defaultView;
66 // since visibility is inherited we can exit early
67 if (window.getComputedStyle(element).visibility === 'hidden') {
68 return true;
69 }
70 let currentElement = element;
71 while (currentElement) {
72 if (isSubtreeInaccessibleImpl(currentElement)) {
73 return true;
74 }
75 currentElement = currentElement.parentElement;
76 }
77 return false;
78}
79function getImplicitAriaRoles(currentNode) {
80 // eslint bug here:
81 // eslint-disable-next-line no-unused-vars
82 for (const {
83 match,
84 roles
85 } of elementRoleList) {
86 if (match(currentNode)) {
87 return [...roles];
88 }
89 }
90 return [];
91}
92function buildElementRoleList(elementRolesMap) {
93 function makeElementSelector({
94 name,
95 attributes
96 }) {
97 return `${name}${attributes.map(({
98 name: attributeName,
99 value,
100 constraints = []
101 }) => {
102 const shouldNotExist = constraints.indexOf('undefined') !== -1;
103 const shouldBeNonEmpty = constraints.indexOf('set') !== -1;
104 const hasExplicitValue = typeof value !== 'undefined';
105 if (hasExplicitValue) {
106 return `[${attributeName}="${value}"]`;
107 } else if (shouldNotExist) {
108 return `:not([${attributeName}])`;
109 } else if (shouldBeNonEmpty) {
110 return `[${attributeName}]:not([${attributeName}=""])`;
111 }
112 return `[${attributeName}]`;
113 }).join('')}`;
114 }
115 function getSelectorSpecificity({
116 attributes = []
117 }) {
118 return attributes.length;
119 }
120 function bySelectorSpecificity({
121 specificity: leftSpecificity
122 }, {
123 specificity: rightSpecificity
124 }) {
125 return rightSpecificity - leftSpecificity;
126 }
127 function match(element) {
128 let {
129 attributes = []
130 } = element;
131
132 // https://github.com/testing-library/dom-testing-library/issues/814
133 const typeTextIndex = attributes.findIndex(attribute => attribute.value && attribute.name === 'type' && attribute.value === 'text');
134 if (typeTextIndex >= 0) {
135 // not using splice to not mutate the attributes array
136 attributes = [...attributes.slice(0, typeTextIndex), ...attributes.slice(typeTextIndex + 1)];
137 }
138 const selector = makeElementSelector({
139 ...element,
140 attributes
141 });
142 return node => {
143 if (typeTextIndex >= 0 && node.type !== 'text') {
144 return false;
145 }
146 return node.matches(selector);
147 };
148 }
149 let result = [];
150
151 // eslint bug here:
152 // eslint-disable-next-line no-unused-vars
153 for (const [element, roles] of elementRolesMap.entries()) {
154 result = [...result, {
155 match: match(element),
156 roles: Array.from(roles),
157 specificity: getSelectorSpecificity(element)
158 }];
159 }
160 return result.sort(bySelectorSpecificity);
161}
162function getRoles(container, {
163 hidden = false
164} = {}) {
165 function flattenDOM(node) {
166 return [node, ...Array.from(node.children).reduce((acc, child) => [...acc, ...flattenDOM(child)], [])];
167 }
168 return flattenDOM(container).filter(element => {
169 return hidden === false ? isInaccessible(element) === false : true;
170 }).reduce((acc, node) => {
171 let roles = [];
172 // TODO: This violates html-aria which does not allow any role on every element
173 if (node.hasAttribute('role')) {
174 roles = node.getAttribute('role').split(' ').slice(0, 1);
175 } else {
176 roles = getImplicitAriaRoles(node);
177 }
178 return roles.reduce((rolesAcc, role) => Array.isArray(rolesAcc[role]) ? {
179 ...rolesAcc,
180 [role]: [...rolesAcc[role], node]
181 } : {
182 ...rolesAcc,
183 [role]: [node]
184 }, acc);
185 }, {});
186}
187function prettyRoles(dom, {
188 hidden,
189 includeDescription
190}) {
191 const roles = getRoles(dom, {
192 hidden
193 });
194 // We prefer to skip generic role, we don't recommend it
195 return Object.entries(roles).filter(([role]) => role !== 'generic').map(([role, elements]) => {
196 const delimiterBar = '-'.repeat(50);
197 const elementsString = elements.map(el => {
198 const nameString = `Name "${(0, _domAccessibilityApi.computeAccessibleName)(el, {
199 computedStyleSupportsPseudoElements: (0, _config.getConfig)().computedStyleSupportsPseudoElements
200 })}":\n`;
201 const domString = (0, _prettyDom.prettyDOM)(el.cloneNode(false));
202 if (includeDescription) {
203 const descriptionString = `Description "${(0, _domAccessibilityApi.computeAccessibleDescription)(el, {
204 computedStyleSupportsPseudoElements: (0, _config.getConfig)().computedStyleSupportsPseudoElements
205 })}":\n`;
206 return `${nameString}${descriptionString}${domString}`;
207 }
208 return `${nameString}${domString}`;
209 }).join('\n\n');
210 return `${role}:\n\n${elementsString}\n\n${delimiterBar}`;
211 }).join('\n');
212}
213const logRoles = (dom, {
214 hidden = false
215} = {}) => console.log(prettyRoles(dom, {
216 hidden
217}));
218
219/**
220 * @param {Element} element -
221 * @returns {boolean | undefined} - false/true if (not)selected, undefined if not selectable
222 */
223exports.logRoles = logRoles;
224function computeAriaSelected(element) {
225 // implicit value from html-aam mappings: https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
226 // https://www.w3.org/TR/html-aam-1.0/#details-id-97
227 if (element.tagName === 'OPTION') {
228 return element.selected;
229 }
230
231 // explicit value
232 return checkBooleanAttribute(element, 'aria-selected');
233}
234
235/**
236 * @param {Element} element -
237 * @returns {boolean} -
238 */
239function computeAriaBusy(element) {
240 // https://www.w3.org/TR/wai-aria-1.1/#aria-busy
241 return element.getAttribute('aria-busy') === 'true';
242}
243
244/**
245 * @param {Element} element -
246 * @returns {boolean | undefined} - false/true if (not)checked, undefined if not checked-able
247 */
248function computeAriaChecked(element) {
249 // implicit value from html-aam mappings: https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
250 // https://www.w3.org/TR/html-aam-1.0/#details-id-56
251 // https://www.w3.org/TR/html-aam-1.0/#details-id-67
252 if ('indeterminate' in element && element.indeterminate) {
253 return undefined;
254 }
255 if ('checked' in element) {
256 return element.checked;
257 }
258
259 // explicit value
260 return checkBooleanAttribute(element, 'aria-checked');
261}
262
263/**
264 * @param {Element} element -
265 * @returns {boolean | undefined} - false/true if (not)pressed, undefined if not press-able
266 */
267function computeAriaPressed(element) {
268 // https://www.w3.org/TR/wai-aria-1.1/#aria-pressed
269 return checkBooleanAttribute(element, 'aria-pressed');
270}
271
272/**
273 * @param {Element} element -
274 * @returns {boolean | string | null} -
275 */
276function computeAriaCurrent(element) {
277 // https://www.w3.org/TR/wai-aria-1.1/#aria-current
278 return checkBooleanAttribute(element, 'aria-current') ?? element.getAttribute('aria-current') ?? false;
279}
280
281/**
282 * @param {Element} element -
283 * @returns {boolean | undefined} - false/true if (not)expanded, undefined if not expand-able
284 */
285function computeAriaExpanded(element) {
286 // https://www.w3.org/TR/wai-aria-1.1/#aria-expanded
287 return checkBooleanAttribute(element, 'aria-expanded');
288}
289function checkBooleanAttribute(element, attribute) {
290 const attributeValue = element.getAttribute(attribute);
291 if (attributeValue === 'true') {
292 return true;
293 }
294 if (attributeValue === 'false') {
295 return false;
296 }
297 return undefined;
298}
299
300/**
301 * @param {Element} element -
302 * @returns {number | undefined} - number if implicit heading or aria-level present, otherwise undefined
303 */
304function computeHeadingLevel(element) {
305 // https://w3c.github.io/html-aam/#el-h1-h6
306 // https://w3c.github.io/html-aam/#el-h1-h6
307 const implicitHeadingLevels = {
308 H1: 1,
309 H2: 2,
310 H3: 3,
311 H4: 4,
312 H5: 5,
313 H6: 6
314 };
315 // explicit aria-level value
316 // https://www.w3.org/TR/wai-aria-1.2/#aria-level
317 const ariaLevelAttribute = element.getAttribute('aria-level') && Number(element.getAttribute('aria-level'));
318 return ariaLevelAttribute || implicitHeadingLevels[element.tagName];
319}
320
321/**
322 * @param {Element} element -
323 * @returns {number | undefined} -
324 */
325function computeAriaValueNow(element) {
326 const valueNow = element.getAttribute('aria-valuenow');
327 return valueNow === null ? undefined : +valueNow;
328}
329
330/**
331 * @param {Element} element -
332 * @returns {number | undefined} -
333 */
334function computeAriaValueMax(element) {
335 const valueMax = element.getAttribute('aria-valuemax');
336 return valueMax === null ? undefined : +valueMax;
337}
338
339/**
340 * @param {Element} element -
341 * @returns {number | undefined} -
342 */
343function computeAriaValueMin(element) {
344 const valueMin = element.getAttribute('aria-valuemin');
345 return valueMin === null ? undefined : +valueMin;
346}
347
348/**
349 * @param {Element} element -
350 * @returns {string | undefined} -
351 */
352function computeAriaValueText(element) {
353 const valueText = element.getAttribute('aria-valuetext');
354 return valueText === null ? undefined : valueText;
355}
\No newline at end of file