accented 1.2.5 → 1.3.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 (49) hide show
  1. package/dist/constants.d.ts +12 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +32 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/elements/accented-dialog.d.ts +1 -367
  6. package/dist/elements/accented-dialog.d.ts.map +1 -1
  7. package/dist/elements/accented-dialog.js.map +1 -1
  8. package/dist/elements/accented-trigger.d.ts +1 -366
  9. package/dist/elements/accented-trigger.d.ts.map +1 -1
  10. package/dist/elements/accented-trigger.js.map +1 -1
  11. package/dist/logger.d.ts.map +1 -1
  12. package/dist/scanner.d.ts.map +1 -1
  13. package/dist/scanner.js +79 -41
  14. package/dist/scanner.js.map +1 -1
  15. package/dist/utils/create-extended-element-with-issues.d.ts +3 -0
  16. package/dist/utils/create-extended-element-with-issues.d.ts.map +1 -0
  17. package/dist/utils/create-extended-element-with-issues.js +56 -0
  18. package/dist/utils/create-extended-element-with-issues.js.map +1 -0
  19. package/dist/utils/css-transforms.d.ts.map +1 -1
  20. package/dist/utils/css-transforms.js +0 -2
  21. package/dist/utils/css-transforms.js.map +1 -1
  22. package/dist/utils/get-all-rules-from-axe-options.d.ts +3 -0
  23. package/dist/utils/get-all-rules-from-axe-options.d.ts.map +1 -0
  24. package/dist/utils/get-all-rules-from-axe-options.js +51 -0
  25. package/dist/utils/get-all-rules-from-axe-options.js.map +1 -0
  26. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts +1 -9
  27. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -1
  28. package/dist/utils/shadow-dom-aware-mutation-observer.js.map +1 -1
  29. package/dist/utils/transform-violations.d.ts.map +1 -1
  30. package/dist/utils/transform-violations.js +2 -13
  31. package/dist/utils/transform-violations.js.map +1 -1
  32. package/dist/utils/update-elements-with-issues.d.ts +4 -3
  33. package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
  34. package/dist/utils/update-elements-with-issues.js +36 -80
  35. package/dist/utils/update-elements-with-issues.js.map +1 -1
  36. package/package.json +5 -5
  37. package/src/constants.ts +34 -0
  38. package/src/elements/accented-dialog.ts +1 -1
  39. package/src/elements/accented-trigger.ts +1 -1
  40. package/src/logger.ts +1 -1
  41. package/src/scanner.ts +91 -45
  42. package/src/utils/create-extended-element-with-issues.ts +67 -0
  43. package/src/utils/css-transforms.ts +0 -2
  44. package/src/utils/get-all-rules-from-axe-options.test.ts +169 -0
  45. package/src/utils/get-all-rules-from-axe-options.ts +54 -0
  46. package/src/utils/shadow-dom-aware-mutation-observer.ts +4 -1
  47. package/src/utils/transform-violations.ts +2 -14
  48. package/src/utils/update-elements-with-issues.test.ts +223 -139
  49. package/src/utils/update-elements-with-issues.ts +76 -107
@@ -77,6 +77,11 @@ const node3: AxeNode = {
77
77
  element: element3,
78
78
  };
79
79
 
80
+ const htmlNode: AxeNode = {
81
+ ...commonNodeProps,
82
+ element: document.documentElement,
83
+ };
84
+
80
85
  const commonViolationProps = {
81
86
  help: 'help',
82
87
  helpUrl: 'http://example.com',
@@ -109,6 +114,18 @@ const violation4: Violation = {
109
114
  nodes: [node3],
110
115
  };
111
116
 
117
+ const headingViolation: Violation = {
118
+ ...commonViolationProps,
119
+ id: 'page-has-heading-one',
120
+ nodes: [htmlNode],
121
+ };
122
+
123
+ const langViolation: Violation = {
124
+ ...commonViolationProps,
125
+ id: 'html-has-lang',
126
+ nodes: [htmlNode],
127
+ };
128
+
112
129
  const commonIssueProps = {
113
130
  title: 'help',
114
131
  description: 'description',
@@ -131,43 +148,56 @@ const issue3: Issue = {
131
148
  ...commonIssueProps,
132
149
  };
133
150
 
134
- const scanContext = {
151
+ const headingIssue: Issue = {
152
+ id: 'page-has-heading-one',
153
+ ...commonIssueProps,
154
+ };
155
+
156
+ const langIssue: Issue = {
157
+ id: 'html-has-lang',
158
+ ...commonIssueProps,
159
+ };
160
+
161
+ const fullDocumentContext = {
135
162
  include: [document],
136
163
  exclude: [],
137
164
  };
138
165
 
166
+ const narrowContext = {
167
+ include: [element1],
168
+ exclude: [],
169
+ };
170
+
171
+ function createElementsWithIssues(
172
+ items: Array<{ id: number; element: HTMLElement; issues: Array<Issue> }>,
173
+ ): Signal<Array<ExtendedElementWithIssues>> {
174
+ return signal(
175
+ items.map(({ id, element, issues }) => ({
176
+ id,
177
+ element,
178
+ rootNode,
179
+ skipRender: false,
180
+ position,
181
+ visible,
182
+ trigger,
183
+ anchorNameValue: 'none',
184
+ scrollableAncestors,
185
+ issues: signal(issues),
186
+ })),
187
+ );
188
+ }
189
+
139
190
  suite('updateElementsWithIssues', () => {
140
191
  test('no changes', () => {
141
- const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
142
- {
143
- id: 1,
144
- element: element1,
145
- rootNode,
146
- skipRender: false,
147
- position,
148
- visible,
149
- trigger,
150
- anchorNameValue: 'none',
151
- scrollableAncestors,
152
- issues: signal([issue1]),
153
- },
154
- {
155
- id: 2,
156
- element: element2,
157
- rootNode,
158
- skipRender: false,
159
- position,
160
- visible,
161
- trigger,
162
- anchorNameValue: 'none',
163
- scrollableAncestors,
164
- issues: signal([issue2]),
165
- },
192
+ const extendedElementsWithIssues = createElementsWithIssues([
193
+ { id: 1, element: element1, issues: [issue1] },
194
+ { id: 2, element: element2, issues: [issue2] },
166
195
  ]);
167
196
  updateElementsWithIssues({
168
197
  extendedElementsWithIssues,
169
- scanContext,
170
- violations: [violation1, violation2],
198
+ limitedContext: fullDocumentContext,
199
+ limitedContextViolations: [violation1, violation2],
200
+ fullContextViolations: [],
171
201
  name: 'accented',
172
202
  });
173
203
  assert.equal(extendedElementsWithIssues.value.length, 2);
@@ -178,36 +208,15 @@ suite('updateElementsWithIssues', () => {
178
208
  });
179
209
 
180
210
  test('one issue added', () => {
181
- const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
182
- {
183
- id: 1,
184
- element: element1,
185
- rootNode,
186
- skipRender: false,
187
- position,
188
- visible,
189
- trigger,
190
- anchorNameValue: 'none',
191
- scrollableAncestors,
192
- issues: signal([issue1]),
193
- },
194
- {
195
- id: 2,
196
- element: element2,
197
- rootNode,
198
- skipRender: false,
199
- position,
200
- visible,
201
- trigger,
202
- anchorNameValue: 'none',
203
- scrollableAncestors,
204
- issues: signal([issue2]),
205
- },
211
+ const extendedElementsWithIssues = createElementsWithIssues([
212
+ { id: 1, element: element1, issues: [issue1] },
213
+ { id: 2, element: element2, issues: [issue2] },
206
214
  ]);
207
215
  updateElementsWithIssues({
208
216
  extendedElementsWithIssues,
209
- scanContext,
210
- violations: [violation1, violation2, violation3],
217
+ limitedContext: fullDocumentContext,
218
+ limitedContextViolations: [violation1, violation2, violation3],
219
+ fullContextViolations: [],
211
220
  name: 'accented',
212
221
  });
213
222
  assert.equal(extendedElementsWithIssues.value.length, 2);
@@ -218,36 +227,15 @@ suite('updateElementsWithIssues', () => {
218
227
  });
219
228
 
220
229
  test('one issue removed', () => {
221
- const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
222
- {
223
- id: 1,
224
- element: element1,
225
- rootNode,
226
- skipRender: false,
227
- position,
228
- visible,
229
- trigger,
230
- anchorNameValue: 'none',
231
- scrollableAncestors,
232
- issues: signal([issue1]),
233
- },
234
- {
235
- id: 2,
236
- element: element2,
237
- rootNode,
238
- skipRender: false,
239
- position,
240
- visible,
241
- trigger,
242
- anchorNameValue: 'none',
243
- scrollableAncestors,
244
- issues: signal([issue2, issue3]),
245
- },
230
+ const extendedElementsWithIssues = createElementsWithIssues([
231
+ { id: 1, element: element1, issues: [issue1] },
232
+ { id: 2, element: element2, issues: [issue2, issue3] },
246
233
  ]);
247
234
  updateElementsWithIssues({
248
235
  extendedElementsWithIssues,
249
- scanContext,
250
- violations: [violation1, violation2],
236
+ limitedContext: fullDocumentContext,
237
+ limitedContextViolations: [violation1, violation2],
238
+ fullContextViolations: [],
251
239
  name: 'accented',
252
240
  });
253
241
  assert.equal(extendedElementsWithIssues.value.length, 2);
@@ -258,24 +246,14 @@ suite('updateElementsWithIssues', () => {
258
246
  });
259
247
 
260
248
  test('one element added', () => {
261
- const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
262
- {
263
- id: 1,
264
- element: element1,
265
- rootNode,
266
- skipRender: false,
267
- position,
268
- visible,
269
- trigger,
270
- anchorNameValue: 'none',
271
- scrollableAncestors,
272
- issues: signal([issue1]),
273
- },
249
+ const extendedElementsWithIssues = createElementsWithIssues([
250
+ { id: 1, element: element1, issues: [issue1] },
274
251
  ]);
275
252
  updateElementsWithIssues({
276
253
  extendedElementsWithIssues,
277
- scanContext,
278
- violations: [violation1, violation2],
254
+ limitedContext: fullDocumentContext,
255
+ limitedContextViolations: [violation1, violation2],
256
+ fullContextViolations: [],
279
257
  name: 'accented',
280
258
  });
281
259
  assert.equal(extendedElementsWithIssues.value.length, 2);
@@ -286,24 +264,14 @@ suite('updateElementsWithIssues', () => {
286
264
  });
287
265
 
288
266
  test('one disconnected element added', () => {
289
- const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
290
- {
291
- id: 1,
292
- element: element1,
293
- rootNode,
294
- skipRender: false,
295
- position,
296
- visible,
297
- trigger,
298
- anchorNameValue: 'none',
299
- scrollableAncestors,
300
- issues: signal([issue1]),
301
- },
267
+ const extendedElementsWithIssues = createElementsWithIssues([
268
+ { id: 1, element: element1, issues: [issue1] },
302
269
  ]);
303
270
  updateElementsWithIssues({
304
271
  extendedElementsWithIssues,
305
- scanContext,
306
- violations: [violation1, violation4],
272
+ limitedContext: fullDocumentContext,
273
+ limitedContextViolations: [violation1, violation4],
274
+ fullContextViolations: [],
307
275
  name: 'accented',
308
276
  });
309
277
  assert.equal(extendedElementsWithIssues.value.length, 1);
@@ -311,40 +279,156 @@ suite('updateElementsWithIssues', () => {
311
279
  });
312
280
 
313
281
  test('one element removed', () => {
314
- const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
315
- {
316
- id: 1,
317
- element: element1,
318
- rootNode,
319
- skipRender: false,
320
- position,
321
- visible,
322
- trigger,
323
- anchorNameValue: 'none',
324
- scrollableAncestors,
325
- issues: signal([issue1]),
326
- },
327
- {
328
- id: 2,
329
- element: element2,
330
- rootNode,
331
- skipRender: false,
332
- position,
333
- visible,
334
- trigger,
335
- anchorNameValue: 'none',
336
- scrollableAncestors,
337
- issues: signal([issue2]),
338
- },
282
+ const extendedElementsWithIssues = createElementsWithIssues([
283
+ { id: 1, element: element1, issues: [issue1] },
284
+ { id: 2, element: element2, issues: [issue2] },
339
285
  ]);
340
286
  updateElementsWithIssues({
341
287
  extendedElementsWithIssues,
342
- scanContext,
343
- violations: [violation1],
288
+ limitedContext: fullDocumentContext,
289
+ limitedContextViolations: [violation1],
290
+ fullContextViolations: [],
344
291
  name: 'accented',
345
292
  });
346
293
  assert.equal(extendedElementsWithIssues.value.length, 1);
347
294
  assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
348
295
  assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
349
296
  });
297
+
298
+ test('strips descendant-dependent issue from element outside limited context when full context no longer reports it', () => {
299
+ const extendedElementsWithIssues = createElementsWithIssues([
300
+ { id: 1, element: document.documentElement, issues: [headingIssue] },
301
+ ]);
302
+ updateElementsWithIssues({
303
+ extendedElementsWithIssues,
304
+ limitedContext: narrowContext,
305
+ limitedContextViolations: [],
306
+ fullContextViolations: [],
307
+ name: 'accented',
308
+ });
309
+ assert.equal(extendedElementsWithIssues.value.length, 0);
310
+ });
311
+
312
+ test('keeps descendant-dependent issue on element outside limited context when full context still reports it', () => {
313
+ const extendedElementsWithIssues = createElementsWithIssues([
314
+ { id: 1, element: document.documentElement, issues: [headingIssue] },
315
+ ]);
316
+ updateElementsWithIssues({
317
+ extendedElementsWithIssues,
318
+ limitedContext: narrowContext,
319
+ limitedContextViolations: [],
320
+ fullContextViolations: [headingViolation],
321
+ name: 'accented',
322
+ });
323
+ assert.equal(extendedElementsWithIssues.value.length, 1);
324
+ assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
325
+ assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
326
+ assert.equal(extendedElementsWithIssues.value[0]?.issues.value[0]?.id, 'page-has-heading-one');
327
+ });
328
+
329
+ test('keeps non-descendant-dependent issue on element outside limited context', () => {
330
+ const extendedElementsWithIssues = createElementsWithIssues([
331
+ { id: 1, element: document.documentElement, issues: [langIssue] },
332
+ ]);
333
+ updateElementsWithIssues({
334
+ extendedElementsWithIssues,
335
+ limitedContext: narrowContext,
336
+ limitedContextViolations: [],
337
+ fullContextViolations: [],
338
+ name: 'accented',
339
+ });
340
+ assert.equal(extendedElementsWithIssues.value.length, 1);
341
+ assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
342
+ assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
343
+ assert.equal(extendedElementsWithIssues.value[0]?.issues.value[0]?.id, 'html-has-lang');
344
+ });
345
+
346
+ test('adds a new element reported only by full context violations', () => {
347
+ const extendedElementsWithIssues = createElementsWithIssues([]);
348
+ updateElementsWithIssues({
349
+ extendedElementsWithIssues,
350
+ limitedContext: narrowContext,
351
+ limitedContextViolations: [],
352
+ fullContextViolations: [headingViolation],
353
+ name: 'accented',
354
+ });
355
+ assert.equal(extendedElementsWithIssues.value.length, 1);
356
+ assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
357
+ assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
358
+ assert.equal(extendedElementsWithIssues.value[0]?.issues.value[0]?.id, 'page-has-heading-one');
359
+ });
360
+
361
+ test('adds a new element with merged issues from limited and full context violations', () => {
362
+ const extendedElementsWithIssues = createElementsWithIssues([]);
363
+ updateElementsWithIssues({
364
+ extendedElementsWithIssues,
365
+ limitedContext: fullDocumentContext,
366
+ limitedContextViolations: [langViolation],
367
+ fullContextViolations: [headingViolation],
368
+ name: 'accented',
369
+ });
370
+ assert.equal(extendedElementsWithIssues.value.length, 1);
371
+ assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
372
+ assert.deepEqual(
373
+ extendedElementsWithIssues.value[0]?.issues.value.map((issue) => issue.id),
374
+ ['html-has-lang', 'page-has-heading-one'],
375
+ );
376
+ });
377
+
378
+ test('merges limited and full context violations onto an existing element', () => {
379
+ const extendedElementsWithIssues = createElementsWithIssues([
380
+ { id: 1, element: document.documentElement, issues: [langIssue] },
381
+ ]);
382
+ updateElementsWithIssues({
383
+ extendedElementsWithIssues,
384
+ limitedContext: fullDocumentContext,
385
+ limitedContextViolations: [langViolation],
386
+ fullContextViolations: [headingViolation],
387
+ name: 'accented',
388
+ });
389
+ assert.equal(extendedElementsWithIssues.value.length, 1);
390
+ assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
391
+ assert.deepEqual(
392
+ extendedElementsWithIssues.value[0]?.issues.value.map((issue) => issue.id),
393
+ ['html-has-lang', 'page-has-heading-one'],
394
+ );
395
+ });
396
+
397
+ test('strips only descendant-dependent issues from an element outside limited context with mixed issues', () => {
398
+ const extendedElementsWithIssues = createElementsWithIssues([
399
+ { id: 1, element: document.documentElement, issues: [langIssue, headingIssue] },
400
+ ]);
401
+ updateElementsWithIssues({
402
+ extendedElementsWithIssues,
403
+ limitedContext: narrowContext,
404
+ limitedContextViolations: [],
405
+ fullContextViolations: [],
406
+ name: 'accented',
407
+ });
408
+ assert.equal(extendedElementsWithIssues.value.length, 1);
409
+ assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
410
+ assert.deepEqual(
411
+ extendedElementsWithIssues.value[0]?.issues.value.map((issue) => issue.id),
412
+ ['html-has-lang'],
413
+ );
414
+ });
415
+
416
+ test('keeps existing issue on element outside limited context and adds a new full context issue', () => {
417
+ const extendedElementsWithIssues = createElementsWithIssues([
418
+ { id: 1, element: document.documentElement, issues: [langIssue] },
419
+ ]);
420
+ updateElementsWithIssues({
421
+ extendedElementsWithIssues,
422
+ limitedContext: narrowContext,
423
+ limitedContextViolations: [],
424
+ fullContextViolations: [headingViolation],
425
+ name: 'accented',
426
+ });
427
+ assert.equal(extendedElementsWithIssues.value.length, 1);
428
+ assert.equal(extendedElementsWithIssues.value[0]?.element, document.documentElement);
429
+ assert.deepEqual(
430
+ extendedElementsWithIssues.value[0]?.issues.value.map((issue) => issue.id),
431
+ ['html-has-lang', 'page-has-heading-one'],
432
+ );
433
+ });
350
434
  });
@@ -1,139 +1,108 @@
1
1
  import type { Signal } from '@preact/signals-core';
2
- import { batch, signal } from '@preact/signals-core';
2
+ import { batch } from '@preact/signals-core';
3
3
  import type { AxeResults } from 'axe-core';
4
- import type { AccentedDialog } from '../elements/accented-dialog.ts';
5
- import type { AccentedTrigger } from '../elements/accented-trigger.ts';
6
- import type { ExtendedElementWithIssues, ScanContext } from '../types.ts';
4
+ import { descendantDependentRules } from '../constants.js';
5
+ import type {
6
+ BaseElementWithIssues,
7
+ ElementWithIssues,
8
+ ExtendedElementWithIssues,
9
+ Issue,
10
+ ScanContext,
11
+ } from '../types.ts';
7
12
  import { areElementsWithIssuesEqual } from './are-elements-with-issues-equal.js';
8
13
  import { areIssueSetsEqual } from './are-issue-sets-equal.js';
9
- import { isSvgElement } from './dom-helpers.js';
10
- import { getElementPosition } from './get-element-position.js';
11
- import { getParent } from './get-parent.js';
12
- import { getScrollableAncestors } from './get-scrollable-ancestors.js';
14
+ import { createExtendedElementWithIssues } from './create-extended-element-with-issues.js';
13
15
  import { isNodeInScanContext } from './is-node-in-scan-context.js';
14
- import { supportsAnchorPositioning } from './supports-anchor-positioning.js';
15
16
  import { transformViolations } from './transform-violations.js';
16
17
 
17
- function shouldSkipRender(element: Element): boolean {
18
- // Skip rendering if the element is inside an SVG:
19
- // https://github.com/pomerantsev/accented/issues/62
20
- const parent = getParent(element);
21
- const isInsideSvg = Boolean(parent && isSvgElement(parent));
22
-
23
- // Some issues, such as meta-viewport, are on <head> descendants,
24
- // but since <head> is never rendered, we don't want to output anything
25
- // for those in the DOM.
26
- // We're not anticipating the use of shadow DOM in <head>,
27
- // so the use of .closest() should be fine.
28
- const isInsideHead = element.closest('head') !== null;
29
-
30
- return isInsideSvg || isInsideHead;
18
+ function getIssuesForElement(
19
+ element: BaseElementWithIssues,
20
+ list: Array<ElementWithIssues>,
21
+ ): Array<Issue> {
22
+ return list.find((entry) => areElementsWithIssuesEqual(entry, element))?.issues ?? [];
31
23
  }
32
24
 
33
- let count = 0;
25
+ function mergeLimitedContextAndFullContextViolations(
26
+ elementsFromLimitedContext: Array<ElementWithIssues>,
27
+ elementsFromFullContext: Array<ElementWithIssues>,
28
+ ): Array<ElementWithIssues> {
29
+ const fromLimitedWithFullIssuesMerged = elementsFromLimitedContext.map((limited) => {
30
+ const fullMatch = elementsFromFullContext.find((full) =>
31
+ areElementsWithIssuesEqual(full, limited),
32
+ );
33
+ return fullMatch ? { ...limited, issues: [...limited.issues, ...fullMatch.issues] } : limited;
34
+ });
35
+ const onlyInFullContext = elementsFromFullContext.filter(
36
+ (full) =>
37
+ !elementsFromLimitedContext.some((limited) => areElementsWithIssuesEqual(limited, full)),
38
+ );
39
+ return [...fromLimitedWithFullIssuesMerged, ...onlyInFullContext];
40
+ }
34
41
 
35
42
  export function updateElementsWithIssues({
36
43
  extendedElementsWithIssues,
37
- scanContext,
38
- violations,
44
+ limitedContext,
45
+ limitedContextViolations,
46
+ fullContextViolations,
39
47
  name,
40
48
  }: {
41
49
  extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>>;
42
- scanContext: ScanContext;
43
- violations: typeof AxeResults.violations;
50
+ limitedContext: ScanContext;
51
+ limitedContextViolations: AxeResults['violations'];
52
+ fullContextViolations: AxeResults['violations'];
44
53
  name: string;
45
54
  }) {
46
- const updatedElementsWithIssues = transformViolations(violations, name);
55
+ const updatedElementsFromLimitedContext = transformViolations(limitedContextViolations, name);
56
+ const updatedElementsFromFullContext = transformViolations(fullContextViolations, name);
57
+
58
+ const allUpdatedElements = mergeLimitedContextAndFullContextViolations(
59
+ updatedElementsFromLimitedContext,
60
+ updatedElementsFromFullContext,
61
+ );
47
62
 
48
63
  batch(() => {
49
- for (const updatedElementWithIssues of updatedElementsWithIssues) {
50
- const existingElementIndex = extendedElementsWithIssues.value.findIndex(
51
- (extendedElementWithIssues) =>
52
- areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues),
53
- );
54
- if (
55
- existingElementIndex > -1 &&
56
- extendedElementsWithIssues.value[existingElementIndex] &&
57
- !areIssueSetsEqual(
58
- extendedElementsWithIssues.value[existingElementIndex].issues.value,
59
- updatedElementWithIssues.issues,
60
- )
61
- ) {
62
- extendedElementsWithIssues.value[existingElementIndex].issues.value =
63
- updatedElementWithIssues.issues;
64
+ for (const existing of extendedElementsWithIssues.value) {
65
+ // If the element is inside the limited context, axe just rescanned
66
+ // it — replace its issues with whatever was reported. If it's outside, keep its
67
+ // existing issues, except descendant-dependent ones, which may have changed due
68
+ // to mutations elsewhere; those get repopulated from the full-context scan below.
69
+ const newLimitedContextIssues = isNodeInScanContext(existing.element, limitedContext)
70
+ ? getIssuesForElement(existing, updatedElementsFromLimitedContext)
71
+ : existing.issues.value.filter((issue) => !descendantDependentRules.has(issue.id));
72
+
73
+ const newFullContextIssues = getIssuesForElement(existing, updatedElementsFromFullContext);
74
+
75
+ const newIssues = [...newLimitedContextIssues, ...newFullContextIssues];
76
+
77
+ if (!areIssueSetsEqual(existing.issues.value, newIssues)) {
78
+ existing.issues.value = newIssues;
64
79
  }
65
80
  }
66
81
 
67
- const addedElementsWithIssues = updatedElementsWithIssues.filter((updatedElementWithIssues) => {
68
- return !extendedElementsWithIssues.value.some((extendedElementWithIssues) =>
69
- areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues),
70
- );
71
- });
82
+ const addedElementsWithIssues = allUpdatedElements.filter(
83
+ (updated) =>
84
+ updated.element.isConnected &&
85
+ !extendedElementsWithIssues.value.some((existing) =>
86
+ areElementsWithIssuesEqual(existing, updated),
87
+ ),
88
+ );
72
89
 
73
- // Only consider an element to be removed in two cases:
74
- // 1. It has been removed from the DOM.
75
- // 2. It is within the scan context, but not among updatedElementsWithIssues.
76
90
  const removedElementsWithIssues = extendedElementsWithIssues.value.filter(
77
- (extendedElementWithIssues) => {
78
- const isConnected = extendedElementWithIssues.element.isConnected;
79
- const hasNoMoreIssues =
80
- isNodeInScanContext(extendedElementWithIssues.element, scanContext) &&
81
- !updatedElementsWithIssues.some((updatedElementWithIssues) =>
82
- areElementsWithIssuesEqual(updatedElementWithIssues, extendedElementWithIssues),
83
- );
84
- return !isConnected || hasNoMoreIssues;
85
- },
91
+ (existing) => !existing.element.isConnected || existing.issues.value.length === 0,
86
92
  );
87
93
 
94
+ // Only rebuild the outer signal when set membership changes; per-element issue
95
+ // updates were already made in the loop above.
88
96
  if (addedElementsWithIssues.length > 0 || removedElementsWithIssues.length > 0) {
89
97
  extendedElementsWithIssues.value = [...extendedElementsWithIssues.value]
90
- .filter((extendedElementWithIssues) => {
91
- return !removedElementsWithIssues.some((removedElementWithIssues) =>
92
- areElementsWithIssuesEqual(removedElementWithIssues, extendedElementWithIssues),
93
- );
94
- })
98
+ .filter(
99
+ (existing) =>
100
+ !removedElementsWithIssues.some((removed) =>
101
+ areElementsWithIssuesEqual(removed, existing),
102
+ ),
103
+ )
95
104
  .concat(
96
- addedElementsWithIssues
97
- .filter((addedElementWithIssues) => addedElementWithIssues.element.isConnected)
98
- .map((addedElementWithIssues) => {
99
- const id = count++;
100
- const trigger = document.createElement(`${name}-trigger`) as AccentedTrigger;
101
- const elementZIndex = Number.parseInt(
102
- getComputedStyle(addedElementWithIssues.element).zIndex,
103
- 10,
104
- );
105
- if (!Number.isNaN(elementZIndex)) {
106
- trigger.style.setProperty('z-index', (elementZIndex + 1).toString(), 'important');
107
- }
108
- trigger.style.setProperty('position-anchor', `--${name}-anchor-${id}`, 'important');
109
- trigger.dataset.id = id.toString();
110
- const accentedDialog = document.createElement(`${name}-dialog`) as AccentedDialog;
111
- trigger.dialog = accentedDialog;
112
- const position = getElementPosition(addedElementWithIssues.element);
113
- trigger.position = signal(position);
114
- trigger.visible = signal(true);
115
- trigger.element = addedElementWithIssues.element;
116
- const scrollableAncestors = supportsAnchorPositioning()
117
- ? new Set<HTMLElement>()
118
- : getScrollableAncestors(addedElementWithIssues.element);
119
- const issues = signal(addedElementWithIssues.issues);
120
- accentedDialog.issues = issues;
121
- accentedDialog.element = addedElementWithIssues.element;
122
- return {
123
- id,
124
- element: addedElementWithIssues.element,
125
- skipRender: shouldSkipRender(addedElementWithIssues.element),
126
- rootNode: addedElementWithIssues.rootNode,
127
- visible: trigger.visible,
128
- position: trigger.position,
129
- scrollableAncestors: signal(scrollableAncestors),
130
- anchorNameValue:
131
- addedElementWithIssues.element.style.getPropertyValue('anchor-name') ||
132
- getComputedStyle(addedElementWithIssues.element).getPropertyValue('anchor-name'),
133
- trigger,
134
- issues,
135
- };
136
- }),
105
+ addedElementsWithIssues.map((added) => createExtendedElementWithIssues(added, name)),
137
106
  );
138
107
  }
139
108
  });