accented 0.0.0-20250303013509 → 0.0.0-20250424114613
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 +10 -4
- package/dist/accented.d.ts +2 -2
- package/dist/accented.d.ts.map +1 -1
- package/dist/accented.js +10 -5
- 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 +66 -25
- package/dist/dom-updater.js.map +1 -1
- package/dist/elements/accented-dialog.d.ts +11 -7
- package/dist/elements/accented-dialog.d.ts.map +1 -1
- package/dist/elements/accented-dialog.js +85 -86
- package/dist/elements/accented-dialog.js.map +1 -1
- package/dist/elements/accented-trigger.d.ts +9 -5
- package/dist/elements/accented-trigger.d.ts.map +1 -1
- package/dist/elements/accented-trigger.js +35 -11
- 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/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -1
- package/dist/logger.js.map +1 -1
- package/dist/scanner.d.ts +2 -2
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +33 -19
- 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/task-queue.d.ts +2 -2
- package/dist/task-queue.d.ts.map +1 -1
- package/dist/task-queue.js +2 -1
- package/dist/task-queue.js.map +1 -1
- package/dist/types.d.ts +42 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.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/containing-blocks.d.ts +3 -0
- package/dist/utils/containing-blocks.d.ts.map +1 -0
- package/dist/utils/containing-blocks.js +46 -0
- package/dist/utils/containing-blocks.js.map +1 -0
- package/dist/utils/contains.d.ts +2 -0
- package/dist/utils/contains.d.ts.map +1 -0
- package/dist/utils/contains.js +19 -0
- package/dist/utils/contains.js.map +1 -0
- package/dist/utils/deduplicate-nodes.d.ts +2 -0
- package/dist/utils/deduplicate-nodes.d.ts.map +1 -0
- package/dist/utils/deduplicate-nodes.js +5 -0
- package/dist/utils/deduplicate-nodes.js.map +1 -0
- package/dist/utils/dom-helpers.d.ts +9 -0
- package/dist/utils/dom-helpers.d.ts.map +1 -0
- package/dist/utils/dom-helpers.js +32 -0
- package/dist/utils/dom-helpers.js.map +1 -0
- package/dist/utils/ensure-non-empty.d.ts +2 -0
- package/dist/utils/ensure-non-empty.d.ts.map +1 -0
- package/dist/utils/ensure-non-empty.js +7 -0
- package/dist/utils/ensure-non-empty.js.map +1 -0
- package/dist/utils/get-element-position.d.ts +8 -0
- package/dist/utils/get-element-position.d.ts.map +1 -1
- package/dist/utils/get-element-position.js +27 -11
- 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-scan-context.d.ts +3 -0
- package/dist/utils/get-scan-context.d.ts.map +1 -0
- package/dist/utils/get-scan-context.js +28 -0
- package/dist/utils/get-scan-context.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/is-node-in-scan-context.d.ts +3 -0
- package/dist/utils/is-node-in-scan-context.d.ts.map +1 -0
- package/dist/utils/is-node-in-scan-context.js +26 -0
- package/dist/utils/is-node-in-scan-context.js.map +1 -0
- package/dist/utils/normalize-context.d.ts +3 -0
- package/dist/utils/normalize-context.d.ts.map +1 -0
- package/dist/utils/normalize-context.js +57 -0
- package/dist/utils/normalize-context.js.map +1 -0
- 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 +10 -4
- package/dist/utils/update-elements-with-issues.d.ts.map +1 -1
- package/dist/utils/update-elements-with-issues.js +33 -6
- package/dist/utils/update-elements-with-issues.js.map +1 -1
- package/dist/validate-options.d.ts.map +1 -1
- package/dist/validate-options.js +86 -0
- package/dist/validate-options.js.map +1 -1
- package/package.json +8 -3
- package/src/accented.ts +10 -5
- package/src/constants.ts +1 -0
- package/src/dom-updater.ts +70 -24
- package/src/elements/accented-dialog.ts +88 -90
- package/src/elements/accented-trigger.ts +36 -12
- package/src/fullscreen-listener.ts +17 -0
- package/src/logger.ts +9 -1
- package/src/scanner.ts +37 -20
- package/src/state.ts +10 -2
- package/src/task-queue.ts +6 -4
- package/src/types.ts +55 -9
- package/src/utils/are-elements-with-issues-equal.ts +9 -0
- package/src/utils/containing-blocks.ts +57 -0
- package/src/utils/contains.test.ts +55 -0
- package/src/utils/contains.ts +19 -0
- package/src/utils/deduplicate-nodes.ts +3 -0
- package/src/utils/dom-helpers.ts +38 -0
- package/src/utils/ensure-non-empty.ts +6 -0
- package/src/utils/get-element-position.ts +28 -11
- package/src/utils/get-parent.ts +14 -0
- package/src/utils/get-scan-context.test.ts +79 -0
- package/src/utils/get-scan-context.ts +39 -0
- package/src/utils/get-scrollable-ancestors.ts +10 -5
- package/src/utils/is-node-in-scan-context.test.ts +70 -0
- package/src/utils/is-node-in-scan-context.ts +29 -0
- package/src/utils/normalize-context.test.ts +105 -0
- package/src/utils/normalize-context.ts +58 -0
- 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 +102 -15
- package/src/utils/update-elements-with-issues.ts +51 -7
- package/src/validate-options.ts +88 -1
- 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/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "accented",
|
|
3
|
-
"version": "0.0.0-
|
|
3
|
+
"version": "0.0.0-20250424114613",
|
|
4
4
|
"description": "Continuous accessibility testing and issue highlighting for web development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/accented.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
9
|
-
"src"
|
|
9
|
+
"src",
|
|
10
|
+
"NOTICE"
|
|
10
11
|
],
|
|
11
12
|
"repository": {
|
|
12
13
|
"type": "git",
|
|
@@ -26,7 +27,11 @@
|
|
|
26
27
|
"homepage": "https://github.com/pomerantsev/accented#readme",
|
|
27
28
|
"dependencies": {
|
|
28
29
|
"@preact/signals-core": "^1.8.0",
|
|
29
|
-
"axe-core": "^4.10.
|
|
30
|
+
"axe-core": "^4.10.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/jsdom": "^21.1.7",
|
|
34
|
+
"jsdom": "^26.0.0"
|
|
30
35
|
},
|
|
31
36
|
"scripts": {
|
|
32
37
|
"build": "tsc",
|
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';
|
|
@@ -12,6 +13,7 @@ import type { AccentedOptions, DisableAccented } from './types';
|
|
|
12
13
|
import validateOptions from './validate-options.js';
|
|
13
14
|
import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
|
|
14
15
|
import logAndRethrow from './log-and-rethrow.js';
|
|
16
|
+
import { initializeContainingBlockSupportSet } from './utils/containing-blocks.js';
|
|
15
17
|
|
|
16
18
|
export type { AccentedOptions, DisableAccented };
|
|
17
19
|
|
|
@@ -34,9 +36,9 @@ export type { AccentedOptions, DisableAccented };
|
|
|
34
36
|
* wait: 500,
|
|
35
37
|
* leading: false
|
|
36
38
|
* },
|
|
37
|
-
* callback: ({ elementsWithIssues,
|
|
39
|
+
* callback: ({ elementsWithIssues, performance }) => {
|
|
38
40
|
* console.log('Elements with issues:', elementsWithIssues);
|
|
39
|
-
* console.log('
|
|
41
|
+
* console.log('Total blocking time:', performance.totalBlockingTime);
|
|
40
42
|
* }
|
|
41
43
|
* });
|
|
42
44
|
*/
|
|
@@ -66,7 +68,7 @@ export default function accented(options: AccentedOptions = {}): DisableAccented
|
|
|
66
68
|
// * update examples in the accented() function JSDoc;
|
|
67
69
|
// * update examples in the Readme.
|
|
68
70
|
const defaultOptions: Required<AccentedOptions> = {
|
|
69
|
-
|
|
71
|
+
context: document,
|
|
70
72
|
axeOptions: {},
|
|
71
73
|
name: 'accented',
|
|
72
74
|
output: defaultOutput,
|
|
@@ -74,7 +76,7 @@ export default function accented(options: AccentedOptions = {}): DisableAccented
|
|
|
74
76
|
callback: () => {}
|
|
75
77
|
};
|
|
76
78
|
|
|
77
|
-
const {
|
|
79
|
+
const {context, axeOptions, name, output, throttle, callback} = deepMerge(defaultOptions, options);
|
|
78
80
|
|
|
79
81
|
if (enabled.value) {
|
|
80
82
|
// Add link to the recipes section of the docs (#56).
|
|
@@ -87,14 +89,16 @@ export default function accented(options: AccentedOptions = {}): DisableAccented
|
|
|
87
89
|
|
|
88
90
|
enabled.value = true;
|
|
89
91
|
|
|
92
|
+
initializeContainingBlockSupportSet();
|
|
90
93
|
registerElements(name);
|
|
91
94
|
|
|
92
95
|
const {disconnect: cleanupIntersectionObserver, intersectionObserver } = supportsAnchorPositioning(window) ? {} : setupIntersectionObserver();
|
|
93
|
-
const cleanupScanner = createScanner(name,
|
|
96
|
+
const cleanupScanner = createScanner(name, context, axeOptions, throttle, callback);
|
|
94
97
|
const cleanupDomUpdater = createDomUpdater(name, intersectionObserver);
|
|
95
98
|
const cleanupLogger = output.console ? createLogger() : () => {};
|
|
96
99
|
const cleanupScrollListeners = supportsAnchorPositioning(window) ? () => {} : setupScrollListeners();
|
|
97
100
|
const cleanupResizeListener = supportsAnchorPositioning(window) ? () => {} : setupResizeListener();
|
|
101
|
+
const cleanupFullscreenListener = supportsAnchorPositioning(window) ? () => {} : setupFullscreenListener();
|
|
98
102
|
|
|
99
103
|
return () => {
|
|
100
104
|
try {
|
|
@@ -105,6 +109,7 @@ export default function accented(options: AccentedOptions = {}): DisableAccented
|
|
|
105
109
|
cleanupLogger();
|
|
106
110
|
cleanupScrollListeners();
|
|
107
111
|
cleanupResizeListener();
|
|
112
|
+
cleanupFullscreenListener();
|
|
108
113
|
if (cleanupIntersectionObserver) {
|
|
109
114
|
cleanupIntersectionObserver();
|
|
110
115
|
}
|
package/src/constants.ts
CHANGED
package/src/dom-updater.ts
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
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';
|
|
8
|
+
|
|
9
|
+
const shouldInsertTriggerInsideElement = (element: Element): boolean => {
|
|
10
|
+
/**
|
|
11
|
+
* No parent means that the element is a root node,
|
|
12
|
+
* which cannot have siblings.
|
|
13
|
+
*/
|
|
14
|
+
const noParent = !getParent(element);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Table cells get a special treatment because if a sibling to a TH or TD is inserted,
|
|
18
|
+
* it alters the table layout, no matter how that sibling is positioned.
|
|
19
|
+
* We don't want tables to look broken, so we're inserting the trigger inside the table cell.
|
|
20
|
+
*/
|
|
21
|
+
const isTableCell = element.nodeName === 'TH' || element.nodeName === 'TD';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* We want to put the trigger inside the <summary> element,
|
|
25
|
+
* because otherwise it will be hidden by the browser when the <details> element is collapsed
|
|
26
|
+
* (since none of the siblings of <summary> are visible then).
|
|
27
|
+
*/
|
|
28
|
+
const isSummary = element.nodeName === 'SUMMARY';
|
|
29
|
+
|
|
30
|
+
return noParent || isTableCell || isSummary;
|
|
31
|
+
};
|
|
5
32
|
|
|
6
33
|
export default function createDomUpdater(name: string, intersectionObserver?: IntersectionObserver) {
|
|
7
34
|
const attrName = `data-${name}`;
|
|
@@ -13,8 +40,8 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
13
40
|
.filter(anchorName => anchorName.startsWith('--'));
|
|
14
41
|
}
|
|
15
42
|
|
|
16
|
-
function setAnchorName (
|
|
17
|
-
const anchorNameValue =
|
|
43
|
+
function setAnchorName (elementWithIssues: ExtendedElementWithIssues) {
|
|
44
|
+
const { element, id, anchorNameValue } = elementWithIssues;
|
|
18
45
|
const anchorNames = getAnchorNames(anchorNameValue);
|
|
19
46
|
if (anchorNames.length > 0) {
|
|
20
47
|
element.style.setProperty('anchor-name', `${anchorNameValue}, --${name}-anchor-${id}`);
|
|
@@ -23,28 +50,30 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
23
50
|
}
|
|
24
51
|
}
|
|
25
52
|
|
|
26
|
-
function removeAnchorName (
|
|
27
|
-
const anchorNameValue =
|
|
53
|
+
function removeAnchorName (elementWithIssues: ExtendedElementWithIssues) {
|
|
54
|
+
const { element, anchorNameValue } = elementWithIssues;
|
|
28
55
|
const anchorNames = getAnchorNames(anchorNameValue);
|
|
29
|
-
|
|
30
|
-
|
|
56
|
+
if (anchorNames.length > 0) {
|
|
57
|
+
element.style.setProperty('anchor-name', anchorNames.join(', '));
|
|
58
|
+
} else {
|
|
31
59
|
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
60
|
}
|
|
35
61
|
}
|
|
36
62
|
|
|
37
63
|
function setIssues (extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
|
|
38
64
|
for (const elementWithIssues of extendedElementsWithIssues) {
|
|
65
|
+
if (elementWithIssues.skipRender) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
39
68
|
elementWithIssues.element.setAttribute(attrName, elementWithIssues.id.toString());
|
|
40
69
|
if (supportsAnchorPositioning(window)) {
|
|
41
|
-
setAnchorName(elementWithIssues
|
|
70
|
+
setAnchorName(elementWithIssues);
|
|
42
71
|
}
|
|
43
72
|
|
|
44
|
-
if (elementWithIssues.element
|
|
45
|
-
elementWithIssues.element.insertAdjacentElement('afterend', elementWithIssues.trigger);
|
|
46
|
-
} else {
|
|
73
|
+
if (shouldInsertTriggerInsideElement(elementWithIssues.element)) {
|
|
47
74
|
elementWithIssues.element.insertAdjacentElement('beforeend', elementWithIssues.trigger);
|
|
75
|
+
} else {
|
|
76
|
+
elementWithIssues.element.insertAdjacentElement('afterend', elementWithIssues.trigger);
|
|
48
77
|
}
|
|
49
78
|
if (intersectionObserver) {
|
|
50
79
|
intersectionObserver.observe(elementWithIssues.element);
|
|
@@ -54,9 +83,12 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
54
83
|
|
|
55
84
|
function removeIssues (extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
|
|
56
85
|
for (const elementWithIssues of extendedElementsWithIssues) {
|
|
86
|
+
if (elementWithIssues.skipRender) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
57
89
|
elementWithIssues.element.removeAttribute(attrName);
|
|
58
90
|
if (supportsAnchorPositioning(window)) {
|
|
59
|
-
removeAnchorName(elementWithIssues
|
|
91
|
+
removeAnchorName(elementWithIssues);
|
|
60
92
|
}
|
|
61
93
|
elementWithIssues.trigger.remove();
|
|
62
94
|
if (intersectionObserver) {
|
|
@@ -69,8 +101,10 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
69
101
|
stylesheet.replaceSync(`
|
|
70
102
|
@layer ${name} {
|
|
71
103
|
:root {
|
|
72
|
-
|
|
73
|
-
|
|
104
|
+
/* Ensure that the primary / secondary color combination meets WCAG 1.4.3 Contrast (Minimum) */
|
|
105
|
+
/* OKLCH stuff: https://oklch.com/ */
|
|
106
|
+
--${name}-primary-color: oklch(0.5 0.3 0);
|
|
107
|
+
--${name}-secondary-color: oklch(0.98 0 0);
|
|
74
108
|
--${name}-outline-width: 2px;
|
|
75
109
|
--${name}-outline-style: solid;
|
|
76
110
|
}
|
|
@@ -86,19 +120,31 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
86
120
|
|
|
87
121
|
let previousExtendedElementsWithIssues: Array<ExtendedElementWithIssues> = [];
|
|
88
122
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
123
|
+
let previousRootNodes: Set<Node> = new Set();
|
|
124
|
+
|
|
125
|
+
const disposeOfStyleSheetsEffect = effect(() => {
|
|
126
|
+
const newRootNodes = rootNodes.value;
|
|
127
|
+
const addedRootNodes = [...newRootNodes].filter(rootNode => !previousRootNodes.has(rootNode));
|
|
128
|
+
const removedRootNodes = [...previousRootNodes].filter(rootNode => !newRootNodes.has(rootNode));
|
|
129
|
+
for (const rootNode of addedRootNodes) {
|
|
130
|
+
if (isDocument(rootNode) || (isDocumentFragment(rootNode) && isShadowRoot(rootNode))) {
|
|
131
|
+
rootNode.adoptedStyleSheets.push(stylesheet);
|
|
132
|
+
}
|
|
93
133
|
}
|
|
94
|
-
|
|
134
|
+
for (const rootNode of removedRootNodes) {
|
|
135
|
+
if (isDocument(rootNode) || (isDocumentFragment(rootNode) && isShadowRoot(rootNode))) {
|
|
136
|
+
rootNode.adoptedStyleSheets.splice(rootNode.adoptedStyleSheets.indexOf(stylesheet), 1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
previousRootNodes = newRootNodes;
|
|
140
|
+
});
|
|
95
141
|
|
|
96
142
|
const disposeOfElementsEffect = effect(() => {
|
|
97
143
|
const added = extendedElementsWithIssues.value.filter(elementWithIssues => {
|
|
98
|
-
return !previousExtendedElementsWithIssues.some(previousElementWithIssues => previousElementWithIssues
|
|
144
|
+
return !previousExtendedElementsWithIssues.some(previousElementWithIssues => areElementsWithIssuesEqual(previousElementWithIssues, elementWithIssues));
|
|
99
145
|
});
|
|
100
146
|
const removed = previousExtendedElementsWithIssues.filter(previousElementWithIssues => {
|
|
101
|
-
return !extendedElementsWithIssues.value.some(elementWithIssues => elementWithIssues
|
|
147
|
+
return !extendedElementsWithIssues.value.some(elementWithIssues => areElementsWithIssuesEqual(elementWithIssues, previousElementWithIssues));
|
|
102
148
|
});
|
|
103
149
|
removeIssues(removed);
|
|
104
150
|
setIssues(added);
|
|
@@ -106,7 +152,7 @@ export default function createDomUpdater(name: string, intersectionObserver?: In
|
|
|
106
152
|
});
|
|
107
153
|
|
|
108
154
|
return () => {
|
|
109
|
-
|
|
155
|
+
disposeOfStyleSheetsEffect();
|
|
110
156
|
disposeOfElementsEffect();
|
|
111
157
|
};
|
|
112
158
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Issue } from '../types';
|
|
2
2
|
import type { Signal } from '@preact/signals-core';
|
|
3
|
-
import { effect } from '@preact/signals-core';
|
|
4
3
|
import getElementHtml from '../utils/get-element-html.js';
|
|
5
4
|
import { accentedUrl } from '../constants.js';
|
|
6
5
|
import logAndRethrow from '../log-and-rethrow.js';
|
|
@@ -9,6 +8,7 @@ export interface AccentedDialog extends HTMLElement {
|
|
|
9
8
|
issues: Signal<Array<Issue>> | undefined;
|
|
10
9
|
element: Element | undefined;
|
|
11
10
|
showModal: () => void;
|
|
11
|
+
open: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// We want Accented to not throw an error in Node, and use static imports,
|
|
@@ -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
62
|
|
|
63
|
-
--
|
|
64
|
-
--
|
|
65
|
-
|
|
66
|
-
--impact-
|
|
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;
|
|
81
|
+
|
|
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
|
|
|
@@ -213,15 +241,13 @@ export default () => {
|
|
|
213
241
|
`);
|
|
214
242
|
|
|
215
243
|
return class extends HTMLElement implements AccentedDialog {
|
|
216
|
-
#disposeOfEffect: (() => void) | undefined;
|
|
217
|
-
|
|
218
244
|
#abortController: AbortController | undefined;
|
|
219
245
|
|
|
220
246
|
issues: Signal<Array<Issue>> | undefined;
|
|
221
247
|
|
|
222
248
|
element: Element | undefined;
|
|
223
249
|
|
|
224
|
-
|
|
250
|
+
open: boolean = false;
|
|
225
251
|
|
|
226
252
|
constructor() {
|
|
227
253
|
try {
|
|
@@ -270,75 +296,52 @@ export default () => {
|
|
|
270
296
|
}
|
|
271
297
|
}, { signal: this.#abortController.signal });
|
|
272
298
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
descriptionList.appendChild(li);
|
|
301
|
-
}
|
|
302
|
-
description.appendChild(descriptionContent);
|
|
299
|
+
if (this.issues) {
|
|
300
|
+
const issues = this.issues.value;
|
|
301
|
+
const issuesList = shadowRoot.getElementById('issues');
|
|
302
|
+
if (issuesList) {
|
|
303
|
+
issuesList.innerHTML = '';
|
|
304
|
+
for (const issue of issues) {
|
|
305
|
+
const issueContent = issueTemplate.content.cloneNode(true) as Element;
|
|
306
|
+
const title = issueContent.querySelector('a');
|
|
307
|
+
const impact = issueContent.querySelector('.impact');
|
|
308
|
+
const description = issueContent.querySelector('.description');
|
|
309
|
+
if (title && impact && description) {
|
|
310
|
+
title.textContent = issue.title + ' (' + issue.id + ')';
|
|
311
|
+
title.href = issue.url;
|
|
312
|
+
|
|
313
|
+
impact.textContent = 'User impact: ' + issue.impact;
|
|
314
|
+
impact.setAttribute('data-impact', String(issue.impact));
|
|
315
|
+
|
|
316
|
+
const descriptionItems = issue.description.split(/\n\s*/);
|
|
317
|
+
const descriptionContent = descriptionTemplate.content.cloneNode(true) as Element;
|
|
318
|
+
const descriptionTitle = descriptionContent.querySelector('span');
|
|
319
|
+
const descriptionList = descriptionContent.querySelector('ul');
|
|
320
|
+
if (descriptionTitle && descriptionList && descriptionItems.length > 1) {
|
|
321
|
+
descriptionTitle.textContent = descriptionItems[0]!;
|
|
322
|
+
for (const descriptionItem of descriptionItems.slice(1)) {
|
|
323
|
+
const li = document.createElement('li');
|
|
324
|
+
li.textContent = descriptionItem;
|
|
325
|
+
descriptionList.appendChild(li);
|
|
303
326
|
}
|
|
327
|
+
description.appendChild(descriptionContent);
|
|
304
328
|
}
|
|
305
|
-
issuesList.appendChild(issueContent);
|
|
306
329
|
}
|
|
330
|
+
issuesList.appendChild(issueContent);
|
|
307
331
|
}
|
|
308
332
|
}
|
|
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();
|
|
333
|
+
}
|
|
321
334
|
|
|
322
|
-
this.#elementMutationObserver = new MutationObserver(() => {
|
|
323
|
-
try {
|
|
324
|
-
updateElementHtml();
|
|
325
|
-
} catch (error) {
|
|
326
|
-
logAndRethrow(error);
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
335
|
if (this.element) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.#elementMutationObserver.observe(this.element, {
|
|
335
|
-
attributes: true,
|
|
336
|
-
childList: true
|
|
337
|
-
});
|
|
336
|
+
const elementHtmlContainer = shadowRoot.getElementById('element-html');
|
|
337
|
+
if (elementHtmlContainer) {
|
|
338
|
+
elementHtmlContainer.textContent = getElementHtml(this.element);
|
|
339
|
+
}
|
|
338
340
|
}
|
|
339
341
|
|
|
340
342
|
dialog?.addEventListener('close', () => {
|
|
341
343
|
try {
|
|
344
|
+
this.open = false;
|
|
342
345
|
this.dispatchEvent(new Event('close'));
|
|
343
346
|
} catch (error) {
|
|
344
347
|
logAndRethrow(error);
|
|
@@ -352,15 +355,9 @@ export default () => {
|
|
|
352
355
|
|
|
353
356
|
disconnectedCallback() {
|
|
354
357
|
try {
|
|
355
|
-
if (this.#disposeOfEffect) {
|
|
356
|
-
this.#disposeOfEffect();
|
|
357
|
-
}
|
|
358
358
|
if (this.#abortController) {
|
|
359
359
|
this.#abortController.abort();
|
|
360
360
|
}
|
|
361
|
-
if (this.#elementMutationObserver) {
|
|
362
|
-
this.#elementMutationObserver.disconnect();
|
|
363
|
-
}
|
|
364
361
|
} catch (error) {
|
|
365
362
|
logAndRethrow(error);
|
|
366
363
|
}
|
|
@@ -371,6 +368,7 @@ export default () => {
|
|
|
371
368
|
const dialog = this.shadowRoot.querySelector('dialog');
|
|
372
369
|
if (dialog) {
|
|
373
370
|
dialog.showModal();
|
|
371
|
+
this.open = true;
|
|
374
372
|
}
|
|
375
373
|
}
|
|
376
374
|
}
|
|
@@ -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;
|
|
@@ -44,13 +44,19 @@ export default (name: string) => {
|
|
|
44
44
|
#trigger {
|
|
45
45
|
pointer-events: auto;
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
margin-inline-start: auto;
|
|
48
|
+
margin-inline-end: 4px;
|
|
49
|
+
margin-block-start: 4px;
|
|
49
50
|
|
|
50
51
|
box-sizing: border-box;
|
|
51
|
-
font-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
font-family: system-ui;
|
|
53
|
+
font-size: calc(var(--ratio) * var(--ratio) * var(--base-size));
|
|
54
|
+
inline-size: calc(2 * var(--base-size));
|
|
55
|
+
block-size: calc(2 * var(--base-size));
|
|
56
|
+
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
54
60
|
|
|
55
61
|
/* Make it look better in forced-colors mode. */
|
|
56
62
|
border: 2px solid transparent;
|
|
@@ -58,6 +64,10 @@ export default (name: string) => {
|
|
|
58
64
|
background-color: var(--${name}-primary-color);
|
|
59
65
|
color: var(--${name}-secondary-color);
|
|
60
66
|
|
|
67
|
+
padding: 0;
|
|
68
|
+
|
|
69
|
+
border-radius: calc(0.25 * var(--base-size));
|
|
70
|
+
|
|
61
71
|
outline-offset: -4px;
|
|
62
72
|
outline-color: currentColor;
|
|
63
73
|
outline-width: 2px;
|
|
@@ -72,7 +82,7 @@ export default (name: string) => {
|
|
|
72
82
|
}
|
|
73
83
|
}
|
|
74
84
|
</style>
|
|
75
|
-
<button id="trigger" lang="en"
|
|
85
|
+
<button id="trigger" lang="en">á</button>
|
|
76
86
|
`;
|
|
77
87
|
|
|
78
88
|
return class extends HTMLElement implements AccentedTrigger {
|
|
@@ -135,8 +145,14 @@ export default (name: string) => {
|
|
|
135
145
|
this.#abortController = new AbortController();
|
|
136
146
|
trigger?.addEventListener('click', (event) => {
|
|
137
147
|
try {
|
|
148
|
+
// event.preventDefault() ensures that if the issue is within a link,
|
|
149
|
+
// the link's default behavior (following the URL) is prevented.
|
|
138
150
|
event.preventDefault();
|
|
139
151
|
|
|
152
|
+
// event.stopPropagation() ensures that if there's a click handler on the trigger's ancestor
|
|
153
|
+
// (a link, or a button, or anything else), it doesn't get triggered.
|
|
154
|
+
event.stopPropagation();
|
|
155
|
+
|
|
140
156
|
// We append the dialog when the button is clicked,
|
|
141
157
|
// and remove it from the DOM when the dialog is closed.
|
|
142
158
|
// This gives us a performance improvement since Axe
|
|
@@ -185,7 +201,7 @@ export default (name: string) => {
|
|
|
185
201
|
if (this.#abortController) {
|
|
186
202
|
this.#abortController.abort();
|
|
187
203
|
}
|
|
188
|
-
if (this.#dialogCloseAbortController) {
|
|
204
|
+
if (this.#dialogCloseAbortController && !this.dialog?.open) {
|
|
189
205
|
this.#dialogCloseAbortController.abort();
|
|
190
206
|
this.dialog?.remove();
|
|
191
207
|
}
|
|
@@ -206,9 +222,17 @@ export default (name: string) => {
|
|
|
206
222
|
}
|
|
207
223
|
|
|
208
224
|
#setTransform() {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
225
|
+
// We read and write values in separate animation frames to avoid layout thrashing.
|
|
226
|
+
window.requestAnimationFrame(() => {
|
|
227
|
+
if (this.element) {
|
|
228
|
+
const transform = window.getComputedStyle(this.element).getPropertyValue('transform');
|
|
229
|
+
if (transform !== 'none') {
|
|
230
|
+
window.requestAnimationFrame(() => {
|
|
231
|
+
this.style.setProperty('transform', transform, 'important');
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
212
236
|
}
|
|
213
237
|
};
|
|
214
238
|
};
|