accented 0.0.0-20250223121749 → 0.0.0-20250404114312

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 (83) hide show
  1. package/NOTICE +14 -0
  2. package/README.md +7 -3
  3. package/dist/accented.d.ts +2 -2
  4. package/dist/accented.d.ts.map +1 -1
  5. package/dist/accented.js +5 -2
  6. package/dist/accented.js.map +1 -1
  7. package/dist/constants.d.ts +1 -0
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/constants.js +1 -0
  10. package/dist/constants.js.map +1 -1
  11. package/dist/dom-updater.d.ts.map +1 -1
  12. package/dist/dom-updater.js +38 -23
  13. package/dist/dom-updater.js.map +1 -1
  14. package/dist/elements/accented-dialog.d.ts.map +1 -1
  15. package/dist/elements/accented-dialog.js +60 -22
  16. package/dist/elements/accented-dialog.js.map +1 -1
  17. package/dist/elements/accented-trigger.d.ts +2 -0
  18. package/dist/elements/accented-trigger.d.ts.map +1 -1
  19. package/dist/elements/accented-trigger.js +64 -12
  20. package/dist/elements/accented-trigger.js.map +1 -1
  21. package/dist/fullscreen-listener.d.ts +2 -0
  22. package/dist/fullscreen-listener.d.ts.map +1 -0
  23. package/dist/fullscreen-listener.js +18 -0
  24. package/dist/fullscreen-listener.js.map +1 -0
  25. package/dist/scanner.d.ts.map +1 -1
  26. package/dist/scanner.js +15 -6
  27. package/dist/scanner.js.map +1 -1
  28. package/dist/state.d.ts +2 -1
  29. package/dist/state.d.ts.map +1 -1
  30. package/dist/state.js +3 -0
  31. package/dist/state.js.map +1 -1
  32. package/dist/types.d.ts +21 -8
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/utils/are-elements-with-issues-equal.d.ts +3 -0
  35. package/dist/utils/are-elements-with-issues-equal.d.ts.map +1 -0
  36. package/dist/utils/are-elements-with-issues-equal.js +5 -0
  37. package/dist/utils/are-elements-with-issues-equal.js.map +1 -0
  38. package/dist/utils/dom-helpers.d.ts +6 -0
  39. package/dist/utils/dom-helpers.d.ts.map +1 -0
  40. package/dist/utils/dom-helpers.js +19 -0
  41. package/dist/utils/dom-helpers.js.map +1 -0
  42. package/dist/utils/get-element-position.d.ts.map +1 -1
  43. package/dist/utils/get-element-position.js +53 -16
  44. package/dist/utils/get-element-position.js.map +1 -1
  45. package/dist/utils/get-parent.d.ts +2 -0
  46. package/dist/utils/get-parent.d.ts.map +1 -0
  47. package/dist/utils/get-parent.js +12 -0
  48. package/dist/utils/get-parent.js.map +1 -0
  49. package/dist/utils/get-scrollable-ancestors.d.ts +1 -1
  50. package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -1
  51. package/dist/utils/get-scrollable-ancestors.js +6 -2
  52. package/dist/utils/get-scrollable-ancestors.js.map +1 -1
  53. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts +10 -0
  54. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -0
  55. package/dist/utils/shadow-dom-aware-mutation-observer.js +64 -0
  56. package/dist/utils/shadow-dom-aware-mutation-observer.js.map +1 -0
  57. package/dist/utils/transform-violations.d.ts +1 -1
  58. package/dist/utils/transform-violations.d.ts.map +1 -1
  59. package/dist/utils/transform-violations.js +18 -5
  60. package/dist/utils/transform-violations.js.map +1 -1
  61. package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
  62. package/dist/utils/update-elements-with-issues.js +9 -5
  63. package/dist/utils/update-elements-with-issues.js.map +1 -1
  64. package/package.json +4 -3
  65. package/src/accented.ts +5 -2
  66. package/src/constants.ts +1 -0
  67. package/src/dom-updater.ts +38 -22
  68. package/src/elements/accented-dialog.ts +60 -22
  69. package/src/elements/accented-trigger.ts +70 -12
  70. package/src/fullscreen-listener.ts +17 -0
  71. package/src/scanner.ts +17 -6
  72. package/src/state.ts +10 -2
  73. package/src/types.ts +23 -9
  74. package/src/utils/are-elements-with-issues-equal.ts +9 -0
  75. package/src/utils/dom-helpers.ts +22 -0
  76. package/src/utils/get-element-position.ts +54 -15
  77. package/src/utils/get-parent.ts +14 -0
  78. package/src/utils/get-scrollable-ancestors.ts +10 -5
  79. package/src/utils/shadow-dom-aware-mutation-observer.ts +78 -0
  80. package/src/utils/transform-violations.test.ts +10 -8
  81. package/src/utils/transform-violations.ts +20 -6
  82. package/src/utils/update-elements-with-issues.test.ts +46 -11
  83. package/src/utils/update-elements-with-issues.ts +10 -5
package/src/state.ts CHANGED
@@ -8,10 +8,18 @@ export const extendedElementsWithIssues = signal<Array<ExtendedElementWithIssues
8
8
 
9
9
  export const elementsWithIssues = computed<Array<ElementWithIssues>>(() => extendedElementsWithIssues.value.map(extendedElementWithIssues => ({
10
10
  element: extendedElementWithIssues.element,
11
+ rootNode: extendedElementWithIssues.rootNode,
11
12
  issues: extendedElementWithIssues.issues.value
12
13
  })));
13
14
 
14
- export const scrollableAncestors = computed<Set<HTMLElement>>(() =>
15
+ export const rootNodes = computed<Set<Node>>(() =>
16
+ new Set(
17
+ (enabled.value ? [document as Node] : [])
18
+ .concat(...(extendedElementsWithIssues.value.map(extendedElementWithIssues => extendedElementWithIssues.rootNode)))
19
+ )
20
+ );
21
+
22
+ export const scrollableAncestors = computed<Set<Element>>(() =>
15
23
  extendedElementsWithIssues.value.reduce(
16
24
  (scrollableAncestors, extendedElementWithIssues) => {
17
25
  for (const scrollableAncestor of extendedElementWithIssues.scrollableAncestors.value) {
@@ -19,6 +27,6 @@ export const scrollableAncestors = computed<Set<HTMLElement>>(() =>
19
27
  }
20
28
  return scrollableAncestors;
21
29
  },
22
- new Set<HTMLElement>()
30
+ new Set<Element>()
23
31
  )
24
32
  );
package/src/types.ts CHANGED
@@ -42,9 +42,17 @@ type CallbackParams = {
42
42
  elementsWithIssues: Array<ElementWithIssues>,
43
43
 
44
44
  /**
45
- * How long the scan took in milliseconds.
45
+ * * `performance`: runtime performance of the last scan. An object:
46
+ * * `totalBlockingTime`: how long the main thread was blocked by Accented during the last scan, in milliseconds.
47
+ * It’s further divided into the `scan` and `domUpdate` phases.
48
+ * * `scan`: how long the `scan` phase took, in milliseconds.
49
+ * * `domUpdate`: how long the `domUpdate` phase took, in milliseconds.
46
50
  * */
47
- scanDuration: number
51
+ performance: {
52
+ totalBlockingTime: number,
53
+ scan: number,
54
+ domUpdate: number
55
+ }
48
56
  }
49
57
 
50
58
  export type Callback = (params: CallbackParams) => void;
@@ -127,9 +135,10 @@ export type AccentedOptions = {
127
135
  export type DisableAccented = () => void;
128
136
 
129
137
  export type Position = {
130
- inlineEndLeft: number,
131
- blockStartTop: number,
132
- direction: 'ltr' | 'rtl'
138
+ left: number,
139
+ top: number,
140
+ width: number,
141
+ height: number
133
142
  };
134
143
 
135
144
  export type Issue = {
@@ -140,16 +149,21 @@ export type Issue = {
140
149
  impact: axe.ImpactValue
141
150
  };
142
151
 
143
- export type ElementWithIssues = {
152
+ export type BaseElementWithIssues = {
144
153
  element: HTMLElement,
154
+ rootNode: Node
155
+ };
156
+
157
+ export type ElementWithIssues = BaseElementWithIssues &{
145
158
  issues: Array<Issue>
146
- }
159
+ };
147
160
 
148
- export type ExtendedElementWithIssues = Omit<ElementWithIssues, 'issues'> & {
161
+ export type ExtendedElementWithIssues = BaseElementWithIssues & {
149
162
  issues: Signal<ElementWithIssues['issues']>,
150
163
  visible: Signal<boolean>,
151
164
  trigger: AccentedTrigger,
152
165
  position: Signal<Position>,
153
- scrollableAncestors: Signal<Set<HTMLElement>>
166
+ anchorNameValue: string,
167
+ scrollableAncestors: Signal<Set<Element>>
154
168
  id: number
155
169
  };
@@ -0,0 +1,9 @@
1
+ import type { BaseElementWithIssues } from "../types";
2
+
3
+ export default function areElementsWithIssuesEqual(
4
+ elementWithIssues1: BaseElementWithIssues,
5
+ elementWithIssues2: BaseElementWithIssues
6
+ ) {
7
+ return elementWithIssues1.element === elementWithIssues2.element
8
+ && elementWithIssues1.rootNode === elementWithIssues2.rootNode;
9
+ }
@@ -0,0 +1,22 @@
1
+ export function isElement(node: Node): node is Element {
2
+ return typeof Node !== 'undefined' && node.nodeType === Node.ELEMENT_NODE;
3
+ }
4
+
5
+ export function isDocument(node: Node): node is Document {
6
+ return typeof Node !== 'undefined' && node.nodeType === Node.DOCUMENT_NODE;
7
+ }
8
+
9
+ export function isDocumentFragment(node: Node): node is DocumentFragment {
10
+ return typeof Node !== 'undefined' && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
11
+ }
12
+
13
+ export function isShadowRoot(documentFragment: DocumentFragment): documentFragment is ShadowRoot {
14
+ return 'host' in documentFragment;
15
+ }
16
+
17
+ export function isHtmlElement(element: Element): element is HTMLElement {
18
+ // We can't use instanceof because it may not work across contexts
19
+ // (such as when an element is moved from an iframe).
20
+ // This heuristic seems to be the most robust and fastest that I could think of.
21
+ return element.constructor.name.startsWith('HTML');
22
+ }
@@ -1,21 +1,60 @@
1
1
  import type { Position } from '../types';
2
+ import { isHtmlElement } from './dom-helpers.js';
3
+ import getParent from './get-parent.js';
4
+
5
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_display/Containing_block#identifying_the_containing_block
6
+ function isContainingBlock(element: Element, win: Window): boolean {
7
+ const style = win.getComputedStyle(element);
8
+ const { transform, perspective } = style;
9
+ // TODO: https://github.com/pomerantsev/accented/issues/119
10
+ // Support other types of containing blocks
11
+ return transform !== 'none'
12
+ || perspective !== 'none';
13
+ }
14
+
15
+ function getNonInitialContainingBlock(element: Element, win: Window): Element | null {
16
+ let currentElement: Element | null = element;
17
+ while (currentElement) {
18
+ currentElement = getParent(currentElement);
19
+ if (currentElement && isContainingBlock(currentElement, win)) {
20
+ return currentElement;
21
+ }
22
+ }
23
+ return null;
24
+ }
2
25
 
3
26
  export default function getElementPosition(element: Element, win: Window): Position {
4
- const rect = element.getBoundingClientRect();
5
- const direction = win.getComputedStyle(element).direction;
6
- if (direction === 'ltr') {
7
- return {
8
- inlineEndLeft: rect.right,
9
- blockStartTop: rect.top,
10
- direction
11
- };
12
- } else if (direction === 'rtl') {
13
- return {
14
- inlineEndLeft: rect.left,
15
- blockStartTop: rect.top,
16
- direction
17
- };
27
+ const nonInitialContainingBlock = getNonInitialContainingBlock(element, win);
28
+ // If an element has an ancestor whose transform is not 'none',
29
+ // fixed positioning works differently.
30
+ // https://achrafkassioui.com/blog/position-fixed-and-CSS-transforms/
31
+ if (nonInitialContainingBlock) {
32
+ if (isHtmlElement(element)) {
33
+ const width = element.offsetWidth;
34
+ const height = element.offsetHeight;
35
+ let left = element.offsetLeft;
36
+ let top = element.offsetTop;
37
+ let currentElement = element.offsetParent as HTMLElement | null;
38
+ // Non-initial containing block may not be an offset parent, we have to account for that as well.
39
+ while (currentElement && currentElement !== nonInitialContainingBlock) {
40
+ left += currentElement.offsetLeft;
41
+ top += currentElement.offsetTop;
42
+ currentElement = currentElement.offsetParent as HTMLElement | null;
43
+ }
44
+ return { top, left, width, height };
45
+ } else {
46
+ // TODO: https://github.com/pomerantsev/accented/issues/116
47
+ // This is half-baked. It works incorrectly with scaled / rotated elements with issues.
48
+ const elementRect = element.getBoundingClientRect();
49
+ const nonInitialContainingBlockRect = nonInitialContainingBlock.getBoundingClientRect();
50
+ return {
51
+ top: elementRect.top - nonInitialContainingBlockRect.top,
52
+ height: elementRect.height,
53
+ left: elementRect.left - nonInitialContainingBlockRect.left,
54
+ width: elementRect.width
55
+ };
56
+ }
18
57
  } else {
19
- throw new Error(`The element ${element} has a direction "${direction}", which is not supported.`);
58
+ return element.getBoundingClientRect();
20
59
  }
21
60
  }
@@ -0,0 +1,14 @@
1
+ import { isDocumentFragment, isShadowRoot } from './dom-helpers.js';
2
+
3
+ export default function getParent (element: Element): Element | null {
4
+ if (element.parentElement) {
5
+ return element.parentElement;
6
+ }
7
+
8
+ const rootNode = element.getRootNode();
9
+ if (isDocumentFragment(rootNode) && isShadowRoot(rootNode)) {
10
+ return rootNode.host;
11
+ }
12
+
13
+ return null;
14
+ }
@@ -1,10 +1,15 @@
1
+ import getParent from './get-parent.js';
2
+
1
3
  const scrollableOverflowValues = new Set(['auto', 'scroll', 'hidden']);
2
4
 
3
- export default function getScrollableAncestors (element: HTMLElement, win: Window) {
4
- let currentElement = element;
5
- let scrollableAncestors = new Set<HTMLElement>();
6
- while (currentElement.parentElement) {
7
- currentElement = currentElement.parentElement;
5
+ export default function getScrollableAncestors (element: Element, win: Window) {
6
+ let currentElement: Element | null = element;
7
+ let scrollableAncestors = new Set<Element>();
8
+ while (true) {
9
+ currentElement = getParent(currentElement);
10
+ if (!currentElement) {
11
+ break;
12
+ }
8
13
  const computedStyle = win.getComputedStyle(currentElement);
9
14
  if (scrollableOverflowValues.has(computedStyle.overflowX) || scrollableOverflowValues.has(computedStyle.overflowY)) {
10
15
  scrollableAncestors.add(currentElement);
@@ -0,0 +1,78 @@
1
+ import { isElement, isDocument, isDocumentFragment } from './dom-helpers.js';
2
+ import { getAccentedElementNames } from '../constants.js';
3
+
4
+ export default function createShadowDOMAwareMutationObserver (name: string, callback: MutationCallback) {
5
+ class ShadowDOMAwareMutationObserver extends MutationObserver {
6
+ #shadowRoots = new Set();
7
+
8
+ #options: MutationObserverInit | undefined;
9
+
10
+ constructor(callback: MutationCallback) {
11
+ super((mutations, observer) => {
12
+ const accentedElementNames = getAccentedElementNames(name);
13
+ const childListMutations = mutations
14
+ .filter(mutation => mutation.type === 'childList')
15
+
16
+ const newElements = childListMutations
17
+ .map(mutation => [...mutation.addedNodes])
18
+ .flat()
19
+ .filter(node => isElement(node))
20
+ .filter(node => !accentedElementNames.includes(node.nodeName.toLowerCase()));
21
+
22
+ this.#observeShadowRoots(newElements);
23
+
24
+ const removedElements = childListMutations
25
+ .map(mutation => [...mutation.removedNodes])
26
+ .flat()
27
+ .filter(node => isElement(node))
28
+ .filter(node => !accentedElementNames.includes(node.nodeName.toLowerCase()));
29
+
30
+ // Mutation observer has no "unobserve" method, so we're simply deleting
31
+ // the elements from the set of shadow roots.
32
+ this.#deleteShadowRoots(removedElements);
33
+
34
+ callback(mutations, observer);
35
+ });
36
+ }
37
+
38
+ override observe(target: Node, options?: MutationObserverInit): void {
39
+ this.#options ??= options;
40
+ if (isElement(target) || isDocument(target) || isDocumentFragment(target)) {
41
+ this.#observeShadowRoots([target]);
42
+ }
43
+ super.observe(target, options);
44
+ }
45
+
46
+ override disconnect(): void {
47
+ this.#shadowRoots.clear();
48
+ super.disconnect();
49
+ }
50
+
51
+ #observeShadowRoots = (elements: Array<Element | Document | DocumentFragment>) => {
52
+ const shadowRoots = elements
53
+ .map(element => [...element.querySelectorAll('*')])
54
+ .flat()
55
+ .filter(element => element.shadowRoot)
56
+ .map(element => element.shadowRoot!);
57
+
58
+ for (const shadowRoot of shadowRoots) {
59
+ this.#shadowRoots.add(shadowRoot);
60
+ this.observe(shadowRoot, this.#options);
61
+ }
62
+ }
63
+
64
+ #deleteShadowRoots = (elements: Array<Element | Document | DocumentFragment>) => {
65
+ const shadowRoots = elements
66
+ .map(element => [...element.querySelectorAll('*')])
67
+ .flat()
68
+ .filter(element => element.shadowRoot)
69
+ .map(element => element.shadowRoot!);
70
+
71
+ for (const shadowRoot of shadowRoots) {
72
+ this.#shadowRoots.delete(shadowRoot);
73
+ }
74
+ }
75
+ }
76
+
77
+ return new ShadowDOMAwareMutationObserver(callback);
78
+ }
@@ -24,12 +24,14 @@ const commonViolationProps2: Omit<Violation, 'nodes'> = {
24
24
  impact: 'serious'
25
25
  };
26
26
 
27
+ const getRootNode = (): Node => ({} as Node);
28
+
27
29
  // @ts-expect-error element is not HTMLElement
28
- const element1: HTMLElement = {};
30
+ const element1: HTMLElement = {getRootNode};
29
31
  // @ts-expect-error element is not HTMLElement
30
- const element2: HTMLElement = {};
32
+ const element2: HTMLElement = {getRootNode};
31
33
  // @ts-expect-error element is not HTMLElement
32
- const element3: HTMLElement = {};
34
+ const element3: HTMLElement = {getRootNode};
33
35
 
34
36
  const commonNodeProps = {
35
37
  html: '<div></div>',
@@ -65,7 +67,7 @@ suite('transformViolations', () => {
65
67
  ...commonViolationProps1,
66
68
  nodes: [node1]
67
69
  };
68
- const elementsWithIssues = transformViolations([violation]);
70
+ const elementsWithIssues = transformViolations([violation], 'accented');
69
71
  assert.equal(elementsWithIssues.length, 1);
70
72
  assert.equal(elementsWithIssues[0]?.element, element1);
71
73
  assert.equal(elementsWithIssues[0].issues.length, 1);
@@ -82,7 +84,7 @@ suite('transformViolations', () => {
82
84
  ...commonViolationProps2,
83
85
  nodes: [node1, node3]
84
86
  };
85
- const elementsWithIssues = transformViolations([violation1, violation2]);
87
+ const elementsWithIssues = transformViolations([violation1, violation2], 'accented');
86
88
  assert.equal(elementsWithIssues.length, 3);
87
89
  const elementWithTwoIssues = elementsWithIssues.find(elementWithIssues => elementWithIssues.element === element1);
88
90
  assert.equal(elementWithTwoIssues?.issues.length, 2);
@@ -101,7 +103,7 @@ suite('transformViolations', () => {
101
103
  nodes: [node]
102
104
  };
103
105
 
104
- const elementsWithIssues = transformViolations([violation]);
106
+ const elementsWithIssues = transformViolations([violation], 'accented');
105
107
  assert.equal(elementsWithIssues.length, 0);
106
108
  });
107
109
 
@@ -118,7 +120,7 @@ suite('transformViolations', () => {
118
120
  nodes: [node]
119
121
  };
120
122
 
121
- const elementsWithIssues = transformViolations([violation]);
122
- assert.equal(elementsWithIssues.length, 0);
123
+ const elementsWithIssues = transformViolations([violation], 'accented');
124
+ assert.equal(elementsWithIssues.length, 1);
123
125
  });
124
126
  });
@@ -1,12 +1,29 @@
1
1
  import type { AxeResults, ImpactValue } from 'axe-core';
2
2
  import type { Issue, ElementWithIssues } from '../types';
3
3
 
4
+ // This is a list of axe-core violations (their ids) that may be flagged by axe-core
5
+ // as false positives if an Accented trigger is a descendant of the element with the issue.
6
+ const violationsAffectedByAccentedTriggers = [
7
+ 'aria-hidden-focus',
8
+ 'aria-text',
9
+ 'definition-list',
10
+ 'label-content-name-mismatch',
11
+ 'list',
12
+ 'nested-interactive',
13
+ 'scrollable-region-focusable' // The Accented trigger might make the content grow such that scrolling is required.
14
+ ];
15
+
16
+ function maybeCausedByAccented(violationId: string, element: HTMLElement, name: string) {
17
+ return violationsAffectedByAccentedTriggers.includes(violationId)
18
+ && Boolean(element.querySelector(`${name}-trigger`));
19
+ }
20
+
4
21
  function impactCompare(a: ImpactValue, b: ImpactValue) {
5
22
  const impactOrder = [null, 'minor', 'moderate', 'serious', 'critical'];
6
23
  return impactOrder.indexOf(a) - impactOrder.indexOf(b);
7
24
  }
8
25
 
9
- export default function transformViolations(violations: typeof AxeResults.violations) {
26
+ export default function transformViolations(violations: typeof AxeResults.violations, name: string) {
10
27
  const elementsWithIssues: Array<ElementWithIssues> = [];
11
28
 
12
29
  for (const violation of violations) {
@@ -21,11 +38,7 @@ export default function transformViolations(violations: typeof AxeResults.violat
21
38
  // A consumer of Accented can instead scan the iframed document by calling Accented initialization from that document.
22
39
  const isInIframe = target.length > 1;
23
40
 
24
- // Highlighting elements in shadow DOM is not yet supported, see https://github.com/pomerantsev/accented/issues/25
25
- // Until then, we don’t want such elements to be added to the set.
26
- const isInShadowDOM = Array.isArray(target[0]);
27
-
28
- if (element && !isInIframe && !isInShadowDOM) {
41
+ if (element && !isInIframe && !maybeCausedByAccented(violation.id, element, name)) {
29
42
  const issue: Issue = {
30
43
  id: violation.id,
31
44
  title: violation.help,
@@ -37,6 +50,7 @@ export default function transformViolations(violations: typeof AxeResults.violat
37
50
  if (existingElementIndex === -1) {
38
51
  elementsWithIssues.push({
39
52
  element,
53
+ rootNode: element.getRootNode(),
40
54
  issues: [issue]
41
55
  });
42
56
  } else {
@@ -8,7 +8,7 @@ import updateElementsWithIssues from './update-elements-with-issues';
8
8
  import type { AxeResults, ImpactValue } from 'axe-core';
9
9
  import type { AccentedTrigger } from '../elements/accented-trigger';
10
10
  type Violation = AxeResults['violations'][number];
11
- type Node = Violation['nodes'][number];
11
+ type AxeNode = Violation['nodes'][number];
12
12
 
13
13
  const win: Window & { CSS: typeof CSS } = {
14
14
  document: {
@@ -23,7 +23,8 @@ const win: Window & { CSS: typeof CSS } = {
23
23
  // @ts-expect-error we're missing a lot of properties
24
24
  getComputedStyle: () => ({
25
25
  zIndex: '',
26
- direction: 'ltr'
26
+ direction: 'ltr',
27
+ getPropertyValue: () => 'none'
27
28
  }),
28
29
  // @ts-expect-error we're missing a lot of properties
29
30
  CSS: {
@@ -33,19 +34,33 @@ const win: Window & { CSS: typeof CSS } = {
33
34
 
34
35
  const getBoundingClientRect = () => ({});
35
36
 
37
+ const getRootNode = (): Node => ({} as Node);
38
+
39
+ const baseElement = {
40
+ getBoundingClientRect,
41
+ getRootNode,
42
+ style: {
43
+ getPropertyValue: () => ''
44
+ }
45
+ }
46
+
36
47
  // @ts-expect-error element is not HTMLElement
37
- const element1: HTMLElement = {getBoundingClientRect, isConnected: true};
48
+ const element1: HTMLElement = {...baseElement, isConnected: true};
38
49
  // @ts-expect-error element is not HTMLElement
39
- const element2: HTMLElement = {getBoundingClientRect, isConnected: true};
50
+ const element2: HTMLElement = {...baseElement, isConnected: true};
40
51
  // @ts-expect-error element is not HTMLElement
41
- const element3: HTMLElement = {getBoundingClientRect, isConnected: false};
52
+ const element3: HTMLElement = {...baseElement, isConnected: false};
53
+
54
+ // @ts-expect-error rootNode is not Node
55
+ const rootNode: Node = {};
42
56
 
43
57
  const trigger = win.document.createElement('accented-trigger') as AccentedTrigger;
44
58
 
45
59
  const position = signal({
46
- inlineEndLeft: 0,
47
- blockStartTop: 0,
48
- direction: 'ltr' as const
60
+ left: 0,
61
+ width: 100,
62
+ top: 0,
63
+ height: 100
49
64
  });
50
65
 
51
66
  const visible = signal(true);
@@ -60,17 +75,17 @@ const commonNodeProps = {
60
75
  target: ['div']
61
76
  };
62
77
 
63
- const node1: Node = {
78
+ const node1: AxeNode = {
64
79
  ...commonNodeProps,
65
80
  element: element1,
66
81
  };
67
82
 
68
- const node2: Node = {
83
+ const node2: AxeNode = {
69
84
  ...commonNodeProps,
70
85
  element: element2,
71
86
  };
72
87
 
73
- const node3: Node = {
88
+ const node3: AxeNode = {
74
89
  ...commonNodeProps,
75
90
  element: element3,
76
91
  };
@@ -135,18 +150,22 @@ suite('updateElementsWithIssues', () => {
135
150
  {
136
151
  id: 1,
137
152
  element: element1,
153
+ rootNode,
138
154
  position,
139
155
  visible,
140
156
  trigger,
157
+ anchorNameValue: 'none',
141
158
  scrollableAncestors,
142
159
  issues: signal([issue1])
143
160
  },
144
161
  {
145
162
  id: 2,
146
163
  element: element2,
164
+ rootNode,
147
165
  position,
148
166
  visible,
149
167
  trigger,
168
+ anchorNameValue: 'none',
150
169
  scrollableAncestors,
151
170
  issues: signal([issue2])
152
171
  }
@@ -164,18 +183,22 @@ suite('updateElementsWithIssues', () => {
164
183
  {
165
184
  id: 1,
166
185
  element: element1,
186
+ rootNode,
167
187
  position,
168
188
  visible,
169
189
  trigger,
190
+ anchorNameValue: 'none',
170
191
  scrollableAncestors,
171
192
  issues: signal([issue1])
172
193
  },
173
194
  {
174
195
  id: 2,
175
196
  element: element2,
197
+ rootNode,
176
198
  position,
177
199
  visible,
178
200
  trigger,
201
+ anchorNameValue: 'none',
179
202
  scrollableAncestors,
180
203
  issues: signal([issue2])
181
204
  }
@@ -193,18 +216,22 @@ suite('updateElementsWithIssues', () => {
193
216
  {
194
217
  id: 1,
195
218
  element: element1,
219
+ rootNode,
196
220
  position,
197
221
  visible,
198
222
  trigger,
223
+ anchorNameValue: 'none',
199
224
  scrollableAncestors,
200
225
  issues: signal([issue1])
201
226
  },
202
227
  {
203
228
  id: 2,
204
229
  element: element2,
230
+ rootNode,
205
231
  position,
206
232
  visible,
207
233
  trigger,
234
+ anchorNameValue: 'none',
208
235
  scrollableAncestors,
209
236
  issues: signal([issue2, issue3])
210
237
  }
@@ -222,9 +249,11 @@ suite('updateElementsWithIssues', () => {
222
249
  {
223
250
  id: 1,
224
251
  element: element1,
252
+ rootNode,
225
253
  position,
226
254
  visible,
227
255
  trigger,
256
+ anchorNameValue: 'none',
228
257
  scrollableAncestors,
229
258
  issues: signal([issue1])
230
259
  }
@@ -242,9 +271,11 @@ suite('updateElementsWithIssues', () => {
242
271
  {
243
272
  id: 1,
244
273
  element: element1,
274
+ rootNode,
245
275
  position,
246
276
  visible,
247
277
  trigger,
278
+ anchorNameValue: 'none',
248
279
  scrollableAncestors,
249
280
  issues: signal([issue1])
250
281
  }
@@ -259,18 +290,22 @@ suite('updateElementsWithIssues', () => {
259
290
  {
260
291
  id: 1,
261
292
  element: element1,
293
+ rootNode,
262
294
  position,
263
295
  visible,
264
296
  trigger,
297
+ anchorNameValue: 'none',
265
298
  scrollableAncestors,
266
299
  issues: signal([issue1])
267
300
  },
268
301
  {
269
302
  id: 2,
270
303
  element: element2,
304
+ rootNode,
271
305
  position,
272
306
  visible,
273
307
  trigger,
308
+ anchorNameValue: 'none',
274
309
  scrollableAncestors,
275
310
  issues: signal([issue2])
276
311
  }
@@ -3,6 +3,7 @@ import type { Signal } from '@preact/signals-core';
3
3
  import { batch, signal } from '@preact/signals-core';
4
4
  import type { ExtendedElementWithIssues } from '../types';
5
5
  import transformViolations from './transform-violations.js';
6
+ import areElementsWithIssuesEqual from './are-elements-with-issues-equal.js';
6
7
  import areIssueSetsEqual from './are-issue-sets-equal.js';
7
8
  import type { AccentedTrigger } from '../elements/accented-trigger';
8
9
  import type { AccentedDialog } from '../elements/accented-dialog';
@@ -13,28 +14,28 @@ import supportsAnchorPositioning from './supports-anchor-positioning.js';
13
14
  let count = 0;
14
15
 
15
16
  export default function updateElementsWithIssues(extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>>, violations: typeof AxeResults.violations, win: Window & { CSS: typeof CSS }, name: string) {
16
- const updatedElementsWithIssues = transformViolations(violations);
17
+ const updatedElementsWithIssues = transformViolations(violations, name);
17
18
 
18
19
  batch(() => {
19
20
  for (const updatedElementWithIssues of updatedElementsWithIssues) {
20
- const existingElementIndex = extendedElementsWithIssues.value.findIndex(extendedElementWithIssues => extendedElementWithIssues.element === updatedElementWithIssues.element);
21
+ const existingElementIndex = extendedElementsWithIssues.value.findIndex(extendedElementWithIssues => areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues));
21
22
  if (existingElementIndex > -1 && extendedElementsWithIssues.value[existingElementIndex] && !areIssueSetsEqual(extendedElementsWithIssues.value[existingElementIndex].issues.value, updatedElementWithIssues.issues)) {
22
23
  extendedElementsWithIssues.value[existingElementIndex].issues.value = updatedElementWithIssues.issues;
23
24
  }
24
25
  }
25
26
 
26
27
  const addedElementsWithIssues = updatedElementsWithIssues.filter(updatedElementWithIssues => {
27
- return !extendedElementsWithIssues.value.some(extendedElementWithIssues => extendedElementWithIssues.element === updatedElementWithIssues.element);
28
+ return !extendedElementsWithIssues.value.some(extendedElementWithIssues => areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues));
28
29
  });
29
30
 
30
31
  const removedElementsWithIssues = extendedElementsWithIssues.value.filter(extendedElementWithIssues => {
31
- return !updatedElementsWithIssues.some(updatedElementWithIssues => updatedElementWithIssues.element === extendedElementWithIssues.element);
32
+ return !updatedElementsWithIssues.some(updatedElementWithIssues => areElementsWithIssuesEqual(updatedElementWithIssues, extendedElementWithIssues));
32
33
  });
33
34
 
34
35
  if (addedElementsWithIssues.length > 0 || removedElementsWithIssues.length > 0) {
35
36
  extendedElementsWithIssues.value = [...extendedElementsWithIssues.value]
36
37
  .filter(extendedElementWithIssues => {
37
- return !removedElementsWithIssues.some(removedElementWithIssues => removedElementWithIssues.element === extendedElementWithIssues.element);
38
+ return !removedElementsWithIssues.some(removedElementWithIssues => areElementsWithIssuesEqual(removedElementWithIssues, extendedElementWithIssues));
38
39
  })
39
40
  .concat(addedElementsWithIssues
40
41
  .filter(addedElementWithIssues => addedElementWithIssues.element.isConnected)
@@ -62,9 +63,13 @@ export default function updateElementsWithIssues(extendedElementsWithIssues: Sig
62
63
  return {
63
64
  id,
64
65
  element: addedElementWithIssues.element,
66
+ rootNode: addedElementWithIssues.rootNode,
65
67
  visible: trigger.visible,
66
68
  position: trigger.position,
67
69
  scrollableAncestors: signal(scrollableAncestors),
70
+ anchorNameValue:
71
+ addedElementWithIssues.element.style.getPropertyValue('anchor-name')
72
+ || win.getComputedStyle(addedElementWithIssues.element).getPropertyValue('anchor-name'),
68
73
  trigger,
69
74
  issues
70
75
  };