accented 0.0.2 → 1.0.1

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 (217) 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/strings.d.ts +2 -0
  8. package/dist/common/strings.d.ts.map +1 -0
  9. package/dist/common/strings.js +2 -0
  10. package/dist/common/strings.js.map +1 -0
  11. package/dist/common/tokens.d.ts +7 -0
  12. package/dist/common/tokens.d.ts.map +1 -0
  13. package/dist/common/tokens.js +8 -0
  14. package/dist/common/tokens.js.map +1 -0
  15. package/dist/constants.d.ts +2 -1
  16. package/dist/constants.d.ts.map +1 -1
  17. package/dist/constants.js +2 -1
  18. package/dist/constants.js.map +1 -1
  19. package/dist/dom-updater.d.ts +1 -1
  20. package/dist/dom-updater.d.ts.map +1 -1
  21. package/dist/dom-updater.js +73 -31
  22. package/dist/dom-updater.js.map +1 -1
  23. package/dist/elements/accented-dialog.d.ts +18 -10
  24. package/dist/elements/accented-dialog.d.ts.map +1 -1
  25. package/dist/elements/accented-dialog.js +116 -95
  26. package/dist/elements/accented-dialog.js.map +1 -1
  27. package/dist/elements/accented-trigger.d.ts +14 -9
  28. package/dist/elements/accented-trigger.d.ts.map +1 -1
  29. package/dist/elements/accented-trigger.js +83 -24
  30. package/dist/elements/accented-trigger.js.map +1 -1
  31. package/dist/fullscreen-listener.d.ts +2 -0
  32. package/dist/fullscreen-listener.d.ts.map +1 -0
  33. package/dist/fullscreen-listener.js +17 -0
  34. package/dist/fullscreen-listener.js.map +1 -0
  35. package/dist/intersection-observer.d.ts +1 -1
  36. package/dist/intersection-observer.d.ts.map +1 -1
  37. package/dist/intersection-observer.js +12 -6
  38. package/dist/intersection-observer.js.map +1 -1
  39. package/dist/log-and-rethrow.d.ts +1 -1
  40. package/dist/log-and-rethrow.d.ts.map +1 -1
  41. package/dist/log-and-rethrow.js +2 -3
  42. package/dist/log-and-rethrow.js.map +1 -1
  43. package/dist/logger.d.ts +4 -1
  44. package/dist/logger.d.ts.map +1 -1
  45. package/dist/logger.js +6 -3
  46. package/dist/logger.js.map +1 -1
  47. package/dist/register-elements.d.ts +1 -1
  48. package/dist/register-elements.d.ts.map +1 -1
  49. package/dist/register-elements.js +6 -7
  50. package/dist/register-elements.js.map +1 -1
  51. package/dist/resize-listener.d.ts +1 -1
  52. package/dist/resize-listener.d.ts.map +1 -1
  53. package/dist/resize-listener.js +3 -4
  54. package/dist/resize-listener.js.map +1 -1
  55. package/dist/scanner.d.ts +2 -2
  56. package/dist/scanner.d.ts.map +1 -1
  57. package/dist/scanner.js +76 -43
  58. package/dist/scanner.js.map +1 -1
  59. package/dist/scroll-listeners.d.ts +1 -1
  60. package/dist/scroll-listeners.d.ts.map +1 -1
  61. package/dist/scroll-listeners.js +3 -4
  62. package/dist/scroll-listeners.js.map +1 -1
  63. package/dist/state.d.ts +3 -2
  64. package/dist/state.d.ts.map +1 -1
  65. package/dist/state.js +5 -3
  66. package/dist/state.js.map +1 -1
  67. package/dist/task-queue.d.ts +4 -4
  68. package/dist/task-queue.d.ts.map +1 -1
  69. package/dist/task-queue.js +3 -2
  70. package/dist/task-queue.js.map +1 -1
  71. package/dist/types.d.ts +140 -49
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/types.js.map +1 -1
  74. package/dist/utils/are-elements-with-issues-equal.d.ts +3 -0
  75. package/dist/utils/are-elements-with-issues-equal.d.ts.map +1 -0
  76. package/dist/utils/are-elements-with-issues-equal.js +5 -0
  77. package/dist/utils/are-elements-with-issues-equal.js.map +1 -0
  78. package/dist/utils/are-issue-sets-equal.d.ts +2 -2
  79. package/dist/utils/are-issue-sets-equal.d.ts.map +1 -1
  80. package/dist/utils/are-issue-sets-equal.js +3 -3
  81. package/dist/utils/are-issue-sets-equal.js.map +1 -1
  82. package/dist/utils/containing-blocks.d.ts +3 -0
  83. package/dist/utils/containing-blocks.d.ts.map +1 -0
  84. package/dist/utils/containing-blocks.js +46 -0
  85. package/dist/utils/containing-blocks.js.map +1 -0
  86. package/dist/utils/contains.d.ts +2 -0
  87. package/dist/utils/contains.d.ts.map +1 -0
  88. package/dist/utils/contains.js +19 -0
  89. package/dist/utils/contains.js.map +1 -0
  90. package/dist/utils/deduplicate-nodes.d.ts +2 -0
  91. package/dist/utils/deduplicate-nodes.d.ts.map +1 -0
  92. package/dist/utils/deduplicate-nodes.js +4 -0
  93. package/dist/utils/deduplicate-nodes.js.map +1 -0
  94. package/dist/utils/deep-merge.d.ts +1 -1
  95. package/dist/utils/deep-merge.d.ts.map +1 -1
  96. package/dist/utils/deep-merge.js +8 -5
  97. package/dist/utils/deep-merge.js.map +1 -1
  98. package/dist/utils/dom-helpers.d.ts +9 -0
  99. package/dist/utils/dom-helpers.d.ts.map +1 -0
  100. package/dist/utils/dom-helpers.js +34 -0
  101. package/dist/utils/dom-helpers.js.map +1 -0
  102. package/dist/utils/ensure-non-empty.d.ts +2 -0
  103. package/dist/utils/ensure-non-empty.d.ts.map +1 -0
  104. package/dist/utils/ensure-non-empty.js +7 -0
  105. package/dist/utils/ensure-non-empty.js.map +1 -0
  106. package/dist/utils/get-element-html.d.ts +1 -1
  107. package/dist/utils/get-element-html.d.ts.map +1 -1
  108. package/dist/utils/get-element-html.js +4 -2
  109. package/dist/utils/get-element-html.js.map +1 -1
  110. package/dist/utils/get-element-position.d.ts +10 -2
  111. package/dist/utils/get-element-position.d.ts.map +1 -1
  112. package/dist/utils/get-element-position.js +64 -16
  113. package/dist/utils/get-element-position.js.map +1 -1
  114. package/dist/utils/get-parent.d.ts +2 -0
  115. package/dist/utils/get-parent.d.ts.map +1 -0
  116. package/dist/utils/get-parent.js +12 -0
  117. package/dist/utils/get-parent.js.map +1 -0
  118. package/dist/utils/get-scan-context.d.ts +3 -0
  119. package/dist/utils/get-scan-context.d.ts.map +1 -0
  120. package/dist/utils/get-scan-context.js +28 -0
  121. package/dist/utils/get-scan-context.js.map +1 -0
  122. package/dist/utils/get-scrollable-ancestors.d.ts +1 -1
  123. package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -1
  124. package/dist/utils/get-scrollable-ancestors.js +10 -6
  125. package/dist/utils/get-scrollable-ancestors.js.map +1 -1
  126. package/dist/utils/is-node-in-scan-context.d.ts +3 -0
  127. package/dist/utils/is-node-in-scan-context.d.ts.map +1 -0
  128. package/dist/utils/is-node-in-scan-context.js +26 -0
  129. package/dist/utils/is-node-in-scan-context.js.map +1 -0
  130. package/dist/utils/is-non-empty.d.ts +2 -0
  131. package/dist/utils/is-non-empty.d.ts.map +1 -0
  132. package/dist/utils/is-non-empty.js +4 -0
  133. package/dist/utils/is-non-empty.js.map +1 -0
  134. package/dist/utils/normalize-context.d.ts +3 -0
  135. package/dist/utils/normalize-context.d.ts.map +1 -0
  136. package/dist/utils/normalize-context.js +59 -0
  137. package/dist/utils/normalize-context.js.map +1 -0
  138. package/dist/utils/recalculate-positions.d.ts +1 -1
  139. package/dist/utils/recalculate-positions.d.ts.map +1 -1
  140. package/dist/utils/recalculate-positions.js +5 -5
  141. package/dist/utils/recalculate-positions.js.map +1 -1
  142. package/dist/utils/recalculate-scrollable-ancestors.d.ts +1 -1
  143. package/dist/utils/recalculate-scrollable-ancestors.d.ts.map +1 -1
  144. package/dist/utils/recalculate-scrollable-ancestors.js +4 -4
  145. package/dist/utils/recalculate-scrollable-ancestors.js.map +1 -1
  146. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts +10 -0
  147. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -0
  148. package/dist/utils/shadow-dom-aware-mutation-observer.js +61 -0
  149. package/dist/utils/shadow-dom-aware-mutation-observer.js.map +1 -0
  150. package/dist/utils/supports-anchor-positioning.d.ts +1 -1
  151. package/dist/utils/supports-anchor-positioning.d.ts.map +1 -1
  152. package/dist/utils/supports-anchor-positioning.js +15 -2
  153. package/dist/utils/supports-anchor-positioning.js.map +1 -1
  154. package/dist/utils/transform-violations.d.ts +2 -2
  155. package/dist/utils/transform-violations.d.ts.map +1 -1
  156. package/dist/utils/transform-violations.js +25 -10
  157. package/dist/utils/transform-violations.js.map +1 -1
  158. package/dist/utils/update-elements-with-issues.d.ts +11 -5
  159. package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
  160. package/dist/utils/update-elements-with-issues.js +56 -24
  161. package/dist/utils/update-elements-with-issues.js.map +1 -1
  162. package/dist/validate-options.d.ts +2 -2
  163. package/dist/validate-options.d.ts.map +1 -1
  164. package/dist/validate-options.js +91 -4
  165. package/dist/validate-options.js.map +1 -1
  166. package/package.json +16 -8
  167. package/src/accented.test.ts +2 -2
  168. package/src/accented.ts +45 -34
  169. package/src/common/strings.ts +2 -0
  170. package/src/common/tokens.ts +10 -0
  171. package/src/constants.ts +2 -1
  172. package/src/dom-updater.ts +87 -34
  173. package/src/elements/accented-dialog.ts +163 -123
  174. package/src/elements/accented-trigger.ts +128 -50
  175. package/src/fullscreen-listener.ts +21 -0
  176. package/src/intersection-observer.ts +27 -16
  177. package/src/log-and-rethrow.ts +2 -3
  178. package/src/logger.ts +14 -4
  179. package/src/register-elements.ts +7 -7
  180. package/src/resize-listener.ts +15 -11
  181. package/src/scanner.ts +113 -57
  182. package/src/scroll-listeners.ts +27 -19
  183. package/src/state.ts +27 -16
  184. package/src/task-queue.test.ts +5 -4
  185. package/src/task-queue.ts +8 -6
  186. package/src/types.ts +179 -76
  187. package/src/utils/are-elements-with-issues-equal.ts +11 -0
  188. package/src/utils/are-issue-sets-equal.test.ts +6 -7
  189. package/src/utils/are-issue-sets-equal.ts +8 -6
  190. package/src/utils/containing-blocks.ts +60 -0
  191. package/src/utils/contains.test.ts +54 -0
  192. package/src/utils/contains.ts +19 -0
  193. package/src/utils/deduplicate-nodes.ts +3 -0
  194. package/src/utils/deep-merge.test.ts +8 -1
  195. package/src/utils/deep-merge.ts +14 -8
  196. package/src/utils/dom-helpers.ts +42 -0
  197. package/src/utils/ensure-non-empty.ts +6 -0
  198. package/src/utils/get-element-html.ts +4 -2
  199. package/src/utils/get-element-position.ts +84 -16
  200. package/src/utils/get-parent.ts +14 -0
  201. package/src/utils/get-scan-context.test.ts +85 -0
  202. package/src/utils/get-scan-context.ts +36 -0
  203. package/src/utils/get-scrollable-ancestors.ts +15 -7
  204. package/src/utils/is-node-in-scan-context.test.ts +70 -0
  205. package/src/utils/is-node-in-scan-context.ts +29 -0
  206. package/src/utils/is-non-empty.ts +3 -0
  207. package/src/utils/normalize-context.test.ts +105 -0
  208. package/src/utils/normalize-context.ts +65 -0
  209. package/src/utils/recalculate-positions.ts +5 -5
  210. package/src/utils/recalculate-scrollable-ancestors.ts +4 -4
  211. package/src/utils/shadow-dom-aware-mutation-observer.ts +75 -0
  212. package/src/utils/supports-anchor-positioning.ts +19 -3
  213. package/src/utils/transform-violations.test.ts +29 -25
  214. package/src/utils/transform-violations.ts +32 -12
  215. package/src/utils/update-elements-with-issues.test.ts +145 -53
  216. package/src/utils/update-elements-with-issues.ts +123 -54
  217. package/src/validate-options.ts +154 -14
@@ -1,74 +1,143 @@
1
- import type { AxeResults } from 'axe-core';
2
1
  import type { Signal } from '@preact/signals-core';
3
2
  import { batch, signal } from '@preact/signals-core';
4
- import type { ExtendedElementWithIssues } from '../types';
5
- import transformViolations from './transform-violations.js';
6
- import areIssueSetsEqual from './are-issue-sets-equal.js';
7
- import type { AccentedTrigger } from '../elements/accented-trigger';
8
- import type { AccentedDialog } from '../elements/accented-dialog';
9
- import getElementPosition from './get-element-position.js';
10
- import getScrollableAncestors from './get-scrollable-ancestors.js';
11
- import supportsAnchorPositioning from './supports-anchor-positioning.js';
3
+ import type { AxeResults } from 'axe-core';
4
+ import type { AccentedDialog } from '../elements/accented-dialog.ts';
5
+ import type { AccentedTrigger } from '../elements/accented-trigger.ts';
6
+ import type { ExtendedElementWithIssues, ScanContext } from '../types.ts';
7
+ import { areElementsWithIssuesEqual } from './are-elements-with-issues-equal.js';
8
+ import { areIssueSetsEqual } from './are-issue-sets-equal.js';
9
+ import { isSvgElement } from './dom-helpers.js';
10
+ import { getElementPosition } from './get-element-position.js';
11
+ import { getParent } from './get-parent.js';
12
+ import { getScrollableAncestors } from './get-scrollable-ancestors.js';
13
+ import { isNodeInScanContext } from './is-node-in-scan-context.js';
14
+ import { supportsAnchorPositioning } from './supports-anchor-positioning.js';
15
+ import { transformViolations } from './transform-violations.js';
16
+
17
+ function shouldSkipRender(element: Element): boolean {
18
+ // Skip rendering if the element is inside an SVG:
19
+ // https://github.com/pomerantsev/accented/issues/62
20
+ const parent = getParent(element);
21
+ const isInsideSvg = Boolean(parent && isSvgElement(parent));
22
+
23
+ // Some issues, such as meta-viewport, are on <head> descendants,
24
+ // but since <head> is never rendered, we don't want to output anything
25
+ // for those in the DOM.
26
+ // We're not anticipating the use of shadow DOM in <head>,
27
+ // so the use of .closest() should be fine.
28
+ const isInsideHead = element.closest('head') !== null;
29
+
30
+ return isInsideSvg || isInsideHead;
31
+ }
12
32
 
13
33
  let count = 0;
14
34
 
15
- export default function updateElementsWithIssues(extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>>, violations: typeof AxeResults.violations, win: Window & { CSS: typeof CSS }, name: string) {
16
- const updatedElementsWithIssues = transformViolations(violations);
35
+ export function updateElementsWithIssues({
36
+ extendedElementsWithIssues,
37
+ scanContext,
38
+ violations,
39
+ win,
40
+ name,
41
+ }: {
42
+ extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>>;
43
+ scanContext: ScanContext;
44
+ violations: typeof AxeResults.violations;
45
+ win: Window & { CSS: typeof CSS };
46
+ name: string;
47
+ }) {
48
+ const updatedElementsWithIssues = transformViolations(violations, name);
17
49
 
18
50
  batch(() => {
19
51
  for (const updatedElementWithIssues of updatedElementsWithIssues) {
20
- const existingElementIndex = extendedElementsWithIssues.value.findIndex(extendedElementWithIssues => extendedElementWithIssues.element === updatedElementWithIssues.element);
21
- if (existingElementIndex > -1 && extendedElementsWithIssues.value[existingElementIndex] && !areIssueSetsEqual(extendedElementsWithIssues.value[existingElementIndex].issues.value, updatedElementWithIssues.issues)) {
22
- extendedElementsWithIssues.value[existingElementIndex].issues.value = updatedElementWithIssues.issues;
52
+ const existingElementIndex = extendedElementsWithIssues.value.findIndex(
53
+ (extendedElementWithIssues) =>
54
+ areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues),
55
+ );
56
+ if (
57
+ existingElementIndex > -1 &&
58
+ extendedElementsWithIssues.value[existingElementIndex] &&
59
+ !areIssueSetsEqual(
60
+ extendedElementsWithIssues.value[existingElementIndex].issues.value,
61
+ updatedElementWithIssues.issues,
62
+ )
63
+ ) {
64
+ extendedElementsWithIssues.value[existingElementIndex].issues.value =
65
+ updatedElementWithIssues.issues;
23
66
  }
24
67
  }
25
68
 
26
- const addedElementsWithIssues = updatedElementsWithIssues.filter(updatedElementWithIssues => {
27
- return !extendedElementsWithIssues.value.some(extendedElementWithIssues => extendedElementWithIssues.element === updatedElementWithIssues.element);
69
+ const addedElementsWithIssues = updatedElementsWithIssues.filter((updatedElementWithIssues) => {
70
+ return !extendedElementsWithIssues.value.some((extendedElementWithIssues) =>
71
+ areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues),
72
+ );
28
73
  });
29
74
 
30
- const removedElementsWithIssues = extendedElementsWithIssues.value.filter(extendedElementWithIssues => {
31
- return !updatedElementsWithIssues.some(updatedElementWithIssues => updatedElementWithIssues.element === extendedElementWithIssues.element);
32
- });
75
+ // Only consider an element to be removed in two cases:
76
+ // 1. It has been removed from the DOM.
77
+ // 2. It is within the scan context, but not among updatedElementsWithIssues.
78
+ const removedElementsWithIssues = extendedElementsWithIssues.value.filter(
79
+ (extendedElementWithIssues) => {
80
+ const isConnected = extendedElementWithIssues.element.isConnected;
81
+ const hasNoMoreIssues =
82
+ isNodeInScanContext(extendedElementWithIssues.element, scanContext) &&
83
+ !updatedElementsWithIssues.some((updatedElementWithIssues) =>
84
+ areElementsWithIssuesEqual(updatedElementWithIssues, extendedElementWithIssues),
85
+ );
86
+ return !isConnected || hasNoMoreIssues;
87
+ },
88
+ );
33
89
 
34
90
  if (addedElementsWithIssues.length > 0 || removedElementsWithIssues.length > 0) {
35
91
  extendedElementsWithIssues.value = [...extendedElementsWithIssues.value]
36
- .filter(extendedElementWithIssues => {
37
- return !removedElementsWithIssues.some(removedElementWithIssues => removedElementWithIssues.element === extendedElementWithIssues.element);
92
+ .filter((extendedElementWithIssues) => {
93
+ return !removedElementsWithIssues.some((removedElementWithIssues) =>
94
+ areElementsWithIssuesEqual(removedElementWithIssues, extendedElementWithIssues),
95
+ );
38
96
  })
39
- .concat(addedElementsWithIssues
40
- .filter(addedElementWithIssues => addedElementWithIssues.element.isConnected)
41
- .map(addedElementWithIssues => {
42
- const id = count++;
43
- const trigger = win.document.createElement(`${name}-trigger`) as AccentedTrigger;
44
- const elementZIndex = parseInt(win.getComputedStyle(addedElementWithIssues.element).zIndex, 10);
45
- if (!isNaN(elementZIndex)) {
46
- trigger.style.setProperty('z-index', (elementZIndex + 1).toString(), 'important');
47
- }
48
- trigger.style.setProperty('position-anchor', `--${name}-anchor-${id}`, 'important');
49
- trigger.dataset.id = id.toString();
50
- const accentedDialog = win.document.createElement(`${name}-dialog`) as AccentedDialog;
51
- trigger.dialog = accentedDialog;
52
- const position = getElementPosition(addedElementWithIssues.element, win);
53
- trigger.position = signal(position);
54
- trigger.visible = signal(true);
55
- trigger.element = addedElementWithIssues.element;
56
- const scrollableAncestors = supportsAnchorPositioning(win) ?
57
- new Set<HTMLElement>() :
58
- getScrollableAncestors(addedElementWithIssues.element, win);
59
- const issues = signal(addedElementWithIssues.issues);
60
- accentedDialog.issues = issues;
61
- accentedDialog.element = addedElementWithIssues.element;
62
- return {
63
- id,
64
- element: addedElementWithIssues.element,
65
- visible: trigger.visible,
66
- position: trigger.position,
67
- scrollableAncestors: signal(scrollableAncestors),
68
- trigger,
69
- issues
70
- };
71
- })
97
+ .concat(
98
+ addedElementsWithIssues
99
+ .filter((addedElementWithIssues) => addedElementWithIssues.element.isConnected)
100
+ .map((addedElementWithIssues) => {
101
+ const id = count++;
102
+ const trigger = win.document.createElement(`${name}-trigger`) as AccentedTrigger;
103
+ const elementZIndex = Number.parseInt(
104
+ win.getComputedStyle(addedElementWithIssues.element).zIndex,
105
+ 10,
106
+ );
107
+ if (!Number.isNaN(elementZIndex)) {
108
+ trigger.style.setProperty('z-index', (elementZIndex + 1).toString(), 'important');
109
+ }
110
+ trigger.style.setProperty('position-anchor', `--${name}-anchor-${id}`, 'important');
111
+ trigger.dataset.id = id.toString();
112
+ const accentedDialog = win.document.createElement(`${name}-dialog`) as AccentedDialog;
113
+ trigger.dialog = accentedDialog;
114
+ const position = getElementPosition(addedElementWithIssues.element, win);
115
+ trigger.position = signal(position);
116
+ trigger.visible = signal(true);
117
+ trigger.element = addedElementWithIssues.element;
118
+ const scrollableAncestors = supportsAnchorPositioning(win)
119
+ ? new Set<HTMLElement>()
120
+ : getScrollableAncestors(addedElementWithIssues.element, win);
121
+ const issues = signal(addedElementWithIssues.issues);
122
+ accentedDialog.issues = issues;
123
+ accentedDialog.element = addedElementWithIssues.element;
124
+ return {
125
+ id,
126
+ element: addedElementWithIssues.element,
127
+ skipRender: shouldSkipRender(addedElementWithIssues.element),
128
+ rootNode: addedElementWithIssues.rootNode,
129
+ visible: trigger.visible,
130
+ position: trigger.position,
131
+ scrollableAncestors: signal(scrollableAncestors),
132
+ anchorNameValue:
133
+ addedElementWithIssues.element.style.getPropertyValue('anchor-name') ||
134
+ win
135
+ .getComputedStyle(addedElementWithIssues.element)
136
+ .getPropertyValue('anchor-name'),
137
+ trigger,
138
+ issues,
139
+ };
140
+ }),
72
141
  );
73
142
  }
74
143
  });
@@ -1,44 +1,184 @@
1
- import type { AccentedOptions } from './types';
2
1
  import { allowedAxeOptions } from './types.js';
2
+ import type {
3
+ AccentedOptions,
4
+ Context,
5
+ ContextObject,
6
+ ContextProp,
7
+ Selector,
8
+ SelectorList,
9
+ } from './types.ts';
10
+ import { isNode, isNodeList } from './utils/dom-helpers.js';
11
+
12
+ function isSelector(contextFragment: Context): contextFragment is Selector {
13
+ return (
14
+ typeof contextFragment === 'string' ||
15
+ isNode(contextFragment) ||
16
+ 'fromShadowDom' in contextFragment
17
+ );
18
+ }
19
+
20
+ function validateSelector(selector: Selector) {
21
+ if (typeof selector === 'string') {
22
+ return;
23
+ }
24
+ if (isNode(selector)) {
25
+ return;
26
+ }
27
+ if ('fromShadowDom' in selector) {
28
+ if (
29
+ !Array.isArray(selector.fromShadowDom) ||
30
+ selector.fromShadowDom.length < 2 ||
31
+ !selector.fromShadowDom.every((item) => typeof item === 'string')
32
+ ) {
33
+ throw new TypeError(
34
+ `Accented: invalid argument. \`fromShadowDom\` must be an array of strings with at least 2 elements. It’s currently set to ${selector.fromShadowDom}.`,
35
+ );
36
+ }
37
+ return;
38
+ }
39
+ const neverSelector: never = selector;
40
+ throw new TypeError(
41
+ `Accented: invalid argument. The selector must be one of: string, Node, or an object with a \`fromShadowDom\` property. It’s currently set to ${neverSelector}.`,
42
+ );
43
+ }
44
+
45
+ function isSelectorList(contextFragment: Context): contextFragment is SelectorList {
46
+ return (
47
+ (typeof contextFragment === 'object' && isNodeList(contextFragment)) ||
48
+ (Array.isArray(contextFragment) && contextFragment.every((item) => isSelector(item)))
49
+ );
50
+ }
51
+
52
+ function validateSelectorList(selectorList: SelectorList) {
53
+ if (isNodeList(selectorList)) {
54
+ return;
55
+ }
56
+ if (Array.isArray(selectorList)) {
57
+ for (const selector of selectorList) {
58
+ validateSelector(selector);
59
+ }
60
+ } else {
61
+ const neverSelectorList: never = selectorList;
62
+ throw new TypeError(
63
+ `Accented: invalid argument. The selector list must either be a NodeList or an array. It’s currently set to ${neverSelectorList}.`,
64
+ );
65
+ }
66
+ }
67
+
68
+ function isContextProp(contextFragment: Context): contextFragment is ContextProp {
69
+ return isSelector(contextFragment) || isSelectorList(contextFragment);
70
+ }
71
+
72
+ function validateContextProp(context: Selector | SelectorList) {
73
+ if (isSelector(context)) {
74
+ validateSelector(context);
75
+ } else if (isSelectorList(context)) {
76
+ validateSelectorList(context);
77
+ } else {
78
+ const neverContext: never = context;
79
+ throw new TypeError(
80
+ `Accented: invalid argument. The context property must either be a selector or a selector list. It’s currently set to ${neverContext}.`,
81
+ );
82
+ }
83
+ }
84
+
85
+ function isContextObject(contextFragment: Context): contextFragment is ContextObject {
86
+ return (
87
+ typeof contextFragment === 'object' &&
88
+ contextFragment !== null &&
89
+ ('include' in contextFragment || 'exclude' in contextFragment)
90
+ );
91
+ }
92
+
93
+ function validateContextObject(contextObject: ContextObject) {
94
+ if ('include' in contextObject && contextObject.include !== undefined) {
95
+ validateContextProp(contextObject.include);
96
+ }
97
+ if ('exclude' in contextObject && contextObject.exclude !== undefined) {
98
+ validateContextProp(contextObject.exclude);
99
+ }
100
+ }
101
+
102
+ function validateContext(context: Context) {
103
+ if (isContextProp(context)) {
104
+ validateContextProp(context);
105
+ } else if (isContextObject(context)) {
106
+ validateContextObject(context);
107
+ } else {
108
+ const neverContext: never = context;
109
+ throw new TypeError(
110
+ `Accented: invalid context argument. It’s currently set to ${neverContext}.`,
111
+ );
112
+ }
113
+ }
3
114
 
4
115
  // The space of valid CSS and HTML names is wider than this,
5
116
  // but with Unicode it gets complicated quickly, so I'm sticking to only allowing
6
117
  // lowercase alphanumeric names that possibly contain dashes that start with a letter.
7
118
  const nameRegex = /^[a-z]([a-z0-9]|-)+$/;
8
119
 
9
- export default function validateOptions(options: AccentedOptions) {
120
+ export function validateOptions(options: AccentedOptions) {
10
121
  if (typeof options !== 'object' || options === null) {
11
- throw new TypeError(`Accented: invalid argument. The options parameter must be an object if provided. It’s currently set to ${options}.`);
122
+ throw new TypeError(
123
+ `Accented: invalid argument. The options parameter must be an object if provided. It’s currently set to ${options}.`,
124
+ );
12
125
  }
13
126
  if (options.throttle !== undefined) {
14
127
  if (typeof options.throttle !== 'object' || options.throttle === null) {
15
- throw new TypeError(`Accented: invalid argument. \`throttle\` option must be an object if provided. It’s currently set to ${options.throttle}.`);
128
+ throw new TypeError(
129
+ `Accented: invalid argument. \`throttle\` option must be an object if provided. It’s currently set to ${options.throttle}.`,
130
+ );
16
131
  }
17
- if (options.throttle.wait !== undefined && (typeof options.throttle.wait !== 'number' || options.throttle.wait < 0)) {
18
- throw new TypeError(`Accented: invalid argument. \`throttle.wait\` option must be a non-negative number if provided. It’s currently set to ${options.throttle.wait}.`);
132
+ if (
133
+ options.throttle.wait !== undefined &&
134
+ (typeof options.throttle.wait !== 'number' || options.throttle.wait < 0)
135
+ ) {
136
+ throw new TypeError(
137
+ `Accented: invalid argument. \`throttle.wait\` option must be a non-negative number if provided. It’s currently set to ${options.throttle.wait}.`,
138
+ );
19
139
  }
20
140
  }
21
141
  if (options.output !== undefined) {
22
142
  if (typeof options.output !== 'object' || options.output === null) {
23
- throw new TypeError(`Accented: invalid argument. \`output\` option must be an object if provided. It’s currently set to ${options.output}.`);
143
+ throw new TypeError(
144
+ `Accented: invalid argument. \`output\` option must be an object if provided. It’s currently set to ${options.output}.`,
145
+ );
24
146
  }
25
147
  if (options.output.console !== undefined && typeof options.output.console !== 'boolean') {
26
- console.warn(`Accented: invalid argument. \`output.console\` option is expected to be a boolean. It’s currently set to ${options.output.console}.`);
148
+ console.warn(
149
+ `Accented: invalid argument. \`output.console\` option is expected to be a boolean. It’s currently set to ${options.output.console}.`,
150
+ );
27
151
  }
28
152
  }
29
153
  if (options.callback !== undefined && typeof options.callback !== 'function') {
30
- throw new TypeError(`Accented: invalid argument. \`callback\` option must be a function if provided. It’s currently set to ${options.callback}.`);
154
+ throw new TypeError(
155
+ `Accented: invalid argument. \`callback\` option must be a function if provided. It’s currently set to ${options.callback}.`,
156
+ );
31
157
  }
32
- if (options.name !== undefined && (typeof options.name !== 'string' || !options.name.match(nameRegex))) {
33
- throw new TypeError(`Accented: invalid argument. \`name\` option must be a string that starts with a lowercase letter and only contains lowercase alphanumeric characters and dashes. It’s currently set to ${options.name}.`);
158
+ if (
159
+ options.name !== undefined &&
160
+ (typeof options.name !== 'string' || !options.name.match(nameRegex))
161
+ ) {
162
+ throw new TypeError(
163
+ `Accented: invalid argument. \`name\` option must be a string that starts with a lowercase letter and only contains lowercase alphanumeric characters and dashes. It’s currently set to ${options.name}.`,
164
+ );
34
165
  }
35
166
  if (options.axeOptions !== undefined) {
36
167
  if (typeof options.axeOptions !== 'object' || options.axeOptions === null) {
37
- throw new TypeError(`Accented: invalid argument. \`axeOptions\` option must be an object if provided. It’s currently set to ${options.axeOptions}.`);
168
+ throw new TypeError(
169
+ `Accented: invalid argument. \`axeOptions\` option must be an object if provided. It’s currently set to ${options.axeOptions}.`,
170
+ );
38
171
  }
39
- const unsupportedKeys = Object.keys(options.axeOptions).filter(key => !(allowedAxeOptions as unknown as Array<string>).includes(key));
172
+ const unsupportedKeys = Object.keys(options.axeOptions).filter(
173
+ (key) => !(allowedAxeOptions as unknown as Array<string>).includes(key),
174
+ );
40
175
  if (unsupportedKeys.length > 0) {
41
- throw new TypeError(`Accented: invalid argument. \`axeOptions\` contains the following unsupported keys: ${unsupportedKeys.join(', ')}. Valid options are: ${allowedAxeOptions.join(', ')}.`);
176
+ throw new TypeError(
177
+ `Accented: invalid argument. \`axeOptions\` contains the following unsupported keys: ${unsupportedKeys.join(', ')}. Valid options are: ${allowedAxeOptions.join(', ')}.`,
178
+ );
42
179
  }
43
180
  }
181
+ if (options.context !== undefined) {
182
+ validateContext(options.context);
183
+ }
44
184
  }