accented 0.0.2 → 1.0.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 (212) hide show
  1. package/NOTICE +14 -0
  2. package/README.md +44 -187
  3. package/dist/accented.d.ts +8 -8
  4. package/dist/accented.d.ts.map +1 -1
  5. package/dist/accented.js +37 -30
  6. package/dist/accented.js.map +1 -1
  7. package/dist/common/tokens.d.ts +7 -0
  8. package/dist/common/tokens.d.ts.map +1 -0
  9. package/dist/common/tokens.js +8 -0
  10. package/dist/common/tokens.js.map +1 -0
  11. package/dist/constants.d.ts +2 -1
  12. package/dist/constants.d.ts.map +1 -1
  13. package/dist/constants.js +2 -1
  14. package/dist/constants.js.map +1 -1
  15. package/dist/dom-updater.d.ts +1 -1
  16. package/dist/dom-updater.d.ts.map +1 -1
  17. package/dist/dom-updater.js +73 -31
  18. package/dist/dom-updater.js.map +1 -1
  19. package/dist/elements/accented-dialog.d.ts +13 -10
  20. package/dist/elements/accented-dialog.d.ts.map +1 -1
  21. package/dist/elements/accented-dialog.js +110 -94
  22. package/dist/elements/accented-dialog.js.map +1 -1
  23. package/dist/elements/accented-trigger.d.ts +14 -9
  24. package/dist/elements/accented-trigger.d.ts.map +1 -1
  25. package/dist/elements/accented-trigger.js +77 -22
  26. package/dist/elements/accented-trigger.js.map +1 -1
  27. package/dist/fullscreen-listener.d.ts +2 -0
  28. package/dist/fullscreen-listener.d.ts.map +1 -0
  29. package/dist/fullscreen-listener.js +17 -0
  30. package/dist/fullscreen-listener.js.map +1 -0
  31. package/dist/intersection-observer.d.ts +1 -1
  32. package/dist/intersection-observer.d.ts.map +1 -1
  33. package/dist/intersection-observer.js +12 -6
  34. package/dist/intersection-observer.js.map +1 -1
  35. package/dist/log-and-rethrow.d.ts +1 -1
  36. package/dist/log-and-rethrow.d.ts.map +1 -1
  37. package/dist/log-and-rethrow.js +2 -3
  38. package/dist/log-and-rethrow.js.map +1 -1
  39. package/dist/logger.d.ts +1 -1
  40. package/dist/logger.d.ts.map +1 -1
  41. package/dist/logger.js +6 -3
  42. package/dist/logger.js.map +1 -1
  43. package/dist/register-elements.d.ts +1 -1
  44. package/dist/register-elements.d.ts.map +1 -1
  45. package/dist/register-elements.js +6 -7
  46. package/dist/register-elements.js.map +1 -1
  47. package/dist/resize-listener.d.ts +1 -1
  48. package/dist/resize-listener.d.ts.map +1 -1
  49. package/dist/resize-listener.js +3 -4
  50. package/dist/resize-listener.js.map +1 -1
  51. package/dist/scanner.d.ts +2 -2
  52. package/dist/scanner.d.ts.map +1 -1
  53. package/dist/scanner.js +76 -43
  54. package/dist/scanner.js.map +1 -1
  55. package/dist/scroll-listeners.d.ts +1 -1
  56. package/dist/scroll-listeners.d.ts.map +1 -1
  57. package/dist/scroll-listeners.js +3 -4
  58. package/dist/scroll-listeners.js.map +1 -1
  59. package/dist/state.d.ts +3 -2
  60. package/dist/state.d.ts.map +1 -1
  61. package/dist/state.js +5 -3
  62. package/dist/state.js.map +1 -1
  63. package/dist/task-queue.d.ts +4 -4
  64. package/dist/task-queue.d.ts.map +1 -1
  65. package/dist/task-queue.js +3 -2
  66. package/dist/task-queue.js.map +1 -1
  67. package/dist/types.d.ts +140 -49
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js.map +1 -1
  70. package/dist/utils/are-elements-with-issues-equal.d.ts +3 -0
  71. package/dist/utils/are-elements-with-issues-equal.d.ts.map +1 -0
  72. package/dist/utils/are-elements-with-issues-equal.js +5 -0
  73. package/dist/utils/are-elements-with-issues-equal.js.map +1 -0
  74. package/dist/utils/are-issue-sets-equal.d.ts +2 -2
  75. package/dist/utils/are-issue-sets-equal.d.ts.map +1 -1
  76. package/dist/utils/are-issue-sets-equal.js +3 -3
  77. package/dist/utils/are-issue-sets-equal.js.map +1 -1
  78. package/dist/utils/containing-blocks.d.ts +3 -0
  79. package/dist/utils/containing-blocks.d.ts.map +1 -0
  80. package/dist/utils/containing-blocks.js +46 -0
  81. package/dist/utils/containing-blocks.js.map +1 -0
  82. package/dist/utils/contains.d.ts +2 -0
  83. package/dist/utils/contains.d.ts.map +1 -0
  84. package/dist/utils/contains.js +19 -0
  85. package/dist/utils/contains.js.map +1 -0
  86. package/dist/utils/deduplicate-nodes.d.ts +2 -0
  87. package/dist/utils/deduplicate-nodes.d.ts.map +1 -0
  88. package/dist/utils/deduplicate-nodes.js +4 -0
  89. package/dist/utils/deduplicate-nodes.js.map +1 -0
  90. package/dist/utils/deep-merge.d.ts +1 -1
  91. package/dist/utils/deep-merge.d.ts.map +1 -1
  92. package/dist/utils/deep-merge.js +8 -5
  93. package/dist/utils/deep-merge.js.map +1 -1
  94. package/dist/utils/dom-helpers.d.ts +9 -0
  95. package/dist/utils/dom-helpers.d.ts.map +1 -0
  96. package/dist/utils/dom-helpers.js +34 -0
  97. package/dist/utils/dom-helpers.js.map +1 -0
  98. package/dist/utils/ensure-non-empty.d.ts +2 -0
  99. package/dist/utils/ensure-non-empty.d.ts.map +1 -0
  100. package/dist/utils/ensure-non-empty.js +7 -0
  101. package/dist/utils/ensure-non-empty.js.map +1 -0
  102. package/dist/utils/get-element-html.d.ts +1 -1
  103. package/dist/utils/get-element-html.d.ts.map +1 -1
  104. package/dist/utils/get-element-html.js +4 -2
  105. package/dist/utils/get-element-html.js.map +1 -1
  106. package/dist/utils/get-element-position.d.ts +10 -2
  107. package/dist/utils/get-element-position.d.ts.map +1 -1
  108. package/dist/utils/get-element-position.js +64 -16
  109. package/dist/utils/get-element-position.js.map +1 -1
  110. package/dist/utils/get-parent.d.ts +2 -0
  111. package/dist/utils/get-parent.d.ts.map +1 -0
  112. package/dist/utils/get-parent.js +12 -0
  113. package/dist/utils/get-parent.js.map +1 -0
  114. package/dist/utils/get-scan-context.d.ts +3 -0
  115. package/dist/utils/get-scan-context.d.ts.map +1 -0
  116. package/dist/utils/get-scan-context.js +28 -0
  117. package/dist/utils/get-scan-context.js.map +1 -0
  118. package/dist/utils/get-scrollable-ancestors.d.ts +1 -1
  119. package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -1
  120. package/dist/utils/get-scrollable-ancestors.js +10 -6
  121. package/dist/utils/get-scrollable-ancestors.js.map +1 -1
  122. package/dist/utils/is-node-in-scan-context.d.ts +3 -0
  123. package/dist/utils/is-node-in-scan-context.d.ts.map +1 -0
  124. package/dist/utils/is-node-in-scan-context.js +26 -0
  125. package/dist/utils/is-node-in-scan-context.js.map +1 -0
  126. package/dist/utils/is-non-empty.d.ts +2 -0
  127. package/dist/utils/is-non-empty.d.ts.map +1 -0
  128. package/dist/utils/is-non-empty.js +4 -0
  129. package/dist/utils/is-non-empty.js.map +1 -0
  130. package/dist/utils/normalize-context.d.ts +3 -0
  131. package/dist/utils/normalize-context.d.ts.map +1 -0
  132. package/dist/utils/normalize-context.js +59 -0
  133. package/dist/utils/normalize-context.js.map +1 -0
  134. package/dist/utils/recalculate-positions.d.ts +1 -1
  135. package/dist/utils/recalculate-positions.d.ts.map +1 -1
  136. package/dist/utils/recalculate-positions.js +5 -5
  137. package/dist/utils/recalculate-positions.js.map +1 -1
  138. package/dist/utils/recalculate-scrollable-ancestors.d.ts +1 -1
  139. package/dist/utils/recalculate-scrollable-ancestors.d.ts.map +1 -1
  140. package/dist/utils/recalculate-scrollable-ancestors.js +4 -4
  141. package/dist/utils/recalculate-scrollable-ancestors.js.map +1 -1
  142. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts +10 -0
  143. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -0
  144. package/dist/utils/shadow-dom-aware-mutation-observer.js +61 -0
  145. package/dist/utils/shadow-dom-aware-mutation-observer.js.map +1 -0
  146. package/dist/utils/supports-anchor-positioning.d.ts +1 -1
  147. package/dist/utils/supports-anchor-positioning.d.ts.map +1 -1
  148. package/dist/utils/supports-anchor-positioning.js +1 -1
  149. package/dist/utils/supports-anchor-positioning.js.map +1 -1
  150. package/dist/utils/transform-violations.d.ts +2 -2
  151. package/dist/utils/transform-violations.d.ts.map +1 -1
  152. package/dist/utils/transform-violations.js +23 -10
  153. package/dist/utils/transform-violations.js.map +1 -1
  154. package/dist/utils/update-elements-with-issues.d.ts +11 -5
  155. package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
  156. package/dist/utils/update-elements-with-issues.js +56 -24
  157. package/dist/utils/update-elements-with-issues.js.map +1 -1
  158. package/dist/validate-options.d.ts +2 -2
  159. package/dist/validate-options.d.ts.map +1 -1
  160. package/dist/validate-options.js +91 -4
  161. package/dist/validate-options.js.map +1 -1
  162. package/package.json +15 -7
  163. package/src/accented.test.ts +2 -2
  164. package/src/accented.ts +45 -34
  165. package/src/common/tokens.ts +10 -0
  166. package/src/constants.ts +2 -1
  167. package/src/dom-updater.ts +87 -34
  168. package/src/elements/accented-dialog.ts +157 -122
  169. package/src/elements/accented-trigger.ts +119 -47
  170. package/src/fullscreen-listener.ts +21 -0
  171. package/src/intersection-observer.ts +27 -16
  172. package/src/log-and-rethrow.ts +2 -3
  173. package/src/logger.ts +14 -4
  174. package/src/register-elements.ts +7 -7
  175. package/src/resize-listener.ts +15 -11
  176. package/src/scanner.ts +113 -57
  177. package/src/scroll-listeners.ts +27 -19
  178. package/src/state.ts +27 -16
  179. package/src/task-queue.test.ts +5 -4
  180. package/src/task-queue.ts +8 -6
  181. package/src/types.ts +179 -76
  182. package/src/utils/are-elements-with-issues-equal.ts +11 -0
  183. package/src/utils/are-issue-sets-equal.test.ts +10 -6
  184. package/src/utils/are-issue-sets-equal.ts +8 -6
  185. package/src/utils/containing-blocks.ts +60 -0
  186. package/src/utils/contains.test.ts +54 -0
  187. package/src/utils/contains.ts +19 -0
  188. package/src/utils/deduplicate-nodes.ts +3 -0
  189. package/src/utils/deep-merge.test.ts +8 -1
  190. package/src/utils/deep-merge.ts +14 -8
  191. package/src/utils/dom-helpers.ts +42 -0
  192. package/src/utils/ensure-non-empty.ts +6 -0
  193. package/src/utils/get-element-html.ts +4 -2
  194. package/src/utils/get-element-position.ts +84 -16
  195. package/src/utils/get-parent.ts +14 -0
  196. package/src/utils/get-scan-context.test.ts +85 -0
  197. package/src/utils/get-scan-context.ts +36 -0
  198. package/src/utils/get-scrollable-ancestors.ts +15 -7
  199. package/src/utils/is-node-in-scan-context.test.ts +70 -0
  200. package/src/utils/is-node-in-scan-context.ts +29 -0
  201. package/src/utils/is-non-empty.ts +3 -0
  202. package/src/utils/normalize-context.test.ts +105 -0
  203. package/src/utils/normalize-context.ts +65 -0
  204. package/src/utils/recalculate-positions.ts +5 -5
  205. package/src/utils/recalculate-scrollable-ancestors.ts +4 -4
  206. package/src/utils/shadow-dom-aware-mutation-observer.ts +75 -0
  207. package/src/utils/supports-anchor-positioning.ts +3 -3
  208. package/src/utils/transform-violations.test.ts +28 -24
  209. package/src/utils/transform-violations.ts +30 -12
  210. package/src/utils/update-elements-with-issues.test.ts +139 -51
  211. package/src/utils/update-elements-with-issues.ts +123 -54
  212. package/src/validate-options.ts +154 -14
@@ -0,0 +1,21 @@
1
+ import { logAndRethrow } from './log-and-rethrow.js';
2
+ import { recalculatePositions } from './utils/recalculate-positions.js';
3
+
4
+ export function setupResizeListener() {
5
+ const abortController = new AbortController();
6
+ window.addEventListener(
7
+ 'fullscreenchange',
8
+ () => {
9
+ try {
10
+ recalculatePositions();
11
+ } catch (error) {
12
+ logAndRethrow(error);
13
+ }
14
+ },
15
+ { signal: abortController.signal },
16
+ );
17
+
18
+ return () => {
19
+ abortController.abort();
20
+ };
21
+ }
@@ -1,28 +1,39 @@
1
- import logAndRethrow from './log-and-rethrow.js';
1
+ import { logAndRethrow } from './log-and-rethrow.js';
2
2
  import { extendedElementsWithIssues } from './state.js';
3
- import getElementPosition from './utils/get-element-position.js';
3
+ import { getElementPosition } from './utils/get-element-position.js';
4
+ import { supportsAnchorPositioning } from './utils/supports-anchor-positioning.js';
4
5
 
5
- export default function setupIntersectionObserver() {
6
- const intersectionObserver = new IntersectionObserver((entries) => {
7
- try {
8
- for (const entry of entries) {
9
- const extendedElementWithIssues = extendedElementsWithIssues.value.find(el => el.element === entry.target);
10
- if (extendedElementWithIssues) {
11
- extendedElementWithIssues.visible.value = entry.isIntersecting;
12
- if (entry.isIntersecting) {
13
- extendedElementWithIssues.position.value = getElementPosition(entry.target, window);
6
+ export function setupIntersectionObserver() {
7
+ const intersectionObserver = new IntersectionObserver(
8
+ (entries) => {
9
+ try {
10
+ for (const entry of entries) {
11
+ const extendedElementWithIssues = extendedElementsWithIssues.value.find(
12
+ (el) => el.element === entry.target,
13
+ );
14
+ if (extendedElementWithIssues) {
15
+ // We initially treated setting visibility in the intersection observer
16
+ // as a fallback option for browsers that don't support `position-visibility`,
17
+ // but then we realized that this `position-visibility` actually works
18
+ // in an unexpected way when the container has `overflow: visible`.
19
+ // So now we always set visibility in the intersection observer.
20
+ extendedElementWithIssues.visible.value = entry.isIntersecting;
21
+ if (entry.isIntersecting && !supportsAnchorPositioning(window)) {
22
+ extendedElementWithIssues.position.value = getElementPosition(entry.target, window);
23
+ }
14
24
  }
15
25
  }
26
+ } catch (error) {
27
+ logAndRethrow(error);
16
28
  }
17
- } catch (error) {
18
- logAndRethrow(error);
19
- }
20
- }, { threshold: 0 });
29
+ },
30
+ { threshold: 0 },
31
+ );
21
32
 
22
33
  return {
23
34
  intersectionObserver,
24
35
  disconnect: () => {
25
36
  intersectionObserver.disconnect();
26
- }
37
+ },
27
38
  };
28
39
  }
@@ -1,9 +1,8 @@
1
1
  import { issuesUrl } from './constants.js';
2
2
 
3
- export default function logAndRethrow(error: unknown) {
3
+ export function logAndRethrow(error: unknown) {
4
4
  console.error(
5
- `Accented threw an error (see below). Try updating your browser to the latest version. ` +
6
- `If you’re still seeing the error, file an issue at ${issuesUrl}.`
5
+ `Accented threw an error (see below). Try updating your browser to the latest version. If you’re still seeing the error, file an issue at ${issuesUrl}.`,
7
6
  );
8
7
  throw error;
9
8
  }
package/src/logger.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import { effect } from '@preact/signals-core';
2
- import { elementsWithIssues, enabled } from './state.js';
3
2
  import { accentedUrl } from './constants.js';
3
+ import { elementsWithIssues, enabled } from './state.js';
4
+ import type { ElementWithIssues } from './types.ts';
4
5
 
5
- export default function createLogger() {
6
+ function filterPropsForOutput(elements: Array<ElementWithIssues>) {
7
+ return elements.map(({ element, issues }) => ({ element, issues }));
8
+ }
6
9
 
10
+ export function createLogger() {
7
11
  let firstRun = true;
8
12
 
9
13
  return effect(() => {
@@ -13,8 +17,14 @@ export default function createLogger() {
13
17
 
14
18
  const elementCount = elementsWithIssues.value.length;
15
19
  if (elementCount > 0) {
16
- const issueCount = elementsWithIssues.value.reduce((acc, { issues }) => acc + issues.length, 0);
17
- console.log(`${issueCount} accessibility issue${issueCount === 1 ? '' : 's'} found in ${elementCount} element${issueCount === 1 ? '' : 's'} (Accented, ${accentedUrl}):\n`, elementsWithIssues.value);
20
+ const issueCount = elementsWithIssues.value.reduce(
21
+ (acc, { issues }) => acc + issues.length,
22
+ 0,
23
+ );
24
+ console.log(
25
+ `${issueCount} accessibility issue${issueCount === 1 ? '' : 's'} found in ${elementCount} element${issueCount === 1 ? '' : 's'} (Accented, ${accentedUrl}):\n`,
26
+ filterPropsForOutput(elementsWithIssues.value),
27
+ );
18
28
  } else {
19
29
  if (firstRun) {
20
30
  firstRun = false;
@@ -1,16 +1,16 @@
1
- import getAccentedTrigger from './elements/accented-trigger.js';
2
- import getAccentedDialog from './elements/accented-dialog.js';
1
+ import { getAccentedDialog } from './elements/accented-dialog.js';
2
+ import { getAccentedTrigger } from './elements/accented-trigger.js';
3
3
 
4
- export default function registerElements(name: string): void {
4
+ export function registerElements(name: string): void {
5
5
  const elements = [
6
6
  {
7
7
  elementName: `${name}-trigger`,
8
- Component: getAccentedTrigger(name)
8
+ Component: getAccentedTrigger(name),
9
9
  },
10
10
  {
11
11
  elementName: `${name}-dialog`,
12
- Component: getAccentedDialog()
13
- }
12
+ Component: getAccentedDialog(),
13
+ },
14
14
  ];
15
15
 
16
16
  for (const { elementName, Component } of elements) {
@@ -18,4 +18,4 @@ export default function registerElements(name: string): void {
18
18
  customElements.define(elementName, Component);
19
19
  }
20
20
  }
21
- };
21
+ }
@@ -1,17 +1,21 @@
1
- import logAndRethrow from './log-and-rethrow.js';
2
- import recalculatePositions from './utils/recalculate-positions.js';
1
+ import { logAndRethrow } from './log-and-rethrow.js';
2
+ import { recalculatePositions } from './utils/recalculate-positions.js';
3
3
 
4
- export default function setupResizeListener() {
4
+ export function setupResizeListener() {
5
5
  const abortController = new AbortController();
6
- window.addEventListener('resize', () => {
7
- try {
8
- recalculatePositions();
9
- } catch (error) {
10
- logAndRethrow(error);
11
- }
12
- }, { signal: abortController.signal });
6
+ window.addEventListener(
7
+ 'resize',
8
+ () => {
9
+ try {
10
+ recalculatePositions();
11
+ } catch (error) {
12
+ logAndRethrow(error);
13
+ }
14
+ },
15
+ { signal: abortController.signal },
16
+ );
13
17
 
14
18
  return () => {
15
19
  abortController.abort();
16
20
  };
17
- };
21
+ }
package/src/scanner.ts CHANGED
@@ -1,18 +1,26 @@
1
1
  import axe from 'axe-core';
2
- import TaskQueue from './task-queue.js';
2
+ import { getAccentedElementNames, issuesUrl } from './constants.js';
3
+ import { logAndRethrow } from './log-and-rethrow.js';
3
4
  import { elementsWithIssues, enabled, extendedElementsWithIssues } from './state.js';
4
- import type { AxeOptions, Throttle, Callback, AxeContext } from './types';
5
- import updateElementsWithIssues from './utils/update-elements-with-issues.js';
6
- import recalculatePositions from './utils/recalculate-positions.js';
7
- import recalculateScrollableAncestors from './utils/recalculate-scrollable-ancestors.js';
8
- import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
9
- import { issuesUrl } from './constants.js';
10
- import logAndRethrow from './log-and-rethrow.js';
11
-
12
- export default function createScanner(name: string, axeContext: AxeContext, axeOptions: AxeOptions, throttle: Required<Throttle>, callback: Callback) {
5
+ import { TaskQueue } from './task-queue.js';
6
+ import type { AxeOptions, Callback, Context, Throttle } from './types.ts';
7
+ import { getScanContext } from './utils/get-scan-context.js';
8
+ import { recalculatePositions } from './utils/recalculate-positions.js';
9
+ import { recalculateScrollableAncestors } from './utils/recalculate-scrollable-ancestors.js';
10
+ import { createShadowDOMAwareMutationObserver } from './utils/shadow-dom-aware-mutation-observer.js';
11
+ import { supportsAnchorPositioning } from './utils/supports-anchor-positioning.js';
12
+ import { updateElementsWithIssues } from './utils/update-elements-with-issues.js';
13
+
14
+ export function createScanner(
15
+ name: string,
16
+ context: Context,
17
+ axeOptions: AxeOptions,
18
+ throttle: Required<Throttle>,
19
+ callback: Callback,
20
+ ) {
13
21
  const axeRunningWindowProp = `__${name}_axe_running__`;
14
- const win: Record<string, any> = window;
15
- const taskQueue = new TaskQueue<Node>(async () => {
22
+ const win = window as unknown as Record<string, boolean>;
23
+ const taskQueue = new TaskQueue<Node>(async (nodes) => {
16
24
  // We may see errors coming from axe-core when Accented is toggled off and on in qiuck succession,
17
25
  // which I've seen happen with hot reloading of a React application.
18
26
  // This window property serves as a circuit breaker for that particular case.
@@ -21,50 +29,91 @@ export default function createScanner(name: string, axeContext: AxeContext, axeO
21
29
  }
22
30
 
23
31
  try {
24
-
25
- performance.mark('axe-start');
32
+ performance.mark('scan-start');
26
33
 
27
34
  win[axeRunningWindowProp] = true;
28
35
 
29
- let result;
36
+ const scanContext = getScanContext(nodes, context);
37
+
38
+ let result: axe.AxeResults | undefined;
30
39
 
31
40
  try {
32
- // TODO (https://github.com/pomerantsev/accented/issues/102):
33
- // only run Axe on what's changed, not on the whole axeContext
34
- result = await axe.run(axeContext, {
41
+ result = await axe.run(scanContext, {
42
+ /**
43
+ * By default, axe-core doesn't include element refs
44
+ * in the violations array,
45
+ * and we need those element refs.
46
+ */
35
47
  elementRef: true,
36
- // Although axe-core can perform iframe scanning, I haven't succeeded in it,
37
- // and the docs suggest that the axe-core script should be explicitly included
38
- // in each of the iframed documents anyway.
39
- // It seems preferable to disallow iframe scanning and not report issues in elements within iframes
40
- // in the case that such issues are for some reason reported by axe-core.
41
- // A consumer of Accented can instead scan the iframed document by calling Accented initialization from that document.
48
+
49
+ /**
50
+ * Although axe-core can perform iframe scanning, I haven't succeeded in it,
51
+ * and the docs suggest that the axe-core script should be explicitly included
52
+ * in each of the iframed documents anyway.
53
+ * It seems preferable to disallow iframe scanning and not report issues in elements within iframes
54
+ * in the case that such issues are for some reason reported by axe-core.
55
+ * A consumer of Accented can instead scan the iframed document by calling Accented initialization from that document.
56
+ */
42
57
  iframes: false,
58
+
59
+ /**
60
+ * The `preload` docs are not clear to me,
61
+ * but when it's set to `true` by default,
62
+ * axe-core tries to fetch cross-origin CSS,
63
+ * which fails in the absence of CORS headers.
64
+ * I'm not sure why axe-core needs to preload
65
+ * those resources in the first place,
66
+ * so disabling it seems to be the safe option.
67
+ */
68
+ preload: false,
69
+
70
+ /**
71
+ * We're only interested in violations,
72
+ * not in passes or incomplete results.
73
+ */
43
74
  resultTypes: ['violations'],
44
- ...axeOptions
75
+
76
+ ...axeOptions,
45
77
  });
46
78
  } catch (error) {
47
79
  console.error(
48
- 'Accented: axe-core (the accessibility testing engine) threw an error. ' +
49
- 'Check the `axeOptions` property that you’re passing to Accented. ' +
50
- `If you still think it’s a bug in Accented, file an issue at ${issuesUrl}.\n`,
51
- error
80
+ `Accented: axe-core (the accessibility testing engine) threw an error. Check the \`axeOptions\` property (https://www.accented.dev/api#axeoptions) that you’re passing to Accented. If you still think it’s a bug in Accented, file an issue at ${issuesUrl}.\n`,
81
+ error,
52
82
  );
53
- result = { violations: [] };
54
83
  }
55
84
  win[axeRunningWindowProp] = false;
56
85
 
57
- const axeMeasure = performance.measure('axe', 'axe-start');
86
+ const scanMeasure = performance.measure('scan', 'scan-start');
87
+ const scanDuration = Math.round(scanMeasure.duration);
58
88
 
59
- if (!enabled.value) {
89
+ if (!enabled.value || !result) {
60
90
  return;
61
91
  }
62
92
 
63
- updateElementsWithIssues(extendedElementsWithIssues, result.violations, window, name);
93
+ performance.mark('dom-update-start');
94
+
95
+ updateElementsWithIssues({
96
+ extendedElementsWithIssues,
97
+ scanContext,
98
+ violations: result.violations,
99
+ win: window,
100
+ name,
101
+ });
102
+
103
+ const domUpdateMeasure = performance.measure('dom-update', 'dom-update-start');
104
+ const domUpdateDuration = Math.round(domUpdateMeasure.duration);
64
105
 
65
106
  callback({
107
+ // Assuming that the {include, exclude} shape of the context object will be used less often
108
+ // than other variants, we'll output just the `include` array in case nothing is excluded
109
+ // in the scan.
110
+ scanContext: scanContext.exclude.length > 0 ? scanContext : scanContext.include,
66
111
  elementsWithIssues: elementsWithIssues.value,
67
- scanDuration: Math.round(axeMeasure.duration)
112
+ performance: {
113
+ totalBlockingTime: scanDuration + domUpdateDuration,
114
+ scan: scanDuration,
115
+ domUpdate: domUpdateDuration,
116
+ },
68
117
  });
69
118
  } catch (error) {
70
119
  win[axeRunningWindowProp] = false;
@@ -72,21 +121,24 @@ export default function createScanner(name: string, axeContext: AxeContext, axeO
72
121
  }
73
122
  }, throttle);
74
123
 
75
- // TODO (https://github.com/pomerantsev/accented/issues/102):
76
- // limit to what's in axeContext,
77
- // if that's an element or array of elements (not a selector).
78
124
  taskQueue.add(document);
79
125
 
80
- const accentedElementNames = [`${name}-trigger`, `${name}-dialog`];
81
- const mutationObserver = new MutationObserver(mutationList => {
126
+ const accentedElementNames = getAccentedElementNames(name);
127
+ const mutationObserver = createShadowDOMAwareMutationObserver(name, (mutationList) => {
82
128
  try {
83
129
  // We're not interested in mutations that are caused exclusively by the custom elements
84
130
  // introduced by Accented.
85
- const listWithoutAccentedElements = mutationList.filter(mutationRecord => {
86
- const onlyAccentedElementsAddedOrRemoved = mutationRecord.type === 'childList' &&
87
- [...mutationRecord.addedNodes].every(node => accentedElementNames.includes(node.nodeName.toLowerCase())) &&
88
- [...mutationRecord.removedNodes].every(node => accentedElementNames.includes(node.nodeName.toLowerCase()));
89
- const accentedElementChanged = mutationRecord.type === 'attributes' &&
131
+ const listWithoutAccentedElements = mutationList.filter((mutationRecord) => {
132
+ const onlyAccentedElementsAddedOrRemoved =
133
+ mutationRecord.type === 'childList' &&
134
+ [...mutationRecord.addedNodes].every((node) =>
135
+ accentedElementNames.includes(node.nodeName.toLowerCase()),
136
+ ) &&
137
+ [...mutationRecord.removedNodes].every((node) =>
138
+ accentedElementNames.includes(node.nodeName.toLowerCase()),
139
+ );
140
+ const accentedElementChanged =
141
+ mutationRecord.type === 'attributes' &&
90
142
  accentedElementNames.includes(mutationRecord.target.nodeName.toLowerCase());
91
143
  return !(onlyAccentedElementsAddedOrRemoved || accentedElementChanged);
92
144
  });
@@ -106,31 +158,35 @@ export default function createScanner(name: string, axeContext: AxeContext, axeO
106
158
  // If we simply exclude all mutations where attributeName = `data-${name}`,
107
159
  // we may miss other mutations on those same elements caused by Accented,
108
160
  // leading to extra runs of the mutation observer.
109
- const elementsWithAccentedAttributeChanges = listWithoutAccentedElements.reduce((nodes, mutationRecord) => {
110
- if (mutationRecord.type === 'attributes' && mutationRecord.attributeName === `data-${name}`) {
111
- nodes.add(mutationRecord.target);
112
- }
113
- return nodes;
114
- }, new Set<Node>());
115
-
116
- const filteredMutationList = listWithoutAccentedElements.filter(mutationRecord => {
161
+ const elementsWithAccentedAttributeChanges = listWithoutAccentedElements.reduce(
162
+ (nodes, mutationRecord) => {
163
+ if (
164
+ mutationRecord.type === 'attributes' &&
165
+ mutationRecord.attributeName === `data-${name}`
166
+ ) {
167
+ nodes.add(mutationRecord.target);
168
+ }
169
+ return nodes;
170
+ },
171
+ new Set<Node>(),
172
+ );
173
+
174
+ const filteredMutationList = listWithoutAccentedElements.filter((mutationRecord) => {
117
175
  return !elementsWithAccentedAttributeChanges.has(mutationRecord.target);
118
176
  });
119
177
 
120
- taskQueue.addMultiple(filteredMutationList.map(mutationRecord => mutationRecord.target));
178
+ const nodes = filteredMutationList.map((mutationRecord) => mutationRecord.target);
179
+ taskQueue.addMultiple(nodes);
121
180
  } catch (error) {
122
181
  logAndRethrow(error);
123
182
  }
124
183
  });
125
184
 
126
- // TODO (https://github.com/pomerantsev/accented/issues/102):
127
- // possibly limit the observer to what's in axeContext,
128
- // if that's an element or array of elements (not a selector).
129
185
  mutationObserver.observe(document, {
130
186
  subtree: true,
131
187
  childList: true,
132
188
  attributes: true,
133
- characterData: true
189
+ characterData: true,
134
190
  });
135
191
 
136
192
  return () => {
@@ -1,37 +1,45 @@
1
1
  import { effect } from '@preact/signals-core';
2
- import recalculatePositions from './utils/recalculate-positions.js';
2
+ import { logAndRethrow } from './log-and-rethrow.js';
3
3
  import { scrollableAncestors } from './state.js';
4
- import logAndRethrow from './log-and-rethrow.js';
4
+ import { recalculatePositions } from './utils/recalculate-positions.js';
5
5
 
6
- export default function setupScrollListeners() {
6
+ export function setupScrollListeners() {
7
7
  const documentAbortController = new AbortController();
8
- document.addEventListener('scroll', () => {
9
- try {
10
- recalculatePositions();
11
- } catch (error) {
12
- logAndRethrow(error);
13
- }
14
- }, { signal: documentAbortController.signal });
8
+ document.addEventListener(
9
+ 'scroll',
10
+ () => {
11
+ try {
12
+ recalculatePositions();
13
+ } catch (error) {
14
+ logAndRethrow(error);
15
+ }
16
+ },
17
+ { signal: documentAbortController.signal },
18
+ );
15
19
 
16
20
  const disposeOfEffect = effect(() => {
17
21
  // TODO: optimize performance, issue #81
18
22
  const elementAbortController = new AbortController();
19
23
  for (const scrollableAncestor of scrollableAncestors.value) {
20
- scrollableAncestor.addEventListener('scroll', () => {
21
- try {
22
- recalculatePositions();
23
- } catch (error) {
24
- logAndRethrow(error);
25
- }
26
- }, { signal: elementAbortController.signal });
24
+ scrollableAncestor.addEventListener(
25
+ 'scroll',
26
+ () => {
27
+ try {
28
+ recalculatePositions();
29
+ } catch (error) {
30
+ logAndRethrow(error);
31
+ }
32
+ },
33
+ { signal: elementAbortController.signal },
34
+ );
27
35
  }
28
36
  return () => {
29
37
  elementAbortController.abort();
30
- }
38
+ };
31
39
  });
32
40
 
33
41
  return () => {
34
42
  documentAbortController.abort();
35
43
  disposeOfEffect();
36
44
  };
37
- };
45
+ }
package/src/state.ts CHANGED
@@ -1,24 +1,35 @@
1
- import { signal, computed } from '@preact/signals-core';
1
+ import { computed, signal } from '@preact/signals-core';
2
2
 
3
- import type { ElementWithIssues, ExtendedElementWithIssues } from './types';
3
+ import type { ElementWithIssues, ExtendedElementWithIssues } from './types.ts';
4
4
 
5
5
  export const enabled = signal(false);
6
6
 
7
7
  export const extendedElementsWithIssues = signal<Array<ExtendedElementWithIssues>>([]);
8
8
 
9
- export const elementsWithIssues = computed<Array<ElementWithIssues>>(() => extendedElementsWithIssues.value.map(extendedElementWithIssues => ({
10
- element: extendedElementWithIssues.element,
11
- issues: extendedElementWithIssues.issues.value
12
- })));
9
+ export const elementsWithIssues = computed<Array<ElementWithIssues>>(() =>
10
+ extendedElementsWithIssues.value.map((extendedElementWithIssues) => ({
11
+ element: extendedElementWithIssues.element,
12
+ rootNode: extendedElementWithIssues.rootNode,
13
+ issues: extendedElementWithIssues.issues.value,
14
+ })),
15
+ );
16
+
17
+ export const rootNodes = computed<Set<Node>>(
18
+ () =>
19
+ new Set(
20
+ (enabled.value ? [document as Node] : []).concat(
21
+ ...extendedElementsWithIssues.value.map(
22
+ (extendedElementWithIssues) => extendedElementWithIssues.rootNode,
23
+ ),
24
+ ),
25
+ ),
26
+ );
13
27
 
14
- export const scrollableAncestors = computed<Set<HTMLElement>>(() =>
15
- extendedElementsWithIssues.value.reduce(
16
- (scrollableAncestors, extendedElementWithIssues) => {
17
- for (const scrollableAncestor of extendedElementWithIssues.scrollableAncestors.value) {
18
- scrollableAncestors.add(scrollableAncestor);
19
- }
20
- return scrollableAncestors;
21
- },
22
- new Set<HTMLElement>()
23
- )
28
+ export const scrollableAncestors = computed<Set<Element>>(() =>
29
+ extendedElementsWithIssues.value.reduce((scrollableAncestors, extendedElementWithIssues) => {
30
+ for (const scrollableAncestor of extendedElementWithIssues.scrollableAncestors.value) {
31
+ scrollableAncestors.add(scrollableAncestor);
32
+ }
33
+ return scrollableAncestors;
34
+ }, new Set<Element>()),
24
35
  );
@@ -1,11 +1,12 @@
1
1
  import assert from 'node:assert/strict';
2
- import {mock, suite, test} from 'node:test';
2
+ import { mock, suite, test } from 'node:test';
3
3
 
4
- import TaskQueue from './task-queue.js';
4
+ import { TaskQueue } from './task-queue';
5
5
 
6
- const wait = (duration: number) => new Promise(resolve => setTimeout(resolve, duration));
6
+ const wait = (duration: number) => new Promise((resolve) => setTimeout(resolve, duration));
7
7
 
8
- const createAsyncCallback = (duration: number) => mock.fn(() => new Promise(resolve => setTimeout(resolve, duration)));
8
+ const createAsyncCallback = (duration: number) =>
9
+ mock.fn(() => new Promise((resolve) => setTimeout(resolve, duration)));
9
10
 
10
11
  suite('TaskQueue', () => {
11
12
  test('callback is not called after a TaskQueue is created, even after a timeout', async () => {
package/src/task-queue.ts CHANGED
@@ -1,15 +1,15 @@
1
- import type { Throttle } from './types';
1
+ import type { Throttle } from './types.ts';
2
2
 
3
- type TaskCallback = () => void;
3
+ type TaskCallback<T> = (items: Array<T>) => void;
4
4
 
5
- export default class TaskQueue<T> {
5
+ export class TaskQueue<T> {
6
6
  #throttle: Throttle;
7
- #asyncCallback: TaskCallback | null = null;
7
+ #asyncCallback: TaskCallback<T> | null = null;
8
8
 
9
9
  #items = new Set<T>();
10
10
  #inRunLoop = false;
11
11
 
12
- constructor(asyncCallback: TaskCallback, throttle: Required<Throttle>) {
12
+ constructor(asyncCallback: TaskCallback<T>, throttle: Required<Throttle>) {
13
13
  this.#asyncCallback = asyncCallback;
14
14
  this.#throttle = throttle;
15
15
  }
@@ -33,10 +33,12 @@ export default class TaskQueue<T> {
33
33
  return;
34
34
  }
35
35
 
36
+ const items = Array.from(this.#items);
37
+
36
38
  this.#items.clear();
37
39
 
38
40
  if (this.#asyncCallback) {
39
- await this.#asyncCallback();
41
+ await this.#asyncCallback(items);
40
42
  }
41
43
 
42
44
  await new Promise((resolve) => setTimeout(resolve, this.#throttle.wait));