accented 0.0.0-20250404114312 → 0.0.0-20250424114613

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 (99) hide show
  1. package/README.md +3 -1
  2. package/dist/accented.d.ts.map +1 -1
  3. package/dist/accented.js +5 -3
  4. package/dist/accented.js.map +1 -1
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/dom-updater.d.ts.map +1 -1
  7. package/dist/dom-updater.js +29 -3
  8. package/dist/dom-updater.js.map +1 -1
  9. package/dist/elements/accented-dialog.d.ts +11 -7
  10. package/dist/elements/accented-dialog.d.ts.map +1 -1
  11. package/dist/elements/accented-dialog.js +35 -64
  12. package/dist/elements/accented-dialog.js.map +1 -1
  13. package/dist/elements/accented-trigger.d.ts +9 -5
  14. package/dist/elements/accented-trigger.d.ts.map +1 -1
  15. package/dist/elements/accented-trigger.js +6 -5
  16. package/dist/elements/accented-trigger.js.map +1 -1
  17. package/dist/logger.d.ts.map +1 -1
  18. package/dist/logger.js +4 -1
  19. package/dist/logger.js.map +1 -1
  20. package/dist/scanner.d.ts +2 -2
  21. package/dist/scanner.d.ts.map +1 -1
  22. package/dist/scanner.js +19 -14
  23. package/dist/scanner.js.map +1 -1
  24. package/dist/task-queue.d.ts +2 -2
  25. package/dist/task-queue.d.ts.map +1 -1
  26. package/dist/task-queue.js +2 -1
  27. package/dist/task-queue.js.map +1 -1
  28. package/dist/types.d.ts +25 -3
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/types.js.map +1 -1
  31. package/dist/utils/containing-blocks.d.ts +3 -0
  32. package/dist/utils/containing-blocks.d.ts.map +1 -0
  33. package/dist/utils/containing-blocks.js +46 -0
  34. package/dist/utils/containing-blocks.js.map +1 -0
  35. package/dist/utils/contains.d.ts +2 -0
  36. package/dist/utils/contains.d.ts.map +1 -0
  37. package/dist/utils/contains.js +19 -0
  38. package/dist/utils/contains.js.map +1 -0
  39. package/dist/utils/deduplicate-nodes.d.ts +2 -0
  40. package/dist/utils/deduplicate-nodes.d.ts.map +1 -0
  41. package/dist/utils/deduplicate-nodes.js +5 -0
  42. package/dist/utils/deduplicate-nodes.js.map +1 -0
  43. package/dist/utils/dom-helpers.d.ts +3 -0
  44. package/dist/utils/dom-helpers.d.ts.map +1 -1
  45. package/dist/utils/dom-helpers.js +13 -0
  46. package/dist/utils/dom-helpers.js.map +1 -1
  47. package/dist/utils/ensure-non-empty.d.ts +2 -0
  48. package/dist/utils/ensure-non-empty.d.ts.map +1 -0
  49. package/dist/utils/ensure-non-empty.js +7 -0
  50. package/dist/utils/ensure-non-empty.js.map +1 -0
  51. package/dist/utils/get-element-position.d.ts +8 -0
  52. package/dist/utils/get-element-position.d.ts.map +1 -1
  53. package/dist/utils/get-element-position.js +22 -7
  54. package/dist/utils/get-element-position.js.map +1 -1
  55. package/dist/utils/get-scan-context.d.ts +3 -0
  56. package/dist/utils/get-scan-context.d.ts.map +1 -0
  57. package/dist/utils/get-scan-context.js +28 -0
  58. package/dist/utils/get-scan-context.js.map +1 -0
  59. package/dist/utils/is-node-in-scan-context.d.ts +3 -0
  60. package/dist/utils/is-node-in-scan-context.d.ts.map +1 -0
  61. package/dist/utils/is-node-in-scan-context.js +26 -0
  62. package/dist/utils/is-node-in-scan-context.js.map +1 -0
  63. package/dist/utils/normalize-context.d.ts +3 -0
  64. package/dist/utils/normalize-context.d.ts.map +1 -0
  65. package/dist/utils/normalize-context.js +57 -0
  66. package/dist/utils/normalize-context.js.map +1 -0
  67. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -1
  68. package/dist/utils/update-elements-with-issues.d.ts +10 -4
  69. package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
  70. package/dist/utils/update-elements-with-issues.js +25 -2
  71. package/dist/utils/update-elements-with-issues.js.map +1 -1
  72. package/dist/validate-options.d.ts.map +1 -1
  73. package/dist/validate-options.js +86 -0
  74. package/dist/validate-options.js.map +1 -1
  75. package/package.json +7 -3
  76. package/src/accented.ts +5 -3
  77. package/src/dom-updater.ts +33 -3
  78. package/src/elements/accented-dialog.ts +38 -68
  79. package/src/elements/accented-trigger.ts +6 -5
  80. package/src/logger.ts +9 -1
  81. package/src/scanner.ts +21 -15
  82. package/src/task-queue.ts +6 -4
  83. package/src/types.ts +38 -5
  84. package/src/utils/containing-blocks.ts +57 -0
  85. package/src/utils/contains.test.ts +55 -0
  86. package/src/utils/contains.ts +19 -0
  87. package/src/utils/deduplicate-nodes.ts +3 -0
  88. package/src/utils/dom-helpers.ts +16 -0
  89. package/src/utils/ensure-non-empty.ts +6 -0
  90. package/src/utils/get-element-position.ts +23 -7
  91. package/src/utils/get-scan-context.test.ts +79 -0
  92. package/src/utils/get-scan-context.ts +39 -0
  93. package/src/utils/is-node-in-scan-context.test.ts +70 -0
  94. package/src/utils/is-node-in-scan-context.ts +29 -0
  95. package/src/utils/normalize-context.test.ts +105 -0
  96. package/src/utils/normalize-context.ts +58 -0
  97. package/src/utils/update-elements-with-issues.test.ts +61 -8
  98. package/src/utils/update-elements-with-issues.ts +42 -3
  99. package/src/validate-options.ts +88 -1
@@ -0,0 +1,105 @@
1
+ import { JSDOM } from 'jsdom';
2
+ import assert from 'node:assert/strict';
3
+ import { suite, test } from 'node:test';
4
+ import normalizeContext from './normalize-context';
5
+
6
+ suite('normalizeContext', () => {
7
+ test('when document is passed, only document is returned in include', () => {
8
+ const dom = new JSDOM('<div id="test"></div>');
9
+ const { document } = dom.window;
10
+ const normalizedContext = normalizeContext(document);
11
+
12
+ assert.deepEqual(normalizedContext, {
13
+ include: [document],
14
+ exclude: []
15
+ });
16
+ });
17
+
18
+ test('when an element is passed, only that element is returned in include', () => {
19
+ const dom = new JSDOM('<div id="test"></div>');
20
+ const { document } = dom.window;
21
+ const element = document.querySelector('#test')!;
22
+ const normalizedContext = normalizeContext(element);
23
+
24
+ assert.deepEqual(normalizedContext, {
25
+ include: [element],
26
+ exclude: []
27
+ });
28
+ });
29
+
30
+ test('when a selector is passed and elements matching that selector exist, all those elements are included', () => {
31
+ const dom = new JSDOM(`<div>
32
+ <div class="matches"></div>
33
+ <div class="matches"></div>
34
+ <div class="doesnt-match"></div>
35
+ </div>`);
36
+ const { document } = dom.window;
37
+ global.document = document;
38
+ const matchingElements = Array.from(document.querySelectorAll('.matches'));
39
+ const normalizedContext = normalizeContext('.matches');
40
+
41
+ assert.equal(matchingElements.length, 2);
42
+ assert.deepEqual(normalizedContext, {
43
+ include: matchingElements,
44
+ exclude: []
45
+ });
46
+ });
47
+
48
+ test('when a selector is passed and no elements matching that selector exist, `include` is empty', () => {
49
+ const dom = new JSDOM(`<div>
50
+ <div class="doesnt-match"></div>
51
+ <div class="doesnt-match"></div>
52
+ <div class="doesnt-match"></div>
53
+ </div>`);
54
+ const { document } = dom.window;
55
+ global.document = document;
56
+ const normalizedContext = normalizeContext('.matches');
57
+
58
+ assert.deepEqual(normalizedContext, {
59
+ include: [],
60
+ exclude: []
61
+ });
62
+ });
63
+
64
+ test('when a node list is passed, all the nodes from the list are included', () => {
65
+ const dom = new JSDOM(`<div>
66
+ <div class="matches"></div>
67
+ <div class="matches"></div>
68
+ <div class="doesnt-match"></div>
69
+ </div>`);
70
+ const { document } = dom.window;
71
+ const matchingElements = document.querySelectorAll('.matches');
72
+ const normalizedContext = normalizeContext(matchingElements);
73
+
74
+ assert.equal(matchingElements.length, 2);
75
+ assert.deepEqual(normalizedContext, {
76
+ include: Array.from(matchingElements),
77
+ exclude: []
78
+ });
79
+ });
80
+
81
+ test('when an object with `fromShadowDom` is passed, all the matching nodes are included', () => {
82
+ const dom = new JSDOM(`<div>
83
+ <div class="host"></div>
84
+ <div class="host"></div>
85
+ </div>`);
86
+ const { document } = dom.window;
87
+ global.document = document;
88
+ const hosts = document.querySelectorAll('.host');
89
+ const matchingElements = [];
90
+ for (const host of hosts) {
91
+ const shadowRoot = host.attachShadow({ mode: 'open' });
92
+ const matchingElement = document.createElement('div');
93
+ matchingElement.classList.add('matches');
94
+ shadowRoot.appendChild(matchingElement);
95
+ matchingElements.push(matchingElement);
96
+ }
97
+ const normalizedContext = normalizeContext({fromShadowDom: ['.host', '.matches']});
98
+
99
+ assert.equal(matchingElements.length, 2);
100
+ assert.deepEqual(normalizedContext, {
101
+ include: matchingElements,
102
+ exclude: []
103
+ });
104
+ });
105
+ });
@@ -0,0 +1,58 @@
1
+ import type { Context, ContextProp, Selector, ScanContext } from '../types';
2
+ import { isNode, isNodeList } from './dom-helpers.js';
3
+ import { deduplicateNodes } from './deduplicate-nodes.js';
4
+
5
+ function recursiveSelectAll(selectors: Array<string>, root: Document | ShadowRoot): Array<Node> {
6
+ const nodesOnCurrentLevel = root.querySelectorAll(selectors[0]!);
7
+ if (selectors.length === 1) {
8
+ return Array.from(nodesOnCurrentLevel);
9
+ }
10
+ const restSelectors: Array<string> = selectors.slice(1);
11
+ const selected = [];
12
+ for (const node of nodesOnCurrentLevel) {
13
+ if (node.shadowRoot) {
14
+ selected.push(...recursiveSelectAll(restSelectors, node.shadowRoot));
15
+ }
16
+ }
17
+ return selected;
18
+ }
19
+
20
+ function selectorToNodes(selector: Selector): Array<Node> {
21
+ if (typeof selector === 'string') {
22
+ return recursiveSelectAll([selector], document);
23
+ } else if (isNode(selector)) {
24
+ return [selector];
25
+ } else {
26
+ return recursiveSelectAll(selector.fromShadowDom, document);
27
+ }
28
+ }
29
+
30
+ function contextPropToNodes(contextProp: ContextProp): Array<Node> {
31
+ let nodes: Array<Node> = [];
32
+ if (typeof contextProp === 'object' && (Array.isArray(contextProp) || isNodeList(contextProp))) {
33
+ nodes = Array.from(contextProp).map(item => selectorToNodes(item)).flat();
34
+ } else {
35
+ nodes = selectorToNodes(contextProp);
36
+ }
37
+ return deduplicateNodes(nodes);
38
+ }
39
+
40
+ export default function normalizeContext(context: Context): ScanContext {
41
+ let contextInclude: Array<Node> = [];
42
+ let contextExclude: Array<Node> = [];
43
+ if (typeof context === 'object' && ('include' in context || 'exclude' in context)) {
44
+ if (context.include !== undefined) {
45
+ contextInclude = contextPropToNodes(context.include);
46
+ }
47
+ if (context.exclude !== undefined) {
48
+ contextExclude = contextPropToNodes(context.exclude);
49
+ }
50
+ } else {
51
+ contextInclude = contextPropToNodes(context);
52
+ }
53
+
54
+ return {
55
+ include: contextInclude,
56
+ exclude: contextExclude
57
+ };
58
+ }
@@ -18,7 +18,8 @@ const win: Window & { CSS: typeof CSS } = {
18
18
  setProperty: () => {}
19
19
  },
20
20
  dataset: {}
21
- })
21
+ }),
22
+ contains: () => true,
22
23
  },
23
24
  // @ts-expect-error we're missing a lot of properties
24
25
  getComputedStyle: () => ({
@@ -41,7 +42,8 @@ const baseElement = {
41
42
  getRootNode,
42
43
  style: {
43
44
  getPropertyValue: () => ''
44
- }
45
+ },
46
+ closest: () => null,
45
47
  }
46
48
 
47
49
  // @ts-expect-error element is not HTMLElement
@@ -144,6 +146,11 @@ const issue3: Issue = {
144
146
  ...commonIssueProps
145
147
  };
146
148
 
149
+ const scanContext = {
150
+ include: [win.document],
151
+ exclude: []
152
+ }
153
+
147
154
  suite('updateElementsWithIssues', () => {
148
155
  test('no changes', () => {
149
156
  const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
@@ -151,6 +158,7 @@ suite('updateElementsWithIssues', () => {
151
158
  id: 1,
152
159
  element: element1,
153
160
  rootNode,
161
+ skipRender: false,
154
162
  position,
155
163
  visible,
156
164
  trigger,
@@ -162,6 +170,7 @@ suite('updateElementsWithIssues', () => {
162
170
  id: 2,
163
171
  element: element2,
164
172
  rootNode,
173
+ skipRender: false,
165
174
  position,
166
175
  visible,
167
176
  trigger,
@@ -170,7 +179,13 @@ suite('updateElementsWithIssues', () => {
170
179
  issues: signal([issue2])
171
180
  }
172
181
  ]);
173
- updateElementsWithIssues(extendedElementsWithIssues, [violation1, violation2], win, 'accented');
182
+ updateElementsWithIssues({
183
+ extendedElementsWithIssues,
184
+ scanContext,
185
+ violations: [violation1, violation2],
186
+ win,
187
+ name: 'accented'
188
+ });
174
189
  assert.equal(extendedElementsWithIssues.value.length, 2);
175
190
  assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
176
191
  assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
@@ -184,6 +199,7 @@ suite('updateElementsWithIssues', () => {
184
199
  id: 1,
185
200
  element: element1,
186
201
  rootNode,
202
+ skipRender: false,
187
203
  position,
188
204
  visible,
189
205
  trigger,
@@ -195,6 +211,7 @@ suite('updateElementsWithIssues', () => {
195
211
  id: 2,
196
212
  element: element2,
197
213
  rootNode,
214
+ skipRender: false,
198
215
  position,
199
216
  visible,
200
217
  trigger,
@@ -203,7 +220,13 @@ suite('updateElementsWithIssues', () => {
203
220
  issues: signal([issue2])
204
221
  }
205
222
  ]);
206
- updateElementsWithIssues(extendedElementsWithIssues, [violation1, violation2, violation3], win, 'accented');
223
+ updateElementsWithIssues({
224
+ extendedElementsWithIssues,
225
+ scanContext,
226
+ violations: [violation1, violation2, violation3],
227
+ win,
228
+ name: 'accented'
229
+ });
207
230
  assert.equal(extendedElementsWithIssues.value.length, 2);
208
231
  assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
209
232
  assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
@@ -217,6 +240,7 @@ suite('updateElementsWithIssues', () => {
217
240
  id: 1,
218
241
  element: element1,
219
242
  rootNode,
243
+ skipRender: false,
220
244
  position,
221
245
  visible,
222
246
  trigger,
@@ -228,6 +252,7 @@ suite('updateElementsWithIssues', () => {
228
252
  id: 2,
229
253
  element: element2,
230
254
  rootNode,
255
+ skipRender: false,
231
256
  position,
232
257
  visible,
233
258
  trigger,
@@ -236,7 +261,13 @@ suite('updateElementsWithIssues', () => {
236
261
  issues: signal([issue2, issue3])
237
262
  }
238
263
  ]);
239
- updateElementsWithIssues(extendedElementsWithIssues, [violation1, violation2], win, 'accented');
264
+ updateElementsWithIssues({
265
+ extendedElementsWithIssues,
266
+ scanContext,
267
+ violations: [violation1, violation2],
268
+ win,
269
+ name: 'accented'
270
+ });
240
271
  assert.equal(extendedElementsWithIssues.value.length, 2);
241
272
  assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
242
273
  assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
@@ -250,6 +281,7 @@ suite('updateElementsWithIssues', () => {
250
281
  id: 1,
251
282
  element: element1,
252
283
  rootNode,
284
+ skipRender: false,
253
285
  position,
254
286
  visible,
255
287
  trigger,
@@ -258,7 +290,13 @@ suite('updateElementsWithIssues', () => {
258
290
  issues: signal([issue1])
259
291
  }
260
292
  ]);
261
- updateElementsWithIssues(extendedElementsWithIssues, [violation1, violation2], win, 'accented');
293
+ updateElementsWithIssues({
294
+ extendedElementsWithIssues,
295
+ scanContext,
296
+ violations: [violation1, violation2],
297
+ win,
298
+ name: 'accented'
299
+ });
262
300
  assert.equal(extendedElementsWithIssues.value.length, 2);
263
301
  assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
264
302
  assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
@@ -272,6 +310,7 @@ suite('updateElementsWithIssues', () => {
272
310
  id: 1,
273
311
  element: element1,
274
312
  rootNode,
313
+ skipRender: false,
275
314
  position,
276
315
  visible,
277
316
  trigger,
@@ -280,7 +319,13 @@ suite('updateElementsWithIssues', () => {
280
319
  issues: signal([issue1])
281
320
  }
282
321
  ]);
283
- updateElementsWithIssues(extendedElementsWithIssues, [violation1, violation4], win, 'accented');
322
+ updateElementsWithIssues({
323
+ extendedElementsWithIssues,
324
+ scanContext,
325
+ violations: [violation1, violation4],
326
+ win,
327
+ name: 'accented'
328
+ });
284
329
  assert.equal(extendedElementsWithIssues.value.length, 1);
285
330
  assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
286
331
  });
@@ -291,6 +336,7 @@ suite('updateElementsWithIssues', () => {
291
336
  id: 1,
292
337
  element: element1,
293
338
  rootNode,
339
+ skipRender: false,
294
340
  position,
295
341
  visible,
296
342
  trigger,
@@ -302,6 +348,7 @@ suite('updateElementsWithIssues', () => {
302
348
  id: 2,
303
349
  element: element2,
304
350
  rootNode,
351
+ skipRender: false,
305
352
  position,
306
353
  visible,
307
354
  trigger,
@@ -310,7 +357,13 @@ suite('updateElementsWithIssues', () => {
310
357
  issues: signal([issue2])
311
358
  }
312
359
  ]);
313
- updateElementsWithIssues(extendedElementsWithIssues, [violation1], win, 'accented');
360
+ updateElementsWithIssues({
361
+ extendedElementsWithIssues,
362
+ scanContext,
363
+ violations: [violation1],
364
+ win,
365
+ name: 'accented'
366
+ });
314
367
  assert.equal(extendedElementsWithIssues.value.length, 1);
315
368
  assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
316
369
  assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
@@ -1,19 +1,51 @@
1
1
  import type { AxeResults } from 'axe-core';
2
2
  import type { Signal } from '@preact/signals-core';
3
3
  import { batch, signal } from '@preact/signals-core';
4
- import type { ExtendedElementWithIssues } from '../types';
4
+ import type { ExtendedElementWithIssues, ScanContext } from '../types';
5
5
  import transformViolations from './transform-violations.js';
6
6
  import areElementsWithIssuesEqual from './are-elements-with-issues-equal.js';
7
7
  import areIssueSetsEqual from './are-issue-sets-equal.js';
8
+ import isNodeInScanContext from './is-node-in-scan-context.js';
8
9
  import type { AccentedTrigger } from '../elements/accented-trigger';
9
10
  import type { AccentedDialog } from '../elements/accented-dialog';
10
11
  import getElementPosition from './get-element-position.js';
11
12
  import getScrollableAncestors from './get-scrollable-ancestors.js';
12
13
  import supportsAnchorPositioning from './supports-anchor-positioning.js';
14
+ import { isSvgElement } from './dom-helpers.js';
15
+ import getParent from './get-parent.js';
16
+
17
+ function shouldSkipRender(element: Element): boolean {
18
+
19
+ // Skip rendering if the element is inside an SVG:
20
+ // https://github.com/pomerantsev/accented/issues/62
21
+ const parent = getParent(element);
22
+ const isInsideSvg = Boolean(parent && isSvgElement(parent));
23
+
24
+ // Some issues, such as meta-viewport, are on <head> descendants,
25
+ // but since <head> is never rendered, we don't want to output anything
26
+ // for those in the DOM.
27
+ // We're not anticipating the use of shadow DOM in <head>,
28
+ // so the use of .closest() should be fine.
29
+ const isInsideHead = element.closest('head') !== null;
30
+
31
+ return isInsideSvg || isInsideHead;
32
+ }
13
33
 
14
34
  let count = 0;
15
35
 
16
- export default function updateElementsWithIssues(extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>>, violations: typeof AxeResults.violations, win: Window & { CSS: typeof CSS }, name: string) {
36
+ export default function updateElementsWithIssues({
37
+ extendedElementsWithIssues,
38
+ scanContext,
39
+ violations,
40
+ win,
41
+ name
42
+ }: {
43
+ extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>>,
44
+ scanContext: ScanContext,
45
+ violations: typeof AxeResults.violations,
46
+ win: Window & { CSS: typeof CSS },
47
+ name: string
48
+ }) {
17
49
  const updatedElementsWithIssues = transformViolations(violations, name);
18
50
 
19
51
  batch(() => {
@@ -28,8 +60,14 @@ export default function updateElementsWithIssues(extendedElementsWithIssues: Sig
28
60
  return !extendedElementsWithIssues.value.some(extendedElementWithIssues => areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues));
29
61
  });
30
62
 
63
+ // Only consider an element to be removed in two cases:
64
+ // 1. It has been removed from the DOM.
65
+ // 2. It is within the scan context, but not among updatedElementsWithIssues.
31
66
  const removedElementsWithIssues = extendedElementsWithIssues.value.filter(extendedElementWithIssues => {
32
- return !updatedElementsWithIssues.some(updatedElementWithIssues => areElementsWithIssuesEqual(updatedElementWithIssues, extendedElementWithIssues));
67
+ const isConnected = extendedElementWithIssues.element.isConnected;
68
+ const hasNoMoreIssues = isNodeInScanContext(extendedElementWithIssues.element, scanContext)
69
+ && !updatedElementsWithIssues.some(updatedElementWithIssues => areElementsWithIssuesEqual(updatedElementWithIssues, extendedElementWithIssues));
70
+ return !isConnected || hasNoMoreIssues;
33
71
  });
34
72
 
35
73
  if (addedElementsWithIssues.length > 0 || removedElementsWithIssues.length > 0) {
@@ -63,6 +101,7 @@ export default function updateElementsWithIssues(extendedElementsWithIssues: Sig
63
101
  return {
64
102
  id,
65
103
  element: addedElementWithIssues.element,
104
+ skipRender: shouldSkipRender(addedElementWithIssues.element),
66
105
  rootNode: addedElementWithIssues.rootNode,
67
106
  visible: trigger.visible,
68
107
  position: trigger.position,
@@ -1,5 +1,89 @@
1
- import type { AccentedOptions } from './types';
1
+ import type { Selector, SelectorList, ContextProp, ContextObject, AccentedOptions, Context } from './types';
2
2
  import { allowedAxeOptions } from './types.js';
3
+ import { isNode, isNodeList } from './utils/dom-helpers.js';
4
+
5
+ function isSelector(contextFragment: Context): contextFragment is Selector {
6
+ return typeof contextFragment === 'string'
7
+ || isNode(contextFragment)
8
+ || 'fromShadowDom' in contextFragment;
9
+ }
10
+
11
+ function validateSelector(selector: Selector) {
12
+ if (typeof selector === 'string') {
13
+ return;
14
+ } else if (isNode(selector)) {
15
+ return;
16
+ } else if ('fromShadowDom' in selector) {
17
+ if (!Array.isArray(selector.fromShadowDom)
18
+ || selector.fromShadowDom.length < 2 ||
19
+ !selector.fromShadowDom.every(item => typeof item === 'string')
20
+ ) {
21
+ throw new TypeError(`Accented: invalid argument. \`fromShadowDom\` must be an array of strings with at least 2 elements. It’s currently set to ${selector.fromShadowDom}.`);
22
+ }
23
+ return;
24
+ } else {
25
+ const neverSelector: never = selector;
26
+ throw new TypeError(`Accented: invalid argument. The selector must be one of: string, Node, or an object with a \`fromShadowDom\` property. It’s currently set to ${neverSelector}.`);
27
+ }
28
+ }
29
+
30
+ function isSelectorList(contextFragment: Context): contextFragment is SelectorList {
31
+ return (typeof contextFragment === 'object' && isNodeList(contextFragment))
32
+ || (Array.isArray(contextFragment) && contextFragment.every(item => isSelector(item)));
33
+ }
34
+
35
+ function validateSelectorList(selectorList: SelectorList) {
36
+ if (isNodeList(selectorList)) {
37
+ return;
38
+ } else if (Array.isArray(selectorList)) {
39
+ for (const selector of selectorList) {
40
+ validateSelector(selector);
41
+ }
42
+ } else {
43
+ const neverSelectorList: never = selectorList;
44
+ throw new TypeError(`Accented: invalid argument. The selector list must either be a NodeList or an array. It’s currently set to ${neverSelectorList}.`);
45
+ }
46
+ }
47
+
48
+ function isContextProp(contextFragment: Context): contextFragment is ContextProp {
49
+ return isSelector(contextFragment) || isSelectorList(contextFragment);
50
+ }
51
+
52
+ function validateContextProp(context: Selector | SelectorList) {
53
+ if (isSelector(context)) {
54
+ validateSelector(context);
55
+ } else if (isSelectorList(context)) {
56
+ validateSelectorList(context);
57
+ } else {
58
+ const neverContext: never = context;
59
+ throw new TypeError(`Accented: invalid argument. The context property must either be a selector or a selector list. It’s currently set to ${neverContext}.`);
60
+ }
61
+ }
62
+
63
+ function isContextObject(contextFragment: Context): contextFragment is ContextObject {
64
+ return typeof contextFragment === 'object' && contextFragment !== null
65
+ && ('include' in contextFragment || 'exclude' in contextFragment);
66
+ }
67
+
68
+ function validateContextObject(contextObject: ContextObject) {
69
+ if ('include' in contextObject) {
70
+ validateContextProp(contextObject.include!);
71
+ }
72
+ if ('exclude' in contextObject) {
73
+ validateContextProp(contextObject.exclude!);
74
+ }
75
+ }
76
+
77
+ function validateContext(context: Context) {
78
+ if (isContextProp(context)) {
79
+ validateContextProp(context);
80
+ } else if (isContextObject(context)) {
81
+ validateContextObject(context);
82
+ } else {
83
+ const neverContext: never = context;
84
+ throw new TypeError(`Accented: invalid context argument. It’s currently set to ${neverContext}.`);
85
+ }
86
+ }
3
87
 
4
88
  // The space of valid CSS and HTML names is wider than this,
5
89
  // but with Unicode it gets complicated quickly, so I'm sticking to only allowing
@@ -41,4 +125,7 @@ export default function validateOptions(options: AccentedOptions) {
41
125
  throw new TypeError(`Accented: invalid argument. \`axeOptions\` contains the following unsupported keys: ${unsupportedKeys.join(', ')}. Valid options are: ${allowedAxeOptions.join(', ')}.`);
42
126
  }
43
127
  }
128
+ if (options.context !== undefined) {
129
+ validateContext(options.context);
130
+ }
44
131
  }