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.
Files changed (67) hide show
  1. package/README.md +2 -2
  2. package/dist/accented.d.ts.map +1 -1
  3. package/dist/accented.js +2 -4
  4. package/dist/accented.js.map +1 -1
  5. package/dist/dom-updater.js +6 -6
  6. package/dist/dom-updater.js.map +1 -1
  7. package/dist/elements/accented-dialog.d.ts +2 -2
  8. package/dist/elements/accented-dialog.d.ts.map +1 -1
  9. package/dist/elements/accented-dialog.js +15 -14
  10. package/dist/elements/accented-dialog.js.map +1 -1
  11. package/dist/elements/accented-trigger.d.ts +6 -6
  12. package/dist/elements/accented-trigger.d.ts.map +1 -1
  13. package/dist/elements/accented-trigger.js +2 -2
  14. package/dist/elements/accented-trigger.js.map +1 -1
  15. package/dist/intersection-observer.js +2 -2
  16. package/dist/intersection-observer.js.map +1 -1
  17. package/dist/logger.d.ts +1 -4
  18. package/dist/logger.d.ts.map +1 -1
  19. package/dist/logger.js +15 -15
  20. package/dist/logger.js.map +1 -1
  21. package/dist/scanner.d.ts.map +1 -1
  22. package/dist/scanner.js +4 -5
  23. package/dist/scanner.js.map +1 -1
  24. package/dist/state.js +4 -4
  25. package/dist/state.js.map +1 -1
  26. package/dist/utils/get-element-position.d.ts +1 -1
  27. package/dist/utils/get-element-position.d.ts.map +1 -1
  28. package/dist/utils/get-element-position.js +6 -6
  29. package/dist/utils/get-element-position.js.map +1 -1
  30. package/dist/utils/get-scrollable-ancestors.d.ts +1 -1
  31. package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -1
  32. package/dist/utils/get-scrollable-ancestors.js +2 -2
  33. package/dist/utils/get-scrollable-ancestors.js.map +1 -1
  34. package/dist/utils/recalculate-positions.js +1 -1
  35. package/dist/utils/recalculate-positions.js.map +1 -1
  36. package/dist/utils/recalculate-scrollable-ancestors.js +1 -1
  37. package/dist/utils/recalculate-scrollable-ancestors.js.map +1 -1
  38. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts +4 -4
  39. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -1
  40. package/dist/utils/shadow-dom-aware-mutation-observer.js +29 -29
  41. package/dist/utils/shadow-dom-aware-mutation-observer.js.map +1 -1
  42. package/dist/utils/supports-anchor-positioning.d.ts +1 -5
  43. package/dist/utils/supports-anchor-positioning.d.ts.map +1 -1
  44. package/dist/utils/supports-anchor-positioning.js +4 -6
  45. package/dist/utils/supports-anchor-positioning.js.map +1 -1
  46. package/dist/utils/update-elements-with-issues.d.ts +1 -4
  47. package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
  48. package/dist/utils/update-elements-with-issues.js +8 -10
  49. package/dist/utils/update-elements-with-issues.js.map +1 -1
  50. package/package.json +5 -5
  51. package/src/accented.ts +2 -4
  52. package/src/dom-updater.ts +6 -6
  53. package/src/elements/accented-dialog.ts +16 -17
  54. package/src/elements/accented-trigger.ts +2 -2
  55. package/src/intersection-observer.ts +2 -2
  56. package/src/logger.ts +15 -15
  57. package/src/scanner.ts +4 -5
  58. package/src/state.ts +5 -5
  59. package/src/utils/get-element-position.ts +6 -6
  60. package/src/utils/get-scrollable-ancestors.ts +2 -2
  61. package/src/utils/recalculate-positions.ts +1 -1
  62. package/src/utils/recalculate-scrollable-ancestors.ts +1 -1
  63. package/src/utils/shadow-dom-aware-mutation-observer.test.ts +413 -0
  64. package/src/utils/shadow-dom-aware-mutation-observer.ts +36 -30
  65. package/src/utils/supports-anchor-positioning.ts +4 -10
  66. package/src/utils/update-elements-with-issues.test.ts +29 -54
  67. 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 Set();
28
+ #shadowRoots: ObserverMap = new Map();
7
29
 
8
30
  #options: MutationObserverInit | undefined;
9
31
 
10
- constructor(callback: MutationCallback) {
32
+ constructor(mutationCallback: MutationCallback) {
11
33
  super((mutations, observer) => {
12
- const accentedElementNames = getAccentedElementNames(name);
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 = childListMutations
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
- // Mutation observer has no "unobserve" method, so we're simply deleting
28
- // the elements from the set of shadow roots.
29
- this.#deleteShadowRoots(removedElements);
40
+ this.#unobserveShadowRoots(removedElements);
30
41
 
31
- callback(mutations, observer);
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
- if (shadowRoot) {
56
- this.#shadowRoots.add(shadowRoot);
57
- this.observe(shadowRoot, this.#options);
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
- #deleteShadowRoots = (elements: Array<Element | Document | DocumentFragment>) => {
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(win: Window) {
10
- const ua = win.navigator.userAgent;
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(win: WindowWithCSS) {
13
+ export function supportsAnchorPositioning() {
18
14
  return (
19
- win.CSS.supports('anchor-name: --foo') &&
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
- const win: Window & { CSS: typeof CSS } = {
14
- document: {
15
- // @ts-expect-error the return value is of incorrect type.
16
- createElement: () => ({
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 getBoundingClientRect = () => ({});
41
-
42
- const getRootNode = (): Node => ({}) as Node;
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
- // @ts-expect-error element is not HTMLElement
54
- const element1: HTMLElement = { ...baseElement, isConnected: true };
55
- // @ts-expect-error element is not HTMLElement
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
- // @ts-expect-error rootNode is not Node
61
- const rootNode: Node = {};
42
+ const rootNode = document;
62
43
 
63
- const trigger = win.document.createElement('accented-trigger') as AccentedTrigger;
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: [win.document],
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 = win.document.createElement(`${name}-trigger`) as AccentedTrigger;
100
+ const trigger = document.createElement(`${name}-trigger`) as AccentedTrigger;
103
101
  const elementZIndex = Number.parseInt(
104
- win.getComputedStyle(addedElementWithIssues.element).zIndex,
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 = win.document.createElement(`${name}-dialog`) as AccentedDialog;
110
+ const accentedDialog = document.createElement(`${name}-dialog`) as AccentedDialog;
113
111
  trigger.dialog = accentedDialog;
114
- const position = getElementPosition(addedElementWithIssues.element, win);
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(win)
116
+ const scrollableAncestors = supportsAnchorPositioning()
119
117
  ? new Set<HTMLElement>()
120
- : getScrollableAncestors(addedElementWithIssues.element, win);
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
- win
135
- .getComputedStyle(addedElementWithIssues.element)
136
- .getPropertyValue('anchor-name'),
132
+ getComputedStyle(addedElementWithIssues.element).getPropertyValue('anchor-name'),
137
133
  trigger,
138
134
  issues,
139
135
  };