accented 1.1.1 → 1.2.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.
- package/README.md +2 -2
- package/dist/accented.d.ts.map +1 -1
- package/dist/accented.js +2 -4
- package/dist/accented.js.map +1 -1
- package/dist/dom-updater.js +6 -6
- package/dist/dom-updater.js.map +1 -1
- package/dist/elements/accented-dialog.d.ts +2 -2
- package/dist/elements/accented-dialog.d.ts.map +1 -1
- package/dist/elements/accented-dialog.js +15 -14
- package/dist/elements/accented-dialog.js.map +1 -1
- package/dist/elements/accented-trigger.d.ts +6 -6
- package/dist/elements/accented-trigger.d.ts.map +1 -1
- package/dist/elements/accented-trigger.js +2 -2
- package/dist/elements/accented-trigger.js.map +1 -1
- package/dist/intersection-observer.js +2 -2
- package/dist/intersection-observer.js.map +1 -1
- package/dist/logger.d.ts +1 -4
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +15 -15
- package/dist/logger.js.map +1 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +4 -5
- package/dist/scanner.js.map +1 -1
- package/dist/state.js +4 -4
- package/dist/state.js.map +1 -1
- package/dist/utils/get-element-position.d.ts +1 -1
- package/dist/utils/get-element-position.d.ts.map +1 -1
- package/dist/utils/get-element-position.js +6 -6
- package/dist/utils/get-element-position.js.map +1 -1
- 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 +2 -2
- package/dist/utils/get-scrollable-ancestors.js.map +1 -1
- package/dist/utils/recalculate-positions.js +1 -1
- package/dist/utils/recalculate-positions.js.map +1 -1
- package/dist/utils/recalculate-scrollable-ancestors.js +1 -1
- package/dist/utils/recalculate-scrollable-ancestors.js.map +1 -1
- package/dist/utils/shadow-dom-aware-mutation-observer.d.ts +4 -4
- package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -1
- package/dist/utils/shadow-dom-aware-mutation-observer.js +29 -29
- package/dist/utils/shadow-dom-aware-mutation-observer.js.map +1 -1
- package/dist/utils/supports-anchor-positioning.d.ts +1 -5
- package/dist/utils/supports-anchor-positioning.d.ts.map +1 -1
- package/dist/utils/supports-anchor-positioning.js +4 -6
- package/dist/utils/supports-anchor-positioning.js.map +1 -1
- package/dist/utils/update-elements-with-issues.d.ts +1 -4
- package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
- package/dist/utils/update-elements-with-issues.js +8 -10
- package/dist/utils/update-elements-with-issues.js.map +1 -1
- package/package.json +5 -5
- package/src/accented.ts +2 -4
- package/src/dom-updater.ts +6 -6
- package/src/elements/accented-dialog.ts +16 -17
- package/src/elements/accented-trigger.ts +2 -2
- package/src/intersection-observer.ts +2 -2
- package/src/logger.ts +15 -15
- package/src/scanner.ts +4 -5
- package/src/state.ts +5 -5
- package/src/utils/get-element-position.ts +6 -6
- package/src/utils/get-scrollable-ancestors.ts +2 -2
- package/src/utils/recalculate-positions.ts +1 -1
- package/src/utils/recalculate-scrollable-ancestors.ts +1 -1
- package/src/utils/shadow-dom-aware-mutation-observer.test.ts +413 -0
- package/src/utils/shadow-dom-aware-mutation-observer.ts +36 -30
- package/src/utils/supports-anchor-positioning.ts +4 -10
- package/src/utils/update-elements-with-issues.test.ts +29 -54
- package/src/utils/update-elements-with-issues.ts +7 -11
|
@@ -1,34 +1,45 @@
|
|
|
1
1
|
import { getAccentedElementNames } from '../constants.js';
|
|
2
2
|
import { isDocument, isDocumentFragment, isElement } from './dom-helpers.js';
|
|
3
3
|
|
|
4
|
+
function getShadowRoots(elements: Array<Element | Document | DocumentFragment>) {
|
|
5
|
+
return elements
|
|
6
|
+
.flatMap((element) => [element, ...Array.from(element.querySelectorAll('*'))])
|
|
7
|
+
.reduce<Array<ShadowRoot>>(
|
|
8
|
+
(acc, element) =>
|
|
9
|
+
isElement(element) && element.shadowRoot ? acc.concat(element.shadowRoot) : acc,
|
|
10
|
+
[],
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
export function createShadowDOMAwareMutationObserver(name: string, callback: MutationCallback) {
|
|
15
|
+
type ObserverMap = Map<ShadowRoot, ShadowDOMAwareMutationObserver>;
|
|
16
|
+
|
|
17
|
+
const accentedElementNames = getAccentedElementNames(name);
|
|
18
|
+
|
|
19
|
+
function getMutationNodes(mutations: Array<MutationRecord>, type: 'addedNodes' | 'removedNodes') {
|
|
20
|
+
return mutations
|
|
21
|
+
.filter((mutation) => mutation.type === 'childList')
|
|
22
|
+
.flatMap((mutation) => Array.from(mutation[type]))
|
|
23
|
+
.filter((node) => isElement(node))
|
|
24
|
+
.filter((node) => !accentedElementNames.includes(node.nodeName.toLowerCase()));
|
|
25
|
+
}
|
|
26
|
+
|
|
5
27
|
class ShadowDOMAwareMutationObserver extends MutationObserver {
|
|
6
|
-
#shadowRoots = new
|
|
28
|
+
#shadowRoots: ObserverMap = new Map();
|
|
7
29
|
|
|
8
30
|
#options: MutationObserverInit | undefined;
|
|
9
31
|
|
|
10
|
-
constructor(
|
|
32
|
+
constructor(mutationCallback: MutationCallback) {
|
|
11
33
|
super((mutations, observer) => {
|
|
12
|
-
const
|
|
13
|
-
const childListMutations = mutations.filter((mutation) => mutation.type === 'childList');
|
|
14
|
-
|
|
15
|
-
const newElements = childListMutations
|
|
16
|
-
.flatMap((mutation) => [...mutation.addedNodes])
|
|
17
|
-
.filter((node) => isElement(node))
|
|
18
|
-
.filter((node) => !accentedElementNames.includes(node.nodeName.toLowerCase()));
|
|
34
|
+
const newElements = getMutationNodes(mutations, 'addedNodes');
|
|
19
35
|
|
|
20
36
|
this.#observeShadowRoots(newElements);
|
|
21
37
|
|
|
22
|
-
const removedElements =
|
|
23
|
-
.flatMap((mutation) => [...mutation.removedNodes])
|
|
24
|
-
.filter((node) => isElement(node))
|
|
25
|
-
.filter((node) => !accentedElementNames.includes(node.nodeName.toLowerCase()));
|
|
38
|
+
const removedElements = getMutationNodes(mutations, 'removedNodes');
|
|
26
39
|
|
|
27
|
-
|
|
28
|
-
// the elements from the set of shadow roots.
|
|
29
|
-
this.#deleteShadowRoots(removedElements);
|
|
40
|
+
this.#unobserveShadowRoots(removedElements);
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
mutationCallback(mutations, observer);
|
|
32
43
|
});
|
|
33
44
|
}
|
|
34
45
|
|
|
@@ -41,31 +52,26 @@ export function createShadowDOMAwareMutationObserver(name: string, callback: Mut
|
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
override disconnect(): void {
|
|
55
|
+
this.#unobserveShadowRoots(Array.from(this.#shadowRoots.keys()));
|
|
44
56
|
this.#shadowRoots.clear();
|
|
45
57
|
super.disconnect();
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
#observeShadowRoots = (elements: Array<Element | Document | DocumentFragment>) => {
|
|
49
|
-
const shadowRoots = elements
|
|
50
|
-
.flatMap((element) => [...element.querySelectorAll('*')])
|
|
51
|
-
.filter((element) => element.shadowRoot)
|
|
52
|
-
.map((element) => element.shadowRoot);
|
|
61
|
+
const shadowRoots = getShadowRoots(elements);
|
|
53
62
|
|
|
54
63
|
for (const shadowRoot of shadowRoots) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
64
|
+
const observer = new ShadowDOMAwareMutationObserver(callback);
|
|
65
|
+
observer.observe(shadowRoot, this.#options);
|
|
66
|
+
this.#shadowRoots.set(shadowRoot, observer);
|
|
59
67
|
}
|
|
60
68
|
};
|
|
61
69
|
|
|
62
|
-
#
|
|
63
|
-
const shadowRoots = elements
|
|
64
|
-
.flatMap((element) => [...element.querySelectorAll('*')])
|
|
65
|
-
.filter((element) => element.shadowRoot)
|
|
66
|
-
.map((element) => element.shadowRoot);
|
|
70
|
+
#unobserveShadowRoots = (elements: Array<Element | Document | DocumentFragment>) => {
|
|
71
|
+
const shadowRoots = getShadowRoots(elements);
|
|
67
72
|
|
|
68
73
|
for (const shadowRoot of shadowRoots) {
|
|
74
|
+
this.#shadowRoots.get(shadowRoot)?.disconnect();
|
|
69
75
|
this.#shadowRoots.delete(shadowRoot);
|
|
70
76
|
}
|
|
71
77
|
};
|
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
type WindowWithCSS = Window & {
|
|
2
|
-
CSS: typeof CSS;
|
|
3
|
-
};
|
|
4
|
-
|
|
5
1
|
/**
|
|
6
2
|
* We have to do browser sniffing now and explicitly turn off Anchor positioning in Safari
|
|
7
3
|
* since anchor positioning is not working correctly in Safari 26 Technology Preview.
|
|
8
4
|
*/
|
|
9
|
-
function isWebKit(
|
|
10
|
-
const ua =
|
|
5
|
+
function isWebKit() {
|
|
6
|
+
const ua = navigator.userAgent;
|
|
11
7
|
return (/AppleWebKit/.test(ua) && !/Chrome/.test(ua)) || /\b(iPad|iPhone|iPod)\b/.test(ua);
|
|
12
8
|
}
|
|
13
9
|
|
|
14
10
|
// ATTENTION: sync with the implementation in end-to-end tests.
|
|
15
11
|
// I didn't find a way to sync this with automatically with the implementation of supportsAnchorPositioning
|
|
16
12
|
// in end-to-end tests, so it has to be synced manually.
|
|
17
|
-
export function supportsAnchorPositioning(
|
|
13
|
+
export function supportsAnchorPositioning() {
|
|
18
14
|
return (
|
|
19
|
-
|
|
20
|
-
win.CSS.supports('position-anchor: --foo') &&
|
|
21
|
-
!isWebKit(win)
|
|
15
|
+
CSS.supports('anchor-name: --foo') && CSS.supports('position-anchor: --foo') && !isWebKit()
|
|
22
16
|
);
|
|
23
17
|
}
|
|
@@ -3,64 +3,45 @@ import { suite, test } from 'node:test';
|
|
|
3
3
|
import type { Signal } from '@preact/signals-core';
|
|
4
4
|
import { signal } from '@preact/signals-core';
|
|
5
5
|
import type { AxeResults, ImpactValue } from 'axe-core';
|
|
6
|
+
import { JSDOM } from 'jsdom';
|
|
6
7
|
import type { AccentedTrigger } from '../elements/accented-trigger';
|
|
7
8
|
import type { ExtendedElementWithIssues, Issue } from '../types';
|
|
8
9
|
import { updateElementsWithIssues } from './update-elements-with-issues';
|
|
9
10
|
|
|
11
|
+
const dom = new JSDOM();
|
|
12
|
+
global.document = dom.window.document;
|
|
13
|
+
global.getComputedStyle = dom.window.getComputedStyle;
|
|
14
|
+
// JSDOM doesn't seem to have CSS, so we mock it
|
|
15
|
+
global.CSS = {
|
|
16
|
+
supports: () => true,
|
|
17
|
+
} as any;
|
|
18
|
+
// Node already has a global `navigator` object,
|
|
19
|
+
// so we're mocking it differently than other globals.
|
|
20
|
+
Object.defineProperty(global, 'navigator', {
|
|
21
|
+
value: { userAgent: dom.window.navigator.userAgent },
|
|
22
|
+
writable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
10
26
|
type Violation = AxeResults['violations'][number];
|
|
11
27
|
type AxeNode = Violation['nodes'][number];
|
|
12
28
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
style: {
|
|
18
|
-
setProperty: () => {},
|
|
19
|
-
},
|
|
20
|
-
dataset: {},
|
|
21
|
-
}),
|
|
22
|
-
contains: () => true,
|
|
23
|
-
},
|
|
24
|
-
// @ts-expect-error we're missing a lot of properties
|
|
25
|
-
getComputedStyle: () => ({
|
|
26
|
-
zIndex: '',
|
|
27
|
-
direction: 'ltr',
|
|
28
|
-
getPropertyValue: () => 'none',
|
|
29
|
-
}),
|
|
30
|
-
// @ts-expect-error we're missing a lot of properties
|
|
31
|
-
CSS: {
|
|
32
|
-
supports: () => true,
|
|
33
|
-
},
|
|
34
|
-
// @ts-expect-error we're missing a lot of properties
|
|
35
|
-
navigator: {
|
|
36
|
-
userAgent: '',
|
|
37
|
-
},
|
|
38
|
-
};
|
|
29
|
+
// Create real DOM elements using JSDOM
|
|
30
|
+
const element1 = document.createElement('div');
|
|
31
|
+
element1.setAttribute('id', 'element1');
|
|
32
|
+
document.body.appendChild(element1);
|
|
39
33
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const baseElement = {
|
|
45
|
-
getBoundingClientRect,
|
|
46
|
-
getRootNode,
|
|
47
|
-
style: {
|
|
48
|
-
getPropertyValue: () => '',
|
|
49
|
-
},
|
|
50
|
-
closest: () => null,
|
|
51
|
-
};
|
|
34
|
+
const element2 = document.createElement('div');
|
|
35
|
+
element2.setAttribute('id', 'element2');
|
|
36
|
+
document.body.appendChild(element2);
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
const element2: HTMLElement = { ...baseElement, isConnected: true };
|
|
57
|
-
// @ts-expect-error element is not HTMLElement
|
|
58
|
-
const element3: HTMLElement = { ...baseElement, isConnected: false };
|
|
38
|
+
const element3 = document.createElement('div');
|
|
39
|
+
element3.setAttribute('id', 'element3');
|
|
40
|
+
// element3 is not connected (not added to document)
|
|
59
41
|
|
|
60
|
-
|
|
61
|
-
const rootNode: Node = {};
|
|
42
|
+
const rootNode = document;
|
|
62
43
|
|
|
63
|
-
const trigger =
|
|
44
|
+
const trigger = document.createElement('accented-trigger') as AccentedTrigger;
|
|
64
45
|
|
|
65
46
|
const position = signal({
|
|
66
47
|
left: 0,
|
|
@@ -151,7 +132,7 @@ const issue3: Issue = {
|
|
|
151
132
|
};
|
|
152
133
|
|
|
153
134
|
const scanContext = {
|
|
154
|
-
include: [
|
|
135
|
+
include: [document],
|
|
155
136
|
exclude: [],
|
|
156
137
|
};
|
|
157
138
|
|
|
@@ -187,7 +168,6 @@ suite('updateElementsWithIssues', () => {
|
|
|
187
168
|
extendedElementsWithIssues,
|
|
188
169
|
scanContext,
|
|
189
170
|
violations: [violation1, violation2],
|
|
190
|
-
win,
|
|
191
171
|
name: 'accented',
|
|
192
172
|
});
|
|
193
173
|
assert.equal(extendedElementsWithIssues.value.length, 2);
|
|
@@ -228,7 +208,6 @@ suite('updateElementsWithIssues', () => {
|
|
|
228
208
|
extendedElementsWithIssues,
|
|
229
209
|
scanContext,
|
|
230
210
|
violations: [violation1, violation2, violation3],
|
|
231
|
-
win,
|
|
232
211
|
name: 'accented',
|
|
233
212
|
});
|
|
234
213
|
assert.equal(extendedElementsWithIssues.value.length, 2);
|
|
@@ -269,7 +248,6 @@ suite('updateElementsWithIssues', () => {
|
|
|
269
248
|
extendedElementsWithIssues,
|
|
270
249
|
scanContext,
|
|
271
250
|
violations: [violation1, violation2],
|
|
272
|
-
win,
|
|
273
251
|
name: 'accented',
|
|
274
252
|
});
|
|
275
253
|
assert.equal(extendedElementsWithIssues.value.length, 2);
|
|
@@ -298,7 +276,6 @@ suite('updateElementsWithIssues', () => {
|
|
|
298
276
|
extendedElementsWithIssues,
|
|
299
277
|
scanContext,
|
|
300
278
|
violations: [violation1, violation2],
|
|
301
|
-
win,
|
|
302
279
|
name: 'accented',
|
|
303
280
|
});
|
|
304
281
|
assert.equal(extendedElementsWithIssues.value.length, 2);
|
|
@@ -327,7 +304,6 @@ suite('updateElementsWithIssues', () => {
|
|
|
327
304
|
extendedElementsWithIssues,
|
|
328
305
|
scanContext,
|
|
329
306
|
violations: [violation1, violation4],
|
|
330
|
-
win,
|
|
331
307
|
name: 'accented',
|
|
332
308
|
});
|
|
333
309
|
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
@@ -365,7 +341,6 @@ suite('updateElementsWithIssues', () => {
|
|
|
365
341
|
extendedElementsWithIssues,
|
|
366
342
|
scanContext,
|
|
367
343
|
violations: [violation1],
|
|
368
|
-
win,
|
|
369
344
|
name: 'accented',
|
|
370
345
|
});
|
|
371
346
|
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
@@ -36,13 +36,11 @@ export function updateElementsWithIssues({
|
|
|
36
36
|
extendedElementsWithIssues,
|
|
37
37
|
scanContext,
|
|
38
38
|
violations,
|
|
39
|
-
win,
|
|
40
39
|
name,
|
|
41
40
|
}: {
|
|
42
41
|
extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>>;
|
|
43
42
|
scanContext: ScanContext;
|
|
44
43
|
violations: typeof AxeResults.violations;
|
|
45
|
-
win: Window & { CSS: typeof CSS };
|
|
46
44
|
name: string;
|
|
47
45
|
}) {
|
|
48
46
|
const updatedElementsWithIssues = transformViolations(violations, name);
|
|
@@ -99,9 +97,9 @@ export function updateElementsWithIssues({
|
|
|
99
97
|
.filter((addedElementWithIssues) => addedElementWithIssues.element.isConnected)
|
|
100
98
|
.map((addedElementWithIssues) => {
|
|
101
99
|
const id = count++;
|
|
102
|
-
const trigger =
|
|
100
|
+
const trigger = document.createElement(`${name}-trigger`) as AccentedTrigger;
|
|
103
101
|
const elementZIndex = Number.parseInt(
|
|
104
|
-
|
|
102
|
+
getComputedStyle(addedElementWithIssues.element).zIndex,
|
|
105
103
|
10,
|
|
106
104
|
);
|
|
107
105
|
if (!Number.isNaN(elementZIndex)) {
|
|
@@ -109,15 +107,15 @@ export function updateElementsWithIssues({
|
|
|
109
107
|
}
|
|
110
108
|
trigger.style.setProperty('position-anchor', `--${name}-anchor-${id}`, 'important');
|
|
111
109
|
trigger.dataset.id = id.toString();
|
|
112
|
-
const accentedDialog =
|
|
110
|
+
const accentedDialog = document.createElement(`${name}-dialog`) as AccentedDialog;
|
|
113
111
|
trigger.dialog = accentedDialog;
|
|
114
|
-
const position = getElementPosition(addedElementWithIssues.element
|
|
112
|
+
const position = getElementPosition(addedElementWithIssues.element);
|
|
115
113
|
trigger.position = signal(position);
|
|
116
114
|
trigger.visible = signal(true);
|
|
117
115
|
trigger.element = addedElementWithIssues.element;
|
|
118
|
-
const scrollableAncestors = supportsAnchorPositioning(
|
|
116
|
+
const scrollableAncestors = supportsAnchorPositioning()
|
|
119
117
|
? new Set<HTMLElement>()
|
|
120
|
-
: getScrollableAncestors(addedElementWithIssues.element
|
|
118
|
+
: getScrollableAncestors(addedElementWithIssues.element);
|
|
121
119
|
const issues = signal(addedElementWithIssues.issues);
|
|
122
120
|
accentedDialog.issues = issues;
|
|
123
121
|
accentedDialog.element = addedElementWithIssues.element;
|
|
@@ -131,9 +129,7 @@ export function updateElementsWithIssues({
|
|
|
131
129
|
scrollableAncestors: signal(scrollableAncestors),
|
|
132
130
|
anchorNameValue:
|
|
133
131
|
addedElementWithIssues.element.style.getPropertyValue('anchor-name') ||
|
|
134
|
-
|
|
135
|
-
.getComputedStyle(addedElementWithIssues.element)
|
|
136
|
-
.getPropertyValue('anchor-name'),
|
|
132
|
+
getComputedStyle(addedElementWithIssues.element).getPropertyValue('anchor-name'),
|
|
137
133
|
trigger,
|
|
138
134
|
issues,
|
|
139
135
|
};
|