accented 0.0.0-20250404114312 → 0.0.0-20250424114613
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 +3 -1
- package/dist/accented.d.ts.map +1 -1
- package/dist/accented.js +5 -3
- package/dist/accented.js.map +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/dom-updater.d.ts.map +1 -1
- package/dist/dom-updater.js +29 -3
- package/dist/dom-updater.js.map +1 -1
- package/dist/elements/accented-dialog.d.ts +11 -7
- package/dist/elements/accented-dialog.d.ts.map +1 -1
- package/dist/elements/accented-dialog.js +35 -64
- package/dist/elements/accented-dialog.js.map +1 -1
- package/dist/elements/accented-trigger.d.ts +9 -5
- package/dist/elements/accented-trigger.d.ts.map +1 -1
- package/dist/elements/accented-trigger.js +6 -5
- package/dist/elements/accented-trigger.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -1
- package/dist/logger.js.map +1 -1
- package/dist/scanner.d.ts +2 -2
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +19 -14
- package/dist/scanner.js.map +1 -1
- package/dist/task-queue.d.ts +2 -2
- package/dist/task-queue.d.ts.map +1 -1
- package/dist/task-queue.js +2 -1
- package/dist/task-queue.js.map +1 -1
- package/dist/types.d.ts +25 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/containing-blocks.d.ts +3 -0
- package/dist/utils/containing-blocks.d.ts.map +1 -0
- package/dist/utils/containing-blocks.js +46 -0
- package/dist/utils/containing-blocks.js.map +1 -0
- package/dist/utils/contains.d.ts +2 -0
- package/dist/utils/contains.d.ts.map +1 -0
- package/dist/utils/contains.js +19 -0
- package/dist/utils/contains.js.map +1 -0
- package/dist/utils/deduplicate-nodes.d.ts +2 -0
- package/dist/utils/deduplicate-nodes.d.ts.map +1 -0
- package/dist/utils/deduplicate-nodes.js +5 -0
- package/dist/utils/deduplicate-nodes.js.map +1 -0
- package/dist/utils/dom-helpers.d.ts +3 -0
- package/dist/utils/dom-helpers.d.ts.map +1 -1
- package/dist/utils/dom-helpers.js +13 -0
- package/dist/utils/dom-helpers.js.map +1 -1
- package/dist/utils/ensure-non-empty.d.ts +2 -0
- package/dist/utils/ensure-non-empty.d.ts.map +1 -0
- package/dist/utils/ensure-non-empty.js +7 -0
- package/dist/utils/ensure-non-empty.js.map +1 -0
- package/dist/utils/get-element-position.d.ts +8 -0
- package/dist/utils/get-element-position.d.ts.map +1 -1
- package/dist/utils/get-element-position.js +22 -7
- package/dist/utils/get-element-position.js.map +1 -1
- package/dist/utils/get-scan-context.d.ts +3 -0
- package/dist/utils/get-scan-context.d.ts.map +1 -0
- package/dist/utils/get-scan-context.js +28 -0
- package/dist/utils/get-scan-context.js.map +1 -0
- package/dist/utils/is-node-in-scan-context.d.ts +3 -0
- package/dist/utils/is-node-in-scan-context.d.ts.map +1 -0
- package/dist/utils/is-node-in-scan-context.js +26 -0
- package/dist/utils/is-node-in-scan-context.js.map +1 -0
- package/dist/utils/normalize-context.d.ts +3 -0
- package/dist/utils/normalize-context.d.ts.map +1 -0
- package/dist/utils/normalize-context.js +57 -0
- package/dist/utils/normalize-context.js.map +1 -0
- package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -1
- package/dist/utils/update-elements-with-issues.d.ts +10 -4
- package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
- package/dist/utils/update-elements-with-issues.js +25 -2
- package/dist/utils/update-elements-with-issues.js.map +1 -1
- package/dist/validate-options.d.ts.map +1 -1
- package/dist/validate-options.js +86 -0
- package/dist/validate-options.js.map +1 -1
- package/package.json +7 -3
- package/src/accented.ts +5 -3
- package/src/dom-updater.ts +33 -3
- package/src/elements/accented-dialog.ts +38 -68
- package/src/elements/accented-trigger.ts +6 -5
- package/src/logger.ts +9 -1
- package/src/scanner.ts +21 -15
- package/src/task-queue.ts +6 -4
- package/src/types.ts +38 -5
- package/src/utils/containing-blocks.ts +57 -0
- package/src/utils/contains.test.ts +55 -0
- package/src/utils/contains.ts +19 -0
- package/src/utils/deduplicate-nodes.ts +3 -0
- package/src/utils/dom-helpers.ts +16 -0
- package/src/utils/ensure-non-empty.ts +6 -0
- package/src/utils/get-element-position.ts +23 -7
- package/src/utils/get-scan-context.test.ts +79 -0
- package/src/utils/get-scan-context.ts +39 -0
- package/src/utils/is-node-in-scan-context.test.ts +70 -0
- package/src/utils/is-node-in-scan-context.ts +29 -0
- package/src/utils/normalize-context.test.ts +105 -0
- package/src/utils/normalize-context.ts +58 -0
- package/src/utils/update-elements-with-issues.test.ts +61 -8
- package/src/utils/update-elements-with-issues.ts +42 -3
- package/src/validate-options.ts +88 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { suite, test } from 'node:test';
|
|
4
|
+
import normalizeContext from './normalize-context';
|
|
5
|
+
|
|
6
|
+
suite('normalizeContext', () => {
|
|
7
|
+
test('when document is passed, only document is returned in include', () => {
|
|
8
|
+
const dom = new JSDOM('<div id="test"></div>');
|
|
9
|
+
const { document } = dom.window;
|
|
10
|
+
const normalizedContext = normalizeContext(document);
|
|
11
|
+
|
|
12
|
+
assert.deepEqual(normalizedContext, {
|
|
13
|
+
include: [document],
|
|
14
|
+
exclude: []
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('when an element is passed, only that element is returned in include', () => {
|
|
19
|
+
const dom = new JSDOM('<div id="test"></div>');
|
|
20
|
+
const { document } = dom.window;
|
|
21
|
+
const element = document.querySelector('#test')!;
|
|
22
|
+
const normalizedContext = normalizeContext(element);
|
|
23
|
+
|
|
24
|
+
assert.deepEqual(normalizedContext, {
|
|
25
|
+
include: [element],
|
|
26
|
+
exclude: []
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('when a selector is passed and elements matching that selector exist, all those elements are included', () => {
|
|
31
|
+
const dom = new JSDOM(`<div>
|
|
32
|
+
<div class="matches"></div>
|
|
33
|
+
<div class="matches"></div>
|
|
34
|
+
<div class="doesnt-match"></div>
|
|
35
|
+
</div>`);
|
|
36
|
+
const { document } = dom.window;
|
|
37
|
+
global.document = document;
|
|
38
|
+
const matchingElements = Array.from(document.querySelectorAll('.matches'));
|
|
39
|
+
const normalizedContext = normalizeContext('.matches');
|
|
40
|
+
|
|
41
|
+
assert.equal(matchingElements.length, 2);
|
|
42
|
+
assert.deepEqual(normalizedContext, {
|
|
43
|
+
include: matchingElements,
|
|
44
|
+
exclude: []
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('when a selector is passed and no elements matching that selector exist, `include` is empty', () => {
|
|
49
|
+
const dom = new JSDOM(`<div>
|
|
50
|
+
<div class="doesnt-match"></div>
|
|
51
|
+
<div class="doesnt-match"></div>
|
|
52
|
+
<div class="doesnt-match"></div>
|
|
53
|
+
</div>`);
|
|
54
|
+
const { document } = dom.window;
|
|
55
|
+
global.document = document;
|
|
56
|
+
const normalizedContext = normalizeContext('.matches');
|
|
57
|
+
|
|
58
|
+
assert.deepEqual(normalizedContext, {
|
|
59
|
+
include: [],
|
|
60
|
+
exclude: []
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('when a node list is passed, all the nodes from the list are included', () => {
|
|
65
|
+
const dom = new JSDOM(`<div>
|
|
66
|
+
<div class="matches"></div>
|
|
67
|
+
<div class="matches"></div>
|
|
68
|
+
<div class="doesnt-match"></div>
|
|
69
|
+
</div>`);
|
|
70
|
+
const { document } = dom.window;
|
|
71
|
+
const matchingElements = document.querySelectorAll('.matches');
|
|
72
|
+
const normalizedContext = normalizeContext(matchingElements);
|
|
73
|
+
|
|
74
|
+
assert.equal(matchingElements.length, 2);
|
|
75
|
+
assert.deepEqual(normalizedContext, {
|
|
76
|
+
include: Array.from(matchingElements),
|
|
77
|
+
exclude: []
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('when an object with `fromShadowDom` is passed, all the matching nodes are included', () => {
|
|
82
|
+
const dom = new JSDOM(`<div>
|
|
83
|
+
<div class="host"></div>
|
|
84
|
+
<div class="host"></div>
|
|
85
|
+
</div>`);
|
|
86
|
+
const { document } = dom.window;
|
|
87
|
+
global.document = document;
|
|
88
|
+
const hosts = document.querySelectorAll('.host');
|
|
89
|
+
const matchingElements = [];
|
|
90
|
+
for (const host of hosts) {
|
|
91
|
+
const shadowRoot = host.attachShadow({ mode: 'open' });
|
|
92
|
+
const matchingElement = document.createElement('div');
|
|
93
|
+
matchingElement.classList.add('matches');
|
|
94
|
+
shadowRoot.appendChild(matchingElement);
|
|
95
|
+
matchingElements.push(matchingElement);
|
|
96
|
+
}
|
|
97
|
+
const normalizedContext = normalizeContext({fromShadowDom: ['.host', '.matches']});
|
|
98
|
+
|
|
99
|
+
assert.equal(matchingElements.length, 2);
|
|
100
|
+
assert.deepEqual(normalizedContext, {
|
|
101
|
+
include: matchingElements,
|
|
102
|
+
exclude: []
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Context, ContextProp, Selector, ScanContext } from '../types';
|
|
2
|
+
import { isNode, isNodeList } from './dom-helpers.js';
|
|
3
|
+
import { deduplicateNodes } from './deduplicate-nodes.js';
|
|
4
|
+
|
|
5
|
+
function recursiveSelectAll(selectors: Array<string>, root: Document | ShadowRoot): Array<Node> {
|
|
6
|
+
const nodesOnCurrentLevel = root.querySelectorAll(selectors[0]!);
|
|
7
|
+
if (selectors.length === 1) {
|
|
8
|
+
return Array.from(nodesOnCurrentLevel);
|
|
9
|
+
}
|
|
10
|
+
const restSelectors: Array<string> = selectors.slice(1);
|
|
11
|
+
const selected = [];
|
|
12
|
+
for (const node of nodesOnCurrentLevel) {
|
|
13
|
+
if (node.shadowRoot) {
|
|
14
|
+
selected.push(...recursiveSelectAll(restSelectors, node.shadowRoot));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return selected;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function selectorToNodes(selector: Selector): Array<Node> {
|
|
21
|
+
if (typeof selector === 'string') {
|
|
22
|
+
return recursiveSelectAll([selector], document);
|
|
23
|
+
} else if (isNode(selector)) {
|
|
24
|
+
return [selector];
|
|
25
|
+
} else {
|
|
26
|
+
return recursiveSelectAll(selector.fromShadowDom, document);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function contextPropToNodes(contextProp: ContextProp): Array<Node> {
|
|
31
|
+
let nodes: Array<Node> = [];
|
|
32
|
+
if (typeof contextProp === 'object' && (Array.isArray(contextProp) || isNodeList(contextProp))) {
|
|
33
|
+
nodes = Array.from(contextProp).map(item => selectorToNodes(item)).flat();
|
|
34
|
+
} else {
|
|
35
|
+
nodes = selectorToNodes(contextProp);
|
|
36
|
+
}
|
|
37
|
+
return deduplicateNodes(nodes);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function normalizeContext(context: Context): ScanContext {
|
|
41
|
+
let contextInclude: Array<Node> = [];
|
|
42
|
+
let contextExclude: Array<Node> = [];
|
|
43
|
+
if (typeof context === 'object' && ('include' in context || 'exclude' in context)) {
|
|
44
|
+
if (context.include !== undefined) {
|
|
45
|
+
contextInclude = contextPropToNodes(context.include);
|
|
46
|
+
}
|
|
47
|
+
if (context.exclude !== undefined) {
|
|
48
|
+
contextExclude = contextPropToNodes(context.exclude);
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
contextInclude = contextPropToNodes(context);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
include: contextInclude,
|
|
56
|
+
exclude: contextExclude
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -18,7 +18,8 @@ const win: Window & { CSS: typeof CSS } = {
|
|
|
18
18
|
setProperty: () => {}
|
|
19
19
|
},
|
|
20
20
|
dataset: {}
|
|
21
|
-
})
|
|
21
|
+
}),
|
|
22
|
+
contains: () => true,
|
|
22
23
|
},
|
|
23
24
|
// @ts-expect-error we're missing a lot of properties
|
|
24
25
|
getComputedStyle: () => ({
|
|
@@ -41,7 +42,8 @@ const baseElement = {
|
|
|
41
42
|
getRootNode,
|
|
42
43
|
style: {
|
|
43
44
|
getPropertyValue: () => ''
|
|
44
|
-
}
|
|
45
|
+
},
|
|
46
|
+
closest: () => null,
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
// @ts-expect-error element is not HTMLElement
|
|
@@ -144,6 +146,11 @@ const issue3: Issue = {
|
|
|
144
146
|
...commonIssueProps
|
|
145
147
|
};
|
|
146
148
|
|
|
149
|
+
const scanContext = {
|
|
150
|
+
include: [win.document],
|
|
151
|
+
exclude: []
|
|
152
|
+
}
|
|
153
|
+
|
|
147
154
|
suite('updateElementsWithIssues', () => {
|
|
148
155
|
test('no changes', () => {
|
|
149
156
|
const extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>> = signal([
|
|
@@ -151,6 +158,7 @@ suite('updateElementsWithIssues', () => {
|
|
|
151
158
|
id: 1,
|
|
152
159
|
element: element1,
|
|
153
160
|
rootNode,
|
|
161
|
+
skipRender: false,
|
|
154
162
|
position,
|
|
155
163
|
visible,
|
|
156
164
|
trigger,
|
|
@@ -162,6 +170,7 @@ suite('updateElementsWithIssues', () => {
|
|
|
162
170
|
id: 2,
|
|
163
171
|
element: element2,
|
|
164
172
|
rootNode,
|
|
173
|
+
skipRender: false,
|
|
165
174
|
position,
|
|
166
175
|
visible,
|
|
167
176
|
trigger,
|
|
@@ -170,7 +179,13 @@ suite('updateElementsWithIssues', () => {
|
|
|
170
179
|
issues: signal([issue2])
|
|
171
180
|
}
|
|
172
181
|
]);
|
|
173
|
-
updateElementsWithIssues(
|
|
182
|
+
updateElementsWithIssues({
|
|
183
|
+
extendedElementsWithIssues,
|
|
184
|
+
scanContext,
|
|
185
|
+
violations: [violation1, violation2],
|
|
186
|
+
win,
|
|
187
|
+
name: 'accented'
|
|
188
|
+
});
|
|
174
189
|
assert.equal(extendedElementsWithIssues.value.length, 2);
|
|
175
190
|
assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
|
|
176
191
|
assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
|
|
@@ -184,6 +199,7 @@ suite('updateElementsWithIssues', () => {
|
|
|
184
199
|
id: 1,
|
|
185
200
|
element: element1,
|
|
186
201
|
rootNode,
|
|
202
|
+
skipRender: false,
|
|
187
203
|
position,
|
|
188
204
|
visible,
|
|
189
205
|
trigger,
|
|
@@ -195,6 +211,7 @@ suite('updateElementsWithIssues', () => {
|
|
|
195
211
|
id: 2,
|
|
196
212
|
element: element2,
|
|
197
213
|
rootNode,
|
|
214
|
+
skipRender: false,
|
|
198
215
|
position,
|
|
199
216
|
visible,
|
|
200
217
|
trigger,
|
|
@@ -203,7 +220,13 @@ suite('updateElementsWithIssues', () => {
|
|
|
203
220
|
issues: signal([issue2])
|
|
204
221
|
}
|
|
205
222
|
]);
|
|
206
|
-
updateElementsWithIssues(
|
|
223
|
+
updateElementsWithIssues({
|
|
224
|
+
extendedElementsWithIssues,
|
|
225
|
+
scanContext,
|
|
226
|
+
violations: [violation1, violation2, violation3],
|
|
227
|
+
win,
|
|
228
|
+
name: 'accented'
|
|
229
|
+
});
|
|
207
230
|
assert.equal(extendedElementsWithIssues.value.length, 2);
|
|
208
231
|
assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
|
|
209
232
|
assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
|
|
@@ -217,6 +240,7 @@ suite('updateElementsWithIssues', () => {
|
|
|
217
240
|
id: 1,
|
|
218
241
|
element: element1,
|
|
219
242
|
rootNode,
|
|
243
|
+
skipRender: false,
|
|
220
244
|
position,
|
|
221
245
|
visible,
|
|
222
246
|
trigger,
|
|
@@ -228,6 +252,7 @@ suite('updateElementsWithIssues', () => {
|
|
|
228
252
|
id: 2,
|
|
229
253
|
element: element2,
|
|
230
254
|
rootNode,
|
|
255
|
+
skipRender: false,
|
|
231
256
|
position,
|
|
232
257
|
visible,
|
|
233
258
|
trigger,
|
|
@@ -236,7 +261,13 @@ suite('updateElementsWithIssues', () => {
|
|
|
236
261
|
issues: signal([issue2, issue3])
|
|
237
262
|
}
|
|
238
263
|
]);
|
|
239
|
-
updateElementsWithIssues(
|
|
264
|
+
updateElementsWithIssues({
|
|
265
|
+
extendedElementsWithIssues,
|
|
266
|
+
scanContext,
|
|
267
|
+
violations: [violation1, violation2],
|
|
268
|
+
win,
|
|
269
|
+
name: 'accented'
|
|
270
|
+
});
|
|
240
271
|
assert.equal(extendedElementsWithIssues.value.length, 2);
|
|
241
272
|
assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
|
|
242
273
|
assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
|
|
@@ -250,6 +281,7 @@ suite('updateElementsWithIssues', () => {
|
|
|
250
281
|
id: 1,
|
|
251
282
|
element: element1,
|
|
252
283
|
rootNode,
|
|
284
|
+
skipRender: false,
|
|
253
285
|
position,
|
|
254
286
|
visible,
|
|
255
287
|
trigger,
|
|
@@ -258,7 +290,13 @@ suite('updateElementsWithIssues', () => {
|
|
|
258
290
|
issues: signal([issue1])
|
|
259
291
|
}
|
|
260
292
|
]);
|
|
261
|
-
updateElementsWithIssues(
|
|
293
|
+
updateElementsWithIssues({
|
|
294
|
+
extendedElementsWithIssues,
|
|
295
|
+
scanContext,
|
|
296
|
+
violations: [violation1, violation2],
|
|
297
|
+
win,
|
|
298
|
+
name: 'accented'
|
|
299
|
+
});
|
|
262
300
|
assert.equal(extendedElementsWithIssues.value.length, 2);
|
|
263
301
|
assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
|
|
264
302
|
assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
|
|
@@ -272,6 +310,7 @@ suite('updateElementsWithIssues', () => {
|
|
|
272
310
|
id: 1,
|
|
273
311
|
element: element1,
|
|
274
312
|
rootNode,
|
|
313
|
+
skipRender: false,
|
|
275
314
|
position,
|
|
276
315
|
visible,
|
|
277
316
|
trigger,
|
|
@@ -280,7 +319,13 @@ suite('updateElementsWithIssues', () => {
|
|
|
280
319
|
issues: signal([issue1])
|
|
281
320
|
}
|
|
282
321
|
]);
|
|
283
|
-
updateElementsWithIssues(
|
|
322
|
+
updateElementsWithIssues({
|
|
323
|
+
extendedElementsWithIssues,
|
|
324
|
+
scanContext,
|
|
325
|
+
violations: [violation1, violation4],
|
|
326
|
+
win,
|
|
327
|
+
name: 'accented'
|
|
328
|
+
});
|
|
284
329
|
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
285
330
|
assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
|
|
286
331
|
});
|
|
@@ -291,6 +336,7 @@ suite('updateElementsWithIssues', () => {
|
|
|
291
336
|
id: 1,
|
|
292
337
|
element: element1,
|
|
293
338
|
rootNode,
|
|
339
|
+
skipRender: false,
|
|
294
340
|
position,
|
|
295
341
|
visible,
|
|
296
342
|
trigger,
|
|
@@ -302,6 +348,7 @@ suite('updateElementsWithIssues', () => {
|
|
|
302
348
|
id: 2,
|
|
303
349
|
element: element2,
|
|
304
350
|
rootNode,
|
|
351
|
+
skipRender: false,
|
|
305
352
|
position,
|
|
306
353
|
visible,
|
|
307
354
|
trigger,
|
|
@@ -310,7 +357,13 @@ suite('updateElementsWithIssues', () => {
|
|
|
310
357
|
issues: signal([issue2])
|
|
311
358
|
}
|
|
312
359
|
]);
|
|
313
|
-
updateElementsWithIssues(
|
|
360
|
+
updateElementsWithIssues({
|
|
361
|
+
extendedElementsWithIssues,
|
|
362
|
+
scanContext,
|
|
363
|
+
violations: [violation1],
|
|
364
|
+
win,
|
|
365
|
+
name: 'accented'
|
|
366
|
+
});
|
|
314
367
|
assert.equal(extendedElementsWithIssues.value.length, 1);
|
|
315
368
|
assert.equal(extendedElementsWithIssues.value[0]?.element, element1);
|
|
316
369
|
assert.equal(extendedElementsWithIssues.value[0]?.issues.value.length, 1);
|
|
@@ -1,19 +1,51 @@
|
|
|
1
1
|
import type { AxeResults } from 'axe-core';
|
|
2
2
|
import type { Signal } from '@preact/signals-core';
|
|
3
3
|
import { batch, signal } from '@preact/signals-core';
|
|
4
|
-
import type { ExtendedElementWithIssues } from '../types';
|
|
4
|
+
import type { ExtendedElementWithIssues, ScanContext } from '../types';
|
|
5
5
|
import transformViolations from './transform-violations.js';
|
|
6
6
|
import areElementsWithIssuesEqual from './are-elements-with-issues-equal.js';
|
|
7
7
|
import areIssueSetsEqual from './are-issue-sets-equal.js';
|
|
8
|
+
import isNodeInScanContext from './is-node-in-scan-context.js';
|
|
8
9
|
import type { AccentedTrigger } from '../elements/accented-trigger';
|
|
9
10
|
import type { AccentedDialog } from '../elements/accented-dialog';
|
|
10
11
|
import getElementPosition from './get-element-position.js';
|
|
11
12
|
import getScrollableAncestors from './get-scrollable-ancestors.js';
|
|
12
13
|
import supportsAnchorPositioning from './supports-anchor-positioning.js';
|
|
14
|
+
import { isSvgElement } from './dom-helpers.js';
|
|
15
|
+
import getParent from './get-parent.js';
|
|
16
|
+
|
|
17
|
+
function shouldSkipRender(element: Element): boolean {
|
|
18
|
+
|
|
19
|
+
// Skip rendering if the element is inside an SVG:
|
|
20
|
+
// https://github.com/pomerantsev/accented/issues/62
|
|
21
|
+
const parent = getParent(element);
|
|
22
|
+
const isInsideSvg = Boolean(parent && isSvgElement(parent));
|
|
23
|
+
|
|
24
|
+
// Some issues, such as meta-viewport, are on <head> descendants,
|
|
25
|
+
// but since <head> is never rendered, we don't want to output anything
|
|
26
|
+
// for those in the DOM.
|
|
27
|
+
// We're not anticipating the use of shadow DOM in <head>,
|
|
28
|
+
// so the use of .closest() should be fine.
|
|
29
|
+
const isInsideHead = element.closest('head') !== null;
|
|
30
|
+
|
|
31
|
+
return isInsideSvg || isInsideHead;
|
|
32
|
+
}
|
|
13
33
|
|
|
14
34
|
let count = 0;
|
|
15
35
|
|
|
16
|
-
export default function updateElementsWithIssues(
|
|
36
|
+
export default function updateElementsWithIssues({
|
|
37
|
+
extendedElementsWithIssues,
|
|
38
|
+
scanContext,
|
|
39
|
+
violations,
|
|
40
|
+
win,
|
|
41
|
+
name
|
|
42
|
+
}: {
|
|
43
|
+
extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>>,
|
|
44
|
+
scanContext: ScanContext,
|
|
45
|
+
violations: typeof AxeResults.violations,
|
|
46
|
+
win: Window & { CSS: typeof CSS },
|
|
47
|
+
name: string
|
|
48
|
+
}) {
|
|
17
49
|
const updatedElementsWithIssues = transformViolations(violations, name);
|
|
18
50
|
|
|
19
51
|
batch(() => {
|
|
@@ -28,8 +60,14 @@ export default function updateElementsWithIssues(extendedElementsWithIssues: Sig
|
|
|
28
60
|
return !extendedElementsWithIssues.value.some(extendedElementWithIssues => areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues));
|
|
29
61
|
});
|
|
30
62
|
|
|
63
|
+
// Only consider an element to be removed in two cases:
|
|
64
|
+
// 1. It has been removed from the DOM.
|
|
65
|
+
// 2. It is within the scan context, but not among updatedElementsWithIssues.
|
|
31
66
|
const removedElementsWithIssues = extendedElementsWithIssues.value.filter(extendedElementWithIssues => {
|
|
32
|
-
|
|
67
|
+
const isConnected = extendedElementWithIssues.element.isConnected;
|
|
68
|
+
const hasNoMoreIssues = isNodeInScanContext(extendedElementWithIssues.element, scanContext)
|
|
69
|
+
&& !updatedElementsWithIssues.some(updatedElementWithIssues => areElementsWithIssuesEqual(updatedElementWithIssues, extendedElementWithIssues));
|
|
70
|
+
return !isConnected || hasNoMoreIssues;
|
|
33
71
|
});
|
|
34
72
|
|
|
35
73
|
if (addedElementsWithIssues.length > 0 || removedElementsWithIssues.length > 0) {
|
|
@@ -63,6 +101,7 @@ export default function updateElementsWithIssues(extendedElementsWithIssues: Sig
|
|
|
63
101
|
return {
|
|
64
102
|
id,
|
|
65
103
|
element: addedElementWithIssues.element,
|
|
104
|
+
skipRender: shouldSkipRender(addedElementWithIssues.element),
|
|
66
105
|
rootNode: addedElementWithIssues.rootNode,
|
|
67
106
|
visible: trigger.visible,
|
|
68
107
|
position: trigger.position,
|
package/src/validate-options.ts
CHANGED
|
@@ -1,5 +1,89 @@
|
|
|
1
|
-
import type { AccentedOptions } from './types';
|
|
1
|
+
import type { Selector, SelectorList, ContextProp, ContextObject, AccentedOptions, Context } from './types';
|
|
2
2
|
import { allowedAxeOptions } from './types.js';
|
|
3
|
+
import { isNode, isNodeList } from './utils/dom-helpers.js';
|
|
4
|
+
|
|
5
|
+
function isSelector(contextFragment: Context): contextFragment is Selector {
|
|
6
|
+
return typeof contextFragment === 'string'
|
|
7
|
+
|| isNode(contextFragment)
|
|
8
|
+
|| 'fromShadowDom' in contextFragment;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function validateSelector(selector: Selector) {
|
|
12
|
+
if (typeof selector === 'string') {
|
|
13
|
+
return;
|
|
14
|
+
} else if (isNode(selector)) {
|
|
15
|
+
return;
|
|
16
|
+
} else if ('fromShadowDom' in selector) {
|
|
17
|
+
if (!Array.isArray(selector.fromShadowDom)
|
|
18
|
+
|| selector.fromShadowDom.length < 2 ||
|
|
19
|
+
!selector.fromShadowDom.every(item => typeof item === 'string')
|
|
20
|
+
) {
|
|
21
|
+
throw new TypeError(`Accented: invalid argument. \`fromShadowDom\` must be an array of strings with at least 2 elements. It’s currently set to ${selector.fromShadowDom}.`);
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
} else {
|
|
25
|
+
const neverSelector: never = selector;
|
|
26
|
+
throw new TypeError(`Accented: invalid argument. The selector must be one of: string, Node, or an object with a \`fromShadowDom\` property. It’s currently set to ${neverSelector}.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isSelectorList(contextFragment: Context): contextFragment is SelectorList {
|
|
31
|
+
return (typeof contextFragment === 'object' && isNodeList(contextFragment))
|
|
32
|
+
|| (Array.isArray(contextFragment) && contextFragment.every(item => isSelector(item)));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function validateSelectorList(selectorList: SelectorList) {
|
|
36
|
+
if (isNodeList(selectorList)) {
|
|
37
|
+
return;
|
|
38
|
+
} else if (Array.isArray(selectorList)) {
|
|
39
|
+
for (const selector of selectorList) {
|
|
40
|
+
validateSelector(selector);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
const neverSelectorList: never = selectorList;
|
|
44
|
+
throw new TypeError(`Accented: invalid argument. The selector list must either be a NodeList or an array. It’s currently set to ${neverSelectorList}.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isContextProp(contextFragment: Context): contextFragment is ContextProp {
|
|
49
|
+
return isSelector(contextFragment) || isSelectorList(contextFragment);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function validateContextProp(context: Selector | SelectorList) {
|
|
53
|
+
if (isSelector(context)) {
|
|
54
|
+
validateSelector(context);
|
|
55
|
+
} else if (isSelectorList(context)) {
|
|
56
|
+
validateSelectorList(context);
|
|
57
|
+
} else {
|
|
58
|
+
const neverContext: never = context;
|
|
59
|
+
throw new TypeError(`Accented: invalid argument. The context property must either be a selector or a selector list. It’s currently set to ${neverContext}.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isContextObject(contextFragment: Context): contextFragment is ContextObject {
|
|
64
|
+
return typeof contextFragment === 'object' && contextFragment !== null
|
|
65
|
+
&& ('include' in contextFragment || 'exclude' in contextFragment);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validateContextObject(contextObject: ContextObject) {
|
|
69
|
+
if ('include' in contextObject) {
|
|
70
|
+
validateContextProp(contextObject.include!);
|
|
71
|
+
}
|
|
72
|
+
if ('exclude' in contextObject) {
|
|
73
|
+
validateContextProp(contextObject.exclude!);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateContext(context: Context) {
|
|
78
|
+
if (isContextProp(context)) {
|
|
79
|
+
validateContextProp(context);
|
|
80
|
+
} else if (isContextObject(context)) {
|
|
81
|
+
validateContextObject(context);
|
|
82
|
+
} else {
|
|
83
|
+
const neverContext: never = context;
|
|
84
|
+
throw new TypeError(`Accented: invalid context argument. It’s currently set to ${neverContext}.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
3
87
|
|
|
4
88
|
// The space of valid CSS and HTML names is wider than this,
|
|
5
89
|
// but with Unicode it gets complicated quickly, so I'm sticking to only allowing
|
|
@@ -41,4 +125,7 @@ export default function validateOptions(options: AccentedOptions) {
|
|
|
41
125
|
throw new TypeError(`Accented: invalid argument. \`axeOptions\` contains the following unsupported keys: ${unsupportedKeys.join(', ')}. Valid options are: ${allowedAxeOptions.join(', ')}.`);
|
|
42
126
|
}
|
|
43
127
|
}
|
|
128
|
+
if (options.context !== undefined) {
|
|
129
|
+
validateContext(options.context);
|
|
130
|
+
}
|
|
44
131
|
}
|