accented 0.0.0-20250124142030 → 0.0.0-20250223121749

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 (124) hide show
  1. package/README.md +55 -3
  2. package/dist/accented.d.ts +3 -1
  3. package/dist/accented.d.ts.map +1 -1
  4. package/dist/accented.js +69 -50
  5. package/dist/accented.js.map +1 -1
  6. package/dist/constants.d.ts +3 -0
  7. package/dist/constants.d.ts.map +1 -0
  8. package/dist/constants.js +3 -0
  9. package/dist/constants.js.map +1 -0
  10. package/dist/dom-updater.d.ts +1 -1
  11. package/dist/dom-updater.d.ts.map +1 -1
  12. package/dist/dom-updater.js +26 -19
  13. package/dist/dom-updater.js.map +1 -1
  14. package/dist/elements/{accented-container.d.ts → accented-dialog.d.ts} +10 -4
  15. package/dist/elements/accented-dialog.d.ts.map +1 -0
  16. package/dist/elements/accented-dialog.js +361 -0
  17. package/dist/elements/accented-dialog.js.map +1 -0
  18. package/dist/elements/accented-trigger.d.ts +359 -0
  19. package/dist/elements/accented-trigger.d.ts.map +1 -0
  20. package/dist/elements/accented-trigger.js +159 -0
  21. package/dist/elements/accented-trigger.js.map +1 -0
  22. package/dist/intersection-observer.d.ts +5 -0
  23. package/dist/intersection-observer.d.ts.map +1 -0
  24. package/dist/intersection-observer.js +28 -0
  25. package/dist/intersection-observer.js.map +1 -0
  26. package/dist/log-and-rethrow.d.ts +2 -0
  27. package/dist/log-and-rethrow.d.ts.map +1 -0
  28. package/dist/log-and-rethrow.js +7 -0
  29. package/dist/log-and-rethrow.js.map +1 -0
  30. package/dist/logger.d.ts.map +1 -1
  31. package/dist/logger.js +10 -5
  32. package/dist/logger.js.map +1 -1
  33. package/dist/register-elements.d.ts +2 -0
  34. package/dist/register-elements.d.ts.map +1 -0
  35. package/dist/register-elements.js +21 -0
  36. package/dist/register-elements.js.map +1 -0
  37. package/dist/resize-listener.d.ts +2 -0
  38. package/dist/resize-listener.d.ts.map +1 -0
  39. package/dist/resize-listener.js +18 -0
  40. package/dist/resize-listener.js.map +1 -0
  41. package/dist/scanner.d.ts +2 -2
  42. package/dist/scanner.d.ts.map +1 -1
  43. package/dist/scanner.js +97 -33
  44. package/dist/scanner.js.map +1 -1
  45. package/dist/scroll-listeners.d.ts +2 -0
  46. package/dist/scroll-listeners.d.ts.map +1 -0
  47. package/dist/scroll-listeners.js +38 -0
  48. package/dist/scroll-listeners.js.map +1 -0
  49. package/dist/state.d.ts +1 -0
  50. package/dist/state.d.ts.map +1 -1
  51. package/dist/state.js +6 -0
  52. package/dist/state.js.map +1 -1
  53. package/dist/types.d.ts +70 -18
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +1 -0
  56. package/dist/types.js.map +1 -1
  57. package/dist/utils/deep-merge.js +1 -1
  58. package/dist/utils/deep-merge.js.map +1 -1
  59. package/dist/utils/get-element-html.d.ts +2 -0
  60. package/dist/utils/get-element-html.d.ts.map +1 -0
  61. package/dist/utils/get-element-html.js +14 -0
  62. package/dist/utils/get-element-html.js.map +1 -0
  63. package/dist/utils/get-element-position.d.ts +3 -0
  64. package/dist/utils/get-element-position.d.ts.map +1 -0
  65. package/dist/utils/get-element-position.js +22 -0
  66. package/dist/utils/get-element-position.js.map +1 -0
  67. package/dist/utils/get-scrollable-ancestors.d.ts +2 -0
  68. package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -0
  69. package/dist/utils/get-scrollable-ancestors.js +15 -0
  70. package/dist/utils/get-scrollable-ancestors.js.map +1 -0
  71. package/dist/utils/recalculate-positions.d.ts +2 -0
  72. package/dist/utils/recalculate-positions.d.ts.map +1 -0
  73. package/dist/utils/recalculate-positions.js +27 -0
  74. package/dist/utils/recalculate-positions.js.map +1 -0
  75. package/dist/utils/recalculate-scrollable-ancestors.d.ts +2 -0
  76. package/dist/utils/recalculate-scrollable-ancestors.d.ts.map +1 -0
  77. package/dist/utils/recalculate-scrollable-ancestors.js +13 -0
  78. package/dist/utils/recalculate-scrollable-ancestors.js.map +1 -0
  79. package/dist/utils/supports-anchor-positioning.d.ts +6 -0
  80. package/dist/utils/supports-anchor-positioning.d.ts.map +1 -0
  81. package/dist/utils/supports-anchor-positioning.js +4 -0
  82. package/dist/utils/supports-anchor-positioning.js.map +1 -0
  83. package/dist/utils/transform-violations.d.ts.map +1 -1
  84. package/dist/utils/transform-violations.js +9 -0
  85. package/dist/utils/transform-violations.js.map +1 -1
  86. package/dist/utils/update-elements-with-issues.d.ts +3 -1
  87. package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
  88. package/dist/utils/update-elements-with-issues.js +25 -7
  89. package/dist/utils/update-elements-with-issues.js.map +1 -1
  90. package/dist/validate-options.d.ts +3 -0
  91. package/dist/validate-options.d.ts.map +1 -0
  92. package/dist/validate-options.js +42 -0
  93. package/dist/validate-options.js.map +1 -0
  94. package/package.json +2 -1
  95. package/src/accented.ts +78 -58
  96. package/src/constants.ts +2 -0
  97. package/src/dom-updater.ts +26 -18
  98. package/src/elements/accented-dialog.ts +384 -0
  99. package/src/elements/accented-trigger.ts +179 -0
  100. package/src/intersection-observer.ts +28 -0
  101. package/src/log-and-rethrow.ts +9 -0
  102. package/src/logger.ts +11 -6
  103. package/src/register-elements.ts +21 -0
  104. package/src/resize-listener.ts +17 -0
  105. package/src/scanner.ts +108 -37
  106. package/src/scroll-listeners.ts +37 -0
  107. package/src/state.ts +12 -0
  108. package/src/types.ts +77 -19
  109. package/src/utils/deep-merge.test.ts +7 -0
  110. package/src/utils/deep-merge.ts +1 -1
  111. package/src/utils/get-element-html.ts +13 -0
  112. package/src/utils/get-element-position.ts +21 -0
  113. package/src/utils/get-scrollable-ancestors.ts +14 -0
  114. package/src/utils/recalculate-positions.ts +27 -0
  115. package/src/utils/recalculate-scrollable-ancestors.ts +13 -0
  116. package/src/utils/supports-anchor-positioning.ts +7 -0
  117. package/src/utils/transform-violations.ts +12 -1
  118. package/src/utils/update-elements-with-issues.test.ts +90 -16
  119. package/src/utils/update-elements-with-issues.ts +40 -20
  120. package/src/validate-options.ts +44 -0
  121. package/dist/elements/accented-container.d.ts.map +0 -1
  122. package/dist/elements/accented-container.js +0 -131
  123. package/dist/elements/accented-container.js.map +0 -1
  124. package/src/elements/accented-container.ts +0 -147
@@ -0,0 +1,384 @@
1
+ import type { Issue } from '../types';
2
+ import type { Signal } from '@preact/signals-core';
3
+ import { effect } from '@preact/signals-core';
4
+ import getElementHtml from '../utils/get-element-html.js';
5
+ import { accentedUrl } from '../constants.js';
6
+ import logAndRethrow from '../log-and-rethrow.js';
7
+
8
+ export interface AccentedDialog extends HTMLElement {
9
+ issues: Signal<Array<Issue>> | undefined;
10
+ element: Element | undefined;
11
+ showModal: () => void;
12
+ }
13
+
14
+ // We want Accented to not throw an error in Node, and use static imports,
15
+ // so we can't export `class extends HTMLElement` because HTMLElement is not available in Node.
16
+ export default () => {
17
+ const dialogTemplate = document.createElement('template');
18
+ dialogTemplate.innerHTML = `
19
+ <dialog dir="ltr" lang="en" aria-labelledby="title">
20
+ <div id="button-container">
21
+ <button id="close" aria-label="Close">✕</button>
22
+ </div>
23
+ <h2 id="title">Accessibility issues</h2>
24
+ <section id="element-html-container" aria-label="Element">
25
+ <code id="element-html"></code>
26
+ </section>
27
+ <ul id="issues"></ul>
28
+ <section id="footer">
29
+ <p>
30
+ Powered by
31
+ <a href="${accentedUrl}" target="_blank" aria-description="Opens in new tab">Accented</a>
32
+ and
33
+ <a href="https://github.com/dequelabs/axe-core" target="_blank" aria-description="Opens in new tab">axe-core</a>.
34
+ </p>
35
+ </section>
36
+ </dialog>
37
+ `;
38
+
39
+ const issueTemplate = document.createElement('template');
40
+ issueTemplate.innerHTML = `
41
+ <li>
42
+ <a target="_blank" aria-description="Opens in new tab"></a>
43
+ <div class="impact"></div>
44
+ <div class="description"></div>
45
+ </li>
46
+ `;
47
+
48
+ const descriptionTemplate = document.createElement('template');
49
+ descriptionTemplate.innerHTML = `
50
+ <span></span>
51
+ <ul></ul>
52
+ `;
53
+
54
+ const stylesheet = new CSSStyleSheet();
55
+ stylesheet.replaceSync(`
56
+ :host {
57
+ all: initial !important;
58
+
59
+ --light-color: white;
60
+ --dark-color: black;
61
+ --focus-color: #0078d4; /* Contrasts with both white and black. */
62
+
63
+ --impact-minor-color: lightgray;
64
+ --impact-moderate-color: gold;
65
+ --impact-serious-color: #ff9e00;
66
+ --impact-critical-color: #f883ec;
67
+
68
+ /* Spacing and typography custom props, inspired by https://utopia.fyi (simplified). */
69
+
70
+ /* @link https://utopia.fyi/type/calculator?c=320,16,1.2,1240,16,1.2,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12 */
71
+ --ratio: 1.2;
72
+ --step-0: 1rem;
73
+ --step-1: calc(var(--step-0) * var(--ratio));
74
+ --step-2: calc(var(--step-1) * var(--ratio));
75
+ --step-3: calc(var(--step-2) * var(--ratio));
76
+ --step-4: calc(var(--step-3) * var(--ratio));
77
+ --step--1: calc(var(--step-0) / var(--ratio));
78
+
79
+ /* @link https://utopia.fyi/space/calculator?c=320,16,1.2,1240,16,1.2,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12 */
80
+ --space-3xs: 0.25rem;
81
+ --space-2xs: 0.5rem;
82
+ --space-xs: 0.75rem;
83
+ --space-s: 1rem;
84
+ --space-m: 1.5rem;
85
+ --space-l: 2rem;
86
+ --space-xl: 3rem;
87
+ --space-2xl: 4rem;
88
+ --space-3xl: 6rem;
89
+ }
90
+
91
+ a[href], button {
92
+ outline-offset: 2px;
93
+ outline-color: var(--focus-color);
94
+ outline-width: 2px;
95
+ outline-style: none;
96
+
97
+ &:focus-visible {
98
+ outline-style: solid;
99
+ }
100
+
101
+ &:hover:not(:focus-visible) {
102
+ outline-style: dashed;
103
+ }
104
+ }
105
+
106
+ a[href] {
107
+ color: currentColor;
108
+ }
109
+
110
+ a[href][target="_blank"]::after {
111
+ content: " ↗";
112
+ }
113
+
114
+ dialog {
115
+ box-sizing: border-box;
116
+ overflow-wrap: break-word;
117
+ font-family: system-ui;
118
+ line-height: 1.5;
119
+ background-color: var(--light-color);
120
+ color: var(--dark-color);
121
+ border: 2px solid currentColor;
122
+ padding: var(--space-l);
123
+ inline-size: min(90ch, calc(100% - var(--space-s)* 2));
124
+ max-block-size: calc(100% - var(--space-s) * 2);
125
+ }
126
+
127
+ #button-container {
128
+ text-align: end;
129
+ }
130
+
131
+ #close {
132
+ background-color: var(--light-color);
133
+ color: var(--dark-color);
134
+ border: 2px solid currentColor;
135
+ padding-inline: var(--space-2xs);
136
+ aspect-ratio: 1 / 1;
137
+ font-size: var(--step-0);
138
+ }
139
+
140
+ h2 {
141
+ font-size: var(--step-4);
142
+ line-height: 1.2;
143
+ margin-block-start: var(--space-s);
144
+ margin-block-end: 0;
145
+ }
146
+
147
+ #element-html-container {
148
+ padding: var(--space-xs);
149
+ border: 2px solid currentColor;
150
+ margin-block-start: var(--space-l);
151
+ }
152
+
153
+ code {
154
+ /* https://systemfontstack.com/ */
155
+ font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
156
+ font-size: var(--step--1);
157
+ }
158
+
159
+ #issues {
160
+ font-size: var(--step-1);
161
+ margin-block-start: var(--space-l);
162
+ padding-inline: 0;
163
+ list-style: none;
164
+
165
+ & > li:not(:first-child) {
166
+ margin-block-start: var(--space-m);
167
+ }
168
+
169
+ a {
170
+ font-weight: bold;
171
+ }
172
+ }
173
+
174
+ .impact {
175
+ margin-block-start: var(--space-2xs);
176
+ font-size: var(--step--1);
177
+
178
+ inline-size: fit-content;
179
+ padding-inline: var(--space-3xs);
180
+
181
+ &[data-impact="minor"] {
182
+ background-color: var(--impact-minor-color);
183
+ }
184
+ &[data-impact="moderate"] {
185
+ background-color: var(--impact-moderate-color);
186
+ }
187
+ &[data-impact="serious"] {
188
+ background-color: var(--impact-serious-color);
189
+ }
190
+ &[data-impact="critical"] {
191
+ background-color: var(--impact-critical-color);
192
+ }
193
+ }
194
+
195
+ .description {
196
+ margin-block-start: var(--space-2xs);
197
+ font-size: var(--step--1);
198
+
199
+ li {
200
+ list-style-type: disc;
201
+ }
202
+ }
203
+
204
+ #footer {
205
+ margin-block-start: var(--space-l);
206
+ font-size: var(--step--1);
207
+
208
+ p {
209
+ margin: 0;
210
+ text-align: end;
211
+ }
212
+ }
213
+ `);
214
+
215
+ return class extends HTMLElement implements AccentedDialog {
216
+ #disposeOfEffect: (() => void) | undefined;
217
+
218
+ #abortController: AbortController | undefined;
219
+
220
+ issues: Signal<Array<Issue>> | undefined;
221
+
222
+ element: Element | undefined;
223
+
224
+ #elementMutationObserver: MutationObserver | undefined;
225
+
226
+ constructor() {
227
+ try {
228
+ super();
229
+ this.attachShadow({ mode: 'open' });
230
+ const content = dialogTemplate.content.cloneNode(true);
231
+ if (this.shadowRoot) {
232
+ this.shadowRoot.adoptedStyleSheets.push(stylesheet);
233
+ this.shadowRoot.append(content);
234
+ }
235
+ } catch (error) {
236
+ logAndRethrow(error);
237
+ }
238
+ }
239
+
240
+ connectedCallback() {
241
+ try {
242
+ if (this.shadowRoot) {
243
+ const { shadowRoot } = this;
244
+ const dialog = shadowRoot.querySelector('dialog');
245
+ const closeButton = shadowRoot.querySelector('#close');
246
+ this.#abortController = new AbortController();
247
+ closeButton?.addEventListener('click', () => {
248
+ try {
249
+ dialog?.close();
250
+ } catch (error) {
251
+ logAndRethrow(error);
252
+ }
253
+ }, { signal: this.#abortController.signal });
254
+
255
+ dialog?.addEventListener('click', (event) => {
256
+ try {
257
+ this.#onDialogClick(event);
258
+ } catch (error) {
259
+ logAndRethrow(error);
260
+ }
261
+ }, { signal: this.#abortController.signal });
262
+
263
+ this.#disposeOfEffect = effect(() => {
264
+ if (this.issues) {
265
+ const issues = this.issues.value;
266
+ const issuesList = shadowRoot.getElementById('issues');
267
+ if (issuesList) {
268
+ issuesList.innerHTML = '';
269
+ for (const issue of issues) {
270
+ const issueContent = issueTemplate.content.cloneNode(true) as Element;
271
+ const title = issueContent.querySelector('a');
272
+ const impact = issueContent.querySelector('.impact');
273
+ const description = issueContent.querySelector('.description');
274
+ if (title && impact && description) {
275
+ title.textContent = issue.title + ' (' + issue.id + ')';
276
+ title.href = issue.url;
277
+
278
+ impact.textContent = 'User impact: ' + issue.impact;
279
+ impact.setAttribute('data-impact', String(issue.impact));
280
+
281
+ const descriptionItems = issue.description.split(/\n\s*/);
282
+ const descriptionContent = descriptionTemplate.content.cloneNode(true) as Element;
283
+ const descriptionTitle = descriptionContent.querySelector('span');
284
+ const descriptionList = descriptionContent.querySelector('ul');
285
+ if (descriptionTitle && descriptionList && descriptionItems.length > 1) {
286
+ descriptionTitle.textContent = descriptionItems[0]!;
287
+ for (const descriptionItem of descriptionItems.slice(1)) {
288
+ const li = document.createElement('li');
289
+ li.textContent = descriptionItem;
290
+ descriptionList.appendChild(li);
291
+ }
292
+ description.appendChild(descriptionContent);
293
+ }
294
+ }
295
+ issuesList.appendChild(issueContent);
296
+ }
297
+ }
298
+ }
299
+ });
300
+
301
+ const updateElementHtml = () => {
302
+ if (this.element) {
303
+ const elementHtmlContainer = shadowRoot.getElementById('element-html');
304
+ if (elementHtmlContainer) {
305
+ elementHtmlContainer.textContent = getElementHtml(this.element);
306
+ }
307
+ }
308
+ };
309
+
310
+ updateElementHtml();
311
+
312
+ this.#elementMutationObserver = new MutationObserver(() => {
313
+ try {
314
+ updateElementHtml();
315
+ } catch (error) {
316
+ logAndRethrow(error);
317
+ }
318
+ });
319
+ if (this.element) {
320
+ // We're only outputting the element itself, not its subtree.
321
+ // However, we're still listening for childList changes, because
322
+ // we display an ellipsis if the element has innerHTML,
323
+ // and we leave it empty if the element is empty.
324
+ this.#elementMutationObserver.observe(this.element, {
325
+ attributes: true,
326
+ childList: true
327
+ });
328
+ }
329
+
330
+ dialog?.addEventListener('close', () => {
331
+ try {
332
+ this.dispatchEvent(new Event('close'));
333
+ } catch (error) {
334
+ logAndRethrow(error);
335
+ }
336
+ }, { signal: this.#abortController.signal });
337
+ }
338
+ } catch (error) {
339
+ logAndRethrow(error);
340
+ }
341
+ }
342
+
343
+ disconnectedCallback() {
344
+ try {
345
+ if (this.#disposeOfEffect) {
346
+ this.#disposeOfEffect();
347
+ }
348
+ if (this.#abortController) {
349
+ this.#abortController.abort();
350
+ }
351
+ if (this.#elementMutationObserver) {
352
+ this.#elementMutationObserver.disconnect();
353
+ }
354
+ } catch (error) {
355
+ logAndRethrow(error);
356
+ }
357
+ }
358
+
359
+ showModal() {
360
+ if (this.shadowRoot) {
361
+ const dialog = this.shadowRoot.querySelector('dialog');
362
+ if (dialog) {
363
+ dialog.showModal();
364
+ }
365
+ }
366
+ }
367
+
368
+ #onDialogClick(event: MouseEvent) {
369
+ const dialog = event.currentTarget as HTMLDialogElement;
370
+ if (!dialog || typeof dialog.getBoundingClientRect !== 'function' || typeof dialog.close !== 'function') {
371
+ return;
372
+ }
373
+ const rect = dialog.getBoundingClientRect();
374
+ const isInsideDialog =
375
+ event.clientX >= rect.left &&
376
+ event.clientX <= rect.right &&
377
+ event.clientY >= rect.top &&
378
+ event.clientY <= rect.bottom;
379
+ if (!isInsideDialog) {
380
+ dialog.close();
381
+ }
382
+ }
383
+ };
384
+ };
@@ -0,0 +1,179 @@
1
+ import type { AccentedDialog } from './accented-dialog';
2
+ import type { Position } from '../types';
3
+ import { effect } from '@preact/signals-core';
4
+ import type { Signal } from '@preact/signals-core';
5
+ import supportsAnchorPositioning from '../utils/supports-anchor-positioning.js';
6
+ import logAndRethrow from '../log-and-rethrow.js';
7
+
8
+ export interface AccentedTrigger extends HTMLElement {
9
+ element: Element | undefined;
10
+ dialog: AccentedDialog | undefined;
11
+ position: Signal<Position> | undefined;
12
+ visible: Signal<boolean> | undefined;
13
+ }
14
+
15
+ const triggerSize = 'max(32px, 2rem)';
16
+
17
+ // We want Accented to not throw an error in Node, and use static imports,
18
+ // so we can't export `class extends HTMLElement` because HTMLElement is not available in Node.
19
+ export default (name: string) => {
20
+ const template = document.createElement('template');
21
+
22
+ // I initially tried creating a CSSStyelSheet object with styles instead of having a <style> element in the template,
23
+ // but that led to a hard-to-catch layout bug in Safari in CI that caused a test to fail.
24
+ // It seems that when using adoptedStyleSheets, the styles may be applied asynchronously,
25
+ // which may have caused the layout bug.
26
+ // Using a <style> element does not seem to lead to any performance issues, so I'm keeping it this way.
27
+ template.innerHTML = `
28
+ <style>
29
+ :host {
30
+ position: fixed !important;
31
+ inset-inline-end: anchor(self-end) !important;
32
+ inset-block-start: anchor(self-start) !important;
33
+
34
+ position-visibility: anchors-visible !important;
35
+
36
+ /* Revert potential effects of white-space: pre; set on a trigger's ancestor. */
37
+ white-space: normal !important;
38
+ }
39
+
40
+ #trigger {
41
+ box-sizing: border-box;
42
+ font-size: 1rem;
43
+ inline-size: ${triggerSize};
44
+ block-size: ${triggerSize};
45
+
46
+ /* Make it look better in forced-colors mode. */
47
+ border: 2px solid transparent;
48
+
49
+ background-color: var(--${name}-primary-color);
50
+ color: var(--${name}-secondary-color);
51
+
52
+ outline-offset: -4px;
53
+ outline-color: currentColor;
54
+ outline-width: 2px;
55
+ outline-style: none;
56
+
57
+ &:focus-visible {
58
+ outline-style: solid;
59
+ }
60
+
61
+ &:hover:not(:focus-visible) {
62
+ outline-style: dashed;
63
+ }
64
+ }
65
+ </style>
66
+ <button id="trigger" lang="en">⚠</button>
67
+ `;
68
+
69
+ return class extends HTMLElement implements AccentedTrigger {
70
+ #abortController: AbortController | undefined;
71
+
72
+ #dialogCloseAbortController: AbortController | undefined;
73
+
74
+ #disposeOfPositionEffect: (() => void) | undefined;
75
+
76
+ #disposeOfVisibilityEffect: (() => void) | undefined;
77
+
78
+ element: Element | undefined;
79
+
80
+ dialog: AccentedDialog | undefined;
81
+
82
+ position: Signal<Position> | undefined;
83
+
84
+ visible: Signal<boolean> | undefined;
85
+
86
+ constructor() {
87
+ try {
88
+ super();
89
+ this.attachShadow({ mode: 'open' });
90
+ const content = template.content.cloneNode(true);
91
+ if (this.shadowRoot) {
92
+ this.shadowRoot.append(content);
93
+ }
94
+ } catch (error) {
95
+ logAndRethrow(error);
96
+ }
97
+ }
98
+
99
+ connectedCallback() {
100
+ try {
101
+ if (this.shadowRoot) {
102
+ const { shadowRoot } = this;
103
+ const trigger = shadowRoot.getElementById('trigger');
104
+ if (trigger && this.element) {
105
+ trigger.ariaLabel = `Accessibility issues in ${this.element.nodeName.toLowerCase()}`;
106
+ }
107
+ this.#abortController = new AbortController();
108
+ trigger?.addEventListener('click', (event) => {
109
+ try {
110
+ event.preventDefault();
111
+
112
+ // We append the dialog when the button is clicked,
113
+ // and remove it from the DOM when the dialog is closed.
114
+ // This gives us a performance improvement since Axe
115
+ // scan time seems to depend on the number of elements in the DOM.
116
+ if (this.dialog) {
117
+ this.#dialogCloseAbortController = new AbortController();
118
+ document.body.append(this.dialog);
119
+ this.dialog.showModal();
120
+ this.dialog.addEventListener('close', () => {
121
+ try {
122
+ this.dialog?.remove();
123
+ this.#dialogCloseAbortController?.abort();
124
+ } catch (error) {
125
+ logAndRethrow(error);
126
+ }
127
+ }, { signal: this.#dialogCloseAbortController.signal });
128
+ }
129
+ } catch (error) {
130
+ logAndRethrow(error);
131
+ }
132
+ }, { signal: this.#abortController.signal });
133
+
134
+ if (!supportsAnchorPositioning(window)) {
135
+ this.#disposeOfPositionEffect = effect(() => {
136
+ if (this.position && trigger) {
137
+ const position = this.position.value;
138
+ this.style.setProperty('top', `${position.blockStartTop}px`, 'important');
139
+ if (position.direction === 'ltr') {
140
+ this.style.setProperty('left', `calc(${position.inlineEndLeft}px - ${triggerSize})`, 'important');
141
+ } else if (this.position.value.direction === 'rtl') {
142
+ this.style.setProperty('left', `${position.inlineEndLeft}px`, 'important');
143
+ }
144
+ }
145
+ });
146
+
147
+ this.#disposeOfVisibilityEffect = effect(() => {
148
+ this.style.setProperty('visibility', this.visible?.value ? 'visible' : 'hidden', 'important');
149
+ });
150
+ }
151
+ }
152
+ } catch (error) {
153
+ logAndRethrow(error);
154
+ }
155
+ }
156
+
157
+ disconnectedCallback() {
158
+ try {
159
+ if (this.#abortController) {
160
+ this.#abortController.abort();
161
+ }
162
+ if (this.#dialogCloseAbortController) {
163
+ this.#dialogCloseAbortController.abort();
164
+ this.dialog?.remove();
165
+ }
166
+ if (this.#disposeOfPositionEffect) {
167
+ this.#disposeOfPositionEffect();
168
+ this.#disposeOfPositionEffect = undefined;
169
+ }
170
+ if (this.#disposeOfVisibilityEffect) {
171
+ this.#disposeOfVisibilityEffect();
172
+ this.#disposeOfVisibilityEffect = undefined;
173
+ }
174
+ } catch (error) {
175
+ logAndRethrow(error);
176
+ }
177
+ }
178
+ };
179
+ };
@@ -0,0 +1,28 @@
1
+ import logAndRethrow from './log-and-rethrow.js';
2
+ import { extendedElementsWithIssues } from './state.js';
3
+ import getElementPosition from './utils/get-element-position.js';
4
+
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);
14
+ }
15
+ }
16
+ }
17
+ } catch (error) {
18
+ logAndRethrow(error);
19
+ }
20
+ }, { threshold: 0 });
21
+
22
+ return {
23
+ intersectionObserver,
24
+ disconnect: () => {
25
+ intersectionObserver.disconnect();
26
+ }
27
+ };
28
+ }
@@ -0,0 +1,9 @@
1
+ import { issuesUrl } from './constants.js';
2
+
3
+ export default function logAndRethrow(error: unknown) {
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}.`
7
+ );
8
+ throw error;
9
+ }
package/src/logger.ts CHANGED
@@ -1,20 +1,25 @@
1
1
  import { effect } from '@preact/signals-core';
2
- import { elementsWithIssues } from './state.js';
3
-
4
- const accentedUrl = 'https://www.npmjs.com/package/accented';
2
+ import { elementsWithIssues, enabled } from './state.js';
3
+ import { accentedUrl } from './constants.js';
5
4
 
6
5
  export default function createLogger() {
7
6
 
8
7
  let firstRun = true;
9
8
 
10
9
  return effect(() => {
11
- if (elementsWithIssues.value.length > 0) {
12
- console.log(`Elements with accessibility issues, identified by Accented (${accentedUrl}):\n`, elementsWithIssues.value);
10
+ if (!enabled.value) {
11
+ return;
12
+ }
13
+
14
+ const elementCount = elementsWithIssues.value.length;
15
+ 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);
13
18
  } else {
14
19
  if (firstRun) {
15
20
  firstRun = false;
16
21
  } else {
17
- console.log(`No elements with accessibility issues identified by Accented (${accentedUrl}).`);
22
+ console.log(`No accessibility issues found (Accented, ${accentedUrl}).`);
18
23
  }
19
24
  }
20
25
  });
@@ -0,0 +1,21 @@
1
+ import getAccentedTrigger from './elements/accented-trigger.js';
2
+ import getAccentedDialog from './elements/accented-dialog.js';
3
+
4
+ export default function registerElements(name: string): void {
5
+ const elements = [
6
+ {
7
+ elementName: `${name}-trigger`,
8
+ Component: getAccentedTrigger(name)
9
+ },
10
+ {
11
+ elementName: `${name}-dialog`,
12
+ Component: getAccentedDialog()
13
+ }
14
+ ];
15
+
16
+ for (const { elementName, Component } of elements) {
17
+ if (!customElements.get(elementName)) {
18
+ customElements.define(elementName, Component);
19
+ }
20
+ }
21
+ };
@@ -0,0 +1,17 @@
1
+ import logAndRethrow from './log-and-rethrow.js';
2
+ import recalculatePositions from './utils/recalculate-positions.js';
3
+
4
+ export default function setupResizeListener() {
5
+ const abortController = new AbortController();
6
+ window.addEventListener('resize', () => {
7
+ try {
8
+ recalculatePositions();
9
+ } catch (error) {
10
+ logAndRethrow(error);
11
+ }
12
+ }, { signal: abortController.signal });
13
+
14
+ return () => {
15
+ abortController.abort();
16
+ };
17
+ };