runline 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/plugins/shiftOcr/src/index.js +13 -0
  2. package/dist/plugins/shiftPages/src/pages.js +72 -15
  3. package/dist/plugins/shiftPages/src/shared.js +5 -0
  4. package/dist/plugins/shiftTranscription/src/index.js +1 -1
  5. package/dist/plugins/shiftTranscription/src/transcription.js +21 -5
  6. package/dist/plugins/shiftWork/src/issues.js +119 -33
  7. package/dist/plugins/steel/src/browser.js +11 -79
  8. package/dist/plugins/steel/src/index.js +2 -0
  9. package/dist/plugins/steel/src/page/bundle.generated.js +11 -0
  10. package/dist/plugins/steel/src/page/cdp.js +232 -0
  11. package/dist/plugins/steel/src/page/driver.js +286 -0
  12. package/dist/plugins/steel/src/page/entry.js +91 -0
  13. package/dist/plugins/steel/src/page/install.js +3 -0
  14. package/dist/plugins/steel/src/page/page-snapshot.js +330 -0
  15. package/dist/plugins/steel/src/page/tools.js +649 -0
  16. package/dist/plugins/steel/src/page/vendor/aria-snapshot/ariaSnapshot.js +411 -0
  17. package/dist/plugins/steel/src/page/vendor/aria-snapshot/ariaSnapshotDistiller.js +207 -0
  18. package/dist/plugins/steel/src/page/vendor/aria-snapshot/ariaTypes.js +62 -0
  19. package/dist/plugins/steel/src/page/vendor/aria-snapshot/cssTokenizer.js +949 -0
  20. package/dist/plugins/steel/src/page/vendor/aria-snapshot/domUtils.js +157 -0
  21. package/dist/plugins/steel/src/page/vendor/aria-snapshot/roleUtils.js +1193 -0
  22. package/dist/plugins/steel/src/page/vendor/aria-snapshot/stringUtils.js +194 -0
  23. package/dist/plugins/steel/src/page/vendor/aria-snapshot/yaml.js +80 -0
  24. package/dist/plugins/steel/src/page/wait-for.js +30 -0
  25. package/dist/plugins/steel/src/sessions.js +2 -7
  26. package/dist/plugins/steel/src/shared.js +12 -0
  27. package/package.json +1 -1
@@ -0,0 +1,1193 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import * as css from './cssTokenizer.js';
17
+ import { beginDOMCaches, closestCrossShadow, elementSafeTagName, enclosingShadowRootOrDocument, endDOMCaches, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils.js';
18
+ function hasExplicitAccessibleName(e) {
19
+ return e.hasAttribute('aria-label') || e.hasAttribute('aria-labelledby');
20
+ }
21
+ // https://www.w3.org/TR/wai-aria-practices/examples/landmarks/HTML5.html
22
+ const kAncestorPreventingLandmark = 'article:not([role]), aside:not([role]), main:not([role]), nav:not([role]), section:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]';
23
+ // https://www.w3.org/TR/wai-aria-1.2/#global_states
24
+ const kGlobalAriaAttributes = [
25
+ ['aria-atomic', undefined],
26
+ ['aria-busy', undefined],
27
+ ['aria-controls', undefined],
28
+ ['aria-current', undefined],
29
+ ['aria-describedby', undefined],
30
+ ['aria-details', undefined],
31
+ // Global use deprecated in ARIA 1.2
32
+ // ['aria-disabled', undefined],
33
+ ['aria-dropeffect', undefined],
34
+ // Global use deprecated in ARIA 1.2
35
+ // ['aria-errormessage', undefined],
36
+ ['aria-flowto', undefined],
37
+ ['aria-grabbed', undefined],
38
+ // Global use deprecated in ARIA 1.2
39
+ // ['aria-haspopup', undefined],
40
+ ['aria-hidden', undefined],
41
+ // Global use deprecated in ARIA 1.2
42
+ // ['aria-invalid', undefined],
43
+ ['aria-keyshortcuts', undefined],
44
+ ['aria-label', ['caption', 'code', 'deletion', 'emphasis', 'generic', 'insertion', 'paragraph', 'presentation', 'strong', 'subscript', 'superscript']],
45
+ ['aria-labelledby', ['caption', 'code', 'deletion', 'emphasis', 'generic', 'insertion', 'paragraph', 'presentation', 'strong', 'subscript', 'superscript']],
46
+ ['aria-live', undefined],
47
+ ['aria-owns', undefined],
48
+ ['aria-relevant', undefined],
49
+ ['aria-roledescription', ['generic']],
50
+ ];
51
+ function hasGlobalAriaAttribute(element, forRole) {
52
+ return kGlobalAriaAttributes.some(([attr, prohibited]) => {
53
+ return !prohibited?.includes(forRole || '') && element.hasAttribute(attr);
54
+ });
55
+ }
56
+ function hasTabIndex(element) {
57
+ return !Number.isNaN(Number(String(element.getAttribute('tabindex'))));
58
+ }
59
+ function isFocusable(element) {
60
+ // TODO:
61
+ // - "inert" attribute makes the whole substree not focusable
62
+ // - when dialog is open on the page - everything but the dialog is not focusable
63
+ return !isNativelyDisabled(element) && (isNativelyFocusable(element) || hasTabIndex(element));
64
+ }
65
+ function isNativelyFocusable(element) {
66
+ const tagName = elementSafeTagName(element);
67
+ if (['BUTTON', 'DETAILS', 'SELECT', 'TEXTAREA'].includes(tagName))
68
+ return true;
69
+ if (tagName === 'A' || tagName === 'AREA')
70
+ return element.hasAttribute('href');
71
+ if (tagName === 'INPUT')
72
+ return !element.hidden;
73
+ return false;
74
+ }
75
+ // https://w3c.github.io/html-aam/#html-element-role-mappings
76
+ // https://www.w3.org/TR/html-aria/#docconformance
77
+ const kImplicitRoleByTagName = {
78
+ 'A': (e) => {
79
+ return e.hasAttribute('href') ? 'link' : null;
80
+ },
81
+ 'AREA': (e) => {
82
+ return e.hasAttribute('href') ? 'link' : null;
83
+ },
84
+ 'ARTICLE': () => 'article',
85
+ 'ASIDE': () => 'complementary',
86
+ 'BLOCKQUOTE': () => 'blockquote',
87
+ 'BUTTON': () => 'button',
88
+ 'CAPTION': () => 'caption',
89
+ 'CODE': () => 'code',
90
+ 'DATALIST': () => 'listbox',
91
+ 'DD': () => 'definition',
92
+ 'DEL': () => 'deletion',
93
+ 'DETAILS': () => 'group',
94
+ 'DFN': () => 'term',
95
+ 'DIALOG': () => 'dialog',
96
+ 'DT': () => 'term',
97
+ 'EM': () => 'emphasis',
98
+ 'FIELDSET': () => 'group',
99
+ 'FIGURE': () => 'figure',
100
+ 'FOOTER': (e) => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : 'contentinfo',
101
+ 'FORM': (e) => hasExplicitAccessibleName(e) ? 'form' : null,
102
+ 'H1': () => 'heading',
103
+ 'H2': () => 'heading',
104
+ 'H3': () => 'heading',
105
+ 'H4': () => 'heading',
106
+ 'H5': () => 'heading',
107
+ 'H6': () => 'heading',
108
+ 'HEADER': (e) => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : 'banner',
109
+ 'HR': () => 'separator',
110
+ 'HTML': () => 'document',
111
+ 'IMG': (e) => (e.getAttribute('alt') === '') && !e.getAttribute('title') && !hasGlobalAriaAttribute(e) && !hasTabIndex(e) ? 'presentation' : 'img',
112
+ 'INPUT': (e) => {
113
+ const type = e.type.toLowerCase();
114
+ if (type === 'search')
115
+ return e.hasAttribute('list') ? 'combobox' : 'searchbox';
116
+ if (['email', 'tel', 'text', 'url', ''].includes(type)) {
117
+ // https://html.spec.whatwg.org/multipage/input.html#concept-input-list
118
+ const list = getIdRefs(e, e.getAttribute('list'))[0];
119
+ return (list && elementSafeTagName(list) === 'DATALIST') ? 'combobox' : 'textbox';
120
+ }
121
+ if (type === 'hidden')
122
+ return null;
123
+ // File inputs do not have a role by the spec: https://www.w3.org/TR/html-aam-1.0/#el-input-file.
124
+ // However, there are open issues about fixing it: https://github.com/w3c/aria/issues/1926.
125
+ // All browsers report it as a button, and it is rendered as a button, so we do "button".
126
+ if (type === 'file')
127
+ return 'button';
128
+ return inputTypeToRole[type] || 'textbox';
129
+ },
130
+ 'INS': () => 'insertion',
131
+ 'LI': () => 'listitem',
132
+ 'MAIN': () => 'main',
133
+ 'MARK': () => 'mark',
134
+ 'MATH': () => 'math',
135
+ 'MENU': () => 'list',
136
+ 'METER': () => 'meter',
137
+ 'NAV': () => 'navigation',
138
+ 'OL': () => 'list',
139
+ 'OPTGROUP': () => 'group',
140
+ 'OPTION': () => 'option',
141
+ 'OUTPUT': () => 'status',
142
+ 'P': () => 'paragraph',
143
+ 'PROGRESS': () => 'progressbar',
144
+ 'SEARCH': () => 'search',
145
+ 'SECTION': (e) => hasExplicitAccessibleName(e) ? 'region' : null,
146
+ 'SELECT': (e) => e.hasAttribute('multiple') || e.size > 1 ? 'listbox' : 'combobox',
147
+ 'STRONG': () => 'strong',
148
+ 'SUB': () => 'subscript',
149
+ 'SUP': () => 'superscript',
150
+ // For <svg> we default to Chrome behavior:
151
+ // - Chrome reports 'img'.
152
+ // - Firefox reports 'diagram' that is not in official ARIA spec yet.
153
+ // - Safari reports 'no role', but still computes accessible name.
154
+ 'SVG': () => 'img',
155
+ 'TABLE': () => 'table',
156
+ 'TBODY': () => 'rowgroup',
157
+ 'TD': (e) => {
158
+ const table = closestCrossShadow(e, 'table');
159
+ const role = table ? getExplicitAriaRole(table) : '';
160
+ return (role === 'grid' || role === 'treegrid') ? 'gridcell' : 'cell';
161
+ },
162
+ 'TEXTAREA': () => 'textbox',
163
+ 'TFOOT': () => 'rowgroup',
164
+ 'TH': (e) => {
165
+ const scope = e.getAttribute('scope');
166
+ if (scope === 'col' || scope === 'colgroup')
167
+ return 'columnheader';
168
+ if (scope === 'row' || scope === 'rowgroup')
169
+ return 'rowheader';
170
+ const nextSibling = e.nextElementSibling;
171
+ const prevSibling = e.previousElementSibling;
172
+ const row = !!e.parentElement && elementSafeTagName(e.parentElement) === 'TR' ? e.parentElement : undefined;
173
+ // Chromium/Safari: A TH that is the only cell in a table is not labeling any content, thus it's technically not a header. Do not assign a role.
174
+ // Firefox: Follows the spec and assigns `columnheader`. We prioritize Chrome/Safari semantics.
175
+ if (!nextSibling && !prevSibling) {
176
+ if (row) {
177
+ const table = closestCrossShadow(row, 'table');
178
+ // If there's only one row in the table, this TH has no column to head
179
+ if (table && table.rows.length <= 1)
180
+ return null;
181
+ }
182
+ return 'columnheader';
183
+ }
184
+ // Tables are built up incrementally by iterating over them in a particular pattern. In order to emulate this,
185
+ // we check only immediate siblings and occasionally the parent row
186
+ // This doesn't seem to directly follow the spec, but matches Chromium behavior
187
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/accessibility/ax_node_object.cc;l=1585-1623
188
+ if (isHeaderCell(nextSibling) && isHeaderCell(prevSibling))
189
+ return 'columnheader';
190
+ if (isNonEmptyDataCell(nextSibling) || isNonEmptyDataCell(prevSibling))
191
+ return 'rowheader';
192
+ // As long as we didn't exclude it above, it's still a TH, so default to columnheader
193
+ return 'columnheader';
194
+ },
195
+ 'THEAD': () => 'rowgroup',
196
+ 'TIME': () => 'time',
197
+ 'TR': () => 'row',
198
+ 'UL': () => 'list',
199
+ };
200
+ function isHeaderCell(element) {
201
+ return !!element && elementSafeTagName(element) === 'TH';
202
+ }
203
+ function isNonEmptyDataCell(element) {
204
+ if (!element || elementSafeTagName(element) !== 'TD')
205
+ return false;
206
+ return !!(element.textContent?.trim() || element.children.length > 0);
207
+ }
208
+ const kPresentationInheritanceParents = {
209
+ 'DD': ['DL', 'DIV'],
210
+ 'DIV': ['DL'],
211
+ 'DT': ['DL', 'DIV'],
212
+ 'LI': ['OL', 'UL'],
213
+ 'TBODY': ['TABLE'],
214
+ 'TD': ['TR'],
215
+ 'TFOOT': ['TABLE'],
216
+ 'TH': ['TR'],
217
+ 'THEAD': ['TABLE'],
218
+ 'TR': ['THEAD', 'TBODY', 'TFOOT', 'TABLE'],
219
+ };
220
+ function getImplicitAriaRole(element) {
221
+ const implicitRole = kImplicitRoleByTagName[elementSafeTagName(element)]?.(element) || '';
222
+ if (!implicitRole)
223
+ return null;
224
+ // Inherit presentation role when required.
225
+ // https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none
226
+ let ancestor = element;
227
+ while (ancestor) {
228
+ const parent = parentElementOrShadowHost(ancestor);
229
+ const parents = kPresentationInheritanceParents[elementSafeTagName(ancestor)];
230
+ if (!parents || !parent || !parents.includes(elementSafeTagName(parent)))
231
+ break;
232
+ const parentExplicitRole = getExplicitAriaRole(parent);
233
+ if ((parentExplicitRole === 'none' || parentExplicitRole === 'presentation') && !hasPresentationConflictResolution(parent, parentExplicitRole))
234
+ return parentExplicitRole;
235
+ ancestor = parent;
236
+ }
237
+ return implicitRole;
238
+ }
239
+ const validRoles = ['alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox',
240
+ 'complementary', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid',
241
+ 'gridcell', 'group', 'heading', 'img', 'insertion', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'mark', 'marquee', 'math', 'meter', 'menu',
242
+ 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'navigation', 'none', 'note', 'option', 'paragraph', 'presentation', 'progressbar', 'radio', 'radiogroup',
243
+ 'region', 'row', 'rowgroup', 'rowheader', 'scrollbar', 'search', 'searchbox', 'separator', 'slider',
244
+ 'spinbutton', 'status', 'strong', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer',
245
+ 'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem'];
246
+ function getExplicitAriaRole(element) {
247
+ // https://www.w3.org/TR/wai-aria-1.2/#document-handling_author-errors_roles
248
+ const roles = (element.getAttribute('role') || '').split(' ').map(role => role.trim());
249
+ return roles.find(role => validRoles.includes(role)) || null;
250
+ }
251
+ function hasPresentationConflictResolution(element, role) {
252
+ // https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none
253
+ return hasGlobalAriaAttribute(element, role) || isFocusable(element);
254
+ }
255
+ export function getAriaRole(element) {
256
+ const explicitRole = getExplicitAriaRole(element);
257
+ if (!explicitRole)
258
+ return getImplicitAriaRole(element);
259
+ if (explicitRole === 'none' || explicitRole === 'presentation') {
260
+ const implicitRole = getImplicitAriaRole(element);
261
+ if (hasPresentationConflictResolution(element, implicitRole))
262
+ return implicitRole;
263
+ }
264
+ return explicitRole;
265
+ }
266
+ function getAriaBoolean(attr) {
267
+ return attr === null ? undefined : attr.toLowerCase() === 'true';
268
+ }
269
+ export function isElementIgnoredForAria(element) {
270
+ return ['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element));
271
+ }
272
+ // https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion, but including "none" and "presentation" roles
273
+ // Not implemented:
274
+ // `Any descendants of elements that have the characteristic "Children Presentational: True"`
275
+ // https://www.w3.org/TR/wai-aria-1.2/#aria-hidden
276
+ export function isElementHiddenForAria(element) {
277
+ if (isElementIgnoredForAria(element))
278
+ return true;
279
+ const style = getElementComputedStyle(element);
280
+ const isSlot = element.nodeName === 'SLOT';
281
+ if (style?.display === 'contents' && !isSlot) {
282
+ // display:contents is not rendered itself, but its child nodes are.
283
+ for (let child = element.firstChild; child; child = child.nextSibling) {
284
+ if (child.nodeType === 1 /* Node.ELEMENT_NODE */ && !isElementHiddenForAria(child))
285
+ return false;
286
+ if (child.nodeType === 3 /* Node.TEXT_NODE */ && isVisibleTextNode(child))
287
+ return false;
288
+ }
289
+ return true;
290
+ }
291
+ // Note: <option> inside <select> are not affected by visibility or content-visibility.
292
+ // Same goes for <slot>.
293
+ const isOptionInsideSelect = element.nodeName === 'OPTION' && !!element.closest('select');
294
+ if (!isOptionInsideSelect && !isSlot && !isElementStyleVisibilityVisible(element, style))
295
+ return true;
296
+ return belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element);
297
+ }
298
+ function belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element) {
299
+ let hidden = cacheIsHidden?.get(element);
300
+ if (hidden === undefined) {
301
+ hidden = false;
302
+ // When parent has a shadow root, all light dom children must be assigned to a slot,
303
+ // otherwise they are not rendered and considered hidden for aria.
304
+ // Note: we can remove this logic once WebKit supports `Element.checkVisibility`.
305
+ if (element.parentElement && element.parentElement.shadowRoot && !element.assignedSlot)
306
+ hidden = true;
307
+ // display:none and aria-hidden=true are considered hidden for aria.
308
+ if (!hidden) {
309
+ const style = getElementComputedStyle(element);
310
+ hidden = !style || style.display === 'none' || getAriaBoolean(element.getAttribute('aria-hidden')) === true;
311
+ }
312
+ // Check recursively.
313
+ if (!hidden) {
314
+ const parent = parentElementOrShadowHost(element);
315
+ if (parent)
316
+ hidden = belongsToDisplayNoneOrAriaHiddenOrNonSlotted(parent);
317
+ }
318
+ cacheIsHidden?.set(element, hidden);
319
+ }
320
+ return hidden;
321
+ }
322
+ function getIdRefs(element, ref) {
323
+ if (!ref)
324
+ return [];
325
+ const root = enclosingShadowRootOrDocument(element);
326
+ if (!root)
327
+ return [];
328
+ try {
329
+ const ids = ref.split(' ').filter(id => !!id);
330
+ const result = [];
331
+ for (const id of ids) {
332
+ // https://www.w3.org/TR/wai-aria-1.2/#mapping_additional_relations_error_processing
333
+ // "If more than one element has the same ID, the user agent SHOULD use the first element found with the given ID"
334
+ const firstElement = root.querySelector('#' + CSS.escape(id));
335
+ if (firstElement && !result.includes(firstElement))
336
+ result.push(firstElement);
337
+ }
338
+ return result;
339
+ }
340
+ catch (e) {
341
+ return [];
342
+ }
343
+ }
344
+ function trimFlatString(s) {
345
+ // "Flat string" at https://w3c.github.io/accname/#terminology
346
+ return s.trim();
347
+ }
348
+ function asFlatString(s) {
349
+ // "Flat string" at https://w3c.github.io/accname/#terminology
350
+ // Note that non-breaking spaces are preserved.
351
+ return s.split('\u00A0').map(chunk => chunk.replace(/\r\n/g, '\n').replace(/[\u200b\u00ad]/g, '').replace(/\s\s*/g, ' ')).join('\u00A0').trim();
352
+ }
353
+ function queryInAriaOwned(element, selector) {
354
+ const result = [...element.querySelectorAll(selector)];
355
+ for (const owned of getIdRefs(element, element.getAttribute('aria-owns'))) {
356
+ if (owned.matches(selector))
357
+ result.push(owned);
358
+ result.push(...owned.querySelectorAll(selector));
359
+ }
360
+ return result;
361
+ }
362
+ export function getCSSContent(element, pseudo) {
363
+ // Relevant spec: 2.6.2 from https://w3c.github.io/accname/#computation-steps.
364
+ // Additional considerations: https://github.com/w3c/accname/issues/204.
365
+ const cache = pseudo === '::before' ? cachePseudoContentBefore : (pseudo === '::after' ? cachePseudoContentAfter : cachePseudoContent);
366
+ if (cache?.has(element))
367
+ return cache?.get(element);
368
+ const style = getElementComputedStyle(element, pseudo);
369
+ let content;
370
+ if (style) {
371
+ const contentValue = style.content;
372
+ if (contentValue && contentValue !== 'none' && contentValue !== 'normal') {
373
+ if (style.display !== 'none' && style.visibility !== 'hidden') {
374
+ // Note: all browsers ignore display:none and visibility:hidden pseudos.
375
+ content = parseCSSContentPropertyAsString(element, contentValue, !!pseudo);
376
+ }
377
+ }
378
+ }
379
+ if (pseudo && content !== undefined) {
380
+ // SPEC DIFFERENCE.
381
+ // Spec says "CSS textual content, without a space", but we account for display
382
+ // to pass "name_file-label-inline-block-styles-manual.html"
383
+ const display = style?.display || 'inline';
384
+ if (display !== 'inline')
385
+ content = ' ' + content + ' ';
386
+ }
387
+ if (cache)
388
+ cache.set(element, content);
389
+ return content;
390
+ }
391
+ function parseCSSContentPropertyAsString(element, content, isPseudo) {
392
+ // Welcome to the mini CSS parser!
393
+ // It aims to support the following syntax and any subset of it:
394
+ // content: "one" attr(...) "two" "three" / "alt" attr(...) "more alt"
395
+ // See https://developer.mozilla.org/en-US/docs/Web/CSS/content for more details.
396
+ if (!content || content === 'none' || content === 'normal') {
397
+ // Common fast path.
398
+ return;
399
+ }
400
+ try {
401
+ let tokens = css.tokenize(content).filter(token => !(token instanceof css.WhitespaceToken));
402
+ const delimIndex = tokens.findIndex(token => token instanceof css.DelimToken && token.value === '/');
403
+ if (delimIndex !== -1) {
404
+ // Use the alternative text part when exists.
405
+ // content: ... / "alternative text"
406
+ tokens = tokens.slice(delimIndex + 1);
407
+ }
408
+ else if (!isPseudo) {
409
+ // For non-pseudo elements, the only valid content is a url() or various gradients.
410
+ // Therefore, we follow Chrome and only consider the alternative text.
411
+ // Firefox, on the other hand, calculates accessible name to be empty.
412
+ return;
413
+ }
414
+ const accumulated = [];
415
+ let index = 0;
416
+ while (index < tokens.length) {
417
+ if (tokens[index] instanceof css.StringToken) {
418
+ // content: "some text"
419
+ accumulated.push(tokens[index].value);
420
+ index++;
421
+ }
422
+ else if (index + 2 < tokens.length && tokens[index] instanceof css.FunctionToken && tokens[index].value === 'attr' && tokens[index + 1] instanceof css.IdentToken && tokens[index + 2] instanceof css.CloseParenToken) {
423
+ // content: attr(...)
424
+ // Firefox does not resolve attribute accessors in content, so we do it manually.
425
+ const attrName = tokens[index + 1].value;
426
+ accumulated.push(element.getAttribute(attrName) || '');
427
+ index += 3;
428
+ }
429
+ else {
430
+ // Failed to parse the content, so ignore it.
431
+ return;
432
+ }
433
+ }
434
+ return accumulated.join('');
435
+ }
436
+ catch {
437
+ }
438
+ }
439
+ export function getAriaLabelledByElements(element) {
440
+ const ref = element.getAttribute('aria-labelledby');
441
+ if (ref === null)
442
+ return null;
443
+ const refs = getIdRefs(element, ref);
444
+ // step 2b:
445
+ // "if the current node has an aria-labelledby attribute that contains at least one valid IDREF"
446
+ // Therefore, if none of the refs match an element, we consider aria-labelledby to be missing.
447
+ return refs.length ? refs : null;
448
+ }
449
+ function allowsNameFromContent(role, targetDescendant) {
450
+ // SPEC: https://w3c.github.io/aria/#namefromcontent
451
+ //
452
+ // Note: there is a spec proposal https://github.com/w3c/aria/issues/1821 that
453
+ // is roughly aligned with what Chrome/Firefox do, and we follow that.
454
+ //
455
+ // See chromium implementation here:
456
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/accessibility/ax_object.cc;l=6338;drc=3decef66bc4c08b142a19db9628e9efe68973e64;bpv=0;bpt=1
457
+ const alwaysAllowsNameFromContent = ['button', 'cell', 'checkbox', 'columnheader', 'gridcell', 'heading', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'row', 'rowheader', 'switch', 'tab', 'tooltip', 'treeitem'].includes(role);
458
+ const descendantAllowsNameFromContent = targetDescendant && ['', 'caption', 'code', 'contentinfo', 'definition', 'deletion', 'emphasis', 'insertion', 'list', 'listitem', 'mark', 'none', 'paragraph', 'presentation', 'region', 'row', 'rowgroup', 'section', 'strong', 'subscript', 'superscript', 'table', 'term', 'time'].includes(role);
459
+ return alwaysAllowsNameFromContent || descendantAllowsNameFromContent;
460
+ }
461
+ function computeAccessibleNameComposite(element, includeHidden, collectElements) {
462
+ // https://w3c.github.io/accname/#computation-steps
463
+ // step 1.
464
+ // https://w3c.github.io/aria/#namefromprohibited
465
+ const elementProhibitsNaming = ['caption', 'code', 'definition', 'deletion', 'emphasis', 'generic', 'insertion', 'mark', 'paragraph', 'presentation', 'strong', 'subscript', 'suggestion', 'superscript', 'term', 'time'].includes(getAriaRole(element) || '');
466
+ if (elementProhibitsNaming)
467
+ return emptyCompositeString();
468
+ // step 2.
469
+ const result = getTextAlternativeInternal(element, {
470
+ includeHidden,
471
+ collectElements,
472
+ visitedElements: new Set(),
473
+ embeddedInTargetElement: 'self',
474
+ });
475
+ return { text: asFlatString(result.text), elements: result.elements };
476
+ }
477
+ export function getElementAccessibleName(element, includeHidden) {
478
+ const cache = (includeHidden ? cacheAccessibleNameHidden : cacheAccessibleName);
479
+ let accessibleName = cache?.get(element);
480
+ if (accessibleName === undefined) {
481
+ accessibleName = computeAccessibleNameComposite(element, includeHidden, true /* collectElements */);
482
+ cache?.set(element, accessibleName);
483
+ }
484
+ return accessibleName;
485
+ }
486
+ export function getElementAccessibleNameText(element, includeHidden) {
487
+ // Reuse the element-collecting composite if it happens to be cached, otherwise compute text only.
488
+ const composite = (includeHidden ? cacheAccessibleNameHidden : cacheAccessibleName)?.get(element);
489
+ if (composite !== undefined)
490
+ return composite.text;
491
+ const cache = (includeHidden ? cacheAccessibleNameTextHidden : cacheAccessibleNameText);
492
+ let text = cache?.get(element);
493
+ if (text === undefined) {
494
+ text = computeAccessibleNameComposite(element, includeHidden, false /* collectElements */).text;
495
+ cache?.set(element, text);
496
+ }
497
+ return text;
498
+ }
499
+ export function getElementAccessibleDescription(element, includeHidden) {
500
+ const cache = (includeHidden ? cacheAccessibleDescriptionHidden : cacheAccessibleDescription);
501
+ let accessibleDescription = cache?.get(element);
502
+ if (accessibleDescription === undefined) {
503
+ // https://w3c.github.io/accname/#mapping_additional_nd_description
504
+ // https://www.w3.org/TR/html-aam-1.0/#accdesc-computation
505
+ accessibleDescription = '';
506
+ if (element.hasAttribute('aria-describedby')) {
507
+ // precedence 1
508
+ const describedBy = getIdRefs(element, element.getAttribute('aria-describedby'));
509
+ accessibleDescription = asFlatString(describedBy.map(ref => getTextAlternativeInternal(ref, {
510
+ includeHidden,
511
+ visitedElements: new Set(),
512
+ embeddedInDescribedBy: { element: ref, hidden: isElementHiddenForAria(ref) },
513
+ }).text).join(' '));
514
+ }
515
+ else if (element.hasAttribute('aria-description')) {
516
+ // precedence 2
517
+ accessibleDescription = asFlatString(element.getAttribute('aria-description') || '');
518
+ }
519
+ else {
520
+ // TODO: handle precedence 3 - html-aam-specific cases like table>caption.
521
+ // https://www.w3.org/TR/html-aam-1.0/#accdesc-computation
522
+ // precedence 4
523
+ accessibleDescription = asFlatString(element.getAttribute('title') || '');
524
+ }
525
+ cache?.set(element, accessibleDescription);
526
+ }
527
+ return accessibleDescription;
528
+ }
529
+ // Roles where `aria-invalid` is conceptually meaningful per WAI-ARIA 1.2.
530
+ // See https://www.w3.org/TR/wai-aria-1.2/#aria-invalid for the supported list.
531
+ export const kAriaInvalidRoles = [
532
+ 'application',
533
+ 'checkbox',
534
+ 'columnheader',
535
+ 'combobox',
536
+ 'gridcell',
537
+ 'listbox',
538
+ 'radiogroup',
539
+ 'rowheader',
540
+ 'searchbox',
541
+ 'slider',
542
+ 'spinbutton',
543
+ 'switch',
544
+ 'textbox',
545
+ 'tree',
546
+ ];
547
+ export function getAriaInvalid(element) {
548
+ // https://www.w3.org/TR/wai-aria-1.2/#aria-invalid
549
+ // This state is being deprecated as a global state in ARIA 1.2.
550
+ // In future versions it will only be allowed on roles where it is specifically supported.
551
+ const ariaInvalid = element.getAttribute('aria-invalid');
552
+ if (!ariaInvalid || ariaInvalid.trim() === '' || ariaInvalid.toLocaleLowerCase() === 'false')
553
+ return 'false';
554
+ if (ariaInvalid === 'true' || ariaInvalid === 'grammar' || ariaInvalid === 'spelling')
555
+ return ariaInvalid;
556
+ return 'true';
557
+ }
558
+ function getValidityInvalid(element) {
559
+ if ('validity' in element) {
560
+ const validity = element.validity;
561
+ return validity?.valid === false;
562
+ }
563
+ return false;
564
+ }
565
+ export function getElementAccessibleErrorMessage(element) {
566
+ // SPEC: https://w3c.github.io/aria/#aria-errormessage
567
+ //
568
+ // TODO: support https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/validationMessage
569
+ const cache = cacheAccessibleErrorMessage;
570
+ let accessibleErrorMessage = cacheAccessibleErrorMessage?.get(element);
571
+ if (accessibleErrorMessage === undefined) {
572
+ accessibleErrorMessage = '';
573
+ const isAriaInvalid = getAriaInvalid(element) !== 'false';
574
+ const isValidityInvalid = getValidityInvalid(element);
575
+ if (isAriaInvalid || isValidityInvalid) {
576
+ const errorMessageId = element.getAttribute('aria-errormessage');
577
+ const errorMessages = getIdRefs(element, errorMessageId);
578
+ // Ideally, this should be a separate "embeddedInErrorMessage", but it would follow the exact same rules.
579
+ // Relevant vague spec: https://w3c.github.io/core-aam/#ariaErrorMessage.
580
+ const parts = errorMessages.map(errorMessage => asFlatString(getTextAlternativeInternal(errorMessage, {
581
+ visitedElements: new Set(),
582
+ embeddedInDescribedBy: { element: errorMessage, hidden: isElementHiddenForAria(errorMessage) },
583
+ }).text));
584
+ accessibleErrorMessage = parts.join(' ').trim();
585
+ }
586
+ cache?.set(element, accessibleErrorMessage);
587
+ }
588
+ return accessibleErrorMessage;
589
+ }
590
+ function getTextAlternativeInternal(element, options) {
591
+ if (options.visitedElements.has(element))
592
+ return emptyCompositeString();
593
+ const childOptions = {
594
+ ...options,
595
+ embeddedInTargetElement: options.embeddedInTargetElement === 'self' ? 'descendant' : options.embeddedInTargetElement,
596
+ };
597
+ // step 2a. Hidden Not Referenced: If the current node is hidden and is:
598
+ // Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that relation was hidden.
599
+ // Nor part of a native host language text alternative element (e.g. label in HTML) or attribute traversal, where the root of that traversal was hidden.
600
+ if (!options.includeHidden) {
601
+ const isEmbeddedInHiddenReferenceTraversal = !!options.embeddedInLabelledBy?.hidden ||
602
+ !!options.embeddedInDescribedBy?.hidden ||
603
+ !!options.embeddedInNativeTextAlternative?.hidden ||
604
+ !!options.embeddedInLabel?.hidden;
605
+ if (isElementIgnoredForAria(element) ||
606
+ (!isEmbeddedInHiddenReferenceTraversal && isElementHiddenForAria(element))) {
607
+ options.visitedElements.add(element);
608
+ return emptyCompositeString();
609
+ }
610
+ }
611
+ const labelledBy = getAriaLabelledByElements(element);
612
+ // step 2b. LabelledBy:
613
+ // Otherwise, if the current node has an aria-labelledby attribute that contains
614
+ // at least one valid IDREF, and the current node is not already part of an ongoing
615
+ // aria-labelledby or aria-describedby traversal, process its IDREFs in the order they occur...
616
+ if (!options.embeddedInLabelledBy) {
617
+ const accessibleName = joinCompositeString((labelledBy || []).map(ref => getTextAlternativeInternal(ref, {
618
+ ...options,
619
+ embeddedInLabelledBy: { element: ref, hidden: isElementHiddenForAria(ref) },
620
+ embeddedInDescribedBy: undefined,
621
+ embeddedInTargetElement: undefined,
622
+ embeddedInLabel: undefined,
623
+ embeddedInNativeTextAlternative: undefined,
624
+ })), ' ', options.collectElements);
625
+ if (accessibleName.text)
626
+ return accessibleName;
627
+ }
628
+ const role = getAriaRole(element) || '';
629
+ const tagName = elementSafeTagName(element);
630
+ // step 2c:
631
+ // if the current node is a control embedded within the label (e.g. any element directly referenced by aria-labelledby) for another widget...
632
+ //
633
+ // also step 2d "skip to rule Embedded Control" section:
634
+ // If traversal of the current node is due to recursion and the current node is an embedded control...
635
+ // Note this is not strictly by the spec, because spec only applies this logic when "aria-label" is present.
636
+ // However, browsers and and wpt test name_heading-combobox-focusable-alternative-manual.html follow this behavior,
637
+ // and there is an issue filed for this: https://github.com/w3c/accname/issues/64
638
+ if (!!options.embeddedInLabel || !!options.embeddedInLabelledBy || options.embeddedInTargetElement === 'descendant') {
639
+ const isOwnLabel = [...element.labels || []].includes(element);
640
+ const isOwnLabelledBy = (labelledBy || []).includes(element);
641
+ if (!isOwnLabel && !isOwnLabelledBy) {
642
+ if (role === 'textbox') {
643
+ options.visitedElements.add(element);
644
+ if (tagName === 'INPUT' || tagName === 'TEXTAREA')
645
+ return compositeString(element.value, element, options.collectElements);
646
+ return compositeString(element.textContent, element, options.collectElements);
647
+ }
648
+ if (['combobox', 'listbox'].includes(role)) {
649
+ options.visitedElements.add(element);
650
+ let selectedOptions;
651
+ if (tagName === 'SELECT') {
652
+ selectedOptions = [...element.selectedOptions];
653
+ if (!selectedOptions.length && element.options.length)
654
+ selectedOptions.push(element.options[0]);
655
+ }
656
+ else {
657
+ const listbox = role === 'combobox' ? queryInAriaOwned(element, '*').find(e => getAriaRole(e) === 'listbox') : element;
658
+ selectedOptions = listbox ? queryInAriaOwned(listbox, '[aria-selected="true"]').filter(e => getAriaRole(e) === 'option') : [];
659
+ }
660
+ if (!selectedOptions.length && tagName === 'INPUT') {
661
+ // SPEC DIFFERENCE:
662
+ // This fallback is not explicitly mentioned in the spec, but all browsers and
663
+ // wpt test name_heading-combobox-focusable-alternative-manual.html do this.
664
+ return compositeString(element.value, element, options.collectElements);
665
+ }
666
+ return joinCompositeString(selectedOptions.map(option => getTextAlternativeInternal(option, childOptions)), ' ', options.collectElements);
667
+ }
668
+ if (['progressbar', 'scrollbar', 'slider', 'spinbutton', 'meter'].includes(role)) {
669
+ options.visitedElements.add(element);
670
+ if (element.hasAttribute('aria-valuetext'))
671
+ return compositeString(element.getAttribute('aria-valuetext'), element, options.collectElements);
672
+ if (element.hasAttribute('aria-valuenow'))
673
+ return compositeString(element.getAttribute('aria-valuenow'), element, options.collectElements);
674
+ return compositeString(element.getAttribute('value'), element, options.collectElements);
675
+ }
676
+ if (['menu'].includes(role)) {
677
+ // https://github.com/w3c/accname/issues/67#issuecomment-553196887
678
+ options.visitedElements.add(element);
679
+ return emptyCompositeString();
680
+ }
681
+ }
682
+ }
683
+ // step 2d.
684
+ const ariaLabel = element.getAttribute('aria-label') || '';
685
+ if (trimFlatString(ariaLabel)) {
686
+ options.visitedElements.add(element);
687
+ return compositeString(ariaLabel, element, options.collectElements);
688
+ }
689
+ // step 2e.
690
+ if (!['presentation', 'none'].includes(role)) {
691
+ // https://w3c.github.io/html-aam/#input-type-button-input-type-submit-and-input-type-reset-accessible-name-computation
692
+ //
693
+ // SPEC DIFFERENCE.
694
+ // Spec says to ignore this when aria-labelledby is defined.
695
+ // WebKit follows the spec, while Chromium and Firefox do not.
696
+ // We align with Chromium and Firefox here.
697
+ if (tagName === 'INPUT' && ['button', 'submit', 'reset'].includes(element.type)) {
698
+ options.visitedElements.add(element);
699
+ const value = element.value || '';
700
+ if (trimFlatString(value))
701
+ return compositeString(value, element, options.collectElements);
702
+ if (element.type === 'submit')
703
+ return compositeString('Submit', element, options.collectElements);
704
+ if (element.type === 'reset')
705
+ return compositeString('Reset', element, options.collectElements);
706
+ const title = element.getAttribute('title') || '';
707
+ return compositeString(title, element, options.collectElements);
708
+ }
709
+ // SPEC DIFFERENCE.
710
+ // There is no spec for this, but Chromium/WebKit do "Choose File" so we follow that.
711
+ // All browsers respect labels, aria-labelledby and aria-label.
712
+ // No browsers respect the title attribute, although w3c accname tests disagree. We follow browsers.
713
+ if (tagName === 'INPUT' && element.type === 'file') {
714
+ options.visitedElements.add(element);
715
+ const labels = element.labels || [];
716
+ if (labels.length && !options.embeddedInLabelledBy)
717
+ return getAccessibleNameFromAssociatedLabels(labels, options);
718
+ return compositeString('Choose File', element, options.collectElements);
719
+ }
720
+ // https://w3c.github.io/html-aam/#input-type-image-accessible-name-computation
721
+ //
722
+ // SPEC DIFFERENCE.
723
+ // Spec says to ignore this when aria-labelledby is defined, but all browsers take it into account.
724
+ if (tagName === 'INPUT' && element.type === 'image') {
725
+ options.visitedElements.add(element);
726
+ const labels = element.labels || [];
727
+ if (labels.length && !options.embeddedInLabelledBy)
728
+ return getAccessibleNameFromAssociatedLabels(labels, options);
729
+ const alt = element.getAttribute('alt') || '';
730
+ if (trimFlatString(alt))
731
+ return compositeString(alt, element, options.collectElements);
732
+ const title = element.getAttribute('title') || '';
733
+ if (trimFlatString(title))
734
+ return compositeString(title, element, options.collectElements);
735
+ // SPEC DIFFERENCE.
736
+ // Spec says return localized "Submit Query", but browsers and axe-core insist on "Submit".
737
+ return compositeString('Submit', element, options.collectElements);
738
+ }
739
+ // https://w3c.github.io/html-aam/#button-element-accessible-name-computation
740
+ if (!labelledBy && tagName === 'BUTTON') {
741
+ options.visitedElements.add(element);
742
+ const labels = element.labels || [];
743
+ if (labels.length)
744
+ return getAccessibleNameFromAssociatedLabels(labels, options);
745
+ // From here, fallthrough to step 2f.
746
+ }
747
+ // https://w3c.github.io/html-aam/#output-element-accessible-name-computation
748
+ if (!labelledBy && tagName === 'OUTPUT') {
749
+ options.visitedElements.add(element);
750
+ const labels = element.labels || [];
751
+ if (labels.length)
752
+ return getAccessibleNameFromAssociatedLabels(labels, options);
753
+ return compositeString(element.getAttribute('title') || '', element, options.collectElements);
754
+ }
755
+ // https://w3c.github.io/html-aam/#input-type-text-input-type-password-input-type-number-input-type-search-input-type-tel-input-type-email-input-type-url-and-textarea-element-accessible-name-computation
756
+ // https://w3c.github.io/html-aam/#other-form-elements-accessible-name-computation
757
+ // For "other form elements", we count select and any other input.
758
+ //
759
+ // Note: WebKit does not follow the spec and uses placeholder when aria-labelledby is present.
760
+ if (!labelledBy && (tagName === 'TEXTAREA' || tagName === 'SELECT' || tagName === 'INPUT')) {
761
+ options.visitedElements.add(element);
762
+ const labels = element.labels || [];
763
+ if (labels.length)
764
+ return getAccessibleNameFromAssociatedLabels(labels, options);
765
+ const usePlaceholder = (tagName === 'INPUT' && ['text', 'password', 'number', 'search', 'tel', 'email', 'url'].includes(element.type)) || tagName === 'TEXTAREA';
766
+ const placeholder = element.getAttribute('placeholder') || '';
767
+ const title = element.getAttribute('title') || '';
768
+ if (!usePlaceholder || title)
769
+ return compositeString(title, element, options.collectElements);
770
+ return compositeString(placeholder, element, options.collectElements);
771
+ }
772
+ // https://w3c.github.io/html-aam/#fieldset-and-legend-elements
773
+ if (!labelledBy && tagName === 'FIELDSET') {
774
+ options.visitedElements.add(element);
775
+ for (let child = element.firstElementChild; child; child = child.nextElementSibling) {
776
+ if (elementSafeTagName(child) === 'LEGEND') {
777
+ return getTextAlternativeInternal(child, {
778
+ ...childOptions,
779
+ embeddedInNativeTextAlternative: { element: child, hidden: isElementHiddenForAria(child) },
780
+ });
781
+ }
782
+ }
783
+ const title = element.getAttribute('title') || '';
784
+ return compositeString(title, element, options.collectElements);
785
+ }
786
+ // https://w3c.github.io/html-aam/#figure-and-figcaption-elements
787
+ if (!labelledBy && tagName === 'FIGURE') {
788
+ options.visitedElements.add(element);
789
+ for (let child = element.firstElementChild; child; child = child.nextElementSibling) {
790
+ if (elementSafeTagName(child) === 'FIGCAPTION') {
791
+ return getTextAlternativeInternal(child, {
792
+ ...childOptions,
793
+ embeddedInNativeTextAlternative: { element: child, hidden: isElementHiddenForAria(child) },
794
+ });
795
+ }
796
+ }
797
+ const title = element.getAttribute('title') || '';
798
+ return compositeString(title, element, options.collectElements);
799
+ }
800
+ // https://w3c.github.io/html-aam/#img-element
801
+ //
802
+ // SPEC DIFFERENCE.
803
+ // Spec says to ignore this when aria-labelledby is defined, but all browsers take it into account.
804
+ if (tagName === 'IMG') {
805
+ options.visitedElements.add(element);
806
+ const alt = element.getAttribute('alt') || '';
807
+ if (trimFlatString(alt))
808
+ return compositeString(alt, element, options.collectElements);
809
+ const title = element.getAttribute('title') || '';
810
+ return compositeString(title, element, options.collectElements);
811
+ }
812
+ // https://w3c.github.io/html-aam/#table-element
813
+ if (tagName === 'TABLE') {
814
+ options.visitedElements.add(element);
815
+ for (let child = element.firstElementChild; child; child = child.nextElementSibling) {
816
+ if (elementSafeTagName(child) === 'CAPTION') {
817
+ return getTextAlternativeInternal(child, {
818
+ ...childOptions,
819
+ embeddedInNativeTextAlternative: { element: child, hidden: isElementHiddenForAria(child) },
820
+ });
821
+ }
822
+ }
823
+ // SPEC DIFFERENCE.
824
+ // Spec does not say a word about <table summary="...">, but all browsers actually support it.
825
+ const summary = element.getAttribute('summary') || '';
826
+ if (summary)
827
+ return compositeString(summary, element, options.collectElements);
828
+ // SPEC DIFFERENCE.
829
+ // Spec says "if the table element has a title attribute, then use that attribute".
830
+ // We ignore title to pass "name_from_content-manual.html".
831
+ }
832
+ // https://w3c.github.io/html-aam/#area-element
833
+ if (tagName === 'AREA') {
834
+ options.visitedElements.add(element);
835
+ const alt = element.getAttribute('alt') || '';
836
+ if (trimFlatString(alt))
837
+ return compositeString(alt, element, options.collectElements);
838
+ const title = element.getAttribute('title') || '';
839
+ return compositeString(title, element, options.collectElements);
840
+ }
841
+ // https://www.w3.org/TR/svg-aam-1.0/#mapping_additional_nd
842
+ if (tagName === 'SVG' || element.ownerSVGElement) {
843
+ options.visitedElements.add(element);
844
+ for (let child = element.firstElementChild; child; child = child.nextElementSibling) {
845
+ if (elementSafeTagName(child) === 'TITLE' && child.ownerSVGElement) {
846
+ return getTextAlternativeInternal(child, {
847
+ ...childOptions,
848
+ embeddedInLabelledBy: { element: child, hidden: isElementHiddenForAria(child) },
849
+ });
850
+ }
851
+ }
852
+ }
853
+ if (element.ownerSVGElement && tagName === 'A') {
854
+ const title = element.getAttribute('xlink:title') || '';
855
+ if (trimFlatString(title)) {
856
+ options.visitedElements.add(element);
857
+ return compositeString(title, element, options.collectElements);
858
+ }
859
+ }
860
+ }
861
+ // See https://w3c.github.io/html-aam/#summary-element-accessible-name-computation for "summary"-specific check.
862
+ const shouldNameFromContentForSummary = tagName === 'SUMMARY' && !['presentation', 'none'].includes(role);
863
+ // step 2f + step 2h.
864
+ if (allowsNameFromContent(role, options.embeddedInTargetElement === 'descendant') ||
865
+ shouldNameFromContentForSummary ||
866
+ !!options.embeddedInLabelledBy || !!options.embeddedInDescribedBy ||
867
+ !!options.embeddedInLabel || !!options.embeddedInNativeTextAlternative) {
868
+ options.visitedElements.add(element);
869
+ const accessibleName = innerAccumulatedElementText(element, childOptions);
870
+ // Spec says "Return the accumulated text if it is not the empty string". However, that is not really
871
+ // compatible with the real browser behavior and wpt tests, where an element with empty contents will fallback to the title.
872
+ // So we follow the spec everywhere except for the target element itself. This can probably be improved.
873
+ const maybeTrimmedAccessibleName = options.embeddedInTargetElement === 'self' ? trimFlatString(accessibleName.text) : accessibleName.text;
874
+ if (maybeTrimmedAccessibleName) {
875
+ // This element owns the accumulated content - record it alongside the descendants it was computed from.
876
+ accessibleName.elements?.add(element);
877
+ return accessibleName;
878
+ }
879
+ }
880
+ // step 2i.
881
+ if (!['presentation', 'none'].includes(role) || tagName === 'IFRAME') {
882
+ options.visitedElements.add(element);
883
+ const title = element.getAttribute('title') || '';
884
+ if (trimFlatString(title))
885
+ return compositeString(title, element, options.collectElements);
886
+ }
887
+ options.visitedElements.add(element);
888
+ return emptyCompositeString();
889
+ }
890
+ function innerAccumulatedElementText(element, options) {
891
+ const tokens = [];
892
+ const elements = options.collectElements ? new Set() : undefined;
893
+ const visit = (node, skipSlotted) => {
894
+ if (skipSlotted && node.assignedSlot)
895
+ return;
896
+ if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {
897
+ const display = getElementComputedStyle(node)?.display || 'inline';
898
+ const childComposite = getTextAlternativeInternal(node, options);
899
+ let token = childComposite.text;
900
+ for (const contributor of childComposite.elements || [])
901
+ elements?.add(contributor);
902
+ // SPEC DIFFERENCE.
903
+ // Spec says "append the result to the accumulated text", assuming "with space".
904
+ // However, multiple tests insist that inline elements do not add a space.
905
+ // Additionally, <br> insists on a space anyway, see "name_file-label-inline-block-elements-manual.html"
906
+ if (display !== 'inline' || node.nodeName === 'BR')
907
+ token = ' ' + token + ' ';
908
+ tokens.push(token);
909
+ }
910
+ else if (node.nodeType === 3 /* Node.TEXT_NODE */) {
911
+ // step 2g.
912
+ tokens.push(node.textContent || '');
913
+ }
914
+ };
915
+ tokens.push(getCSSContent(element, '::before') || '');
916
+ const content = getCSSContent(element);
917
+ if (content !== undefined) {
918
+ // `content` CSS property replaces everything inside the element.
919
+ // I was not able to find any spec or description on how this interacts with accname,
920
+ // so this is a guess based on what browsers do.
921
+ tokens.push(content);
922
+ }
923
+ else {
924
+ // step 2h.
925
+ const assignedNodes = element.nodeName === 'SLOT' ? element.assignedNodes() : [];
926
+ if (assignedNodes.length) {
927
+ for (const child of assignedNodes)
928
+ visit(child, false);
929
+ }
930
+ else {
931
+ for (let child = element.firstChild; child; child = child.nextSibling)
932
+ visit(child, true);
933
+ if (element.shadowRoot) {
934
+ for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling)
935
+ visit(child, true);
936
+ }
937
+ for (const owned of getIdRefs(element, element.getAttribute('aria-owns')))
938
+ visit(owned, true);
939
+ }
940
+ }
941
+ tokens.push(getCSSContent(element, '::after') || '');
942
+ return { text: tokens.join(''), elements };
943
+ }
944
+ export const kAriaSelectedRoles = ['gridcell', 'option', 'row', 'tab', 'rowheader', 'columnheader', 'treeitem'];
945
+ export function getAriaSelected(element) {
946
+ // https://www.w3.org/TR/wai-aria-1.2/#aria-selected
947
+ // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
948
+ if (elementSafeTagName(element) === 'OPTION')
949
+ return element.selected;
950
+ if (kAriaSelectedRoles.includes(getAriaRole(element) || ''))
951
+ return getAriaBoolean(element.getAttribute('aria-selected')) === true;
952
+ return false;
953
+ }
954
+ export const kAriaCheckedRoles = ['checkbox', 'menuitemcheckbox', 'option', 'radio', 'switch', 'menuitemradio', 'treeitem'];
955
+ export function getAriaChecked(element) {
956
+ const result = getChecked(element, true);
957
+ return result === 'error' ? false : result;
958
+ }
959
+ export function getCheckedAllowMixed(element) {
960
+ return getChecked(element, true);
961
+ }
962
+ export function getCheckedWithoutMixed(element) {
963
+ const result = getChecked(element, false);
964
+ return result;
965
+ }
966
+ function getChecked(element, allowMixed) {
967
+ const tagName = elementSafeTagName(element);
968
+ // https://www.w3.org/TR/wai-aria-1.2/#aria-checked
969
+ // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
970
+ if (allowMixed && tagName === 'INPUT' && element.indeterminate)
971
+ return 'mixed';
972
+ if (tagName === 'INPUT' && ['checkbox', 'radio'].includes(element.type))
973
+ return element.checked;
974
+ if (kAriaCheckedRoles.includes(getAriaRole(element) || '')) {
975
+ const checked = element.getAttribute('aria-checked');
976
+ if (checked === 'true')
977
+ return true;
978
+ if (allowMixed && checked === 'mixed')
979
+ return 'mixed';
980
+ return false;
981
+ }
982
+ return 'error';
983
+ }
984
+ // https://w3c.github.io/aria/#aria-readonly
985
+ const kAriaReadonlyRoles = ['checkbox', 'combobox', 'grid', 'gridcell', 'listbox', 'radiogroup', 'slider', 'spinbutton', 'textbox', 'columnheader', 'rowheader', 'searchbox', 'switch', 'treegrid'];
986
+ export function getReadonly(element) {
987
+ const tagName = elementSafeTagName(element);
988
+ // https://www.w3.org/TR/wai-aria-1.2/#aria-checked
989
+ // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
990
+ if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tagName))
991
+ return element.hasAttribute('readonly');
992
+ if (kAriaReadonlyRoles.includes(getAriaRole(element) || ''))
993
+ return element.getAttribute('aria-readonly') === 'true';
994
+ if (element.isContentEditable)
995
+ return false;
996
+ return 'error';
997
+ }
998
+ export const kAriaPressedRoles = ['button'];
999
+ export function getAriaPressed(element) {
1000
+ // https://www.w3.org/TR/wai-aria-1.2/#aria-pressed
1001
+ if (kAriaPressedRoles.includes(getAriaRole(element) || '')) {
1002
+ const pressed = element.getAttribute('aria-pressed');
1003
+ if (pressed === 'true')
1004
+ return true;
1005
+ if (pressed === 'mixed')
1006
+ return 'mixed';
1007
+ }
1008
+ return false;
1009
+ }
1010
+ export const kAriaExpandedRoles = ['application', 'button', 'checkbox', 'combobox', 'gridcell', 'link', 'listbox', 'menuitem', 'row', 'rowheader', 'tab', 'treeitem', 'columnheader', 'menuitemcheckbox', 'menuitemradio', 'rowheader', 'switch'];
1011
+ export function getAriaExpanded(element) {
1012
+ // https://www.w3.org/TR/wai-aria-1.2/#aria-expanded
1013
+ // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
1014
+ if (elementSafeTagName(element) === 'DETAILS')
1015
+ return element.open;
1016
+ if (kAriaExpandedRoles.includes(getAriaRole(element) || '')) {
1017
+ const expanded = element.getAttribute('aria-expanded');
1018
+ if (expanded === null)
1019
+ return undefined;
1020
+ if (expanded === 'true')
1021
+ return true;
1022
+ return false;
1023
+ }
1024
+ return undefined;
1025
+ }
1026
+ export const kAriaLevelRoles = ['heading', 'listitem', 'row', 'treeitem'];
1027
+ export function getAriaLevel(element) {
1028
+ // https://www.w3.org/TR/wai-aria-1.2/#aria-level
1029
+ // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
1030
+ const native = { 'H1': 1, 'H2': 2, 'H3': 3, 'H4': 4, 'H5': 5, 'H6': 6 }[elementSafeTagName(element)];
1031
+ if (native)
1032
+ return native;
1033
+ if (kAriaLevelRoles.includes(getAriaRole(element) || '')) {
1034
+ const attr = element.getAttribute('aria-level');
1035
+ const value = attr === null ? Number.NaN : Number(attr);
1036
+ if (Number.isInteger(value) && value >= 1)
1037
+ return value;
1038
+ }
1039
+ return 0;
1040
+ }
1041
+ export const kAriaDisabledRoles = ['application', 'button', 'composite', 'gridcell', 'group', 'input', 'link', 'menuitem', 'scrollbar', 'separator', 'tab', 'checkbox', 'columnheader', 'combobox', 'grid', 'listbox', 'menu', 'menubar', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'radiogroup', 'row', 'rowheader', 'searchbox', 'select', 'slider', 'spinbutton', 'switch', 'tablist', 'textbox', 'toolbar', 'tree', 'treegrid', 'treeitem'];
1042
+ export function getAriaDisabled(element) {
1043
+ // https://www.w3.org/TR/wai-aria-1.2/#aria-disabled
1044
+ // Note that aria-disabled applies to all descendants, so we look up the hierarchy.
1045
+ return isNativelyDisabled(element) || hasExplicitAriaDisabled(element);
1046
+ }
1047
+ function isNativelyDisabled(element) {
1048
+ // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
1049
+ const isNativeFormControl = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'].includes(elementSafeTagName(element));
1050
+ return isNativeFormControl && (element.hasAttribute('disabled') || belongsToDisabledOptGroup(element) || belongsToDisabledFieldSet(element));
1051
+ }
1052
+ function belongsToDisabledOptGroup(element) {
1053
+ return elementSafeTagName(element) === 'OPTION' && !!element.closest('OPTGROUP[DISABLED]');
1054
+ }
1055
+ function belongsToDisabledFieldSet(element) {
1056
+ const fieldSetElement = element?.closest('FIELDSET[DISABLED]');
1057
+ if (!fieldSetElement)
1058
+ return false;
1059
+ const legendElement = fieldSetElement.querySelector(':scope > LEGEND');
1060
+ return !legendElement || !legendElement.contains(element);
1061
+ }
1062
+ function hasExplicitAriaDisabled(element, isAncestor = false) {
1063
+ if (!element)
1064
+ return false;
1065
+ if (isAncestor || kAriaDisabledRoles.includes(getAriaRole(element) || '')) {
1066
+ const attribute = (element.getAttribute('aria-disabled') || '').toLowerCase();
1067
+ if (attribute === 'true')
1068
+ return true;
1069
+ if (attribute === 'false')
1070
+ return false;
1071
+ // aria-disabled works across shadow boundaries.
1072
+ return hasExplicitAriaDisabled(parentElementOrShadowHost(element), true);
1073
+ }
1074
+ return false;
1075
+ }
1076
+ export function getAriaBusy(element) {
1077
+ // https://www.w3.org/TR/wai-aria-1.2/#aria-busy
1078
+ // aria-busy is a global state with a default value of "false".
1079
+ return getAriaBoolean(element.getAttribute('aria-busy')) === true;
1080
+ }
1081
+ function getAccessibleNameFromAssociatedLabels(labels, options) {
1082
+ return joinCompositeString([...labels].map(label => getTextAlternativeInternal(label, {
1083
+ ...options,
1084
+ embeddedInLabel: { element: label, hidden: isElementHiddenForAria(label) },
1085
+ embeddedInNativeTextAlternative: undefined,
1086
+ embeddedInLabelledBy: undefined,
1087
+ embeddedInDescribedBy: undefined,
1088
+ embeddedInTargetElement: undefined,
1089
+ })).filter(accessibleName => !!accessibleName.text), ' ', options.collectElements);
1090
+ }
1091
+ export function receivesPointerEvents(element) {
1092
+ const cache = cachePointerEvents;
1093
+ let e = element;
1094
+ let result;
1095
+ const parents = [];
1096
+ for (; e; e = parentElementOrShadowHost(e)) {
1097
+ const cached = cache.get(e);
1098
+ if (cached !== undefined) {
1099
+ result = cached;
1100
+ break;
1101
+ }
1102
+ parents.push(e);
1103
+ const style = getElementComputedStyle(e);
1104
+ if (!style) {
1105
+ result = true;
1106
+ break;
1107
+ }
1108
+ const value = style.pointerEvents;
1109
+ if (value) {
1110
+ result = value !== 'none';
1111
+ break;
1112
+ }
1113
+ }
1114
+ if (result === undefined)
1115
+ result = true;
1116
+ for (const parent of parents)
1117
+ cache.set(parent, result);
1118
+ return result;
1119
+ }
1120
+ let cacheAccessibleName;
1121
+ let cacheAccessibleNameHidden;
1122
+ let cacheAccessibleNameText;
1123
+ let cacheAccessibleNameTextHidden;
1124
+ let cacheAccessibleDescription;
1125
+ let cacheAccessibleDescriptionHidden;
1126
+ let cacheAccessibleErrorMessage;
1127
+ let cacheIsHidden;
1128
+ let cachePseudoContent;
1129
+ let cachePseudoContentBefore;
1130
+ let cachePseudoContentAfter;
1131
+ let cachePointerEvents;
1132
+ let cachesCounter = 0;
1133
+ export function beginAriaCaches() {
1134
+ beginDOMCaches();
1135
+ ++cachesCounter;
1136
+ cacheAccessibleName ??= new Map();
1137
+ cacheAccessibleNameHidden ??= new Map();
1138
+ cacheAccessibleNameText ??= new Map();
1139
+ cacheAccessibleNameTextHidden ??= new Map();
1140
+ cacheAccessibleDescription ??= new Map();
1141
+ cacheAccessibleDescriptionHidden ??= new Map();
1142
+ cacheAccessibleErrorMessage ??= new Map();
1143
+ cacheIsHidden ??= new Map();
1144
+ cachePseudoContent ??= new Map();
1145
+ cachePseudoContentBefore ??= new Map();
1146
+ cachePseudoContentAfter ??= new Map();
1147
+ cachePointerEvents ??= new Map();
1148
+ }
1149
+ export function endAriaCaches() {
1150
+ if (!--cachesCounter) {
1151
+ cacheAccessibleName = undefined;
1152
+ cacheAccessibleNameHidden = undefined;
1153
+ cacheAccessibleNameText = undefined;
1154
+ cacheAccessibleNameTextHidden = undefined;
1155
+ cacheAccessibleDescription = undefined;
1156
+ cacheAccessibleDescriptionHidden = undefined;
1157
+ cacheAccessibleErrorMessage = undefined;
1158
+ cacheIsHidden = undefined;
1159
+ cachePseudoContent = undefined;
1160
+ cachePseudoContentBefore = undefined;
1161
+ cachePseudoContentAfter = undefined;
1162
+ cachePointerEvents = undefined;
1163
+ }
1164
+ endDOMCaches();
1165
+ }
1166
+ const inputTypeToRole = {
1167
+ 'button': 'button',
1168
+ 'checkbox': 'checkbox',
1169
+ 'image': 'button',
1170
+ 'number': 'spinbutton',
1171
+ 'radio': 'radio',
1172
+ 'range': 'slider',
1173
+ 'reset': 'button',
1174
+ 'submit': 'button',
1175
+ };
1176
+ function emptyCompositeString() {
1177
+ return { text: '' };
1178
+ }
1179
+ function compositeString(text, element, collectElements) {
1180
+ const elements = text && collectElements ? new Set([element]) : undefined;
1181
+ return { text: text || '', elements };
1182
+ }
1183
+ function joinCompositeString(parts, separator, collectElements) {
1184
+ let elements;
1185
+ if (collectElements) {
1186
+ elements = new Set();
1187
+ for (const part of parts) {
1188
+ for (const element of part.elements || [])
1189
+ elements.add(element);
1190
+ }
1191
+ }
1192
+ return { text: parts.map(part => part.text).join(separator), elements };
1193
+ }