accented 0.0.0-20250404114312 → 0.0.0-20250618181418

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 (207) hide show
  1. package/README.md +0 -207
  2. package/dist/accented.d.ts +2 -2
  3. package/dist/accented.d.ts.map +1 -1
  4. package/dist/accented.js +29 -23
  5. package/dist/accented.js.map +1 -1
  6. package/dist/common/tokens.d.ts +2 -0
  7. package/dist/common/tokens.d.ts.map +1 -0
  8. package/dist/common/tokens.js +2 -0
  9. package/dist/common/tokens.js.map +1 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/dom-updater.d.ts +1 -1
  12. package/dist/dom-updater.d.ts.map +1 -1
  13. package/dist/dom-updater.js +43 -16
  14. package/dist/dom-updater.js.map +1 -1
  15. package/dist/elements/accented-dialog.d.ts +13 -10
  16. package/dist/elements/accented-dialog.d.ts.map +1 -1
  17. package/dist/elements/accented-dialog.js +46 -69
  18. package/dist/elements/accented-dialog.js.map +1 -1
  19. package/dist/elements/accented-trigger.d.ts +12 -9
  20. package/dist/elements/accented-trigger.d.ts.map +1 -1
  21. package/dist/elements/accented-trigger.js +14 -15
  22. package/dist/elements/accented-trigger.js.map +1 -1
  23. package/dist/fullscreen-listener.d.ts +1 -1
  24. package/dist/fullscreen-listener.d.ts.map +1 -1
  25. package/dist/fullscreen-listener.js +3 -4
  26. package/dist/fullscreen-listener.js.map +1 -1
  27. package/dist/intersection-observer.d.ts +1 -1
  28. package/dist/intersection-observer.d.ts.map +1 -1
  29. package/dist/intersection-observer.js +12 -6
  30. package/dist/intersection-observer.js.map +1 -1
  31. package/dist/log-and-rethrow.d.ts +1 -1
  32. package/dist/log-and-rethrow.d.ts.map +1 -1
  33. package/dist/log-and-rethrow.js +2 -3
  34. package/dist/log-and-rethrow.js.map +1 -1
  35. package/dist/logger.d.ts +1 -1
  36. package/dist/logger.d.ts.map +1 -1
  37. package/dist/logger.js +6 -3
  38. package/dist/logger.js.map +1 -1
  39. package/dist/register-elements.d.ts +1 -1
  40. package/dist/register-elements.d.ts.map +1 -1
  41. package/dist/register-elements.js +6 -7
  42. package/dist/register-elements.js.map +1 -1
  43. package/dist/resize-listener.d.ts +1 -1
  44. package/dist/resize-listener.d.ts.map +1 -1
  45. package/dist/resize-listener.js +3 -4
  46. package/dist/resize-listener.js.map +1 -1
  47. package/dist/scanner.d.ts +2 -2
  48. package/dist/scanner.d.ts.map +1 -1
  49. package/dist/scanner.js +39 -36
  50. package/dist/scanner.js.map +1 -1
  51. package/dist/scroll-listeners.d.ts +1 -1
  52. package/dist/scroll-listeners.d.ts.map +1 -1
  53. package/dist/scroll-listeners.js +3 -4
  54. package/dist/scroll-listeners.js.map +1 -1
  55. package/dist/state.d.ts +1 -1
  56. package/dist/state.d.ts.map +1 -1
  57. package/dist/state.js +4 -5
  58. package/dist/state.js.map +1 -1
  59. package/dist/task-queue.d.ts +4 -4
  60. package/dist/task-queue.d.ts.map +1 -1
  61. package/dist/task-queue.js +3 -2
  62. package/dist/task-queue.js.map +1 -1
  63. package/dist/types.d.ts +28 -6
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/types.js.map +1 -1
  66. package/dist/utils/are-elements-with-issues-equal.d.ts +2 -2
  67. package/dist/utils/are-elements-with-issues-equal.d.ts.map +1 -1
  68. package/dist/utils/are-elements-with-issues-equal.js +3 -3
  69. package/dist/utils/are-elements-with-issues-equal.js.map +1 -1
  70. package/dist/utils/are-issue-sets-equal.d.ts +2 -2
  71. package/dist/utils/are-issue-sets-equal.d.ts.map +1 -1
  72. package/dist/utils/are-issue-sets-equal.js +3 -3
  73. package/dist/utils/are-issue-sets-equal.js.map +1 -1
  74. package/dist/utils/containing-blocks.d.ts +3 -0
  75. package/dist/utils/containing-blocks.d.ts.map +1 -0
  76. package/dist/utils/containing-blocks.js +46 -0
  77. package/dist/utils/containing-blocks.js.map +1 -0
  78. package/dist/utils/contains.d.ts +2 -0
  79. package/dist/utils/contains.d.ts.map +1 -0
  80. package/dist/utils/contains.js +19 -0
  81. package/dist/utils/contains.js.map +1 -0
  82. package/dist/utils/deduplicate-nodes.d.ts +2 -0
  83. package/dist/utils/deduplicate-nodes.d.ts.map +1 -0
  84. package/dist/utils/deduplicate-nodes.js +4 -0
  85. package/dist/utils/deduplicate-nodes.js.map +1 -0
  86. package/dist/utils/deep-merge.d.ts +1 -1
  87. package/dist/utils/deep-merge.d.ts.map +1 -1
  88. package/dist/utils/deep-merge.js +6 -5
  89. package/dist/utils/deep-merge.js.map +1 -1
  90. package/dist/utils/dom-helpers.d.ts +3 -0
  91. package/dist/utils/dom-helpers.d.ts.map +1 -1
  92. package/dist/utils/dom-helpers.js +15 -0
  93. package/dist/utils/dom-helpers.js.map +1 -1
  94. package/dist/utils/ensure-non-empty.d.ts +2 -0
  95. package/dist/utils/ensure-non-empty.d.ts.map +1 -0
  96. package/dist/utils/ensure-non-empty.js +7 -0
  97. package/dist/utils/ensure-non-empty.js.map +1 -0
  98. package/dist/utils/get-element-html.d.ts +1 -1
  99. package/dist/utils/get-element-html.d.ts.map +1 -1
  100. package/dist/utils/get-element-html.js +4 -2
  101. package/dist/utils/get-element-html.js.map +1 -1
  102. package/dist/utils/get-element-position.d.ts +10 -2
  103. package/dist/utils/get-element-position.d.ts.map +1 -1
  104. package/dist/utils/get-element-position.js +34 -23
  105. package/dist/utils/get-element-position.js.map +1 -1
  106. package/dist/utils/get-parent.d.ts +1 -1
  107. package/dist/utils/get-parent.d.ts.map +1 -1
  108. package/dist/utils/get-parent.js +1 -1
  109. package/dist/utils/get-parent.js.map +1 -1
  110. package/dist/utils/get-scan-context.d.ts +3 -0
  111. package/dist/utils/get-scan-context.d.ts.map +1 -0
  112. package/dist/utils/get-scan-context.js +28 -0
  113. package/dist/utils/get-scan-context.js.map +1 -0
  114. package/dist/utils/get-scrollable-ancestors.d.ts +1 -1
  115. package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -1
  116. package/dist/utils/get-scrollable-ancestors.js +5 -5
  117. package/dist/utils/get-scrollable-ancestors.js.map +1 -1
  118. package/dist/utils/is-node-in-scan-context.d.ts +3 -0
  119. package/dist/utils/is-node-in-scan-context.d.ts.map +1 -0
  120. package/dist/utils/is-node-in-scan-context.js +26 -0
  121. package/dist/utils/is-node-in-scan-context.js.map +1 -0
  122. package/dist/utils/is-non-empty.d.ts +2 -0
  123. package/dist/utils/is-non-empty.d.ts.map +1 -0
  124. package/dist/utils/is-non-empty.js +4 -0
  125. package/dist/utils/is-non-empty.js.map +1 -0
  126. package/dist/utils/normalize-context.d.ts +3 -0
  127. package/dist/utils/normalize-context.d.ts.map +1 -0
  128. package/dist/utils/normalize-context.js +59 -0
  129. package/dist/utils/normalize-context.js.map +1 -0
  130. package/dist/utils/recalculate-positions.d.ts +1 -1
  131. package/dist/utils/recalculate-positions.d.ts.map +1 -1
  132. package/dist/utils/recalculate-positions.js +5 -5
  133. package/dist/utils/recalculate-positions.js.map +1 -1
  134. package/dist/utils/recalculate-scrollable-ancestors.d.ts +1 -1
  135. package/dist/utils/recalculate-scrollable-ancestors.d.ts.map +1 -1
  136. package/dist/utils/recalculate-scrollable-ancestors.js +4 -4
  137. package/dist/utils/recalculate-scrollable-ancestors.js.map +1 -1
  138. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts +1 -1
  139. package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -1
  140. package/dist/utils/shadow-dom-aware-mutation-observer.js +19 -22
  141. package/dist/utils/shadow-dom-aware-mutation-observer.js.map +1 -1
  142. package/dist/utils/supports-anchor-positioning.d.ts +1 -1
  143. package/dist/utils/supports-anchor-positioning.d.ts.map +1 -1
  144. package/dist/utils/supports-anchor-positioning.js +1 -1
  145. package/dist/utils/supports-anchor-positioning.js.map +1 -1
  146. package/dist/utils/transform-violations.d.ts +2 -2
  147. package/dist/utils/transform-violations.d.ts.map +1 -1
  148. package/dist/utils/transform-violations.js +9 -9
  149. package/dist/utils/transform-violations.js.map +1 -1
  150. package/dist/utils/update-elements-with-issues.d.ts +11 -5
  151. package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
  152. package/dist/utils/update-elements-with-issues.js +54 -26
  153. package/dist/utils/update-elements-with-issues.js.map +1 -1
  154. package/dist/validate-options.d.ts +2 -2
  155. package/dist/validate-options.d.ts.map +1 -1
  156. package/dist/validate-options.js +91 -4
  157. package/dist/validate-options.js.map +1 -1
  158. package/package.json +11 -5
  159. package/src/accented.test.ts +2 -2
  160. package/src/accented.ts +38 -28
  161. package/src/common/tokens.ts +1 -0
  162. package/src/dom-updater.ts +59 -22
  163. package/src/elements/accented-dialog.ts +102 -106
  164. package/src/elements/accented-trigger.ts +58 -48
  165. package/src/fullscreen-listener.ts +15 -11
  166. package/src/intersection-observer.ts +27 -16
  167. package/src/log-and-rethrow.ts +2 -3
  168. package/src/logger.ts +14 -4
  169. package/src/register-elements.ts +7 -7
  170. package/src/resize-listener.ts +15 -11
  171. package/src/scanner.ts +70 -50
  172. package/src/scroll-listeners.ts +27 -19
  173. package/src/state.ts +24 -21
  174. package/src/task-queue.test.ts +5 -4
  175. package/src/task-queue.ts +8 -6
  176. package/src/types.ts +74 -42
  177. package/src/utils/are-elements-with-issues-equal.ts +7 -5
  178. package/src/utils/are-issue-sets-equal.test.ts +10 -6
  179. package/src/utils/are-issue-sets-equal.ts +8 -6
  180. package/src/utils/containing-blocks.ts +60 -0
  181. package/src/utils/contains.test.ts +55 -0
  182. package/src/utils/contains.ts +19 -0
  183. package/src/utils/deduplicate-nodes.ts +3 -0
  184. package/src/utils/deep-merge.test.ts +8 -1
  185. package/src/utils/deep-merge.ts +11 -8
  186. package/src/utils/dom-helpers.ts +20 -0
  187. package/src/utils/ensure-non-empty.ts +6 -0
  188. package/src/utils/get-element-html.ts +4 -2
  189. package/src/utils/get-element-position.ts +51 -22
  190. package/src/utils/get-parent.ts +1 -1
  191. package/src/utils/get-scan-context.test.ts +85 -0
  192. package/src/utils/get-scan-context.ts +36 -0
  193. package/src/utils/get-scrollable-ancestors.ts +8 -5
  194. package/src/utils/is-node-in-scan-context.test.ts +70 -0
  195. package/src/utils/is-node-in-scan-context.ts +29 -0
  196. package/src/utils/is-non-empty.ts +3 -0
  197. package/src/utils/normalize-context.test.ts +105 -0
  198. package/src/utils/normalize-context.ts +65 -0
  199. package/src/utils/recalculate-positions.ts +5 -5
  200. package/src/utils/recalculate-scrollable-ancestors.ts +4 -4
  201. package/src/utils/shadow-dom-aware-mutation-observer.ts +21 -24
  202. package/src/utils/supports-anchor-positioning.ts +3 -3
  203. package/src/utils/transform-violations.test.ts +22 -20
  204. package/src/utils/transform-violations.ts +14 -10
  205. package/src/utils/update-elements-with-issues.test.ts +102 -49
  206. package/src/utils/update-elements-with-issues.ts +122 -58
  207. package/src/validate-options.ts +154 -14
package/src/accented.ts CHANGED
@@ -1,18 +1,18 @@
1
-
2
- import registerElements from './register-elements.js';
3
- import createDomUpdater from './dom-updater.js';
4
- import createLogger from './logger.js';
5
- import createScanner from './scanner.js';
6
- import setupScrollListeners from './scroll-listeners.js';
7
- import setupResizeListener from './resize-listener.js';
8
- import setupFullscreenListener from './fullscreen-listener.js';
9
- import setupIntersectionObserver from './intersection-observer.js';
1
+ import { createDomUpdater } from './dom-updater.js';
2
+ import { setupResizeListener as setupFullscreenListener } from './fullscreen-listener.js';
3
+ import { setupIntersectionObserver } from './intersection-observer.js';
4
+ import { logAndRethrow } from './log-and-rethrow.js';
5
+ import { createLogger } from './logger.js';
6
+ import { registerElements } from './register-elements.js';
7
+ import { setupResizeListener } from './resize-listener.js';
8
+ import { createScanner } from './scanner.js';
9
+ import { setupScrollListeners } from './scroll-listeners.js';
10
10
  import { enabled, extendedElementsWithIssues } from './state.js';
11
- import deepMerge from './utils/deep-merge.js';
12
- import type { AccentedOptions, DisableAccented } from './types';
13
- import validateOptions from './validate-options.js';
14
- import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
15
- import logAndRethrow from './log-and-rethrow.js';
11
+ import type { AccentedOptions, DisableAccented } from './types.ts';
12
+ import { initializeContainingBlockSupportSet } from './utils/containing-blocks.js';
13
+ import { deepMerge } from './utils/deep-merge.js';
14
+ import { supportsAnchorPositioning } from './utils/supports-anchor-positioning.js';
15
+ import { validateOptions } from './validate-options.js';
16
16
 
17
17
  export type { AccentedOptions, DisableAccented };
18
18
 
@@ -41,24 +41,25 @@ export type { AccentedOptions, DisableAccented };
41
41
  * }
42
42
  * });
43
43
  */
44
- export default function accented(options: AccentedOptions = {}): DisableAccented {
45
-
44
+ export function accented(options: AccentedOptions = {}): DisableAccented {
46
45
  validateOptions(options);
47
46
 
48
47
  try {
49
48
  if (typeof window === 'undefined' || typeof document === 'undefined') {
50
- console.warn('Accented: this script can only run in the browser, and it’s likely running on the server now. Exiting.');
49
+ console.warn(
50
+ 'Accented: this script can only run in the browser, and it’s likely running on the server now. Exiting.',
51
+ );
51
52
  console.trace();
52
53
  return () => {};
53
54
  }
54
55
 
55
56
  const defaultOutput: Required<AccentedOptions['output']> = {
56
- console: true
57
+ console: true,
57
58
  };
58
59
 
59
60
  const defaultThrottle: Required<AccentedOptions['throttle']> = {
60
61
  wait: 1000,
61
- leading: true
62
+ leading: true,
62
63
  };
63
64
 
64
65
  // IMPORTANT: when changing any of the properties or values, also do the following:
@@ -67,36 +68,45 @@ export default function accented(options: AccentedOptions = {}): DisableAccented
67
68
  // * update examples in the accented() function JSDoc;
68
69
  // * update examples in the Readme.
69
70
  const defaultOptions: Required<AccentedOptions> = {
70
- axeContext: document,
71
+ context: document,
71
72
  axeOptions: {},
72
73
  name: 'accented',
73
74
  output: defaultOutput,
74
75
  throttle: defaultThrottle,
75
- callback: () => {}
76
+ callback: () => {},
76
77
  };
77
78
 
78
- const {axeContext, axeOptions, name, output, throttle, callback} = deepMerge(defaultOptions, options);
79
+ const { context, axeOptions, name, output, throttle, callback } = deepMerge(
80
+ defaultOptions,
81
+ options,
82
+ );
79
83
 
80
84
  if (enabled.value) {
81
85
  // Add link to the recipes section of the docs (#56).
82
86
  console.warn(
83
87
  'You are trying to run the Accented library more than once. ' +
84
- 'This will likely lead to errors.'
88
+ 'This will likely lead to errors.',
85
89
  );
86
90
  console.trace();
87
91
  }
88
92
 
89
93
  enabled.value = true;
90
94
 
95
+ initializeContainingBlockSupportSet();
91
96
  registerElements(name);
92
97
 
93
- const {disconnect: cleanupIntersectionObserver, intersectionObserver } = supportsAnchorPositioning(window) ? {} : setupIntersectionObserver();
94
- const cleanupScanner = createScanner(name, axeContext, axeOptions, throttle, callback);
98
+ const { disconnect: cleanupIntersectionObserver, intersectionObserver } =
99
+ setupIntersectionObserver();
100
+ const cleanupScanner = createScanner(name, context, axeOptions, throttle, callback);
95
101
  const cleanupDomUpdater = createDomUpdater(name, intersectionObserver);
96
102
  const cleanupLogger = output.console ? createLogger() : () => {};
97
- const cleanupScrollListeners = supportsAnchorPositioning(window) ? () => {} : setupScrollListeners();
98
- const cleanupResizeListener = supportsAnchorPositioning(window) ? () => {} : setupResizeListener();
99
- const cleanupFullscreenListener = supportsAnchorPositioning(window) ? () => {} : setupFullscreenListener();
103
+ const cleanupScrollListeners = setupScrollListeners();
104
+ const cleanupResizeListener = supportsAnchorPositioning(window)
105
+ ? () => {}
106
+ : setupResizeListener();
107
+ const cleanupFullscreenListener = supportsAnchorPositioning(window)
108
+ ? () => {}
109
+ : setupFullscreenListener();
100
110
 
101
111
  return () => {
102
112
  try {
@@ -0,0 +1 @@
1
+ export const primaryColor = 'oklch(0.5 0.3 0)';
@@ -1,22 +1,47 @@
1
1
  import { effect } from '@preact/signals-core';
2
+ import { primaryColor } from './common/tokens.js';
2
3
  import { extendedElementsWithIssues, rootNodes } from './state.js';
3
- import type { ExtendedElementWithIssues } from './types';
4
- import areElementsWithIssuesEqual from './utils/are-elements-with-issues-equal.js';
5
- import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
4
+ import type { ExtendedElementWithIssues } from './types.ts';
5
+ import { areElementsWithIssuesEqual } from './utils/are-elements-with-issues-equal.js';
6
6
  import { isDocument, isDocumentFragment, isShadowRoot } from './utils/dom-helpers.js';
7
- import getParent from './utils/get-parent.js';
7
+ import { getParent } from './utils/get-parent.js';
8
+ import { supportsAnchorPositioning } from './utils/supports-anchor-positioning.js';
8
9
 
9
- export default function createDomUpdater(name: string, intersectionObserver?: IntersectionObserver) {
10
+ const shouldInsertTriggerInsideElement = (element: Element): boolean => {
11
+ /**
12
+ * No parent means that the element is a root node,
13
+ * which cannot have siblings.
14
+ */
15
+ const noParent = !getParent(element);
16
+
17
+ /**
18
+ * Table cells get a special treatment because if a sibling to a TH or TD is inserted,
19
+ * it alters the table layout, no matter how that sibling is positioned.
20
+ * We don't want tables to look broken, so we're inserting the trigger inside the table cell.
21
+ */
22
+ const isTableCell = element.nodeName === 'TH' || element.nodeName === 'TD';
23
+
24
+ /**
25
+ * We want to put the trigger inside the <summary> element,
26
+ * because otherwise it will be hidden by the browser when the <details> element is collapsed
27
+ * (since none of the siblings of <summary> are visible then).
28
+ */
29
+ const isSummary = element.nodeName === 'SUMMARY';
30
+
31
+ return noParent || isTableCell || isSummary;
32
+ };
33
+
34
+ export function createDomUpdater(name: string, intersectionObserver?: IntersectionObserver) {
10
35
  const attrName = `data-${name}`;
11
36
 
12
- function getAnchorNames (anchorNameValue: string) {
37
+ function getAnchorNames(anchorNameValue: string) {
13
38
  return anchorNameValue
14
39
  .split(',')
15
- .map(anchorName => anchorName.trim())
16
- .filter(anchorName => anchorName.startsWith('--'));
40
+ .map((anchorName) => anchorName.trim())
41
+ .filter((anchorName) => anchorName.startsWith('--'));
17
42
  }
18
43
 
19
- function setAnchorName (elementWithIssues: ExtendedElementWithIssues) {
44
+ function setAnchorName(elementWithIssues: ExtendedElementWithIssues) {
20
45
  const { element, id, anchorNameValue } = elementWithIssues;
21
46
  const anchorNames = getAnchorNames(anchorNameValue);
22
47
  if (anchorNames.length > 0) {
@@ -26,7 +51,7 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
26
51
  }
27
52
  }
28
53
 
29
- function removeAnchorName (elementWithIssues: ExtendedElementWithIssues) {
54
+ function removeAnchorName(elementWithIssues: ExtendedElementWithIssues) {
30
55
  const { element, anchorNameValue } = elementWithIssues;
31
56
  const anchorNames = getAnchorNames(anchorNameValue);
32
57
  if (anchorNames.length > 0) {
@@ -36,17 +61,20 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
36
61
  }
37
62
  }
38
63
 
39
- function setIssues (extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
64
+ function setIssues(extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
40
65
  for (const elementWithIssues of extendedElementsWithIssues) {
66
+ if (elementWithIssues.skipRender) {
67
+ continue;
68
+ }
41
69
  elementWithIssues.element.setAttribute(attrName, elementWithIssues.id.toString());
42
70
  if (supportsAnchorPositioning(window)) {
43
71
  setAnchorName(elementWithIssues);
44
72
  }
45
73
 
46
- if (getParent(elementWithIssues.element)) {
47
- elementWithIssues.element.insertAdjacentElement('afterend', elementWithIssues.trigger);
48
- } else {
74
+ if (shouldInsertTriggerInsideElement(elementWithIssues.element)) {
49
75
  elementWithIssues.element.insertAdjacentElement('beforeend', elementWithIssues.trigger);
76
+ } else {
77
+ elementWithIssues.element.insertAdjacentElement('afterend', elementWithIssues.trigger);
50
78
  }
51
79
  if (intersectionObserver) {
52
80
  intersectionObserver.observe(elementWithIssues.element);
@@ -54,8 +82,11 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
54
82
  }
55
83
  }
56
84
 
57
- function removeIssues (extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
85
+ function removeIssues(extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
58
86
  for (const elementWithIssues of extendedElementsWithIssues) {
87
+ if (elementWithIssues.skipRender) {
88
+ continue;
89
+ }
59
90
  elementWithIssues.element.removeAttribute(attrName);
60
91
  if (supportsAnchorPositioning(window)) {
61
92
  removeAnchorName(elementWithIssues);
@@ -73,7 +104,7 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
73
104
  :root {
74
105
  /* Ensure that the primary / secondary color combination meets WCAG 1.4.3 Contrast (Minimum) */
75
106
  /* OKLCH stuff: https://oklch.com/ */
76
- --${name}-primary-color: oklch(0.5 0.3 0);
107
+ --${name}-primary-color: ${primaryColor};
77
108
  --${name}-secondary-color: oklch(0.98 0 0);
78
109
  --${name}-outline-width: 2px;
79
110
  --${name}-outline-style: solid;
@@ -94,8 +125,10 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
94
125
 
95
126
  const disposeOfStyleSheetsEffect = effect(() => {
96
127
  const newRootNodes = rootNodes.value;
97
- const addedRootNodes = [...newRootNodes].filter(rootNode => !previousRootNodes.has(rootNode));
98
- const removedRootNodes = [...previousRootNodes].filter(rootNode => !newRootNodes.has(rootNode));
128
+ const addedRootNodes = [...newRootNodes].filter((rootNode) => !previousRootNodes.has(rootNode));
129
+ const removedRootNodes = [...previousRootNodes].filter(
130
+ (rootNode) => !newRootNodes.has(rootNode),
131
+ );
99
132
  for (const rootNode of addedRootNodes) {
100
133
  if (isDocument(rootNode) || (isDocumentFragment(rootNode) && isShadowRoot(rootNode))) {
101
134
  rootNode.adoptedStyleSheets.push(stylesheet);
@@ -110,11 +143,15 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
110
143
  });
111
144
 
112
145
  const disposeOfElementsEffect = effect(() => {
113
- const added = extendedElementsWithIssues.value.filter(elementWithIssues => {
114
- return !previousExtendedElementsWithIssues.some(previousElementWithIssues => areElementsWithIssuesEqual(previousElementWithIssues, elementWithIssues));
146
+ const added = extendedElementsWithIssues.value.filter((elementWithIssues) => {
147
+ return !previousExtendedElementsWithIssues.some((previousElementWithIssues) =>
148
+ areElementsWithIssuesEqual(previousElementWithIssues, elementWithIssues),
149
+ );
115
150
  });
116
- const removed = previousExtendedElementsWithIssues.filter(previousElementWithIssues => {
117
- return !extendedElementsWithIssues.value.some(elementWithIssues => areElementsWithIssuesEqual(elementWithIssues, previousElementWithIssues));
151
+ const removed = previousExtendedElementsWithIssues.filter((previousElementWithIssues) => {
152
+ return !extendedElementsWithIssues.value.some((elementWithIssues) =>
153
+ areElementsWithIssuesEqual(elementWithIssues, previousElementWithIssues),
154
+ );
118
155
  });
119
156
  removeIssues(removed);
120
157
  setIssues(added);
@@ -1,19 +1,20 @@
1
- import type { Issue } from '../types';
2
1
  import type { Signal } from '@preact/signals-core';
3
- import { effect } from '@preact/signals-core';
4
- import getElementHtml from '../utils/get-element-html.js';
5
2
  import { accentedUrl } from '../constants.js';
6
- import logAndRethrow from '../log-and-rethrow.js';
3
+ import { logAndRethrow } from '../log-and-rethrow.js';
4
+ import type { Issue } from '../types.ts';
5
+ import { getElementHtml } from '../utils/get-element-html.js';
6
+ import { isNonEmpty } from '../utils/is-non-empty.js';
7
7
 
8
8
  export interface AccentedDialog extends HTMLElement {
9
9
  issues: Signal<Array<Issue>> | undefined;
10
10
  element: Element | undefined;
11
11
  showModal: () => void;
12
+ open: boolean;
12
13
  }
13
14
 
14
15
  // We want Accented to not throw an error in Node, and use static imports,
15
16
  // so we can't export `class extends HTMLElement` because HTMLElement is not available in Node.
16
- export default () => {
17
+ export const getAccentedDialog = () => {
17
18
  const dialogTemplate = document.createElement('template');
18
19
  dialogTemplate.innerHTML = `
19
20
  <dialog dir="ltr" lang="en" aria-labelledby="title">
@@ -241,19 +242,17 @@ export default () => {
241
242
  `);
242
243
 
243
244
  return class extends HTMLElement implements AccentedDialog {
244
- #disposeOfEffect: (() => void) | undefined;
245
-
246
245
  #abortController: AbortController | undefined;
247
246
 
248
247
  issues: Signal<Array<Issue>> | undefined;
249
248
 
250
249
  element: Element | undefined;
251
250
 
252
- #elementMutationObserver: MutationObserver | undefined;
251
+ open = false;
253
252
 
254
253
  constructor() {
254
+ super();
255
255
  try {
256
- super();
257
256
  this.attachShadow({ mode: 'open' });
258
257
  const content = dialogTemplate.content.cloneNode(true);
259
258
  if (this.shadowRoot) {
@@ -272,106 +271,104 @@ export default () => {
272
271
  const dialog = shadowRoot.querySelector('dialog');
273
272
  const closeButton = shadowRoot.querySelector('#close');
274
273
  this.#abortController = new AbortController();
275
- closeButton?.addEventListener('click', () => {
276
- try {
277
- dialog?.close();
278
- } catch (error) {
279
- logAndRethrow(error);
280
- }
281
- }, { signal: this.#abortController.signal });
282
-
283
- dialog?.addEventListener('click', (event) => {
284
- try {
285
- this.#onDialogClick(event);
286
- } catch (error) {
287
- logAndRethrow(error);
288
- }
289
- }, { signal: this.#abortController.signal });
290
-
291
- dialog?.addEventListener('keydown', (event) => {
292
- try {
293
- if (event.key === 'Escape') {
294
- event.stopPropagation();
274
+ closeButton?.addEventListener(
275
+ 'click',
276
+ () => {
277
+ try {
278
+ dialog?.close();
279
+ } catch (error) {
280
+ logAndRethrow(error);
295
281
  }
296
- } catch (error) {
297
- logAndRethrow(error);
298
- }
299
- }, { signal: this.#abortController.signal });
300
-
301
- this.#disposeOfEffect = effect(() => {
302
- if (this.issues) {
303
- const issues = this.issues.value;
304
- const issuesList = shadowRoot.getElementById('issues');
305
- if (issuesList) {
306
- issuesList.innerHTML = '';
307
- for (const issue of issues) {
308
- const issueContent = issueTemplate.content.cloneNode(true) as Element;
309
- const title = issueContent.querySelector('a');
310
- const impact = issueContent.querySelector('.impact');
311
- const description = issueContent.querySelector('.description');
312
- if (title && impact && description) {
313
- title.textContent = issue.title + ' (' + issue.id + ')';
314
- title.href = issue.url;
315
-
316
- impact.textContent = 'User impact: ' + issue.impact;
317
- impact.setAttribute('data-impact', String(issue.impact));
318
-
319
- const descriptionItems = issue.description.split(/\n\s*/);
320
- const descriptionContent = descriptionTemplate.content.cloneNode(true) as Element;
321
- const descriptionTitle = descriptionContent.querySelector('span');
322
- const descriptionList = descriptionContent.querySelector('ul');
323
- if (descriptionTitle && descriptionList && descriptionItems.length > 1) {
324
- descriptionTitle.textContent = descriptionItems[0]!;
325
- for (const descriptionItem of descriptionItems.slice(1)) {
326
- const li = document.createElement('li');
327
- li.textContent = descriptionItem;
328
- descriptionList.appendChild(li);
329
- }
330
- description.appendChild(descriptionContent);
282
+ },
283
+ { signal: this.#abortController.signal },
284
+ );
285
+
286
+ dialog?.addEventListener(
287
+ 'click',
288
+ (event) => {
289
+ try {
290
+ this.#onDialogClick(event);
291
+ } catch (error) {
292
+ logAndRethrow(error);
293
+ }
294
+ },
295
+ { signal: this.#abortController.signal },
296
+ );
297
+
298
+ dialog?.addEventListener(
299
+ 'keydown',
300
+ (event) => {
301
+ try {
302
+ if (event.key === 'Escape') {
303
+ event.stopPropagation();
304
+ }
305
+ } catch (error) {
306
+ logAndRethrow(error);
307
+ }
308
+ },
309
+ { signal: this.#abortController.signal },
310
+ );
311
+
312
+ if (this.issues) {
313
+ const issues = this.issues.value;
314
+ const issuesList = shadowRoot.getElementById('issues');
315
+ if (issuesList) {
316
+ issuesList.innerHTML = '';
317
+ for (const issue of issues) {
318
+ const issueContent = issueTemplate.content.cloneNode(true) as Element;
319
+ const title = issueContent.querySelector('a');
320
+ const impact = issueContent.querySelector('.impact');
321
+ const description = issueContent.querySelector('.description');
322
+ if (title && impact && description) {
323
+ title.textContent = `${issue.title} (${issue.id})`;
324
+ title.href = issue.url;
325
+
326
+ impact.textContent = `User impact: ${issue.impact}`;
327
+ impact.setAttribute('data-impact', String(issue.impact));
328
+
329
+ const descriptionItems = issue.description.split(/\n\s*/);
330
+ const descriptionContent = descriptionTemplate.content.cloneNode(true) as Element;
331
+ const descriptionTitle = descriptionContent.querySelector('span');
332
+ const descriptionList = descriptionContent.querySelector('ul');
333
+ if (
334
+ descriptionTitle &&
335
+ descriptionList &&
336
+ isNonEmpty(descriptionItems) &&
337
+ descriptionItems.length > 1
338
+ ) {
339
+ descriptionTitle.textContent = descriptionItems[0];
340
+ for (const descriptionItem of descriptionItems.slice(1)) {
341
+ const li = document.createElement('li');
342
+ li.textContent = descriptionItem;
343
+ descriptionList.appendChild(li);
331
344
  }
345
+ description.appendChild(descriptionContent);
332
346
  }
333
- issuesList.appendChild(issueContent);
334
347
  }
348
+ issuesList.appendChild(issueContent);
335
349
  }
336
350
  }
337
- });
338
-
339
- const updateElementHtml = () => {
340
- if (this.element) {
341
- const elementHtmlContainer = shadowRoot.getElementById('element-html');
342
- if (elementHtmlContainer) {
343
- elementHtmlContainer.textContent = getElementHtml(this.element);
344
- }
345
- }
346
- };
347
-
348
- updateElementHtml();
351
+ }
349
352
 
350
- this.#elementMutationObserver = new MutationObserver(() => {
351
- try {
352
- updateElementHtml();
353
- } catch (error) {
354
- logAndRethrow(error);
355
- }
356
- });
357
353
  if (this.element) {
358
- // We're only outputting the element itself, not its subtree.
359
- // However, we're still listening for childList changes, because
360
- // we display an ellipsis if the element has innerHTML,
361
- // and we leave it empty if the element is empty.
362
- this.#elementMutationObserver.observe(this.element, {
363
- attributes: true,
364
- childList: true
365
- });
354
+ const elementHtmlContainer = shadowRoot.getElementById('element-html');
355
+ if (elementHtmlContainer) {
356
+ elementHtmlContainer.textContent = getElementHtml(this.element);
357
+ }
366
358
  }
367
359
 
368
- dialog?.addEventListener('close', () => {
369
- try {
370
- this.dispatchEvent(new Event('close'));
371
- } catch (error) {
372
- logAndRethrow(error);
373
- }
374
- }, { signal: this.#abortController.signal });
360
+ dialog?.addEventListener(
361
+ 'close',
362
+ () => {
363
+ try {
364
+ this.open = false;
365
+ this.dispatchEvent(new Event('close'));
366
+ } catch (error) {
367
+ logAndRethrow(error);
368
+ }
369
+ },
370
+ { signal: this.#abortController.signal },
371
+ );
375
372
  }
376
373
  } catch (error) {
377
374
  logAndRethrow(error);
@@ -380,15 +377,9 @@ export default () => {
380
377
 
381
378
  disconnectedCallback() {
382
379
  try {
383
- if (this.#disposeOfEffect) {
384
- this.#disposeOfEffect();
385
- }
386
380
  if (this.#abortController) {
387
381
  this.#abortController.abort();
388
382
  }
389
- if (this.#elementMutationObserver) {
390
- this.#elementMutationObserver.disconnect();
391
- }
392
383
  } catch (error) {
393
384
  logAndRethrow(error);
394
385
  }
@@ -399,13 +390,18 @@ export default () => {
399
390
  const dialog = this.shadowRoot.querySelector('dialog');
400
391
  if (dialog) {
401
392
  dialog.showModal();
393
+ this.open = true;
402
394
  }
403
395
  }
404
396
  }
405
397
 
406
398
  #onDialogClick(event: MouseEvent) {
407
399
  const dialog = event.currentTarget as HTMLDialogElement;
408
- if (!dialog || typeof dialog.getBoundingClientRect !== 'function' || typeof dialog.close !== 'function') {
400
+ if (
401
+ !dialog ||
402
+ typeof dialog.getBoundingClientRect !== 'function' ||
403
+ typeof dialog.close !== 'function'
404
+ ) {
409
405
  return;
410
406
  }
411
407
  const rect = dialog.getBoundingClientRect();