accented 0.0.0-20250124142030 → 0.0.0-20250303013509
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.
- package/README.md +55 -3
- package/dist/accented.d.ts +3 -1
- package/dist/accented.d.ts.map +1 -1
- package/dist/accented.js +69 -50
- package/dist/accented.js.map +1 -1
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +3 -0
- package/dist/constants.js.map +1 -0
- package/dist/dom-updater.d.ts +1 -1
- package/dist/dom-updater.d.ts.map +1 -1
- package/dist/dom-updater.js +26 -19
- package/dist/dom-updater.js.map +1 -1
- package/dist/elements/{accented-container.d.ts → accented-dialog.d.ts} +10 -4
- package/dist/elements/accented-dialog.d.ts.map +1 -0
- package/dist/elements/accented-dialog.js +371 -0
- package/dist/elements/accented-dialog.js.map +1 -0
- package/dist/elements/accented-trigger.d.ts +361 -0
- package/dist/elements/accented-trigger.d.ts.map +1 -0
- package/dist/elements/accented-trigger.js +188 -0
- package/dist/elements/accented-trigger.js.map +1 -0
- package/dist/intersection-observer.d.ts +5 -0
- package/dist/intersection-observer.d.ts.map +1 -0
- package/dist/intersection-observer.js +28 -0
- package/dist/intersection-observer.js.map +1 -0
- package/dist/log-and-rethrow.d.ts +2 -0
- package/dist/log-and-rethrow.d.ts.map +1 -0
- package/dist/log-and-rethrow.js +7 -0
- package/dist/log-and-rethrow.js.map +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +10 -5
- package/dist/logger.js.map +1 -1
- package/dist/register-elements.d.ts +2 -0
- package/dist/register-elements.d.ts.map +1 -0
- package/dist/register-elements.js +21 -0
- package/dist/register-elements.js.map +1 -0
- package/dist/resize-listener.d.ts +2 -0
- package/dist/resize-listener.d.ts.map +1 -0
- package/dist/resize-listener.js +18 -0
- package/dist/resize-listener.js.map +1 -0
- package/dist/scanner.d.ts +2 -2
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +97 -33
- package/dist/scanner.js.map +1 -1
- package/dist/scroll-listeners.d.ts +2 -0
- package/dist/scroll-listeners.d.ts.map +1 -0
- package/dist/scroll-listeners.js +38 -0
- package/dist/scroll-listeners.js.map +1 -0
- package/dist/state.d.ts +1 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +6 -0
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +71 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/deep-merge.js +1 -1
- package/dist/utils/deep-merge.js.map +1 -1
- package/dist/utils/get-element-html.d.ts +2 -0
- package/dist/utils/get-element-html.d.ts.map +1 -0
- package/dist/utils/get-element-html.js +14 -0
- package/dist/utils/get-element-html.js.map +1 -0
- package/dist/utils/get-element-position.d.ts +3 -0
- package/dist/utils/get-element-position.d.ts.map +1 -0
- package/dist/utils/get-element-position.js +58 -0
- package/dist/utils/get-element-position.js.map +1 -0
- package/dist/utils/get-scrollable-ancestors.d.ts +2 -0
- package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -0
- package/dist/utils/get-scrollable-ancestors.js +15 -0
- package/dist/utils/get-scrollable-ancestors.js.map +1 -0
- package/dist/utils/is-html-element.d.ts +2 -0
- package/dist/utils/is-html-element.d.ts.map +1 -0
- package/dist/utils/is-html-element.js +7 -0
- package/dist/utils/is-html-element.js.map +1 -0
- package/dist/utils/recalculate-positions.d.ts +2 -0
- package/dist/utils/recalculate-positions.d.ts.map +1 -0
- package/dist/utils/recalculate-positions.js +27 -0
- package/dist/utils/recalculate-positions.js.map +1 -0
- package/dist/utils/recalculate-scrollable-ancestors.d.ts +2 -0
- package/dist/utils/recalculate-scrollable-ancestors.d.ts.map +1 -0
- package/dist/utils/recalculate-scrollable-ancestors.js +13 -0
- package/dist/utils/recalculate-scrollable-ancestors.js.map +1 -0
- package/dist/utils/supports-anchor-positioning.d.ts +6 -0
- package/dist/utils/supports-anchor-positioning.d.ts.map +1 -0
- package/dist/utils/supports-anchor-positioning.js +4 -0
- package/dist/utils/supports-anchor-positioning.js.map +1 -0
- package/dist/utils/transform-violations.d.ts.map +1 -1
- package/dist/utils/transform-violations.js +9 -0
- package/dist/utils/transform-violations.js.map +1 -1
- package/dist/utils/update-elements-with-issues.d.ts +3 -1
- package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
- package/dist/utils/update-elements-with-issues.js +25 -7
- package/dist/utils/update-elements-with-issues.js.map +1 -1
- package/dist/validate-options.d.ts +3 -0
- package/dist/validate-options.d.ts.map +1 -0
- package/dist/validate-options.js +42 -0
- package/dist/validate-options.js.map +1 -0
- package/package.json +2 -1
- package/src/accented.ts +78 -58
- package/src/constants.ts +2 -0
- package/src/dom-updater.ts +26 -18
- package/src/elements/accented-dialog.ts +394 -0
- package/src/elements/accented-trigger.ts +214 -0
- package/src/intersection-observer.ts +28 -0
- package/src/log-and-rethrow.ts +9 -0
- package/src/logger.ts +11 -6
- package/src/register-elements.ts +21 -0
- package/src/resize-listener.ts +17 -0
- package/src/scanner.ts +108 -37
- package/src/scroll-listeners.ts +37 -0
- package/src/state.ts +12 -0
- package/src/types.ts +78 -19
- package/src/utils/deep-merge.test.ts +7 -0
- package/src/utils/deep-merge.ts +1 -1
- package/src/utils/get-element-html.ts +13 -0
- package/src/utils/get-element-position.ts +59 -0
- package/src/utils/get-scrollable-ancestors.ts +14 -0
- package/src/utils/is-html-element.ts +6 -0
- package/src/utils/recalculate-positions.ts +27 -0
- package/src/utils/recalculate-scrollable-ancestors.ts +13 -0
- package/src/utils/supports-anchor-positioning.ts +7 -0
- package/src/utils/transform-violations.ts +12 -1
- package/src/utils/update-elements-with-issues.test.ts +91 -16
- package/src/utils/update-elements-with-issues.ts +40 -20
- package/src/validate-options.ts +44 -0
- package/dist/elements/accented-container.d.ts.map +0 -1
- package/dist/elements/accented-container.js +0 -131
- package/dist/elements/accented-container.js.map +0 -1
- package/src/elements/accented-container.ts +0 -147
package/src/dom-updater.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { effect } from '@preact/signals-core';
|
|
2
2
|
import { extendedElementsWithIssues } from './state.js';
|
|
3
3
|
import type { ExtendedElementWithIssues } from './types';
|
|
4
|
+
import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
|
|
4
5
|
|
|
5
|
-
export default function createDomUpdater(name: string) {
|
|
6
|
+
export default function createDomUpdater(name: string, intersectionObserver?: IntersectionObserver) {
|
|
6
7
|
const attrName = `data-${name}`;
|
|
7
8
|
|
|
8
|
-
function
|
|
9
|
-
return
|
|
9
|
+
function getAnchorNames (anchorNameValue: string) {
|
|
10
|
+
return anchorNameValue
|
|
11
|
+
.split(',')
|
|
12
|
+
.map(anchorName => anchorName.trim())
|
|
13
|
+
.filter(anchorName => anchorName.startsWith('--'));
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
function setAnchorName (element: HTMLElement, id: number) {
|
|
13
17
|
const anchorNameValue = element.style.getPropertyValue('anchor-name') || window.getComputedStyle(element).getPropertyValue('anchor-name');
|
|
14
|
-
const anchorNames = anchorNameValue
|
|
15
|
-
.split(/,\s*/)
|
|
16
|
-
.filter(anchorName => anchorName.startsWith('--'));
|
|
18
|
+
const anchorNames = getAnchorNames(anchorNameValue);
|
|
17
19
|
if (anchorNames.length > 0) {
|
|
18
20
|
element.style.setProperty('anchor-name', `${anchorNameValue}, --${name}-anchor-${id}`);
|
|
19
21
|
} else {
|
|
@@ -23,9 +25,7 @@ export default function createDomUpdater(name: string) {
|
|
|
23
25
|
|
|
24
26
|
function removeAnchorName (element: HTMLElement, id: number) {
|
|
25
27
|
const anchorNameValue = element.style.getPropertyValue('anchor-name');
|
|
26
|
-
const anchorNames = anchorNameValue
|
|
27
|
-
.split(/,\s*/)
|
|
28
|
-
.filter(anchorName => anchorName.startsWith('--'));
|
|
28
|
+
const anchorNames = getAnchorNames(anchorNameValue);
|
|
29
29
|
const index = anchorNames.indexOf(`--${name}-anchor-${id}`);
|
|
30
30
|
if (anchorNames.length === 1 && index === 0) {
|
|
31
31
|
element.style.removeProperty('anchor-name');
|
|
@@ -35,16 +35,19 @@ export default function createDomUpdater(name: string) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function setIssues (extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
|
|
38
|
-
const displayAccentedContainers = supportsAnchorPositioning();
|
|
39
38
|
for (const elementWithIssues of extendedElementsWithIssues) {
|
|
40
39
|
elementWithIssues.element.setAttribute(attrName, elementWithIssues.id.toString());
|
|
41
|
-
if (
|
|
40
|
+
if (supportsAnchorPositioning(window)) {
|
|
42
41
|
setAnchorName(elementWithIssues.element, elementWithIssues.id);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (elementWithIssues.element.parentElement) {
|
|
45
|
+
elementWithIssues.element.insertAdjacentElement('afterend', elementWithIssues.trigger);
|
|
46
|
+
} else {
|
|
47
|
+
elementWithIssues.element.insertAdjacentElement('beforeend', elementWithIssues.trigger);
|
|
48
|
+
}
|
|
49
|
+
if (intersectionObserver) {
|
|
50
|
+
intersectionObserver.observe(elementWithIssues.element);
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
}
|
|
@@ -52,8 +55,13 @@ export default function createDomUpdater(name: string) {
|
|
|
52
55
|
function removeIssues (extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
|
|
53
56
|
for (const elementWithIssues of extendedElementsWithIssues) {
|
|
54
57
|
elementWithIssues.element.removeAttribute(attrName);
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
if (supportsAnchorPositioning(window)) {
|
|
59
|
+
removeAnchorName(elementWithIssues.element, elementWithIssues.id);
|
|
60
|
+
}
|
|
61
|
+
elementWithIssues.trigger.remove();
|
|
62
|
+
if (intersectionObserver) {
|
|
63
|
+
intersectionObserver.unobserve(elementWithIssues.element);
|
|
64
|
+
}
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
67
|
|
|
@@ -0,0 +1,394 @@
|
|
|
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
|
+
dialog?.addEventListener('keydown', (event) => {
|
|
264
|
+
try {
|
|
265
|
+
if (event.key === 'Escape') {
|
|
266
|
+
event.stopPropagation();
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
logAndRethrow(error);
|
|
270
|
+
}
|
|
271
|
+
}, { signal: this.#abortController.signal });
|
|
272
|
+
|
|
273
|
+
this.#disposeOfEffect = effect(() => {
|
|
274
|
+
if (this.issues) {
|
|
275
|
+
const issues = this.issues.value;
|
|
276
|
+
const issuesList = shadowRoot.getElementById('issues');
|
|
277
|
+
if (issuesList) {
|
|
278
|
+
issuesList.innerHTML = '';
|
|
279
|
+
for (const issue of issues) {
|
|
280
|
+
const issueContent = issueTemplate.content.cloneNode(true) as Element;
|
|
281
|
+
const title = issueContent.querySelector('a');
|
|
282
|
+
const impact = issueContent.querySelector('.impact');
|
|
283
|
+
const description = issueContent.querySelector('.description');
|
|
284
|
+
if (title && impact && description) {
|
|
285
|
+
title.textContent = issue.title + ' (' + issue.id + ')';
|
|
286
|
+
title.href = issue.url;
|
|
287
|
+
|
|
288
|
+
impact.textContent = 'User impact: ' + issue.impact;
|
|
289
|
+
impact.setAttribute('data-impact', String(issue.impact));
|
|
290
|
+
|
|
291
|
+
const descriptionItems = issue.description.split(/\n\s*/);
|
|
292
|
+
const descriptionContent = descriptionTemplate.content.cloneNode(true) as Element;
|
|
293
|
+
const descriptionTitle = descriptionContent.querySelector('span');
|
|
294
|
+
const descriptionList = descriptionContent.querySelector('ul');
|
|
295
|
+
if (descriptionTitle && descriptionList && descriptionItems.length > 1) {
|
|
296
|
+
descriptionTitle.textContent = descriptionItems[0]!;
|
|
297
|
+
for (const descriptionItem of descriptionItems.slice(1)) {
|
|
298
|
+
const li = document.createElement('li');
|
|
299
|
+
li.textContent = descriptionItem;
|
|
300
|
+
descriptionList.appendChild(li);
|
|
301
|
+
}
|
|
302
|
+
description.appendChild(descriptionContent);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
issuesList.appendChild(issueContent);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const updateElementHtml = () => {
|
|
312
|
+
if (this.element) {
|
|
313
|
+
const elementHtmlContainer = shadowRoot.getElementById('element-html');
|
|
314
|
+
if (elementHtmlContainer) {
|
|
315
|
+
elementHtmlContainer.textContent = getElementHtml(this.element);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
updateElementHtml();
|
|
321
|
+
|
|
322
|
+
this.#elementMutationObserver = new MutationObserver(() => {
|
|
323
|
+
try {
|
|
324
|
+
updateElementHtml();
|
|
325
|
+
} catch (error) {
|
|
326
|
+
logAndRethrow(error);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
if (this.element) {
|
|
330
|
+
// We're only outputting the element itself, not its subtree.
|
|
331
|
+
// However, we're still listening for childList changes, because
|
|
332
|
+
// we display an ellipsis if the element has innerHTML,
|
|
333
|
+
// and we leave it empty if the element is empty.
|
|
334
|
+
this.#elementMutationObserver.observe(this.element, {
|
|
335
|
+
attributes: true,
|
|
336
|
+
childList: true
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
dialog?.addEventListener('close', () => {
|
|
341
|
+
try {
|
|
342
|
+
this.dispatchEvent(new Event('close'));
|
|
343
|
+
} catch (error) {
|
|
344
|
+
logAndRethrow(error);
|
|
345
|
+
}
|
|
346
|
+
}, { signal: this.#abortController.signal });
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
logAndRethrow(error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
disconnectedCallback() {
|
|
354
|
+
try {
|
|
355
|
+
if (this.#disposeOfEffect) {
|
|
356
|
+
this.#disposeOfEffect();
|
|
357
|
+
}
|
|
358
|
+
if (this.#abortController) {
|
|
359
|
+
this.#abortController.abort();
|
|
360
|
+
}
|
|
361
|
+
if (this.#elementMutationObserver) {
|
|
362
|
+
this.#elementMutationObserver.disconnect();
|
|
363
|
+
}
|
|
364
|
+
} catch (error) {
|
|
365
|
+
logAndRethrow(error);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
showModal() {
|
|
370
|
+
if (this.shadowRoot) {
|
|
371
|
+
const dialog = this.shadowRoot.querySelector('dialog');
|
|
372
|
+
if (dialog) {
|
|
373
|
+
dialog.showModal();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#onDialogClick(event: MouseEvent) {
|
|
379
|
+
const dialog = event.currentTarget as HTMLDialogElement;
|
|
380
|
+
if (!dialog || typeof dialog.getBoundingClientRect !== 'function' || typeof dialog.close !== 'function') {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const rect = dialog.getBoundingClientRect();
|
|
384
|
+
const isInsideDialog =
|
|
385
|
+
event.clientX >= rect.left &&
|
|
386
|
+
event.clientX <= rect.right &&
|
|
387
|
+
event.clientY >= rect.top &&
|
|
388
|
+
event.clientY <= rect.bottom;
|
|
389
|
+
if (!isInsideDialog) {
|
|
390
|
+
dialog.close();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
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-start: anchor(self-start) !important;
|
|
32
|
+
inset-inline-end: anchor(self-end) !important;
|
|
33
|
+
inset-block-start: anchor(self-start) !important;
|
|
34
|
+
inset-block-end: anchor(self-end) !important;
|
|
35
|
+
|
|
36
|
+
position-visibility: anchors-visible !important;
|
|
37
|
+
|
|
38
|
+
/* Revert potential effects of white-space: pre; set on a trigger's ancestor. */
|
|
39
|
+
white-space: normal !important;
|
|
40
|
+
|
|
41
|
+
pointer-events: none !important;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#trigger {
|
|
45
|
+
pointer-events: auto;
|
|
46
|
+
|
|
47
|
+
position: absolute;
|
|
48
|
+
inset-inline-end: 0;
|
|
49
|
+
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
font-size: 1rem;
|
|
52
|
+
inline-size: ${triggerSize};
|
|
53
|
+
block-size: ${triggerSize};
|
|
54
|
+
|
|
55
|
+
/* Make it look better in forced-colors mode. */
|
|
56
|
+
border: 2px solid transparent;
|
|
57
|
+
|
|
58
|
+
background-color: var(--${name}-primary-color);
|
|
59
|
+
color: var(--${name}-secondary-color);
|
|
60
|
+
|
|
61
|
+
outline-offset: -4px;
|
|
62
|
+
outline-color: currentColor;
|
|
63
|
+
outline-width: 2px;
|
|
64
|
+
outline-style: none;
|
|
65
|
+
|
|
66
|
+
&:focus-visible {
|
|
67
|
+
outline-style: solid;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
&:hover:not(:focus-visible) {
|
|
71
|
+
outline-style: dashed;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
</style>
|
|
75
|
+
<button id="trigger" lang="en">⚠</button>
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
return class extends HTMLElement implements AccentedTrigger {
|
|
79
|
+
#abortController: AbortController | undefined;
|
|
80
|
+
|
|
81
|
+
#dialogCloseAbortController: AbortController | undefined;
|
|
82
|
+
|
|
83
|
+
#disposeOfPositionEffect: (() => void) | undefined;
|
|
84
|
+
|
|
85
|
+
#disposeOfVisibilityEffect: (() => void) | undefined;
|
|
86
|
+
|
|
87
|
+
#elementMutationObserver: MutationObserver | undefined;
|
|
88
|
+
|
|
89
|
+
element: Element | undefined;
|
|
90
|
+
|
|
91
|
+
dialog: AccentedDialog | undefined;
|
|
92
|
+
|
|
93
|
+
position: Signal<Position> | undefined;
|
|
94
|
+
|
|
95
|
+
visible: Signal<boolean> | undefined;
|
|
96
|
+
|
|
97
|
+
constructor() {
|
|
98
|
+
try {
|
|
99
|
+
super();
|
|
100
|
+
this.attachShadow({ mode: 'open' });
|
|
101
|
+
const content = template.content.cloneNode(true);
|
|
102
|
+
if (this.shadowRoot) {
|
|
103
|
+
this.shadowRoot.append(content);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
logAndRethrow(error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
connectedCallback() {
|
|
111
|
+
try {
|
|
112
|
+
if (this.shadowRoot) {
|
|
113
|
+
const { shadowRoot } = this;
|
|
114
|
+
const trigger = shadowRoot.getElementById('trigger');
|
|
115
|
+
if (trigger && this.element) {
|
|
116
|
+
trigger.ariaLabel = `Accessibility issues in ${this.element.nodeName.toLowerCase()}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.#setTransform();
|
|
120
|
+
|
|
121
|
+
this.#elementMutationObserver = new MutationObserver(() => {
|
|
122
|
+
try {
|
|
123
|
+
this.#setTransform();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
logAndRethrow(error);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (this.element) {
|
|
130
|
+
this.#elementMutationObserver.observe(this.element, {
|
|
131
|
+
attributes: true
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.#abortController = new AbortController();
|
|
136
|
+
trigger?.addEventListener('click', (event) => {
|
|
137
|
+
try {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
|
|
140
|
+
// We append the dialog when the button is clicked,
|
|
141
|
+
// and remove it from the DOM when the dialog is closed.
|
|
142
|
+
// This gives us a performance improvement since Axe
|
|
143
|
+
// scan time seems to depend on the number of elements in the DOM.
|
|
144
|
+
if (this.dialog) {
|
|
145
|
+
this.#dialogCloseAbortController = new AbortController();
|
|
146
|
+
document.body.append(this.dialog);
|
|
147
|
+
this.dialog.showModal();
|
|
148
|
+
this.dialog.addEventListener('close', () => {
|
|
149
|
+
try {
|
|
150
|
+
this.dialog?.remove();
|
|
151
|
+
this.#dialogCloseAbortController?.abort();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logAndRethrow(error);
|
|
154
|
+
}
|
|
155
|
+
}, { signal: this.#dialogCloseAbortController.signal });
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logAndRethrow(error);
|
|
159
|
+
}
|
|
160
|
+
}, { signal: this.#abortController.signal });
|
|
161
|
+
|
|
162
|
+
if (!supportsAnchorPositioning(window)) {
|
|
163
|
+
this.#disposeOfPositionEffect = effect(() => {
|
|
164
|
+
if (this.position && trigger) {
|
|
165
|
+
const position = this.position.value;
|
|
166
|
+
this.style.setProperty('top', `${position.top}px`, 'important');
|
|
167
|
+
this.style.setProperty('left', `${position.left}px`, 'important');
|
|
168
|
+
this.style.setProperty('width', `${position.width}px`, 'important');
|
|
169
|
+
this.style.setProperty('height', `${position.height}px`, 'important');
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
this.#disposeOfVisibilityEffect = effect(() => {
|
|
174
|
+
this.style.setProperty('visibility', this.visible?.value ? 'visible' : 'hidden', 'important');
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
logAndRethrow(error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
disconnectedCallback() {
|
|
184
|
+
try {
|
|
185
|
+
if (this.#abortController) {
|
|
186
|
+
this.#abortController.abort();
|
|
187
|
+
}
|
|
188
|
+
if (this.#dialogCloseAbortController) {
|
|
189
|
+
this.#dialogCloseAbortController.abort();
|
|
190
|
+
this.dialog?.remove();
|
|
191
|
+
}
|
|
192
|
+
if (this.#disposeOfPositionEffect) {
|
|
193
|
+
this.#disposeOfPositionEffect();
|
|
194
|
+
this.#disposeOfPositionEffect = undefined;
|
|
195
|
+
}
|
|
196
|
+
if (this.#disposeOfVisibilityEffect) {
|
|
197
|
+
this.#disposeOfVisibilityEffect();
|
|
198
|
+
this.#disposeOfVisibilityEffect = undefined;
|
|
199
|
+
}
|
|
200
|
+
if (this.#elementMutationObserver) {
|
|
201
|
+
this.#elementMutationObserver.disconnect();
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
logAndRethrow(error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#setTransform() {
|
|
209
|
+
if (this.element) {
|
|
210
|
+
this.style.setProperty('transform', window.getComputedStyle(this.element).getPropertyValue('transform'), 'important');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
};
|