@motion-proto/live-tokens 0.98.2 → 0.99.0
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/CHANGELOG.md +67 -0
- package/bin/contractRunner.mjs +2 -2
- package/bin/rules/a11y.mjs +1 -1
- package/dist-plugin/index.cjs +71 -15
- package/dist-plugin/index.js +71 -15
- package/package.json +1 -1
- package/src/editor/bootstrap.ts +3 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +9 -10
- package/src/editor/core/a11y/a11yReport.ts +85 -22
- package/src/editor/core/a11y/a11yReportTypes.ts +11 -3
- package/src/editor/core/a11y/contrastFixes.ts +41 -0
- package/src/editor/core/palettes/contrastPairs.ts +4 -1
- package/src/editor/docs/content/accessibility.md +67 -18
- package/src/editor/docs/content/editing-tokens.md +4 -0
- package/src/editor/docs/content.generated.ts +2 -2
- package/src/editor/pages/ComponentEditorPage.svelte +4 -3
- package/src/editor/pages/EditorShell.svelte +10 -8
- package/src/editor/styles/ui-editor.css +2 -0
- package/src/editor/ui/a11y/A11yByEye.svelte +264 -0
- package/src/editor/ui/a11y/A11yComponentIssues.svelte +34 -16
- package/src/editor/ui/a11y/A11yFindingRow.svelte +66 -4
- package/src/editor/ui/a11y/A11yIssueRow.svelte +55 -24
- package/src/editor/ui/a11y/A11yOverview.svelte +72 -68
- package/src/editor/ui/a11y/A11yPanel.svelte +29 -4
- package/src/editor/ui/a11y/A11yPropertyMark.svelte +8 -8
- package/src/editor/ui/a11y/A11yRailMark.svelte +11 -4
- package/src/editor/ui/a11y/AccessibilityTab.svelte +22 -35
- package/src/editor/ui/a11y/a11yView.ts +50 -11
- package/src/live-tokens/data/colors-and-type/halloween.json +1 -1
- package/src/live-tokens/data/themes/autumn.json +16 -16
- package/src/live-tokens/data/themes/halloween.json +17 -17
- package/src/live-tokens/data/themes/midnight-study.json +16 -16
- package/src/live-tokens/data/themes/ocean.json +16 -16
- package/src/live-tokens/data/themes/royal-velvet.json +16 -16
- package/src/live-tokens/data/themes/sketchy.json +16 -16
- package/src/live-tokens/data/themes/spring-meadow.json +16 -16
- package/src/live-tokens/data/themes/sunset.json +16 -16
- package/src/system/components/Button.svelte +3 -3
- package/src/system/components/InlineEditActions.svelte +1 -1
- package/src/system/components/Input.svelte +1 -1
- package/src/system/components/MenuSelect.svelte +1 -1
- package/src/system/components/ProgressBar.svelte +1 -1
- package/src/system/components/SlidePager.svelte +1 -1
- package/src/system/components/Slider.svelte +6 -6
- package/src/system/components/TabBar.svelte +1 -1
- package/src/system/components/Toggle.svelte +1 -1
- package/src/testing-js/{chunk-RWH46IU5.js → chunk-7QXUJRSO.js} +2 -2
- package/src/testing-js/chunk-GZKUYLDW.js +193 -0
- package/src/testing-js/chunk-GZKUYLDW.js.map +1 -0
- package/src/testing-js/{chunk-SHLPDNAQ.js → chunk-RTZQLW3F.js} +172 -181
- package/src/testing-js/chunk-RTZQLW3F.js.map +1 -0
- package/src/testing-js/{chunk-5HNZHIHG.js → chunk-RWOT2XDB.js} +2 -2
- package/src/testing-js/{chunk-5HNZHIHG.js.map → chunk-RWOT2XDB.js.map} +1 -1
- package/src/testing-js/{chunk-R54I7YL6.js → chunk-THWGMSRH.js} +2 -2
- package/src/testing-js/component-a11y.contract.js +53 -23
- package/src/testing-js/component-a11y.contract.js.map +1 -1
- package/src/testing-js/component-behavior.contract.js +1 -1
- package/src/testing-js/component-editor.contract.js +3 -3
- package/src/testing-js/component-render.contract.js +3 -3
- package/src/testing-js/index.js +4 -4
- package/src/testing-js/index.js.map +1 -1
- package/src/testing-js/page-compliance.contract.js +3 -3
- package/src/testing-js/vitest.js +2 -2
- package/template/_gitignore +3 -0
- package/src/testing-js/chunk-EWSGE27V.js +0 -168
- package/src/testing-js/chunk-EWSGE27V.js.map +0 -1
- package/src/testing-js/chunk-SHLPDNAQ.js.map +0 -1
- /package/src/testing-js/{chunk-RWH46IU5.js.map → chunk-7QXUJRSO.js.map} +0 -0
- /package/src/testing-js/{chunk-R54I7YL6.js.map → chunk-THWGMSRH.js.map} +0 -0
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
A11Y_REVIEW_SECTION,
|
|
15
15
|
A11Y_RUN_SECTION,
|
|
16
16
|
a11ySectionId,
|
|
17
|
+
componentSections,
|
|
17
18
|
componentsWithIssues,
|
|
18
19
|
issuesLabel,
|
|
19
20
|
setA11yViewLink,
|
|
@@ -135,12 +136,13 @@
|
|
|
135
136
|
let screenBarHeight = $state(0);
|
|
136
137
|
|
|
137
138
|
let a11yComponentNavItems: NavItem[] = $derived(
|
|
138
|
-
|
|
139
|
+
componentSections($a11yIssues).map(({ component, label, icon, count }) => ({
|
|
139
140
|
id: a11ySectionId(component), label, icon, count,
|
|
140
141
|
})),
|
|
141
142
|
);
|
|
142
|
-
|
|
143
|
-
let
|
|
143
|
+
/** Each marked component, true when its only results are to check by eye. A result marked as looking right marks nothing. */
|
|
144
|
+
let a11yMarked = $derived(new Map(componentsWithIssues($a11yIssues).map((row) => [row.component, row.count === 0])));
|
|
145
|
+
let a11yHasReview = $derived(Object.values($a11yIssues).some((issues) => issues.review.length + issues.confirmed.length > 0));
|
|
144
146
|
|
|
145
147
|
// The narrow layout shows one section at a time, so the run section and the
|
|
146
148
|
// check-by-eye line join the component sections the rail lists.
|
|
@@ -255,7 +257,7 @@
|
|
|
255
257
|
onclick={() => pickSection(item.id)}
|
|
256
258
|
>
|
|
257
259
|
<i class={item.icon}></i>
|
|
258
|
-
<span class="nav-label">{#if $editorView === 'components' && a11yMarked.has(item.id)}<A11yRailMark />{/if}{item.label}</span>
|
|
260
|
+
<span class="nav-label">{#if $editorView === 'components' && a11yMarked.has(item.id)}<A11yRailMark review={a11yMarked.get(item.id)} />{/if}{item.label}</span>
|
|
259
261
|
{#if item.count !== undefined}
|
|
260
262
|
<span class="nav-count">{item.count}</span>
|
|
261
263
|
{/if}
|
|
@@ -382,12 +384,12 @@
|
|
|
382
384
|
onclick={() => selectComponent(item.id)}
|
|
383
385
|
>
|
|
384
386
|
<i class={item.icon}></i>
|
|
385
|
-
<span class="nav-label">{#if a11yMarked.has(item.id)}<A11yRailMark />{/if}{item.label}</span>
|
|
387
|
+
<span class="nav-label">{#if a11yMarked.has(item.id)}<A11yRailMark review={a11yMarked.get(item.id)} />{/if}{item.label}</span>
|
|
386
388
|
{#if $componentDirty[item.id]}
|
|
387
389
|
<span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
|
|
388
390
|
{/if}
|
|
389
391
|
{#if condensed && a11yMarked.has(item.id)}
|
|
390
|
-
<A11yRailMark placement="corner" />
|
|
392
|
+
<A11yRailMark placement="corner" review={a11yMarked.get(item.id)} />
|
|
391
393
|
{/if}
|
|
392
394
|
</button>
|
|
393
395
|
{/each}
|
|
@@ -405,12 +407,12 @@
|
|
|
405
407
|
onclick={() => selectComponent(item.id)}
|
|
406
408
|
>
|
|
407
409
|
<i class={item.icon}></i>
|
|
408
|
-
<span class="nav-label">{#if a11yMarked.has(item.id)}<A11yRailMark />{/if}{item.label}</span>
|
|
410
|
+
<span class="nav-label">{#if a11yMarked.has(item.id)}<A11yRailMark review={a11yMarked.get(item.id)} />{/if}{item.label}</span>
|
|
409
411
|
{#if $componentDirty[item.id]}
|
|
410
412
|
<span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
|
|
411
413
|
{/if}
|
|
412
414
|
{#if condensed && a11yMarked.has(item.id)}
|
|
413
|
-
<A11yRailMark placement="corner" />
|
|
415
|
+
<A11yRailMark placement="corner" review={a11yMarked.get(item.id)} />
|
|
414
416
|
{/if}
|
|
415
417
|
</button>
|
|
416
418
|
{/each}
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
|
|
50
50
|
/* Shared "this is live" amber: selection, focus, dirty state. */
|
|
51
51
|
--ui-highlight: #ffac28;
|
|
52
|
+
/* An accessibility failure the editor now measures as passing. */
|
|
53
|
+
--ui-fixed: #5fd38d;
|
|
52
54
|
|
|
53
55
|
/* Link-state bar: active = attached to control edge, broken = popped into column gap. */
|
|
54
56
|
--ui-link-active: #6cd0c2;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { a11yIssues, confirmReview } from '../../core/a11y/a11yReport';
|
|
4
|
+
import UIPillButton from '../UIPillButton.svelte';
|
|
5
|
+
import A11yFindingRow from './A11yFindingRow.svelte';
|
|
6
|
+
import { A11Y_REVIEW_SECTION, openComponentAt, reviewResults, type A11yReviewResult } from './a11yView';
|
|
7
|
+
|
|
8
|
+
const titleId = $props.id();
|
|
9
|
+
|
|
10
|
+
let results = $derived(reviewResults($a11yIssues));
|
|
11
|
+
let open = $derived(results.filter((r) => !r.confirmed));
|
|
12
|
+
let confirmed = $derived(results.filter((r) => r.confirmed));
|
|
13
|
+
let tally = $derived(
|
|
14
|
+
[open.length > 0 ? `${open.length} to check` : '', confirmed.length > 0 ? `${confirmed.length} look right` : '']
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.join(', '),
|
|
17
|
+
);
|
|
18
|
+
let done = $derived(results.length > 0 ? confirmed.length / results.length : 0);
|
|
19
|
+
|
|
20
|
+
let openList: HTMLUListElement | undefined = $state();
|
|
21
|
+
let confirmedList: HTMLUListElement | undefined = $state();
|
|
22
|
+
let heading: HTMLElement | undefined = $state();
|
|
23
|
+
let markingAll = $state(false);
|
|
24
|
+
let markAllError: string | null = $state(null);
|
|
25
|
+
|
|
26
|
+
const keyOf = (r: A11yReviewResult) => `${r.component}\n${r.row.findings.map((f) => f.message).join('\n')}`;
|
|
27
|
+
|
|
28
|
+
/** Moves focus to the toggle that took the pressed row's place, so Enter or Space walks the list. */
|
|
29
|
+
async function focusAt(list: HTMLUListElement | undefined, index: number) {
|
|
30
|
+
await tick();
|
|
31
|
+
const toggles = [...(list?.querySelectorAll<HTMLButtonElement>('.a11y-confirm button') ?? [])];
|
|
32
|
+
(toggles[index] ?? toggles[index - 1] ?? heading)?.focus();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function markAll() {
|
|
36
|
+
markingAll = true;
|
|
37
|
+
markAllError = null;
|
|
38
|
+
try {
|
|
39
|
+
await confirmReview(open.flatMap((r) => r.row.findings), true);
|
|
40
|
+
await focusAt(undefined, 0);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
markAllError = (err as Error).message;
|
|
43
|
+
} finally {
|
|
44
|
+
markingAll = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<section id={A11Y_REVIEW_SECTION} class="editor-section-card a11y-byeye" aria-labelledby={titleId}>
|
|
50
|
+
<header class="a11y-byeye-head">
|
|
51
|
+
<i class="fa-solid fa-eye a11y-byeye-mark" aria-hidden="true"></i>
|
|
52
|
+
<h3 id={titleId} class="editor-subsection-title" tabindex="-1" bind:this={heading}>Check by eye</h3>
|
|
53
|
+
<span class="a11y-byeye-tally">{tally}</span>
|
|
54
|
+
{#if open.length > 0}
|
|
55
|
+
<UIPillButton size="compact" variant="outline" icon="fa-check-double" disabled={markingAll} onclick={markAll}>
|
|
56
|
+
Mark all as looking right
|
|
57
|
+
</UIPillButton>
|
|
58
|
+
{/if}
|
|
59
|
+
</header>
|
|
60
|
+
|
|
61
|
+
<div
|
|
62
|
+
class="a11y-byeye-meter"
|
|
63
|
+
role="progressbar"
|
|
64
|
+
aria-label="Results marked as looking right"
|
|
65
|
+
aria-valuemin={0}
|
|
66
|
+
aria-valuemax={results.length}
|
|
67
|
+
aria-valuenow={confirmed.length}
|
|
68
|
+
>
|
|
69
|
+
<span class="a11y-byeye-meter-fill" style:transform={`scaleX(${done})`}></span>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{#if markAllError}
|
|
73
|
+
<p class="a11y-byeye-error" role="alert">{markAllError}</p>
|
|
74
|
+
{/if}
|
|
75
|
+
|
|
76
|
+
{#if open.length > 0}
|
|
77
|
+
<p class="a11y-byeye-lead">
|
|
78
|
+
The tools could not decide these. Judge each element against its help, then press <strong>Looks right</strong>.
|
|
79
|
+
The next result moves up under the same button.
|
|
80
|
+
</p>
|
|
81
|
+
<ul class="a11y-byeye-list" bind:this={openList}>
|
|
82
|
+
{#each open as result, i (keyOf(result))}
|
|
83
|
+
<A11yFindingRow
|
|
84
|
+
component={result.component}
|
|
85
|
+
row={result.row}
|
|
86
|
+
review
|
|
87
|
+
prefix={result.label}
|
|
88
|
+
onreveal={(variable) => openComponentAt(result.component, variable)}
|
|
89
|
+
onopen={() => openComponentAt(result.component)}
|
|
90
|
+
onconfirmchange={() => focusAt(openList, i)}
|
|
91
|
+
/>
|
|
92
|
+
{/each}
|
|
93
|
+
</ul>
|
|
94
|
+
{:else}
|
|
95
|
+
<p class="a11y-byeye-clear">
|
|
96
|
+
<i class="fa-solid fa-circle-check" aria-hidden="true"></i>
|
|
97
|
+
Every result looks right. Open <strong>Looks right</strong> below to check one again.
|
|
98
|
+
</p>
|
|
99
|
+
{/if}
|
|
100
|
+
|
|
101
|
+
{#if confirmed.length > 0}
|
|
102
|
+
<details class="a11y-byeye-done">
|
|
103
|
+
<summary class="a11y-byeye-group">
|
|
104
|
+
<i class="fa-solid fa-chevron-right a11y-byeye-chevron" aria-hidden="true"></i>
|
|
105
|
+
Looks right
|
|
106
|
+
<span class="a11y-byeye-group-count">{confirmed.length}</span>
|
|
107
|
+
</summary>
|
|
108
|
+
<ul class="a11y-byeye-list" bind:this={confirmedList}>
|
|
109
|
+
{#each confirmed as result, i (keyOf(result))}
|
|
110
|
+
<A11yFindingRow
|
|
111
|
+
component={result.component}
|
|
112
|
+
row={result.row}
|
|
113
|
+
review
|
|
114
|
+
confirmed
|
|
115
|
+
prefix={result.label}
|
|
116
|
+
onreveal={(variable) => openComponentAt(result.component, variable)}
|
|
117
|
+
onopen={() => openComponentAt(result.component)}
|
|
118
|
+
onconfirmchange={() => focusAt(confirmedList, i)}
|
|
119
|
+
/>
|
|
120
|
+
{/each}
|
|
121
|
+
</ul>
|
|
122
|
+
</details>
|
|
123
|
+
{/if}
|
|
124
|
+
</section>
|
|
125
|
+
|
|
126
|
+
<style>
|
|
127
|
+
.a11y-byeye {
|
|
128
|
+
gap: var(--ui-space-12);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.a11y-byeye-head {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-wrap: wrap;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: var(--ui-space-8) var(--ui-space-12);
|
|
136
|
+
color: var(--ui-text-primary);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.a11y-byeye-head :global(.ui-pill) {
|
|
140
|
+
margin-left: auto;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.a11y-byeye-head h3:focus {
|
|
144
|
+
outline: none;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.a11y-byeye-mark {
|
|
148
|
+
color: var(--ui-text-tertiary);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.a11y-byeye-tally {
|
|
152
|
+
font-size: var(--ui-font-size-sm);
|
|
153
|
+
color: var(--ui-text-tertiary);
|
|
154
|
+
font-variant-numeric: tabular-nums;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.a11y-byeye-meter {
|
|
158
|
+
height: 2px;
|
|
159
|
+
overflow: hidden;
|
|
160
|
+
background: var(--ui-border-low);
|
|
161
|
+
border-radius: var(--ui-radius-full);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.a11y-byeye-meter-fill {
|
|
165
|
+
display: block;
|
|
166
|
+
height: 100%;
|
|
167
|
+
background: var(--ui-text-primary);
|
|
168
|
+
transform-origin: left;
|
|
169
|
+
transition: transform 240ms ease;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@media (prefers-reduced-motion: reduce) {
|
|
173
|
+
.a11y-byeye-meter-fill {
|
|
174
|
+
transition: none;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.a11y-byeye-lead,
|
|
179
|
+
.a11y-byeye-clear,
|
|
180
|
+
.a11y-byeye-error {
|
|
181
|
+
margin: 0;
|
|
182
|
+
font-size: var(--ui-font-size-sm);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.a11y-byeye-lead {
|
|
186
|
+
color: var(--ui-text-tertiary);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.a11y-byeye-lead strong,
|
|
190
|
+
.a11y-byeye-clear strong {
|
|
191
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
192
|
+
color: var(--ui-text-secondary);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.a11y-byeye-clear {
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
gap: var(--ui-space-8);
|
|
199
|
+
padding: var(--ui-space-12) 0;
|
|
200
|
+
color: var(--ui-text-secondary);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.a11y-byeye-error {
|
|
204
|
+
color: var(--ui-highlight);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.a11y-byeye-done {
|
|
208
|
+
margin-top: var(--ui-space-12);
|
|
209
|
+
border-top: 1px solid var(--ui-border);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.a11y-byeye-group {
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
gap: var(--ui-space-8);
|
|
216
|
+
padding-top: var(--ui-space-12);
|
|
217
|
+
font-size: var(--ui-font-size-xs);
|
|
218
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
219
|
+
color: var(--ui-text-secondary);
|
|
220
|
+
list-style: none;
|
|
221
|
+
cursor: pointer;
|
|
222
|
+
transition: color var(--ui-transition-fast);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.a11y-byeye-group::-webkit-details-marker {
|
|
226
|
+
display: none;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.a11y-byeye-group:hover {
|
|
230
|
+
color: var(--ui-text-primary);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.a11y-byeye-chevron {
|
|
234
|
+
font-size: var(--ui-font-size-xs);
|
|
235
|
+
transition: transform var(--ui-transition-fast);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.a11y-byeye-done[open] .a11y-byeye-chevron {
|
|
239
|
+
transform: rotate(90deg);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@media (prefers-reduced-motion: reduce) {
|
|
243
|
+
.a11y-byeye-chevron {
|
|
244
|
+
transition: none;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.a11y-byeye-group-count {
|
|
249
|
+
font-weight: var(--ui-font-weight-normal);
|
|
250
|
+
color: var(--ui-text-tertiary);
|
|
251
|
+
font-variant-numeric: tabular-nums;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.a11y-byeye-list {
|
|
255
|
+
margin: 0;
|
|
256
|
+
padding: 0;
|
|
257
|
+
list-style: none;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.a11y-byeye-list :global(code) {
|
|
261
|
+
font-family: var(--ui-font-mono);
|
|
262
|
+
font-size: var(--ui-font-size-sm);
|
|
263
|
+
}
|
|
264
|
+
</style>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { ComponentA11yIssues } from '../../core/a11y/a11yReport';
|
|
3
|
+
import type { A11yContrastFailure } from '../../core/a11y/a11yReportTypes';
|
|
3
4
|
import { CONTRAST_RULES } from '../../core/palettes/contrast';
|
|
4
5
|
import { formatFloor, formatRatio } from '../contrastReadout';
|
|
5
6
|
import A11yFindingRow from './A11yFindingRow.svelte';
|
|
@@ -23,30 +24,42 @@
|
|
|
23
24
|
let reviewRows = $derived(withReview ? findingRows(issues.review) : []);
|
|
24
25
|
</script>
|
|
25
26
|
|
|
26
|
-
{#
|
|
27
|
+
{#snippet contrastRow(failure: A11yContrastFailure, fixedRatio: number | undefined)}
|
|
28
|
+
{@const label = propertyLabel(component, failure.variable) ?? failure.variable}
|
|
29
|
+
{@const rule = CONTRAST_RULES[failure.role]}
|
|
30
|
+
<A11yIssueRow fixed={fixedRatio !== undefined} href={rule.url} linkLabel={`Success criterion ${rule.criterion}`}>
|
|
31
|
+
{#if fixedRatio === undefined}
|
|
32
|
+
{label} {formatRatio(failure.ratio)}, needs {formatFloor(failure.floor)}
|
|
33
|
+
{:else}
|
|
34
|
+
<i class="fa-solid fa-check a11y-fixed-mark" aria-hidden="true"></i>
|
|
35
|
+
{label} {formatRatio(failure.ratio)} → {formatRatio(fixedRatio)}, meets {formatFloor(failure.floor)}
|
|
36
|
+
{/if}
|
|
37
|
+
{#snippet detail()}
|
|
38
|
+
<code>{failure.variable}</code>{#if failure.context}, {failure.context}{/if}
|
|
39
|
+
{/snippet}
|
|
40
|
+
{#snippet links()}
|
|
41
|
+
{#if onreveal}
|
|
42
|
+
<button type="button" class="a11y-link" aria-label={`Go to ${label}`} title={failure.variable} onclick={() => onreveal(failure.variable)}>
|
|
43
|
+
{label} <i class="fas fa-arrow-right" aria-hidden="true"></i>
|
|
44
|
+
</button>
|
|
45
|
+
{/if}
|
|
46
|
+
{/snippet}
|
|
47
|
+
</A11yIssueRow>
|
|
48
|
+
{/snippet}
|
|
49
|
+
|
|
50
|
+
{#if issues.contrast.length > 0 || propertyRows.length > 0 || issues.fixed.length > 0}
|
|
27
51
|
<div class="a11y-group">
|
|
28
52
|
<h4 class="a11y-group-title">Fix with a property</h4>
|
|
29
53
|
<ul class="a11y-issues">
|
|
30
54
|
{#each issues.contrast as failure, i (i)}
|
|
31
|
-
{@
|
|
32
|
-
{@const rule = CONTRAST_RULES[failure.role]}
|
|
33
|
-
<A11yIssueRow href={rule.url} linkLabel={`Success criterion ${rule.criterion}`}>
|
|
34
|
-
{label} {formatRatio(failure.ratio)}, needs {formatFloor(failure.floor)}
|
|
35
|
-
{#snippet detail()}
|
|
36
|
-
<code>{failure.variable}</code>{#if failure.context}, {failure.context}{/if}
|
|
37
|
-
{/snippet}
|
|
38
|
-
{#snippet links()}
|
|
39
|
-
{#if onreveal}
|
|
40
|
-
<button type="button" class="a11y-link" aria-label={`Go to ${label}`} title={failure.variable} onclick={() => onreveal(failure.variable)}>
|
|
41
|
-
{label} <i class="fas fa-arrow-right" aria-hidden="true"></i>
|
|
42
|
-
</button>
|
|
43
|
-
{/if}
|
|
44
|
-
{/snippet}
|
|
45
|
-
</A11yIssueRow>
|
|
55
|
+
{@render contrastRow(failure, undefined)}
|
|
46
56
|
{/each}
|
|
47
57
|
{#each propertyRows as row, i (i)}
|
|
48
58
|
<A11yFindingRow {component} {row} {onreveal} {onopen} />
|
|
49
59
|
{/each}
|
|
60
|
+
{#each issues.fixed as { failure, ratio }, i (i)}
|
|
61
|
+
{@render contrastRow(failure, ratio)}
|
|
62
|
+
{/each}
|
|
50
63
|
</ul>
|
|
51
64
|
</div>
|
|
52
65
|
{/if}
|
|
@@ -95,6 +108,11 @@
|
|
|
95
108
|
font-family: var(--ui-font-mono);
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
.a11y-fixed-mark {
|
|
112
|
+
margin-right: var(--ui-space-4);
|
|
113
|
+
font-size: var(--ui-font-size-sm);
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
.a11y-issues {
|
|
99
117
|
margin: 0;
|
|
100
118
|
padding: 0;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { confirmReview } from '../../core/a11y/a11yReport';
|
|
3
|
+
import UIPillButton from '../UIPillButton.svelte';
|
|
2
4
|
import A11yIssueRow from './A11yIssueRow.svelte';
|
|
3
5
|
import {
|
|
4
6
|
componentLabel,
|
|
@@ -13,23 +15,61 @@
|
|
|
13
15
|
component: string;
|
|
14
16
|
row: A11yFindingRow;
|
|
15
17
|
review?: boolean;
|
|
18
|
+
/** A result to check by eye that someone marked as looking right. */
|
|
19
|
+
confirmed?: boolean;
|
|
16
20
|
/** Leads the text, as the component's name does in a list that spans components. */
|
|
17
21
|
prefix?: string;
|
|
18
22
|
onreveal?: (variable: string) => void;
|
|
19
23
|
/** Opens the component, for a row that traced no property. Absent on the component's own page. */
|
|
20
24
|
onopen?: () => void;
|
|
25
|
+
/** Runs once a press of Looks right has saved, with the row's new state. */
|
|
26
|
+
onconfirmchange?: (confirmed: boolean) => void;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
let { component, row, review = false, prefix, onreveal, onopen }: Props = $props();
|
|
29
|
+
let { component, row, review = false, confirmed = false, prefix, onreveal, onopen, onconfirmchange }: Props = $props();
|
|
24
30
|
|
|
25
31
|
let properties = $derived(row.variables.map((variable) => ({ variable, label: propertyLabel(component, variable) ?? variable })));
|
|
26
32
|
let where = $derived([row.targets.length > 1 ? elementsLabel(row.targets.length) : '', row.views].filter(Boolean).join(', '));
|
|
27
33
|
let helpUrl = $derived(row.finding.details?.helpUrl);
|
|
28
|
-
let
|
|
34
|
+
let hasNavigation = $derived((properties.length > 0 && onreveal !== undefined) || (properties.length === 0 && onopen !== undefined));
|
|
35
|
+
|
|
36
|
+
let saving = $state(false);
|
|
37
|
+
let saveError: string | null = $state(null);
|
|
38
|
+
|
|
39
|
+
async function toggleConfirmed() {
|
|
40
|
+
saving = true;
|
|
41
|
+
saveError = null;
|
|
42
|
+
// Read before the save: the save moves the row to the other list, which unmounts this one.
|
|
43
|
+
const next = !confirmed;
|
|
44
|
+
const done = onconfirmchange;
|
|
45
|
+
try {
|
|
46
|
+
await confirmReview(row.findings, next);
|
|
47
|
+
done?.(next);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
saveError = (err as Error).message;
|
|
50
|
+
} finally {
|
|
51
|
+
saving = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
29
54
|
</script>
|
|
30
55
|
|
|
31
56
|
{#snippet location()}
|
|
32
57
|
{#if row.targets.length === 1}<code>{row.targets[0]}</code>{#if row.views}{`, ${row.views}`}{/if}{:else}{where}{/if}
|
|
58
|
+
{#if saveError}<span class="a11y-confirm-error" role="alert">{saveError}</span>{/if}
|
|
59
|
+
{/snippet}
|
|
60
|
+
|
|
61
|
+
{#snippet confirmToggle()}
|
|
62
|
+
<span class="a11y-confirm">
|
|
63
|
+
<UIPillButton
|
|
64
|
+
size="compact"
|
|
65
|
+
variant="outline"
|
|
66
|
+
icon={confirmed ? 'fa-circle-check' : 'fa-check'}
|
|
67
|
+
ariaPressed={confirmed}
|
|
68
|
+
disabled={saving}
|
|
69
|
+
title={confirmed ? 'Marked as looking right. Press to clear the mark.' : 'Mark this result as looking right'}
|
|
70
|
+
onclick={toggleConfirmed}
|
|
71
|
+
>Looks right</UIPillButton>
|
|
72
|
+
</span>
|
|
33
73
|
{/snippet}
|
|
34
74
|
|
|
35
75
|
{#snippet links()}
|
|
@@ -48,10 +88,32 @@
|
|
|
48
88
|
|
|
49
89
|
<A11yIssueRow
|
|
50
90
|
{review}
|
|
91
|
+
{confirmed}
|
|
51
92
|
href={helpUrl}
|
|
52
93
|
linkLabel={helpUrl ? guidanceLabel(helpUrl) : undefined}
|
|
53
|
-
detail={row.targets.length > 0 || row.views ? location : undefined}
|
|
54
|
-
links={
|
|
94
|
+
detail={row.targets.length > 0 || row.views || saveError ? location : undefined}
|
|
95
|
+
links={hasNavigation ? links : undefined}
|
|
96
|
+
action={review ? confirmToggle : undefined}
|
|
55
97
|
>
|
|
98
|
+
{#if review}<i class="fa-solid {confirmed ? 'fa-check' : 'fa-eye'} a11y-review-mark" aria-hidden="true"></i>{/if}
|
|
56
99
|
{#if prefix}{prefix}: {/if}{findingText(row.finding)}
|
|
57
100
|
</A11yIssueRow>
|
|
101
|
+
|
|
102
|
+
<style>
|
|
103
|
+
.a11y-review-mark {
|
|
104
|
+
margin-right: var(--ui-space-4);
|
|
105
|
+
font-size: var(--ui-font-size-sm);
|
|
106
|
+
color: var(--ui-text-tertiary);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.a11y-confirm-error {
|
|
110
|
+
margin-left: var(--ui-space-8);
|
|
111
|
+
color: var(--ui-highlight);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* One width for both states, so a column of toggles lines up. */
|
|
115
|
+
.a11y-confirm :global(.ui-pill) {
|
|
116
|
+
min-width: 8.5rem;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
interface Props {
|
|
5
5
|
/** A result to check by eye: its text in grey, where an issue's is amber. */
|
|
6
6
|
review?: boolean;
|
|
7
|
+
/** A result to check by eye marked as looking right: its text dimmer still. */
|
|
8
|
+
confirmed?: boolean;
|
|
9
|
+
/** An issue the editor now measures as passing: its text in green. */
|
|
10
|
+
fixed?: boolean;
|
|
7
11
|
href?: string;
|
|
8
12
|
/** Names what `href` points at, such as "Rule" or "Success criterion 1.4.11". */
|
|
9
13
|
linkLabel?: string;
|
|
@@ -11,44 +15,63 @@
|
|
|
11
15
|
detail?: Snippet;
|
|
12
16
|
/** The in-editor links that lead the link group, such as the traced properties. */
|
|
13
17
|
links?: Snippet;
|
|
18
|
+
/** Sits in its own column at the row's right edge, so it keeps its place from row to row. */
|
|
19
|
+
action?: Snippet;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
let { review = false, href, linkLabel = 'Guidance', children, detail, links }: Props = $props();
|
|
22
|
+
let { review = false, confirmed = false, fixed = false, href, linkLabel = 'Guidance', children, detail, links, action }: Props = $props();
|
|
17
23
|
</script>
|
|
18
24
|
|
|
19
|
-
<li class="a11y-issue" class:review>
|
|
20
|
-
<div class="a11y-issue-
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
<li class="a11y-issue" class:review class:confirmed class:fixed>
|
|
26
|
+
<div class="a11y-issue-body">
|
|
27
|
+
<div class="a11y-issue-head">
|
|
28
|
+
<p class="a11y-issue-text">{@render children()}</p>
|
|
29
|
+
{#if links || href}
|
|
30
|
+
<div class="a11y-issue-links">
|
|
31
|
+
{@render links?.()}
|
|
32
|
+
{#if href}
|
|
33
|
+
<a
|
|
34
|
+
class="a11y-link a11y-issue-guidance"
|
|
35
|
+
{href}
|
|
36
|
+
target="_blank"
|
|
37
|
+
rel="noopener"
|
|
38
|
+
title={href}
|
|
39
|
+
aria-label={`${linkLabel} (opens in a new tab)`}
|
|
40
|
+
>
|
|
41
|
+
{linkLabel}
|
|
42
|
+
<i class="fas fa-arrow-up-right-from-square" aria-hidden="true"></i>
|
|
43
|
+
</a>
|
|
44
|
+
{/if}
|
|
45
|
+
</div>
|
|
46
|
+
{/if}
|
|
47
|
+
</div>
|
|
48
|
+
{#if detail}
|
|
49
|
+
<p class="a11y-issue-detail">{@render detail()}</p>
|
|
39
50
|
{/if}
|
|
40
51
|
</div>
|
|
41
|
-
{#if
|
|
42
|
-
<
|
|
52
|
+
{#if action}
|
|
53
|
+
<div class="a11y-issue-action">{@render action()}</div>
|
|
43
54
|
{/if}
|
|
44
55
|
</li>
|
|
45
56
|
|
|
46
57
|
<style>
|
|
47
58
|
.a11y-issue {
|
|
48
59
|
display: flex;
|
|
60
|
+
gap: var(--ui-space-16);
|
|
61
|
+
padding: var(--ui-space-12) 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.a11y-issue-body {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex: 1;
|
|
49
67
|
flex-direction: column;
|
|
50
68
|
gap: var(--ui-space-4);
|
|
51
|
-
|
|
69
|
+
min-width: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.a11y-issue-action {
|
|
73
|
+
flex: none;
|
|
74
|
+
align-self: flex-start;
|
|
52
75
|
}
|
|
53
76
|
|
|
54
77
|
.a11y-issue + :global(.a11y-issue) {
|
|
@@ -79,6 +102,14 @@
|
|
|
79
102
|
color: var(--ui-text-secondary);
|
|
80
103
|
}
|
|
81
104
|
|
|
105
|
+
.confirmed .a11y-issue-text {
|
|
106
|
+
color: var(--ui-text-tertiary);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.fixed .a11y-issue-text {
|
|
110
|
+
color: var(--ui-fixed);
|
|
111
|
+
}
|
|
112
|
+
|
|
82
113
|
.a11y-issue-detail {
|
|
83
114
|
font-size: var(--ui-font-size-sm);
|
|
84
115
|
color: var(--ui-text-tertiary);
|