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.
- package/NOTICE +14 -0
- package/README.md +7 -3
- package/dist/accented.d.ts +2 -2
- package/dist/accented.d.ts.map +1 -1
- package/dist/accented.js +5 -2
- package/dist/accented.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/dom-updater.d.ts.map +1 -1
- package/dist/dom-updater.js +38 -23
- package/dist/dom-updater.js.map +1 -1
- package/dist/elements/accented-dialog.d.ts.map +1 -1
- package/dist/elements/accented-dialog.js +60 -22
- package/dist/elements/accented-dialog.js.map +1 -1
- package/dist/elements/accented-trigger.d.ts +2 -0
- package/dist/elements/accented-trigger.d.ts.map +1 -1
- package/dist/elements/accented-trigger.js +64 -12
- package/dist/elements/accented-trigger.js.map +1 -1
- package/dist/fullscreen-listener.d.ts +2 -0
- package/dist/fullscreen-listener.d.ts.map +1 -0
- package/dist/fullscreen-listener.js +18 -0
- package/dist/fullscreen-listener.js.map +1 -0
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +15 -6
- package/dist/scanner.js.map +1 -1
- package/dist/state.d.ts +2 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +3 -0
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +21 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/are-elements-with-issues-equal.d.ts +3 -0
- package/dist/utils/are-elements-with-issues-equal.d.ts.map +1 -0
- package/dist/utils/are-elements-with-issues-equal.js +5 -0
- package/dist/utils/are-elements-with-issues-equal.js.map +1 -0
- package/dist/utils/dom-helpers.d.ts +6 -0
- package/dist/utils/dom-helpers.d.ts.map +1 -0
- package/dist/utils/dom-helpers.js +19 -0
- package/dist/utils/dom-helpers.js.map +1 -0
- package/dist/utils/get-element-position.d.ts.map +1 -1
- package/dist/utils/get-element-position.js +53 -16
- package/dist/utils/get-element-position.js.map +1 -1
- package/dist/utils/get-parent.d.ts +2 -0
- package/dist/utils/get-parent.d.ts.map +1 -0
- package/dist/utils/get-parent.js +12 -0
- package/dist/utils/get-parent.js.map +1 -0
- package/dist/utils/get-scrollable-ancestors.d.ts +1 -1
- package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -1
- package/dist/utils/get-scrollable-ancestors.js +6 -2
- package/dist/utils/get-scrollable-ancestors.js.map +1 -1
- package/dist/utils/shadow-dom-aware-mutation-observer.d.ts +10 -0
- package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -0
- package/dist/utils/shadow-dom-aware-mutation-observer.js +64 -0
- package/dist/utils/shadow-dom-aware-mutation-observer.js.map +1 -0
- package/dist/utils/transform-violations.d.ts +1 -1
- package/dist/utils/transform-violations.d.ts.map +1 -1
- package/dist/utils/transform-violations.js +18 -5
- package/dist/utils/transform-violations.js.map +1 -1
- package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
- package/dist/utils/update-elements-with-issues.js +9 -5
- package/dist/utils/update-elements-with-issues.js.map +1 -1
- package/package.json +4 -3
- package/src/accented.ts +5 -2
- package/src/constants.ts +1 -0
- package/src/dom-updater.ts +38 -22
- package/src/elements/accented-dialog.ts +60 -22
- package/src/elements/accented-trigger.ts +70 -12
- package/src/fullscreen-listener.ts +17 -0
- package/src/scanner.ts +17 -6
- package/src/state.ts +10 -2
- package/src/types.ts +23 -9
- package/src/utils/are-elements-with-issues-equal.ts +9 -0
- package/src/utils/dom-helpers.ts +22 -0
- package/src/utils/get-element-position.ts +54 -15
- package/src/utils/get-parent.ts +14 -0
- package/src/utils/get-scrollable-ancestors.ts +10 -5
- package/src/utils/shadow-dom-aware-mutation-observer.ts +78 -0
- package/src/utils/transform-violations.test.ts +10 -8
- package/src/utils/transform-violations.ts +20 -6
- package/src/utils/update-elements-with-issues.test.ts +46 -11
- 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
|
|
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<
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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:
|
|
4
|
-
let currentElement = element;
|
|
5
|
-
let scrollableAncestors = new Set<
|
|
6
|
-
while (
|
|
7
|
-
currentElement = currentElement
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 = {
|
|
48
|
+
const element1: HTMLElement = {...baseElement, isConnected: true};
|
|
38
49
|
// @ts-expect-error element is not HTMLElement
|
|
39
|
-
const element2: HTMLElement = {
|
|
50
|
+
const element2: HTMLElement = {...baseElement, isConnected: true};
|
|
40
51
|
// @ts-expect-error element is not HTMLElement
|
|
41
|
-
const element3: HTMLElement = {
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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:
|
|
78
|
+
const node1: AxeNode = {
|
|
64
79
|
...commonNodeProps,
|
|
65
80
|
element: element1,
|
|
66
81
|
};
|
|
67
82
|
|
|
68
|
-
const node2:
|
|
83
|
+
const node2: AxeNode = {
|
|
69
84
|
...commonNodeProps,
|
|
70
85
|
element: element2,
|
|
71
86
|
};
|
|
72
87
|
|
|
73
|
-
const node3:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|