accented 0.0.0-20250303013509 → 0.0.0-20250404114312
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/NOTICE +14 -0
- package/README.md +7 -3
- package/dist/accented.d.ts +2 -2
- package/dist/accented.d.ts.map +1 -1
- package/dist/accented.js +5 -2
- package/dist/accented.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/dom-updater.d.ts.map +1 -1
- package/dist/dom-updater.js +38 -23
- package/dist/dom-updater.js.map +1 -1
- package/dist/elements/accented-dialog.d.ts.map +1 -1
- package/dist/elements/accented-dialog.js +50 -22
- package/dist/elements/accented-dialog.js.map +1 -1
- package/dist/elements/accented-trigger.d.ts.map +1 -1
- package/dist/elements/accented-trigger.js +32 -9
- package/dist/elements/accented-trigger.js.map +1 -1
- package/dist/fullscreen-listener.d.ts +2 -0
- package/dist/fullscreen-listener.d.ts.map +1 -0
- package/dist/fullscreen-listener.js +18 -0
- package/dist/fullscreen-listener.js.map +1 -0
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +15 -6
- package/dist/scanner.js.map +1 -1
- package/dist/state.d.ts +2 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +3 -0
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +17 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/are-elements-with-issues-equal.d.ts +3 -0
- package/dist/utils/are-elements-with-issues-equal.d.ts.map +1 -0
- package/dist/utils/are-elements-with-issues-equal.js +5 -0
- package/dist/utils/are-elements-with-issues-equal.js.map +1 -0
- package/dist/utils/dom-helpers.d.ts +6 -0
- package/dist/utils/dom-helpers.d.ts.map +1 -0
- package/dist/utils/dom-helpers.js +19 -0
- package/dist/utils/dom-helpers.js.map +1 -0
- package/dist/utils/get-element-position.d.ts.map +1 -1
- package/dist/utils/get-element-position.js +5 -4
- package/dist/utils/get-element-position.js.map +1 -1
- package/dist/utils/get-parent.d.ts +2 -0
- package/dist/utils/get-parent.d.ts.map +1 -0
- package/dist/utils/get-parent.js +12 -0
- package/dist/utils/get-parent.js.map +1 -0
- package/dist/utils/get-scrollable-ancestors.d.ts +1 -1
- package/dist/utils/get-scrollable-ancestors.d.ts.map +1 -1
- package/dist/utils/get-scrollable-ancestors.js +6 -2
- package/dist/utils/get-scrollable-ancestors.js.map +1 -1
- package/dist/utils/shadow-dom-aware-mutation-observer.d.ts +10 -0
- package/dist/utils/shadow-dom-aware-mutation-observer.d.ts.map +1 -0
- package/dist/utils/shadow-dom-aware-mutation-observer.js +64 -0
- package/dist/utils/shadow-dom-aware-mutation-observer.js.map +1 -0
- package/dist/utils/transform-violations.d.ts +1 -1
- package/dist/utils/transform-violations.d.ts.map +1 -1
- package/dist/utils/transform-violations.js +18 -5
- package/dist/utils/transform-violations.js.map +1 -1
- package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
- package/dist/utils/update-elements-with-issues.js +9 -5
- package/dist/utils/update-elements-with-issues.js.map +1 -1
- package/package.json +4 -3
- package/src/accented.ts +5 -2
- package/src/constants.ts +1 -0
- package/src/dom-updater.ts +38 -22
- package/src/elements/accented-dialog.ts +50 -22
- package/src/elements/accented-trigger.ts +33 -10
- package/src/fullscreen-listener.ts +17 -0
- package/src/scanner.ts +17 -6
- package/src/state.ts +10 -2
- package/src/types.ts +19 -6
- package/src/utils/are-elements-with-issues-equal.ts +9 -0
- package/src/utils/dom-helpers.ts +22 -0
- package/src/utils/get-element-position.ts +5 -4
- package/src/utils/get-parent.ts +14 -0
- package/src/utils/get-scrollable-ancestors.ts +10 -5
- package/src/utils/shadow-dom-aware-mutation-observer.ts +78 -0
- package/src/utils/transform-violations.test.ts +10 -8
- package/src/utils/transform-violations.ts +20 -6
- package/src/utils/update-elements-with-issues.test.ts +42 -8
- package/src/utils/update-elements-with-issues.ts +10 -5
- package/dist/utils/is-html-element.d.ts +0 -2
- package/dist/utils/is-html-element.d.ts.map +0 -1
- package/dist/utils/is-html-element.js +0 -7
- package/dist/utils/is-html-element.js.map +0 -1
- package/src/utils/is-html-element.ts +0 -6
package/src/accented.ts
CHANGED
|
@@ -5,6 +5,7 @@ import createLogger from './logger.js';
|
|
|
5
5
|
import createScanner from './scanner.js';
|
|
6
6
|
import setupScrollListeners from './scroll-listeners.js';
|
|
7
7
|
import setupResizeListener from './resize-listener.js';
|
|
8
|
+
import setupFullscreenListener from './fullscreen-listener.js';
|
|
8
9
|
import setupIntersectionObserver from './intersection-observer.js';
|
|
9
10
|
import { enabled, extendedElementsWithIssues } from './state.js';
|
|
10
11
|
import deepMerge from './utils/deep-merge.js';
|
|
@@ -34,9 +35,9 @@ export type { AccentedOptions, DisableAccented };
|
|
|
34
35
|
* wait: 500,
|
|
35
36
|
* leading: false
|
|
36
37
|
* },
|
|
37
|
-
* callback: ({ elementsWithIssues,
|
|
38
|
+
* callback: ({ elementsWithIssues, performance }) => {
|
|
38
39
|
* console.log('Elements with issues:', elementsWithIssues);
|
|
39
|
-
* console.log('
|
|
40
|
+
* console.log('Total blocking time:', performance.totalBlockingTime);
|
|
40
41
|
* }
|
|
41
42
|
* });
|
|
42
43
|
*/
|
|
@@ -95,6 +96,7 @@ export default function accented(options: AccentedOptions = {}): DisableAccented
|
|
|
95
96
|
const cleanupLogger = output.console ? createLogger() : () => {};
|
|
96
97
|
const cleanupScrollListeners = supportsAnchorPositioning(window) ? () => {} : setupScrollListeners();
|
|
97
98
|
const cleanupResizeListener = supportsAnchorPositioning(window) ? () => {} : setupResizeListener();
|
|
99
|
+
const cleanupFullscreenListener = supportsAnchorPositioning(window) ? () => {} : setupFullscreenListener();
|
|
98
100
|
|
|
99
101
|
return () => {
|
|
100
102
|
try {
|
|
@@ -105,6 +107,7 @@ export default function accented(options: AccentedOptions = {}): DisableAccented
|
|
|
105
107
|
cleanupLogger();
|
|
106
108
|
cleanupScrollListeners();
|
|
107
109
|
cleanupResizeListener();
|
|
110
|
+
cleanupFullscreenListener();
|
|
108
111
|
if (cleanupIntersectionObserver) {
|
|
109
112
|
cleanupIntersectionObserver();
|
|
110
113
|
}
|
package/src/constants.ts
CHANGED
package/src/dom-updater.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { effect } from '@preact/signals-core';
|
|
2
|
-
import { extendedElementsWithIssues } from './state.js';
|
|
2
|
+
import { extendedElementsWithIssues, rootNodes } from './state.js';
|
|
3
3
|
import type { ExtendedElementWithIssues } from './types';
|
|
4
|
+
import areElementsWithIssuesEqual from './utils/are-elements-with-issues-equal.js';
|
|
4
5
|
import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
|
|
6
|
+
import { isDocument, isDocumentFragment, isShadowRoot } from './utils/dom-helpers.js';
|
|
7
|
+
import getParent from './utils/get-parent.js';
|
|
5
8
|
|
|
6
9
|
export default function createDomUpdater(name: string, intersectionObserver?: IntersectionObserver) {
|
|
7
10
|
const attrName = `data-${name}`;
|
|
@@ -13,8 +16,8 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
13
16
|
.filter(anchorName => anchorName.startsWith('--'));
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
function setAnchorName (
|
|
17
|
-
const anchorNameValue =
|
|
19
|
+
function setAnchorName (elementWithIssues: ExtendedElementWithIssues) {
|
|
20
|
+
const { element, id, anchorNameValue } = elementWithIssues;
|
|
18
21
|
const anchorNames = getAnchorNames(anchorNameValue);
|
|
19
22
|
if (anchorNames.length > 0) {
|
|
20
23
|
element.style.setProperty('anchor-name', `${anchorNameValue}, --${name}-anchor-${id}`);
|
|
@@ -23,14 +26,13 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
function removeAnchorName (
|
|
27
|
-
const anchorNameValue =
|
|
29
|
+
function removeAnchorName (elementWithIssues: ExtendedElementWithIssues) {
|
|
30
|
+
const { element, anchorNameValue } = elementWithIssues;
|
|
28
31
|
const anchorNames = getAnchorNames(anchorNameValue);
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
if (anchorNames.length > 0) {
|
|
33
|
+
element.style.setProperty('anchor-name', anchorNames.join(', '));
|
|
34
|
+
} else {
|
|
31
35
|
element.style.removeProperty('anchor-name');
|
|
32
|
-
} else if (anchorNames.length > 1 && index > -1) {
|
|
33
|
-
element.style.setProperty('anchor-name', anchorNames.filter((_, i) => i !== index).join(', '));
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -38,10 +40,10 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
38
40
|
for (const elementWithIssues of extendedElementsWithIssues) {
|
|
39
41
|
elementWithIssues.element.setAttribute(attrName, elementWithIssues.id.toString());
|
|
40
42
|
if (supportsAnchorPositioning(window)) {
|
|
41
|
-
setAnchorName(elementWithIssues
|
|
43
|
+
setAnchorName(elementWithIssues);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
if (elementWithIssues.element
|
|
46
|
+
if (getParent(elementWithIssues.element)) {
|
|
45
47
|
elementWithIssues.element.insertAdjacentElement('afterend', elementWithIssues.trigger);
|
|
46
48
|
} else {
|
|
47
49
|
elementWithIssues.element.insertAdjacentElement('beforeend', elementWithIssues.trigger);
|
|
@@ -56,7 +58,7 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
56
58
|
for (const elementWithIssues of extendedElementsWithIssues) {
|
|
57
59
|
elementWithIssues.element.removeAttribute(attrName);
|
|
58
60
|
if (supportsAnchorPositioning(window)) {
|
|
59
|
-
removeAnchorName(elementWithIssues
|
|
61
|
+
removeAnchorName(elementWithIssues);
|
|
60
62
|
}
|
|
61
63
|
elementWithIssues.trigger.remove();
|
|
62
64
|
if (intersectionObserver) {
|
|
@@ -69,8 +71,10 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
69
71
|
stylesheet.replaceSync(`
|
|
70
72
|
@layer ${name} {
|
|
71
73
|
:root {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
/* Ensure that the primary / secondary color combination meets WCAG 1.4.3 Contrast (Minimum) */
|
|
75
|
+
/* OKLCH stuff: https://oklch.com/ */
|
|
76
|
+
--${name}-primary-color: oklch(0.5 0.3 0);
|
|
77
|
+
--${name}-secondary-color: oklch(0.98 0 0);
|
|
74
78
|
--${name}-outline-width: 2px;
|
|
75
79
|
--${name}-outline-style: solid;
|
|
76
80
|
}
|
|
@@ -86,19 +90,31 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
86
90
|
|
|
87
91
|
let previousExtendedElementsWithIssues: Array<ExtendedElementWithIssues> = [];
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
let previousRootNodes: Set<Node> = new Set();
|
|
94
|
+
|
|
95
|
+
const disposeOfStyleSheetsEffect = effect(() => {
|
|
96
|
+
const newRootNodes = rootNodes.value;
|
|
97
|
+
const addedRootNodes = [...newRootNodes].filter(rootNode => !previousRootNodes.has(rootNode));
|
|
98
|
+
const removedRootNodes = [...previousRootNodes].filter(rootNode => !newRootNodes.has(rootNode));
|
|
99
|
+
for (const rootNode of addedRootNodes) {
|
|
100
|
+
if (isDocument(rootNode) || (isDocumentFragment(rootNode) && isShadowRoot(rootNode))) {
|
|
101
|
+
rootNode.adoptedStyleSheets.push(stylesheet);
|
|
102
|
+
}
|
|
93
103
|
}
|
|
94
|
-
|
|
104
|
+
for (const rootNode of removedRootNodes) {
|
|
105
|
+
if (isDocument(rootNode) || (isDocumentFragment(rootNode) && isShadowRoot(rootNode))) {
|
|
106
|
+
rootNode.adoptedStyleSheets.splice(rootNode.adoptedStyleSheets.indexOf(stylesheet), 1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
previousRootNodes = newRootNodes;
|
|
110
|
+
});
|
|
95
111
|
|
|
96
112
|
const disposeOfElementsEffect = effect(() => {
|
|
97
113
|
const added = extendedElementsWithIssues.value.filter(elementWithIssues => {
|
|
98
|
-
return !previousExtendedElementsWithIssues.some(previousElementWithIssues => previousElementWithIssues
|
|
114
|
+
return !previousExtendedElementsWithIssues.some(previousElementWithIssues => areElementsWithIssuesEqual(previousElementWithIssues, elementWithIssues));
|
|
99
115
|
});
|
|
100
116
|
const removed = previousExtendedElementsWithIssues.filter(previousElementWithIssues => {
|
|
101
|
-
return !extendedElementsWithIssues.value.some(elementWithIssues => elementWithIssues
|
|
117
|
+
return !extendedElementsWithIssues.value.some(elementWithIssues => areElementsWithIssuesEqual(elementWithIssues, previousElementWithIssues));
|
|
102
118
|
});
|
|
103
119
|
removeIssues(removed);
|
|
104
120
|
setIssues(added);
|
|
@@ -106,7 +122,7 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
106
122
|
});
|
|
107
123
|
|
|
108
124
|
return () => {
|
|
109
|
-
|
|
125
|
+
disposeOfStyleSheetsEffect();
|
|
110
126
|
disposeOfElementsEffect();
|
|
111
127
|
};
|
|
112
128
|
}
|
|
@@ -56,20 +56,45 @@ export default () => {
|
|
|
56
56
|
:host {
|
|
57
57
|
all: initial !important;
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
--
|
|
61
|
-
--
|
|
59
|
+
/* OKLCH stuff: https://oklch.com/ */
|
|
60
|
+
--light-color: oklch(0.98 0 0);
|
|
61
|
+
--dark-color: oklch(0.22 0 0);
|
|
62
|
+
|
|
63
|
+
--background-color: light-dark(var(--light-color), var(--dark-color));
|
|
64
|
+
--text-color: light-dark(var(--dark-color), var(--light-color));
|
|
65
|
+
|
|
66
|
+
--impact-lightness: 0.80;
|
|
67
|
+
--focus-lightness: 0.45;
|
|
68
|
+
@media (prefers-color-scheme: dark) {
|
|
69
|
+
--impact-lightness: 0.45;
|
|
70
|
+
--focus-lightness: 0.80;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
--blue-hue: 230;
|
|
74
|
+
--gold-hue: 90;
|
|
75
|
+
--red-hue: 0;
|
|
76
|
+
|
|
77
|
+
/* Contrasts with background. */
|
|
78
|
+
--focus-color: oklch(var(--focus-lightness) 0.25 var(--blue-hue));
|
|
79
|
+
|
|
80
|
+
--impact-chroma: 0.16;
|
|
62
81
|
|
|
63
|
-
--impact-
|
|
64
|
-
--impact-
|
|
65
|
-
--impact-
|
|
66
|
-
|
|
82
|
+
--impact-moderate-hue: var(--blue-hue);
|
|
83
|
+
--impact-serious-hue: var(--gold-hue);
|
|
84
|
+
--impact-critical-hue: var(--red-hue);
|
|
85
|
+
|
|
86
|
+
--impact-minor-color: oklch(var(--impact-lightness) 0 0);
|
|
87
|
+
--impact-moderate-color: oklch(var(--impact-lightness) var(--impact-chroma) var(--impact-moderate-hue));
|
|
88
|
+
--impact-serious-color: oklch(var(--impact-lightness) var(--impact-chroma) var(--impact-serious-hue));
|
|
89
|
+
--impact-critical-color: oklch(var(--impact-lightness) var(--impact-chroma) var(--impact-critical-hue));
|
|
90
|
+
|
|
91
|
+
--base-size: max(1rem, 16px);
|
|
67
92
|
|
|
68
93
|
/* Spacing and typography custom props, inspired by https://utopia.fyi (simplified). */
|
|
69
94
|
|
|
70
95
|
/* @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
96
|
--ratio: 1.2;
|
|
72
|
-
--step-0:
|
|
97
|
+
--step-0: var(--base-size);
|
|
73
98
|
--step-1: calc(var(--step-0) * var(--ratio));
|
|
74
99
|
--step-2: calc(var(--step-1) * var(--ratio));
|
|
75
100
|
--step-3: calc(var(--step-2) * var(--ratio));
|
|
@@ -77,15 +102,15 @@ export default () => {
|
|
|
77
102
|
--step--1: calc(var(--step-0) / var(--ratio));
|
|
78
103
|
|
|
79
104
|
/* @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.
|
|
81
|
-
--space-2xs: 0.
|
|
82
|
-
--space-xs: 0.
|
|
83
|
-
--space-s:
|
|
84
|
-
--space-m: 1.
|
|
85
|
-
--space-l:
|
|
86
|
-
--space-xl:
|
|
87
|
-
--space-2xl:
|
|
88
|
-
--space-3xl:
|
|
105
|
+
--space-3xs: calc(0.25 * var(--base-size));
|
|
106
|
+
--space-2xs: calc(0.5 * var(--base-size));
|
|
107
|
+
--space-xs: calc(0.75 * var(--base-size));
|
|
108
|
+
--space-s: var(--base-size);
|
|
109
|
+
--space-m: calc(1.5 * var(--base-size));
|
|
110
|
+
--space-l: calc(2 * var(--base-size));
|
|
111
|
+
--space-xl: calc(3 * var(--base-size));
|
|
112
|
+
--space-2xl: calc(4 * var(--base-size));
|
|
113
|
+
--space-3xl: calc(6 * var(--base-size));
|
|
89
114
|
}
|
|
90
115
|
|
|
91
116
|
a[href], button {
|
|
@@ -116,12 +141,15 @@ export default () => {
|
|
|
116
141
|
overflow-wrap: break-word;
|
|
117
142
|
font-family: system-ui;
|
|
118
143
|
line-height: 1.5;
|
|
119
|
-
|
|
120
|
-
color: var(--
|
|
144
|
+
text-wrap: pretty;
|
|
145
|
+
background-color: var(--background-color);
|
|
146
|
+
color: var(--text-color);
|
|
121
147
|
border: 2px solid currentColor;
|
|
122
148
|
padding: var(--space-l);
|
|
123
149
|
inline-size: min(90ch, calc(100% - var(--space-s)* 2));
|
|
124
150
|
max-block-size: calc(100% - var(--space-s) * 2);
|
|
151
|
+
|
|
152
|
+
color-scheme: light dark;
|
|
125
153
|
}
|
|
126
154
|
|
|
127
155
|
#button-container {
|
|
@@ -129,8 +157,8 @@ export default () => {
|
|
|
129
157
|
}
|
|
130
158
|
|
|
131
159
|
#close {
|
|
132
|
-
background-color: var(--
|
|
133
|
-
color: var(--
|
|
160
|
+
background-color: var(--background-color);
|
|
161
|
+
color: var(--text-color);
|
|
134
162
|
border: 2px solid currentColor;
|
|
135
163
|
padding-inline: var(--space-2xs);
|
|
136
164
|
aspect-ratio: 1 / 1;
|
|
@@ -167,7 +195,7 @@ export default () => {
|
|
|
167
195
|
}
|
|
168
196
|
|
|
169
197
|
a {
|
|
170
|
-
font-weight:
|
|
198
|
+
font-weight: 500;
|
|
171
199
|
}
|
|
172
200
|
}
|
|
173
201
|
|
|
@@ -12,8 +12,6 @@ export interface AccentedTrigger extends HTMLElement {
|
|
|
12
12
|
visible: Signal<boolean> | undefined;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const triggerSize = 'max(32px, 2rem)';
|
|
16
|
-
|
|
17
15
|
// We want Accented to not throw an error in Node, and use static imports,
|
|
18
16
|
// so we can't export `class extends HTMLElement` because HTMLElement is not available in Node.
|
|
19
17
|
export default (name: string) => {
|
|
@@ -27,6 +25,8 @@ export default (name: string) => {
|
|
|
27
25
|
template.innerHTML = `
|
|
28
26
|
<style>
|
|
29
27
|
:host {
|
|
28
|
+
--ratio: 1.2;
|
|
29
|
+
--base-size: max(1rem, 16px);
|
|
30
30
|
position: fixed !important;
|
|
31
31
|
inset-inline-start: anchor(self-start) !important;
|
|
32
32
|
inset-inline-end: anchor(self-end) !important;
|
|
@@ -45,12 +45,17 @@ export default (name: string) => {
|
|
|
45
45
|
pointer-events: auto;
|
|
46
46
|
|
|
47
47
|
position: absolute;
|
|
48
|
-
inset-
|
|
48
|
+
inset-block-start: 4px;
|
|
49
|
+
inset-inline-end: 4px;
|
|
49
50
|
|
|
50
51
|
box-sizing: border-box;
|
|
51
|
-
font-size:
|
|
52
|
-
inline-size:
|
|
53
|
-
block-size:
|
|
52
|
+
font-size: calc(var(--ratio) * var(--ratio) * var(--base-size));
|
|
53
|
+
inline-size: calc(2 * var(--base-size));
|
|
54
|
+
block-size: calc(2 * var(--base-size));
|
|
55
|
+
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: center;
|
|
54
59
|
|
|
55
60
|
/* Make it look better in forced-colors mode. */
|
|
56
61
|
border: 2px solid transparent;
|
|
@@ -58,6 +63,10 @@ export default (name: string) => {
|
|
|
58
63
|
background-color: var(--${name}-primary-color);
|
|
59
64
|
color: var(--${name}-secondary-color);
|
|
60
65
|
|
|
66
|
+
padding: 0;
|
|
67
|
+
|
|
68
|
+
border-radius: calc(0.25 * var(--base-size));
|
|
69
|
+
|
|
61
70
|
outline-offset: -4px;
|
|
62
71
|
outline-color: currentColor;
|
|
63
72
|
outline-width: 2px;
|
|
@@ -72,7 +81,7 @@ export default (name: string) => {
|
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
</style>
|
|
75
|
-
<button id="trigger" lang="en"
|
|
84
|
+
<button id="trigger" lang="en">!</button>
|
|
76
85
|
`;
|
|
77
86
|
|
|
78
87
|
return class extends HTMLElement implements AccentedTrigger {
|
|
@@ -135,8 +144,14 @@ export default (name: string) => {
|
|
|
135
144
|
this.#abortController = new AbortController();
|
|
136
145
|
trigger?.addEventListener('click', (event) => {
|
|
137
146
|
try {
|
|
147
|
+
// event.preventDefault() ensures that if the issue is within a link,
|
|
148
|
+
// the link's default behavior (following the URL) is prevented.
|
|
138
149
|
event.preventDefault();
|
|
139
150
|
|
|
151
|
+
// event.stopPropagation() ensures that if there's a click handler on the trigger's ancestor
|
|
152
|
+
// (a link, or a button, or anything else), it doesn't get triggered.
|
|
153
|
+
event.stopPropagation();
|
|
154
|
+
|
|
140
155
|
// We append the dialog when the button is clicked,
|
|
141
156
|
// and remove it from the DOM when the dialog is closed.
|
|
142
157
|
// This gives us a performance improvement since Axe
|
|
@@ -206,9 +221,17 @@ export default (name: string) => {
|
|
|
206
221
|
}
|
|
207
222
|
|
|
208
223
|
#setTransform() {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
// We read and write values in separate animation frames to avoid layout thrashing.
|
|
225
|
+
window.requestAnimationFrame(() => {
|
|
226
|
+
if (this.element) {
|
|
227
|
+
const transform = window.getComputedStyle(this.element).getPropertyValue('transform');
|
|
228
|
+
if (transform !== 'none') {
|
|
229
|
+
window.requestAnimationFrame(() => {
|
|
230
|
+
this.style.setProperty('transform', transform, 'important');
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
212
235
|
}
|
|
213
236
|
};
|
|
214
237
|
};
|
|
@@ -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('fullscreenchange', () => {
|
|
7
|
+
try {
|
|
8
|
+
recalculatePositions();
|
|
9
|
+
} catch (error) {
|
|
10
|
+
logAndRethrow(error);
|
|
11
|
+
}
|
|
12
|
+
}, { signal: abortController.signal });
|
|
13
|
+
|
|
14
|
+
return () => {
|
|
15
|
+
abortController.abort();
|
|
16
|
+
};
|
|
17
|
+
};
|
package/src/scanner.ts
CHANGED
|
@@ -6,8 +6,9 @@ import updateElementsWithIssues from './utils/update-elements-with-issues.js';
|
|
|
6
6
|
import recalculatePositions from './utils/recalculate-positions.js';
|
|
7
7
|
import recalculateScrollableAncestors from './utils/recalculate-scrollable-ancestors.js';
|
|
8
8
|
import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
|
|
9
|
-
import { issuesUrl } from './constants.js';
|
|
9
|
+
import { getAccentedElementNames, issuesUrl } from './constants.js';
|
|
10
10
|
import logAndRethrow from './log-and-rethrow.js';
|
|
11
|
+
import createShadowDOMAwareMutationObserver from './utils/shadow-dom-aware-mutation-observer.js';
|
|
11
12
|
|
|
12
13
|
export default function createScanner(name: string, axeContext: AxeContext, axeOptions: AxeOptions, throttle: Required<Throttle>, callback: Callback) {
|
|
13
14
|
const axeRunningWindowProp = `__${name}_axe_running__`;
|
|
@@ -22,7 +23,7 @@ export default function createScanner(name: string, axeContext: AxeContext, axeO
|
|
|
22
23
|
|
|
23
24
|
try {
|
|
24
25
|
|
|
25
|
-
performance.mark('
|
|
26
|
+
performance.mark('scan-start');
|
|
26
27
|
|
|
27
28
|
win[axeRunningWindowProp] = true;
|
|
28
29
|
|
|
@@ -54,17 +55,27 @@ export default function createScanner(name: string, axeContext: AxeContext, axeO
|
|
|
54
55
|
}
|
|
55
56
|
win[axeRunningWindowProp] = false;
|
|
56
57
|
|
|
57
|
-
const
|
|
58
|
+
const scanMeasure = performance.measure('scan', 'scan-start');
|
|
59
|
+
const scanDuration = Math.round(scanMeasure.duration);
|
|
58
60
|
|
|
59
61
|
if (!enabled.value) {
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
performance.mark('dom-update-start');
|
|
66
|
+
|
|
63
67
|
updateElementsWithIssues(extendedElementsWithIssues, result.violations, window, name);
|
|
64
68
|
|
|
69
|
+
const domUpdateMeasure = performance.measure('dom-update', 'dom-update-start');
|
|
70
|
+
const domUpdateDuration = Math.round(domUpdateMeasure.duration);
|
|
71
|
+
|
|
65
72
|
callback({
|
|
66
73
|
elementsWithIssues: elementsWithIssues.value,
|
|
67
|
-
|
|
74
|
+
performance: {
|
|
75
|
+
totalBlockingTime: scanDuration + domUpdateDuration,
|
|
76
|
+
scan: scanDuration,
|
|
77
|
+
domUpdate: domUpdateDuration
|
|
78
|
+
}
|
|
68
79
|
});
|
|
69
80
|
} catch (error) {
|
|
70
81
|
win[axeRunningWindowProp] = false;
|
|
@@ -77,8 +88,8 @@ export default function createScanner(name: string, axeContext: AxeContext, axeO
|
|
|
77
88
|
// if that's an element or array of elements (not a selector).
|
|
78
89
|
taskQueue.add(document);
|
|
79
90
|
|
|
80
|
-
const accentedElementNames =
|
|
81
|
-
const mutationObserver =
|
|
91
|
+
const accentedElementNames = getAccentedElementNames(name);
|
|
92
|
+
const mutationObserver = createShadowDOMAwareMutationObserver(name, mutationList => {
|
|
82
93
|
try {
|
|
83
94
|
// We're not interested in mutations that are caused exclusively by the custom elements
|
|
84
95
|
// introduced by Accented.
|
package/src/state.ts
CHANGED
|
@@ -8,10 +8,18 @@ export const extendedElementsWithIssues = signal<Array<ExtendedElementWithIssues
|
|
|
8
8
|
|
|
9
9
|
export const elementsWithIssues = computed<Array<ElementWithIssues>>(() => extendedElementsWithIssues.value.map(extendedElementWithIssues => ({
|
|
10
10
|
element: extendedElementWithIssues.element,
|
|
11
|
+
rootNode: extendedElementWithIssues.rootNode,
|
|
11
12
|
issues: extendedElementWithIssues.issues.value
|
|
12
13
|
})));
|
|
13
14
|
|
|
14
|
-
export const
|
|
15
|
+
export const rootNodes = computed<Set<Node>>(() =>
|
|
16
|
+
new Set(
|
|
17
|
+
(enabled.value ? [document as Node] : [])
|
|
18
|
+
.concat(...(extendedElementsWithIssues.value.map(extendedElementWithIssues => extendedElementWithIssues.rootNode)))
|
|
19
|
+
)
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export const scrollableAncestors = computed<Set<Element>>(() =>
|
|
15
23
|
extendedElementsWithIssues.value.reduce(
|
|
16
24
|
(scrollableAncestors, extendedElementWithIssues) => {
|
|
17
25
|
for (const scrollableAncestor of extendedElementWithIssues.scrollableAncestors.value) {
|
|
@@ -19,6 +27,6 @@ export const scrollableAncestors = computed<Set<HTMLElement>>(() =>
|
|
|
19
27
|
}
|
|
20
28
|
return scrollableAncestors;
|
|
21
29
|
},
|
|
22
|
-
new Set<
|
|
30
|
+
new Set<Element>()
|
|
23
31
|
)
|
|
24
32
|
);
|
package/src/types.ts
CHANGED
|
@@ -42,9 +42,17 @@ type CallbackParams = {
|
|
|
42
42
|
elementsWithIssues: Array<ElementWithIssues>,
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* * `performance`: runtime performance of the last scan. An object:
|
|
46
|
+
* * `totalBlockingTime`: how long the main thread was blocked by Accented during the last scan, in milliseconds.
|
|
47
|
+
* It’s further divided into the `scan` and `domUpdate` phases.
|
|
48
|
+
* * `scan`: how long the `scan` phase took, in milliseconds.
|
|
49
|
+
* * `domUpdate`: how long the `domUpdate` phase took, in milliseconds.
|
|
46
50
|
* */
|
|
47
|
-
|
|
51
|
+
performance: {
|
|
52
|
+
totalBlockingTime: number,
|
|
53
|
+
scan: number,
|
|
54
|
+
domUpdate: number
|
|
55
|
+
}
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
export type Callback = (params: CallbackParams) => void;
|
|
@@ -141,16 +149,21 @@ export type Issue = {
|
|
|
141
149
|
impact: axe.ImpactValue
|
|
142
150
|
};
|
|
143
151
|
|
|
144
|
-
export type
|
|
152
|
+
export type BaseElementWithIssues = {
|
|
145
153
|
element: HTMLElement,
|
|
154
|
+
rootNode: Node
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type ElementWithIssues = BaseElementWithIssues &{
|
|
146
158
|
issues: Array<Issue>
|
|
147
|
-
}
|
|
159
|
+
};
|
|
148
160
|
|
|
149
|
-
export type ExtendedElementWithIssues =
|
|
161
|
+
export type ExtendedElementWithIssues = BaseElementWithIssues & {
|
|
150
162
|
issues: Signal<ElementWithIssues['issues']>,
|
|
151
163
|
visible: Signal<boolean>,
|
|
152
164
|
trigger: AccentedTrigger,
|
|
153
165
|
position: Signal<Position>,
|
|
154
|
-
|
|
166
|
+
anchorNameValue: string,
|
|
167
|
+
scrollableAncestors: Signal<Set<Element>>
|
|
155
168
|
id: number
|
|
156
169
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BaseElementWithIssues } from "../types";
|
|
2
|
+
|
|
3
|
+
export default function areElementsWithIssuesEqual(
|
|
4
|
+
elementWithIssues1: BaseElementWithIssues,
|
|
5
|
+
elementWithIssues2: BaseElementWithIssues
|
|
6
|
+
) {
|
|
7
|
+
return elementWithIssues1.element === elementWithIssues2.element
|
|
8
|
+
&& elementWithIssues1.rootNode === elementWithIssues2.rootNode;
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function isElement(node: Node): node is Element {
|
|
2
|
+
return typeof Node !== 'undefined' && node.nodeType === Node.ELEMENT_NODE;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isDocument(node: Node): node is Document {
|
|
6
|
+
return typeof Node !== 'undefined' && node.nodeType === Node.DOCUMENT_NODE;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isDocumentFragment(node: Node): node is DocumentFragment {
|
|
10
|
+
return typeof Node !== 'undefined' && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isShadowRoot(documentFragment: DocumentFragment): documentFragment is ShadowRoot {
|
|
14
|
+
return 'host' in documentFragment;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isHtmlElement(element: Element): element is HTMLElement {
|
|
18
|
+
// We can't use instanceof because it may not work across contexts
|
|
19
|
+
// (such as when an element is moved from an iframe).
|
|
20
|
+
// This heuristic seems to be the most robust and fastest that I could think of.
|
|
21
|
+
return element.constructor.name.startsWith('HTML');
|
|
22
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Position } from '../types';
|
|
2
|
-
import isHtmlElement from './
|
|
2
|
+
import { isHtmlElement } from './dom-helpers.js';
|
|
3
|
+
import getParent from './get-parent.js';
|
|
3
4
|
|
|
4
5
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_display/Containing_block#identifying_the_containing_block
|
|
5
6
|
function isContainingBlock(element: Element, win: Window): boolean {
|
|
@@ -13,9 +14,9 @@ function isContainingBlock(element: Element, win: Window): boolean {
|
|
|
13
14
|
|
|
14
15
|
function getNonInitialContainingBlock(element: Element, win: Window): Element | null {
|
|
15
16
|
let currentElement: Element | null = element;
|
|
16
|
-
while (currentElement
|
|
17
|
-
currentElement = currentElement
|
|
18
|
-
if (isContainingBlock(currentElement, win)) {
|
|
17
|
+
while (currentElement) {
|
|
18
|
+
currentElement = getParent(currentElement);
|
|
19
|
+
if (currentElement && isContainingBlock(currentElement, win)) {
|
|
19
20
|
return currentElement;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isDocumentFragment, isShadowRoot } from './dom-helpers.js';
|
|
2
|
+
|
|
3
|
+
export default function getParent (element: Element): Element | null {
|
|
4
|
+
if (element.parentElement) {
|
|
5
|
+
return element.parentElement;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const rootNode = element.getRootNode();
|
|
9
|
+
if (isDocumentFragment(rootNode) && isShadowRoot(rootNode)) {
|
|
10
|
+
return rootNode.host;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import getParent from './get-parent.js';
|
|
2
|
+
|
|
1
3
|
const scrollableOverflowValues = new Set(['auto', 'scroll', 'hidden']);
|
|
2
4
|
|
|
3
|
-
export default function getScrollableAncestors (element:
|
|
4
|
-
let currentElement = element;
|
|
5
|
-
let scrollableAncestors = new Set<
|
|
6
|
-
while (
|
|
7
|
-
currentElement = currentElement
|
|
5
|
+
export default function getScrollableAncestors (element: Element, win: Window) {
|
|
6
|
+
let currentElement: Element | null = element;
|
|
7
|
+
let scrollableAncestors = new Set<Element>();
|
|
8
|
+
while (true) {
|
|
9
|
+
currentElement = getParent(currentElement);
|
|
10
|
+
if (!currentElement) {
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
8
13
|
const computedStyle = win.getComputedStyle(currentElement);
|
|
9
14
|
if (scrollableOverflowValues.has(computedStyle.overflowX) || scrollableOverflowValues.has(computedStyle.overflowY)) {
|
|
10
15
|
scrollableAncestors.add(currentElement);
|