accented 1.1.0 → 1.1.2
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/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.map +1 -1
- package/dist/elements/accented-dialog.js +1 -3
- package/dist/elements/accented-dialog.js.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 +2 -2
- package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -1
- package/dist/utils/shadow-dom-aware-mutation-observer.js +29 -27
- 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 +2 -2
- package/src/accented.ts +2 -4
- package/src/dom-updater.ts +6 -6
- package/src/elements/accented-dialog.ts +1 -3
- 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 -28
- 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
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { beforeEach, suite, test } from 'node:test';
|
|
3
|
+
import { JSDOM } from 'jsdom';
|
|
4
|
+
import { createShadowDOMAwareMutationObserver } from './shadow-dom-aware-mutation-observer';
|
|
5
|
+
|
|
6
|
+
type MutationAssertion = {
|
|
7
|
+
type: string;
|
|
8
|
+
target?: Node;
|
|
9
|
+
addedNodes?: { length: number; tagName?: string };
|
|
10
|
+
removedNodes?: { length: number; tagName?: string };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const createTestDOM = (html = '<div id="container"></div>') => {
|
|
14
|
+
const dom = new JSDOM(html);
|
|
15
|
+
return { dom, document: dom.window.document };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const waitForMutation = (ms = 10) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
|
|
20
|
+
const createElementWithShadowRoot = (document: Document, tagName = 'div') => {
|
|
21
|
+
const element = document.createElement(tagName);
|
|
22
|
+
const shadowRoot = element.attachShadow({ mode: 'open' });
|
|
23
|
+
return { element, shadowRoot };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const assertMutationEquals = (mutation: MutationRecord, expected: MutationAssertion) => {
|
|
27
|
+
assert.equal(mutation.type, expected.type);
|
|
28
|
+
if (expected.target) {
|
|
29
|
+
assert.equal(mutation.target, expected.target);
|
|
30
|
+
}
|
|
31
|
+
if (expected.addedNodes) {
|
|
32
|
+
assert.equal(mutation.addedNodes.length, expected.addedNodes.length);
|
|
33
|
+
if (expected.addedNodes.tagName) {
|
|
34
|
+
assert.equal((mutation.addedNodes[0] as Element)?.tagName, expected.addedNodes.tagName);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (expected.removedNodes) {
|
|
38
|
+
assert.equal(mutation.removedNodes.length, expected.removedNodes.length);
|
|
39
|
+
if (expected.removedNodes.tagName) {
|
|
40
|
+
assert.equal((mutation.removedNodes[0] as Element)?.tagName, expected.removedNodes.tagName);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const createAsyncMutationTest = (expectedMutations: number) => {
|
|
46
|
+
let mutationCount = 0;
|
|
47
|
+
let resolveTest: () => void;
|
|
48
|
+
const testComplete = new Promise<void>((resolve) => {
|
|
49
|
+
resolveTest = resolve;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const handleMutation =
|
|
53
|
+
(callback: (mutations: MutationRecord[], count: number) => void) =>
|
|
54
|
+
(mutations: MutationRecord[]) => {
|
|
55
|
+
mutationCount++;
|
|
56
|
+
callback(mutations, mutationCount);
|
|
57
|
+
|
|
58
|
+
if (mutationCount === expectedMutations) {
|
|
59
|
+
resolveTest();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return { handleMutation, testComplete, getMutationCount: () => mutationCount };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
suite('ShadowDOMAwareMutationObserver', () => {
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
const dom = new JSDOM();
|
|
69
|
+
global.Node = dom.window.Node;
|
|
70
|
+
global.MutationObserver = dom.window.MutationObserver;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
suite('inserting elements', () => {
|
|
74
|
+
test('fires when a node is inserted in an observed node', (_t, done) => {
|
|
75
|
+
const { document } = createTestDOM();
|
|
76
|
+
const container = document.querySelector('#container')!;
|
|
77
|
+
|
|
78
|
+
const observer = createShadowDOMAwareMutationObserver('test', (mutations) => {
|
|
79
|
+
assert.equal(mutations.length, 1);
|
|
80
|
+
assertMutationEquals(mutations[0]!, {
|
|
81
|
+
type: 'childList',
|
|
82
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
done();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
observer.observe(container, { childList: true });
|
|
89
|
+
|
|
90
|
+
const newDiv = document.createElement('div');
|
|
91
|
+
container.appendChild(newDiv);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('fires when a node is inserted in a shadow root of an observed element', (_t, done) => {
|
|
95
|
+
const { document } = createTestDOM('<div id="container"><div id="host"></div></div>');
|
|
96
|
+
const container = document.querySelector('#container')!;
|
|
97
|
+
const host = document.querySelector('#host')!;
|
|
98
|
+
|
|
99
|
+
const shadowRoot = host.attachShadow({ mode: 'open' });
|
|
100
|
+
|
|
101
|
+
const observer = createShadowDOMAwareMutationObserver('test', (mutations) => {
|
|
102
|
+
assert.equal(mutations.length, 1);
|
|
103
|
+
assertMutationEquals(mutations[0]!, {
|
|
104
|
+
type: 'childList',
|
|
105
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
done();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
observer.observe(container, { childList: true });
|
|
112
|
+
|
|
113
|
+
const newDiv = document.createElement('div');
|
|
114
|
+
shadowRoot.appendChild(newDiv);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// This is an unfortunate limitation: we cannot be notified when a shadow root is created
|
|
118
|
+
// in the observed DOM. When that happens, the whole shadow root subtree will stay unobserved.
|
|
119
|
+
test('does not fire when a node is inserted in a shadow root created after observer starts', (_t, done) => {
|
|
120
|
+
const { document } = createTestDOM('<div id="container"><div id="host"></div></div>');
|
|
121
|
+
const container = document.querySelector('#container')!;
|
|
122
|
+
const host = document.querySelector('#host')!;
|
|
123
|
+
|
|
124
|
+
let callbackFired = false;
|
|
125
|
+
const observer = createShadowDOMAwareMutationObserver('test', () => {
|
|
126
|
+
callbackFired = true;
|
|
127
|
+
done(new Error('Callback should not have fired'));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
131
|
+
|
|
132
|
+
const shadowRoot = host.attachShadow({ mode: 'open' });
|
|
133
|
+
const newDiv = document.createElement('div');
|
|
134
|
+
shadowRoot.appendChild(newDiv);
|
|
135
|
+
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
assert.equal(
|
|
138
|
+
callbackFired,
|
|
139
|
+
false,
|
|
140
|
+
'Callback should not have fired for shadow root created after observation',
|
|
141
|
+
);
|
|
142
|
+
done();
|
|
143
|
+
}, 50);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('discovers shadow roots when descendants with shadow roots are added', async () => {
|
|
147
|
+
// This test verifies that when a wrapper element containing descendant shadow hosts
|
|
148
|
+
// is added to the DOM, the shadow roots are properly discovered and observed
|
|
149
|
+
const { document } = createTestDOM();
|
|
150
|
+
const container = document.querySelector('#container')!;
|
|
151
|
+
|
|
152
|
+
const { handleMutation, testComplete } = createAsyncMutationTest(2);
|
|
153
|
+
|
|
154
|
+
const { element: hostElement, shadowRoot } = createElementWithShadowRoot(document);
|
|
155
|
+
const wrapper = document.createElement('div');
|
|
156
|
+
wrapper.appendChild(hostElement);
|
|
157
|
+
|
|
158
|
+
const observer = createShadowDOMAwareMutationObserver(
|
|
159
|
+
'test',
|
|
160
|
+
handleMutation((mutations, count) => {
|
|
161
|
+
assert.equal(mutations.length, 1);
|
|
162
|
+
|
|
163
|
+
if (count === 1) {
|
|
164
|
+
// First mutation: wrapper with descendant that has shadow root
|
|
165
|
+
assertMutationEquals(mutations[0]!, {
|
|
166
|
+
type: 'childList',
|
|
167
|
+
target: container,
|
|
168
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
169
|
+
});
|
|
170
|
+
} else if (count === 2) {
|
|
171
|
+
// Second mutation: element added to the shadow root that was discovered
|
|
172
|
+
assertMutationEquals(mutations[0]!, {
|
|
173
|
+
type: 'childList',
|
|
174
|
+
target: shadowRoot,
|
|
175
|
+
addedNodes: { length: 1, tagName: 'SPAN' },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
182
|
+
|
|
183
|
+
container.appendChild(wrapper);
|
|
184
|
+
await waitForMutation();
|
|
185
|
+
|
|
186
|
+
const shadowDiv = document.createElement('span');
|
|
187
|
+
shadowRoot.appendChild(shadowDiv);
|
|
188
|
+
|
|
189
|
+
await testComplete;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('discovers shadow roots on directly added elements', async () => {
|
|
193
|
+
// This test verifies that shadow roots are discovered on elements that are
|
|
194
|
+
// added directly to the observed container (not just on descendants)
|
|
195
|
+
const { document } = createTestDOM();
|
|
196
|
+
const container = document.querySelector('#container')!;
|
|
197
|
+
|
|
198
|
+
const { handleMutation, testComplete } = createAsyncMutationTest(2);
|
|
199
|
+
const { element: hostElement, shadowRoot } = createElementWithShadowRoot(document);
|
|
200
|
+
|
|
201
|
+
const observer = createShadowDOMAwareMutationObserver(
|
|
202
|
+
'test',
|
|
203
|
+
handleMutation((mutations, count) => {
|
|
204
|
+
assert.equal(mutations.length, 1);
|
|
205
|
+
|
|
206
|
+
if (count === 1) {
|
|
207
|
+
// First mutation: element with shadow root added directly
|
|
208
|
+
assertMutationEquals(mutations[0]!, {
|
|
209
|
+
type: 'childList',
|
|
210
|
+
target: container,
|
|
211
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
212
|
+
});
|
|
213
|
+
} else if (count === 2) {
|
|
214
|
+
// Second mutation: element added to the shadow root on the directly added element
|
|
215
|
+
assertMutationEquals(mutations[0]!, {
|
|
216
|
+
type: 'childList',
|
|
217
|
+
target: shadowRoot,
|
|
218
|
+
addedNodes: { length: 1, tagName: 'SPAN' },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
225
|
+
|
|
226
|
+
// Add element with shadow root directly to container
|
|
227
|
+
// This should now work with the bug fix (previously didn't work)
|
|
228
|
+
container.appendChild(hostElement);
|
|
229
|
+
await waitForMutation();
|
|
230
|
+
|
|
231
|
+
const shadowDiv = document.createElement('span');
|
|
232
|
+
shadowRoot.appendChild(shadowDiv);
|
|
233
|
+
|
|
234
|
+
await testComplete;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('discovers and observes nested shadow roots (two levels deep)', async () => {
|
|
238
|
+
const { document } = createTestDOM();
|
|
239
|
+
const container = document.querySelector('#container')!;
|
|
240
|
+
|
|
241
|
+
const { handleMutation, testComplete } = createAsyncMutationTest(3);
|
|
242
|
+
const { element: outerElement, shadowRoot: outerShadowRoot } =
|
|
243
|
+
createElementWithShadowRoot(document);
|
|
244
|
+
const { element: innerElement, shadowRoot: innerShadowRoot } = createElementWithShadowRoot(
|
|
245
|
+
document,
|
|
246
|
+
'span',
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const observer = createShadowDOMAwareMutationObserver(
|
|
250
|
+
'test',
|
|
251
|
+
handleMutation((mutations, count) => {
|
|
252
|
+
assert.equal(mutations.length, 1);
|
|
253
|
+
|
|
254
|
+
if (count === 1) {
|
|
255
|
+
// First mutation: outer element with shadow root added
|
|
256
|
+
assertMutationEquals(mutations[0]!, {
|
|
257
|
+
type: 'childList',
|
|
258
|
+
target: container,
|
|
259
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
260
|
+
});
|
|
261
|
+
} else if (count === 2) {
|
|
262
|
+
// Second mutation: inner element with nested shadow root added to outer shadow root
|
|
263
|
+
assertMutationEquals(mutations[0]!, {
|
|
264
|
+
type: 'childList',
|
|
265
|
+
target: outerShadowRoot,
|
|
266
|
+
addedNodes: { length: 1, tagName: 'SPAN' },
|
|
267
|
+
});
|
|
268
|
+
} else if (count === 3) {
|
|
269
|
+
// Third mutation: element added to nested (inner) shadow root
|
|
270
|
+
assertMutationEquals(mutations[0]!, {
|
|
271
|
+
type: 'childList',
|
|
272
|
+
target: innerShadowRoot,
|
|
273
|
+
addedNodes: { length: 1, tagName: 'P' },
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
280
|
+
|
|
281
|
+
container.appendChild(outerElement);
|
|
282
|
+
await waitForMutation();
|
|
283
|
+
|
|
284
|
+
outerShadowRoot.appendChild(innerElement);
|
|
285
|
+
await waitForMutation();
|
|
286
|
+
|
|
287
|
+
const deepElement = document.createElement('p');
|
|
288
|
+
innerShadowRoot.appendChild(deepElement);
|
|
289
|
+
|
|
290
|
+
await testComplete;
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
suite('removing elements', () => {
|
|
295
|
+
// Tests for proper cleanup when elements are removed from the DOM
|
|
296
|
+
test('does not fire when elements are added to removed elements', async () => {
|
|
297
|
+
const { document } = createTestDOM();
|
|
298
|
+
const container = document.querySelector('#container')!;
|
|
299
|
+
|
|
300
|
+
const { handleMutation, testComplete } = createAsyncMutationTest(2);
|
|
301
|
+
let completionScheduled = false;
|
|
302
|
+
|
|
303
|
+
const observer = createShadowDOMAwareMutationObserver(
|
|
304
|
+
'test',
|
|
305
|
+
handleMutation((mutations, count) => {
|
|
306
|
+
assert.equal(mutations.length, 1);
|
|
307
|
+
|
|
308
|
+
if (count === 1) {
|
|
309
|
+
// First mutation: element added to container
|
|
310
|
+
assertMutationEquals(mutations[0]!, {
|
|
311
|
+
type: 'childList',
|
|
312
|
+
target: container,
|
|
313
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
314
|
+
});
|
|
315
|
+
} else if (count === 2) {
|
|
316
|
+
// Second mutation: element removed from container
|
|
317
|
+
assertMutationEquals(mutations[0]!, {
|
|
318
|
+
type: 'childList',
|
|
319
|
+
target: container,
|
|
320
|
+
removedNodes: { length: 1, tagName: 'DIV' },
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Schedule completion check to ensure no third mutation occurs
|
|
324
|
+
if (!completionScheduled) {
|
|
325
|
+
completionScheduled = true;
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
throw new Error(`Unexpected mutation count: ${count}`);
|
|
329
|
+
}
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
334
|
+
|
|
335
|
+
const testElement = document.createElement('div');
|
|
336
|
+
container.appendChild(testElement);
|
|
337
|
+
await waitForMutation();
|
|
338
|
+
|
|
339
|
+
container.removeChild(testElement);
|
|
340
|
+
await waitForMutation();
|
|
341
|
+
|
|
342
|
+
// Add element to the removed element (should NOT trigger mutation)
|
|
343
|
+
const childElement = document.createElement('span');
|
|
344
|
+
testElement.appendChild(childElement);
|
|
345
|
+
|
|
346
|
+
await testComplete;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('does not fire when elements are added to shadow roots of removed hosts', async () => {
|
|
350
|
+
const { document } = createTestDOM();
|
|
351
|
+
const container = document.querySelector('#container')!;
|
|
352
|
+
|
|
353
|
+
const { handleMutation, testComplete } = createAsyncMutationTest(3);
|
|
354
|
+
const { element: shadowHost, shadowRoot } = createElementWithShadowRoot(document);
|
|
355
|
+
let completionScheduled = false;
|
|
356
|
+
|
|
357
|
+
const observer = createShadowDOMAwareMutationObserver(
|
|
358
|
+
'test',
|
|
359
|
+
handleMutation((mutations, count) => {
|
|
360
|
+
assert.equal(mutations.length, 1);
|
|
361
|
+
|
|
362
|
+
if (count === 1) {
|
|
363
|
+
// First mutation: shadow host added to container
|
|
364
|
+
assertMutationEquals(mutations[0]!, {
|
|
365
|
+
type: 'childList',
|
|
366
|
+
target: container,
|
|
367
|
+
addedNodes: { length: 1, tagName: 'DIV' },
|
|
368
|
+
});
|
|
369
|
+
} else if (count === 2) {
|
|
370
|
+
// Second mutation: element added to shadow root
|
|
371
|
+
assertMutationEquals(mutations[0]!, {
|
|
372
|
+
type: 'childList',
|
|
373
|
+
target: shadowRoot,
|
|
374
|
+
addedNodes: { length: 1, tagName: 'SPAN' },
|
|
375
|
+
});
|
|
376
|
+
} else if (count === 3) {
|
|
377
|
+
// Third mutation: shadow host removed from container
|
|
378
|
+
assertMutationEquals(mutations[0]!, {
|
|
379
|
+
type: 'childList',
|
|
380
|
+
target: container,
|
|
381
|
+
removedNodes: { length: 1, tagName: 'DIV' },
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Schedule completion check to ensure no fourth mutation occurs
|
|
385
|
+
if (!completionScheduled) {
|
|
386
|
+
completionScheduled = true;
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
throw new Error(`Unexpected mutation count: ${count}`);
|
|
390
|
+
}
|
|
391
|
+
}),
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
395
|
+
|
|
396
|
+
container.appendChild(shadowHost);
|
|
397
|
+
await waitForMutation();
|
|
398
|
+
|
|
399
|
+
const shadowChild = document.createElement('span');
|
|
400
|
+
shadowRoot.appendChild(shadowChild);
|
|
401
|
+
await waitForMutation();
|
|
402
|
+
|
|
403
|
+
container.removeChild(shadowHost);
|
|
404
|
+
await waitForMutation();
|
|
405
|
+
|
|
406
|
+
// Add another element to the shadow root of removed host (should NOT trigger mutation)
|
|
407
|
+
const anotherShadowChild = document.createElement('p');
|
|
408
|
+
shadowRoot.appendChild(anotherShadowChild);
|
|
409
|
+
|
|
410
|
+
await testComplete;
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|
|
@@ -1,34 +1,47 @@
|
|
|
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
40
|
// Mutation observer has no "unobserve" method, so we're simply deleting
|
|
28
41
|
// the elements from the set of shadow roots.
|
|
29
|
-
this.#
|
|
42
|
+
this.#unobserveShadowRoots(removedElements);
|
|
30
43
|
|
|
31
|
-
|
|
44
|
+
mutationCallback(mutations, observer);
|
|
32
45
|
});
|
|
33
46
|
}
|
|
34
47
|
|
|
@@ -41,31 +54,26 @@ export function createShadowDOMAwareMutationObserver(name: string, callback: Mut
|
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
override disconnect(): void {
|
|
57
|
+
this.#unobserveShadowRoots(Array.from(this.#shadowRoots.keys()));
|
|
44
58
|
this.#shadowRoots.clear();
|
|
45
59
|
super.disconnect();
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
#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);
|
|
63
|
+
const shadowRoots = getShadowRoots(elements);
|
|
53
64
|
|
|
54
65
|
for (const shadowRoot of shadowRoots) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
66
|
+
const observer = new ShadowDOMAwareMutationObserver(callback);
|
|
67
|
+
observer.observe(shadowRoot, this.#options);
|
|
68
|
+
this.#shadowRoots.set(shadowRoot, observer);
|
|
59
69
|
}
|
|
60
70
|
};
|
|
61
71
|
|
|
62
|
-
#
|
|
63
|
-
const shadowRoots = elements
|
|
64
|
-
.flatMap((element) => [...element.querySelectorAll('*')])
|
|
65
|
-
.filter((element) => element.shadowRoot)
|
|
66
|
-
.map((element) => element.shadowRoot);
|
|
72
|
+
#unobserveShadowRoots = (elements: Array<Element | Document | DocumentFragment>) => {
|
|
73
|
+
const shadowRoots = getShadowRoots(elements);
|
|
67
74
|
|
|
68
75
|
for (const shadowRoot of shadowRoots) {
|
|
76
|
+
this.#shadowRoots.get(shadowRoot)?.disconnect();
|
|
69
77
|
this.#shadowRoots.delete(shadowRoot);
|
|
70
78
|
}
|
|
71
79
|
};
|
|
@@ -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);
|