@setsquare/ui 0.4.0 → 0.6.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 +129 -0
- package/README.md +41 -3
- package/dist/index.d.ts +15 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/english.d.ts +11 -0
- package/dist/lib/english.d.ts.map +1 -0
- package/dist/lib/english.js +65 -0
- package/dist/lib/english.js.map +1 -0
- package/dist/lib/labels.d.ts +38 -8
- package/dist/lib/labels.d.ts.map +1 -1
- package/dist/lib/labels.js +97 -9
- package/dist/lib/labels.js.map +1 -1
- package/dist/lib/locale.d.ts +105 -0
- package/dist/lib/locale.d.ts.map +1 -0
- package/dist/lib/locale.js +42 -0
- package/dist/lib/locale.js.map +1 -0
- package/dist/lint/index.d.ts +5 -0
- package/dist/lint/index.d.ts.map +1 -1
- package/dist/lint/index.js +80 -3
- package/dist/lint/index.js.map +1 -1
- package/dist/shell/dashboard-shell.d.ts +4 -1
- package/dist/shell/dashboard-shell.d.ts.map +1 -1
- package/dist/shell/dashboard-shell.js +3 -2
- package/dist/shell/dashboard-shell.js.map +1 -1
- package/dist/shell/landmarks.d.ts +33 -0
- package/dist/shell/landmarks.d.ts.map +1 -0
- package/dist/shell/landmarks.js +20 -0
- package/dist/shell/landmarks.js.map +1 -0
- package/dist/shell/right-panel.d.ts +7 -2
- package/dist/shell/right-panel.d.ts.map +1 -1
- package/dist/shell/right-panel.js +13 -7
- package/dist/shell/right-panel.js.map +1 -1
- package/dist/shell/sidebar.d.ts +10 -2
- package/dist/shell/sidebar.d.ts.map +1 -1
- package/dist/shell/sidebar.js +31 -14
- package/dist/shell/sidebar.js.map +1 -1
- package/dist/shell/site-menu.d.ts +11 -3
- package/dist/shell/site-menu.d.ts.map +1 -1
- package/dist/shell/site-menu.js +7 -1
- package/dist/shell/site-menu.js.map +1 -1
- package/dist/shell/site-nav.d.ts +4 -1
- package/dist/shell/site-nav.d.ts.map +1 -1
- package/dist/shell/site-nav.js +3 -3
- package/dist/shell/site-nav.js.map +1 -1
- package/dist/shell/site-shell.d.ts +4 -2
- package/dist/shell/site-shell.d.ts.map +1 -1
- package/dist/shell/site-shell.js +3 -2
- package/dist/shell/site-shell.js.map +1 -1
- package/dist/styles/theme.css +2 -0
- package/dist/styles/tokens.css +9 -0
- package/dist/testing/index.d.ts +54 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +246 -9
- package/dist/testing/index.js.map +1 -1
- package/dist/tokens.json +25 -0
- package/dist/ui/alert.d.ts +2 -0
- package/dist/ui/alert.d.ts.map +1 -1
- package/dist/ui/alert.js +11 -9
- package/dist/ui/alert.js.map +1 -1
- package/dist/ui/breadcrumbs.d.ts +17 -1
- package/dist/ui/breadcrumbs.d.ts.map +1 -1
- package/dist/ui/breadcrumbs.js +6 -3
- package/dist/ui/breadcrumbs.js.map +1 -1
- package/dist/ui/button-styles.d.ts.map +1 -1
- package/dist/ui/button-styles.js +16 -6
- package/dist/ui/button-styles.js.map +1 -1
- package/dist/ui/calendar.d.ts +1 -0
- package/dist/ui/calendar.d.ts.map +1 -1
- package/dist/ui/calendar.js +7 -1
- package/dist/ui/calendar.js.map +1 -1
- package/dist/ui/checkbox.d.ts.map +1 -1
- package/dist/ui/checkbox.js +7 -7
- package/dist/ui/checkbox.js.map +1 -1
- package/dist/ui/code-block-parts.d.ts +30 -0
- package/dist/ui/code-block-parts.d.ts.map +1 -0
- package/dist/ui/code-block-parts.js +27 -0
- package/dist/ui/code-block-parts.js.map +1 -0
- package/dist/ui/code-block.d.ts +4 -10
- package/dist/ui/code-block.d.ts.map +1 -1
- package/dist/ui/code-block.js +4 -9
- package/dist/ui/code-block.js.map +1 -1
- package/dist/ui/combo-box.d.ts +13 -2
- package/dist/ui/combo-box.d.ts.map +1 -1
- package/dist/ui/combo-box.js +10 -3
- package/dist/ui/combo-box.js.map +1 -1
- package/dist/ui/copy-button.d.ts +7 -1
- package/dist/ui/copy-button.d.ts.map +1 -1
- package/dist/ui/copy-button.js +11 -10
- package/dist/ui/copy-button.js.map +1 -1
- package/dist/ui/date-field.d.ts +1 -0
- package/dist/ui/date-field.d.ts.map +1 -1
- package/dist/ui/date-field.js +20 -16
- package/dist/ui/date-field.js.map +1 -1
- package/dist/ui/dialog.d.ts +6 -1
- package/dist/ui/dialog.d.ts.map +1 -1
- package/dist/ui/dialog.js +11 -8
- package/dist/ui/dialog.js.map +1 -1
- package/dist/ui/drop-indicator.d.ts +21 -0
- package/dist/ui/drop-indicator.d.ts.map +1 -0
- package/dist/ui/drop-indicator.js +20 -0
- package/dist/ui/drop-indicator.js.map +1 -0
- package/dist/ui/error-state.d.ts +17 -2
- package/dist/ui/error-state.d.ts.map +1 -1
- package/dist/ui/error-state.js +32 -34
- package/dist/ui/error-state.js.map +1 -1
- package/dist/ui/feature-card.d.ts +18 -3
- package/dist/ui/feature-card.d.ts.map +1 -1
- package/dist/ui/feature-card.js +18 -3
- package/dist/ui/feature-card.js.map +1 -1
- package/dist/ui/field.d.ts +28 -9
- package/dist/ui/field.d.ts.map +1 -1
- package/dist/ui/field.js +22 -10
- package/dist/ui/field.js.map +1 -1
- package/dist/ui/file-upload.d.ts +23 -5
- package/dist/ui/file-upload.d.ts.map +1 -1
- package/dist/ui/file-upload.js +54 -36
- package/dist/ui/file-upload.js.map +1 -1
- package/dist/ui/form.d.ts +11 -1
- package/dist/ui/form.d.ts.map +1 -1
- package/dist/ui/form.js +7 -4
- package/dist/ui/form.js.map +1 -1
- package/dist/ui/grid-list.d.ts +10 -3
- package/dist/ui/grid-list.d.ts.map +1 -1
- package/dist/ui/grid-list.js +18 -7
- package/dist/ui/grid-list.js.map +1 -1
- package/dist/ui/number-field.d.ts.map +1 -1
- package/dist/ui/number-field.js +5 -3
- package/dist/ui/number-field.js.map +1 -1
- package/dist/ui/pagination.d.ts +11 -3
- package/dist/ui/pagination.d.ts.map +1 -1
- package/dist/ui/pagination.js +30 -23
- package/dist/ui/pagination.js.map +1 -1
- package/dist/ui/radio-group.d.ts.map +1 -1
- package/dist/ui/radio-group.js +4 -4
- package/dist/ui/radio-group.js.map +1 -1
- package/dist/ui/search.d.ts +3 -0
- package/dist/ui/search.d.ts.map +1 -1
- package/dist/ui/search.js +11 -10
- package/dist/ui/search.js.map +1 -1
- package/dist/ui/select.d.ts +7 -1
- package/dist/ui/select.d.ts.map +1 -1
- package/dist/ui/select.js +7 -3
- package/dist/ui/select.js.map +1 -1
- package/dist/ui/selection-bar.d.ts +17 -4
- package/dist/ui/selection-bar.d.ts.map +1 -1
- package/dist/ui/selection-bar.js +15 -11
- package/dist/ui/selection-bar.js.map +1 -1
- package/dist/ui/sheet.d.ts +1 -0
- package/dist/ui/sheet.d.ts.map +1 -1
- package/dist/ui/sheet.js +4 -1
- package/dist/ui/sheet.js.map +1 -1
- package/dist/ui/slider.d.ts +9 -2
- package/dist/ui/slider.d.ts.map +1 -1
- package/dist/ui/slider.js +13 -5
- package/dist/ui/slider.js.map +1 -1
- package/dist/ui/switch.d.ts +2 -1
- package/dist/ui/switch.d.ts.map +1 -1
- package/dist/ui/switch.js +5 -2
- package/dist/ui/switch.js.map +1 -1
- package/dist/ui/tabs.d.ts +1 -0
- package/dist/ui/tabs.d.ts.map +1 -1
- package/dist/ui/tabs.js +13 -12
- package/dist/ui/tabs.js.map +1 -1
- package/dist/ui/text-field.d.ts.map +1 -1
- package/dist/ui/text-field.js +7 -3
- package/dist/ui/text-field.js.map +1 -1
- package/dist/ui/theme-switcher.d.ts +1 -0
- package/dist/ui/theme-switcher.d.ts.map +1 -1
- package/dist/ui/theme-switcher.js +7 -7
- package/dist/ui/theme-switcher.js.map +1 -1
- package/dist/ui/toast.d.ts +1 -0
- package/dist/ui/toast.d.ts.map +1 -1
- package/dist/ui/toast.js +4 -1
- package/dist/ui/toast.js.map +1 -1
- package/dist/ui/toolbar.d.ts +22 -0
- package/dist/ui/toolbar.d.ts.map +1 -0
- package/dist/ui/toolbar.js +22 -0
- package/dist/ui/toolbar.js.map +1 -0
- package/dist/ui/tree.d.ts +57 -12
- package/dist/ui/tree.d.ts.map +1 -1
- package/dist/ui/tree.js +55 -16
- package/dist/ui/tree.js.map +1 -1
- package/package.json +4 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"site-shell.js","sourceRoot":"","sources":["../../src/shell/site-shell.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"site-shell.js","sourceRoot":"","sources":["../../src/shell/site-shell.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAoB,MAAM,gBAAgB,CAAC;AAY5D;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,EACxB,QAAQ,EACR,aAAa,EACb,MAAM,EACN,MAAM,GAAG,MAAM,EACf,SAAS,GACM;IACf,OAAO,CACL,kCACkB,EAAE,EAClB,SAAS,EAAE,EAAE,CAAC,gEAAgE,EAAE,SAAS,CAAC,aAE1F,KAAC,QAAQ,IAAC,IAAI,EAAE,IAAI,MAAM,EAAE,EAAE,KAAK,EAAC,WAAW,EAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAI,EACvF,QAAQ,IACL,CACP,CAAC;AACJ,CAAC;AAQD;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAE,QAAQ,EAAE,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,EAAiB;IACpF,OAAO,CACL,kBACM,KAAK,EACT,EAAE,EAAE,EAAE,EACN,QAAQ,EAAE,CAAC,CAAC,EACZ,SAAS,EAAE,EAAE,CAAC,8BAA8B,EAAE,SAAS,CAAC,YAEvD,QAAQ,GACJ,CACR,CAAC;AACJ,CAAC"}
|
package/dist/styles/theme.css
CHANGED
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
--color-ring: var(--ring);
|
|
41
41
|
--color-edge: var(--edge);
|
|
42
42
|
--color-edge-selected: var(--edge-selected);
|
|
43
|
+
--color-canvas-mark: var(--canvas-mark);
|
|
44
|
+
--color-canvas-mark-halo: var(--canvas-mark-halo);
|
|
43
45
|
--color-destructive: var(--destructive);
|
|
44
46
|
--color-destructive-foreground: var(--destructive-foreground);
|
|
45
47
|
--color-destructive-subtle: var(--destructive-subtle);
|
package/dist/styles/tokens.css
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
--sidebar-width-aaa: 256px;
|
|
55
55
|
--sidebar-width-collapsed-aa: 56px;
|
|
56
56
|
--sidebar-width-collapsed-aaa: 72px;
|
|
57
|
+
--tree-indent: 16px;
|
|
57
58
|
--rail-width-aa: 72px;
|
|
58
59
|
--rail-width-aaa: 88px;
|
|
59
60
|
--subnav-width-aa: 224px;
|
|
@@ -154,6 +155,8 @@
|
|
|
154
155
|
--ring: var(--brand);
|
|
155
156
|
--edge: var(--input);
|
|
156
157
|
--edge-selected: var(--brand);
|
|
158
|
+
--canvas-mark: #22369f;
|
|
159
|
+
--canvas-mark-halo: #ffffff;
|
|
157
160
|
--destructive: #bf2a2a;
|
|
158
161
|
--destructive-foreground: #ffffff;
|
|
159
162
|
--destructive-subtle: #fdecec;
|
|
@@ -197,6 +200,8 @@
|
|
|
197
200
|
--ring: var(--brand);
|
|
198
201
|
--edge: var(--input);
|
|
199
202
|
--edge-selected: var(--brand);
|
|
203
|
+
--canvas-mark: #22369f;
|
|
204
|
+
--canvas-mark-halo: #ffffff;
|
|
200
205
|
--destructive: #f47a70;
|
|
201
206
|
--destructive-foreground: #0e1116;
|
|
202
207
|
--destructive-subtle: #3a1616;
|
|
@@ -240,6 +245,8 @@
|
|
|
240
245
|
--ring: var(--brand);
|
|
241
246
|
--edge: var(--input);
|
|
242
247
|
--edge-selected: var(--brand);
|
|
248
|
+
--canvas-mark: #22369f;
|
|
249
|
+
--canvas-mark-halo: #ffffff;
|
|
243
250
|
--destructive: #8c1818;
|
|
244
251
|
--destructive-foreground: #ffffff;
|
|
245
252
|
--destructive-subtle: #fbe7e7;
|
|
@@ -283,6 +290,8 @@
|
|
|
283
290
|
--ring: var(--brand);
|
|
284
291
|
--edge: var(--input);
|
|
285
292
|
--edge-selected: var(--brand);
|
|
293
|
+
--canvas-mark: #22369f;
|
|
294
|
+
--canvas-mark-halo: #ffffff;
|
|
286
295
|
--destructive: #ffa39b;
|
|
287
296
|
--destructive-foreground: #07090c;
|
|
288
297
|
--destructive-subtle: #3a1313;
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -126,6 +126,7 @@ export interface ScreenshotPage extends Evaluates {
|
|
|
126
126
|
};
|
|
127
127
|
animations?: 'disabled' | 'allow';
|
|
128
128
|
caret?: 'hide' | 'initial';
|
|
129
|
+
scale?: 'css' | 'device';
|
|
129
130
|
}): Promise<Uint8Array>;
|
|
130
131
|
}
|
|
131
132
|
export interface InvisibleFocus {
|
|
@@ -157,6 +158,59 @@ export interface FocusVisibleOptions {
|
|
|
157
158
|
export declare function findInvisibleFocus(page: ScreenshotPage, options?: FocusVisibleOptions): Promise<InvisibleFocus[]>;
|
|
158
159
|
/** Throws, listing every control whose focus cannot be seen, unless there are none. `trapped` ones are skipped. */
|
|
159
160
|
export declare function expectFocusVisible(page: ScreenshotPage, options?: FocusVisibleOptions): Promise<void>;
|
|
161
|
+
export interface WeakFocus {
|
|
162
|
+
element: string;
|
|
163
|
+
/** CSS px² of the indicator whose colour changed by at least 3:1 when the control took focus. */
|
|
164
|
+
area: number;
|
|
165
|
+
/**
|
|
166
|
+
* CSS px² the indicator needs: a 2 CSS px thick perimeter of the unfocused component, 4h + 4w − (16 − 4π)r for a
|
|
167
|
+
* rectangle with corner radius r (W3C's Understanding 2.4.13, read on 9 October 2026).
|
|
168
|
+
*/
|
|
169
|
+
needed: number;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Focus appearance (WCAG 2.4.13, AAA): for every control a keyboard can reach, the pixels that change colour by at
|
|
173
|
+
* least 3:1 when it takes focus must cover at least the area of a 2 CSS px thick perimeter of the unfocused
|
|
174
|
+
* component: 4h + 4w − (16 − 4π)r CSS px² for a rectangle with corner radius r (W3C's formulas, read on 9 October
|
|
175
|
+
* 2026). The component is its visible presentation, which W3C gauges by the indicator itself: the part whose outline
|
|
176
|
+
* or box shadow changes on focus (a three-layer control's paint), or the control when none does. Screenshots are
|
|
177
|
+
* taken at CSS scale, with
|
|
178
|
+
* focus away and then on the control, as `findInvisibleFocus` takes them. Controls the browser draws the indicator
|
|
179
|
+
* for and the author hasn't touched are exempt in WCAG, and aren't told apart here.
|
|
180
|
+
*/
|
|
181
|
+
export declare function findWeakFocusIndicators(page: ScreenshotPage, options?: FocusVisibleOptions): Promise<WeakFocus[]>;
|
|
182
|
+
/** Throws, listing every control whose focus indicator is too small or too faint for 2.4.13, unless there are none. */
|
|
183
|
+
export declare function expectFocusAppearance(page: ScreenshotPage, options?: FocusVisibleOptions): Promise<void>;
|
|
184
|
+
export interface ObscuredFocus {
|
|
185
|
+
element: string;
|
|
186
|
+
/** How much of it other content covered, of the points sampled across it: 1 is all of it. */
|
|
187
|
+
covered: number;
|
|
188
|
+
}
|
|
189
|
+
export interface FocusObscuredOptions {
|
|
190
|
+
/** `aa`: fails a control that's entirely hidden (2.4.11). `aaa`: fails one that's hidden at all (2.4.12). */
|
|
191
|
+
level?: 'aa' | 'aaa';
|
|
192
|
+
/** How many Tab presses to follow. Default 80. */
|
|
193
|
+
stops?: number;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Focus not obscured (WCAG 2.4.11 at AA, 2.4.12 at AAA): presses Tab, and at each stop samples a grid of points
|
|
197
|
+
* across the focused control. A point is covered when what's on top there is neither the control, something inside
|
|
198
|
+
* it, nor something it's inside. At AA a control fails when every point is covered; at AAA when any is. A visually
|
|
199
|
+
* hidden input is measured by the control around it, and only the part of a control that's in view is sampled:
|
|
200
|
+
* what the window or a scroll container cuts off isn't covered by other content. Elements with
|
|
201
|
+
* `pointer-events: none` can't be seen by the sampling, so a covering layer that lets clicks through goes unnoticed.
|
|
202
|
+
*/
|
|
203
|
+
export declare function findObscuredFocus(page: Evaluates & {
|
|
204
|
+
keyboard: {
|
|
205
|
+
press(key: string): Promise<void>;
|
|
206
|
+
};
|
|
207
|
+
}, options?: FocusObscuredOptions): Promise<ObscuredFocus[]>;
|
|
208
|
+
/** Throws, listing every tab stop whose focused control is hidden by other content, unless there are none. */
|
|
209
|
+
export declare function expectFocusNotObscured(page: Evaluates & {
|
|
210
|
+
keyboard: {
|
|
211
|
+
press(key: string): Promise<void>;
|
|
212
|
+
};
|
|
213
|
+
}, options?: FocusObscuredOptions): Promise<void>;
|
|
160
214
|
export interface LinkFinding {
|
|
161
215
|
/** The link, described as `tag "text"`. */
|
|
162
216
|
element: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"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"}
|
package/dist/testing/index.js
CHANGED
|
@@ -404,6 +404,21 @@ export async function expectTargetsSized(page, options = {}) {
|
|
|
404
404
|
* This is the check that finds a ring styled on an element that never receives focus.
|
|
405
405
|
*/
|
|
406
406
|
export async function findInvisibleFocus(page, options = {}) {
|
|
407
|
+
const out = [];
|
|
408
|
+
await probeFocus(page, options, async ({ box, away, here }) => {
|
|
409
|
+
if (!box || !away || !here)
|
|
410
|
+
return;
|
|
411
|
+
const same = away.length === here.length && away.every((byte, k) => byte === here[k]);
|
|
412
|
+
if (same)
|
|
413
|
+
out.push({ element: box.described, reason: 'unchanged' });
|
|
414
|
+
}, (finding) => out.push(finding));
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Focuses every control a keyboard can reach, one at a time, with focus moved away first, and hands `compare` a
|
|
419
|
+
* screenshot of it (and `pad` px around it) without focus and with it. Shared by the focus checks.
|
|
420
|
+
*/
|
|
421
|
+
async function probeFocus(page, options, compare, skipped) {
|
|
407
422
|
const pad = options.pad ?? 8;
|
|
408
423
|
const count = await page.evaluate((ignore) => {
|
|
409
424
|
const style = document.createElement('style');
|
|
@@ -436,7 +451,6 @@ export async function findInvisibleFocus(page, options = {}) {
|
|
|
436
451
|
// Keyboard modality, for React Aria's data-focus-visible and the browser's :focus-visible. (React Aria
|
|
437
452
|
// ignores Shift and the other modifiers, so it has to be a real key.)
|
|
438
453
|
await page.keyboard.press('Tab');
|
|
439
|
-
const out = [];
|
|
440
454
|
for (let i = 0; i < count; i++) {
|
|
441
455
|
// 1. Put focus on another control, far from this one, and measure this one where it now sits.
|
|
442
456
|
const box = await page.evaluate(async (args) => {
|
|
@@ -461,9 +475,38 @@ export async function findInvisibleFocus(page, options = {}) {
|
|
|
461
475
|
shown = p;
|
|
462
476
|
break;
|
|
463
477
|
}
|
|
478
|
+
// The component, as 2.4.13 measures it: its visible presentation, which the focus indicator follows (W3C's
|
|
479
|
+
// Understanding page uses the indicator itself as the gauge). That's the first part, the control or something
|
|
480
|
+
// inside it, whose outline or box shadow changes when it takes focus; the control itself if none does.
|
|
481
|
+
const parts = [shown, ...shown.querySelectorAll('*')].slice(0, 60);
|
|
482
|
+
// Unfocused first: the first control has focus from the Tab that set keyboard modality.
|
|
483
|
+
if (shown.contains(document.activeElement)) {
|
|
484
|
+
document.activeElement.blur();
|
|
485
|
+
await frames();
|
|
486
|
+
}
|
|
487
|
+
const ring = (e) => [undefined, '::before', '::after'].map((pseudo) => {
|
|
488
|
+
const st = getComputedStyle(e, pseudo);
|
|
489
|
+
// An outline that isn't drawn has a width and colour that mean nothing.
|
|
490
|
+
const outline = st.outlineStyle === 'none'
|
|
491
|
+
? 'none'
|
|
492
|
+
: `${st.outlineStyle} ${st.outlineWidth} ${st.outlineColor}`;
|
|
493
|
+
return `${outline}|${st.boxShadow}`;
|
|
494
|
+
});
|
|
495
|
+
const unfocused = parts.map(ring);
|
|
464
496
|
// Where it is when focused (a skip link only appears then) and when not: compare the union of both.
|
|
465
497
|
el.focus({ preventScroll: true });
|
|
466
498
|
await frames();
|
|
499
|
+
let bearer = shown;
|
|
500
|
+
let bearerPseudo;
|
|
501
|
+
find: for (const [k, part] of parts.entries()) {
|
|
502
|
+
const now = ring(part);
|
|
503
|
+
for (const [j, pseudo] of [undefined, '::before', '::after'].entries())
|
|
504
|
+
if (now[j] !== unfocused[k][j]) {
|
|
505
|
+
bearer = part;
|
|
506
|
+
bearerPseudo = pseudo;
|
|
507
|
+
break find;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
467
510
|
const r1 = shown.getBoundingClientRect();
|
|
468
511
|
const clip = {
|
|
469
512
|
left: r1.left - args.pad,
|
|
@@ -543,20 +586,41 @@ export async function findInvisibleFocus(page, options = {}) {
|
|
|
543
586
|
height,
|
|
544
587
|
trapped: !away,
|
|
545
588
|
described: `${el.tagName.toLowerCase()}${role ? `[role=${role}]` : ''}${name ? ` "${name}"` : ''}`,
|
|
589
|
+
own: (() => {
|
|
590
|
+
const b = bearer.getBoundingClientRect();
|
|
591
|
+
const st = getComputedStyle(bearer, bearerPseudo);
|
|
592
|
+
const corners = [
|
|
593
|
+
st.borderTopLeftRadius,
|
|
594
|
+
st.borderTopRightRadius,
|
|
595
|
+
st.borderBottomLeftRadius,
|
|
596
|
+
st.borderBottomRightRadius,
|
|
597
|
+
].map((v) => parseFloat(v) || 0);
|
|
598
|
+
return {
|
|
599
|
+
width: b.width,
|
|
600
|
+
height: b.height,
|
|
601
|
+
radius: Math.min(Math.min(...corners), b.width / 2, b.height / 2),
|
|
602
|
+
};
|
|
603
|
+
})(),
|
|
546
604
|
};
|
|
547
605
|
}, { i, pad });
|
|
548
606
|
if (!box)
|
|
549
607
|
continue;
|
|
550
608
|
if (box.trapped) {
|
|
551
|
-
|
|
609
|
+
skipped({ element: box.described, reason: 'trapped' });
|
|
552
610
|
continue;
|
|
553
611
|
}
|
|
554
612
|
if (box.width < 1 || box.height < 1) {
|
|
555
|
-
|
|
613
|
+
skipped({ element: box.described, reason: 'offscreen' });
|
|
556
614
|
continue;
|
|
557
615
|
}
|
|
558
616
|
const clip = { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
559
|
-
const
|
|
617
|
+
const shoot = () => page.screenshot({
|
|
618
|
+
clip,
|
|
619
|
+
animations: 'disabled',
|
|
620
|
+
caret: 'hide',
|
|
621
|
+
...(options.scale ? { scale: options.scale } : {}),
|
|
622
|
+
});
|
|
623
|
+
const away = await shoot();
|
|
560
624
|
// 2. Focus this one. If it hands focus to something inside it (a grid to its first row), that element is
|
|
561
625
|
// probed on its own turn.
|
|
562
626
|
const kept = await page.evaluate(async (n) => {
|
|
@@ -569,10 +633,8 @@ export async function findInvisibleFocus(page, options = {}) {
|
|
|
569
633
|
}, i);
|
|
570
634
|
if (!kept)
|
|
571
635
|
continue;
|
|
572
|
-
const here = await
|
|
573
|
-
|
|
574
|
-
if (same)
|
|
575
|
-
out.push({ element: box.described, reason: 'unchanged' });
|
|
636
|
+
const here = await shoot();
|
|
637
|
+
await compare({ box, away, here });
|
|
576
638
|
}
|
|
577
639
|
await page.evaluate(() => {
|
|
578
640
|
document.querySelector('style[data-setsquare-focus-probe-style]')?.remove();
|
|
@@ -581,7 +643,6 @@ export async function findInvisibleFocus(page, options = {}) {
|
|
|
581
643
|
el.removeAttribute('data-setsquare-focus-skip');
|
|
582
644
|
}
|
|
583
645
|
}, undefined);
|
|
584
|
-
return out;
|
|
585
646
|
}
|
|
586
647
|
/** Throws, listing every control whose focus cannot be seen, unless there are none. `trapped` ones are skipped. */
|
|
587
648
|
export async function expectFocusVisible(page, options = {}) {
|
|
@@ -595,6 +656,182 @@ export async function expectFocusVisible(page, options = {}) {
|
|
|
595
656
|
: ` ${f.element}: took focus but could not be seen`),
|
|
596
657
|
].join('\n'));
|
|
597
658
|
}
|
|
659
|
+
/** A PNG's pixels, decoded by the page itself (no image library): RGBA, row by row. */
|
|
660
|
+
const decodeInPage = (page, shots) => {
|
|
661
|
+
const base64 = (bytes) => {
|
|
662
|
+
let text = '';
|
|
663
|
+
for (let i = 0; i < bytes.length; i += 0x8000)
|
|
664
|
+
text += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
665
|
+
return btoa(text);
|
|
666
|
+
};
|
|
667
|
+
return page.evaluate(async (pngs) => {
|
|
668
|
+
const decode = async (b64) => {
|
|
669
|
+
const blob = await (await fetch(`data:image/png;base64,${b64}`)).blob();
|
|
670
|
+
const bitmap = await createImageBitmap(blob);
|
|
671
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
672
|
+
const ctx = canvas.getContext('2d');
|
|
673
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
674
|
+
return {
|
|
675
|
+
width: bitmap.width,
|
|
676
|
+
height: bitmap.height,
|
|
677
|
+
data: [...ctx.getImageData(0, 0, bitmap.width, bitmap.height).data],
|
|
678
|
+
};
|
|
679
|
+
};
|
|
680
|
+
const [a, b] = await Promise.all(pngs.map(decode));
|
|
681
|
+
// Count the pixels whose colour changed by at least 3:1 between the two states.
|
|
682
|
+
const lum = (r, g, bl) => {
|
|
683
|
+
const c = [r, g, bl].map((v) => {
|
|
684
|
+
const s = v / 255;
|
|
685
|
+
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
686
|
+
});
|
|
687
|
+
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
|
|
688
|
+
};
|
|
689
|
+
if (!a || !b || a.width !== b.width || a.height !== b.height)
|
|
690
|
+
return 0;
|
|
691
|
+
let changed = 0;
|
|
692
|
+
for (let k = 0; k < a.data.length; k += 4) {
|
|
693
|
+
const la = lum(a.data[k], a.data[k + 1], a.data[k + 2]);
|
|
694
|
+
const lb = lum(b.data[k], b.data[k + 1], b.data[k + 2]);
|
|
695
|
+
const [hi, lo] = la > lb ? [la, lb] : [lb, la];
|
|
696
|
+
if ((hi + 0.05) / (lo + 0.05) >= 3)
|
|
697
|
+
changed++;
|
|
698
|
+
}
|
|
699
|
+
return changed;
|
|
700
|
+
}, shots.map(base64));
|
|
701
|
+
};
|
|
702
|
+
/**
|
|
703
|
+
* Focus appearance (WCAG 2.4.13, AAA): for every control a keyboard can reach, the pixels that change colour by at
|
|
704
|
+
* least 3:1 when it takes focus must cover at least the area of a 2 CSS px thick perimeter of the unfocused
|
|
705
|
+
* component: 4h + 4w − (16 − 4π)r CSS px² for a rectangle with corner radius r (W3C's formulas, read on 9 October
|
|
706
|
+
* 2026). The component is its visible presentation, which W3C gauges by the indicator itself: the part whose outline
|
|
707
|
+
* or box shadow changes on focus (a three-layer control's paint), or the control when none does. Screenshots are
|
|
708
|
+
* taken at CSS scale, with
|
|
709
|
+
* focus away and then on the control, as `findInvisibleFocus` takes them. Controls the browser draws the indicator
|
|
710
|
+
* for and the author hasn't touched are exempt in WCAG, and aren't told apart here.
|
|
711
|
+
*/
|
|
712
|
+
export async function findWeakFocusIndicators(page, options = {}) {
|
|
713
|
+
const out = [];
|
|
714
|
+
await probeFocus(page, { ...options, scale: 'css' }, async ({ box, away, here }) => {
|
|
715
|
+
// W3C's area for a 2 CSS px perimeter of a rounded rectangle: 4h + 4w − (16 − 4π)r (a rectangle when r is 0).
|
|
716
|
+
const { width: w, height: h, radius: r } = box.own;
|
|
717
|
+
const needed = Math.round(4 * (w + h) - (16 - 4 * Math.PI) * r);
|
|
718
|
+
const area = await decodeInPage(page, [away, here]);
|
|
719
|
+
if (area < needed)
|
|
720
|
+
out.push({ element: box.described, area, needed });
|
|
721
|
+
}, () => { });
|
|
722
|
+
return out;
|
|
723
|
+
}
|
|
724
|
+
/** Throws, listing every control whose focus indicator is too small or too faint for 2.4.13, unless there are none. */
|
|
725
|
+
export async function expectFocusAppearance(page, options = {}) {
|
|
726
|
+
const found = await findWeakFocusIndicators(page, options);
|
|
727
|
+
if (!found.length)
|
|
728
|
+
return;
|
|
729
|
+
throw new Error([
|
|
730
|
+
`The focus indicator is too small or too faint on ${found.length} control${found.length === 1 ? '' : 's'} (2.4.13):`,
|
|
731
|
+
...found.map((f) => ` ${f.element}: ${f.area} CSS px² changed by 3:1, ${f.needed} needed`),
|
|
732
|
+
].join('\n'));
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Focus not obscured (WCAG 2.4.11 at AA, 2.4.12 at AAA): presses Tab, and at each stop samples a grid of points
|
|
736
|
+
* across the focused control. A point is covered when what's on top there is neither the control, something inside
|
|
737
|
+
* it, nor something it's inside. At AA a control fails when every point is covered; at AAA when any is. A visually
|
|
738
|
+
* hidden input is measured by the control around it, and only the part of a control that's in view is sampled:
|
|
739
|
+
* what the window or a scroll container cuts off isn't covered by other content. Elements with
|
|
740
|
+
* `pointer-events: none` can't be seen by the sampling, so a covering layer that lets clicks through goes unnoticed.
|
|
741
|
+
*/
|
|
742
|
+
export async function findObscuredFocus(page, options = {}) {
|
|
743
|
+
const level = options.level ?? 'aa';
|
|
744
|
+
const out = [];
|
|
745
|
+
const seen = new Set();
|
|
746
|
+
for (let i = 0; i < (options.stops ?? 80); i++) {
|
|
747
|
+
await page.keyboard.press('Tab');
|
|
748
|
+
const found = await page.evaluate(async () => {
|
|
749
|
+
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
750
|
+
let el = document.activeElement;
|
|
751
|
+
// Into a same-origin frame: the frame's own focused element is what's focused.
|
|
752
|
+
while (el?.tagName === 'IFRAME') {
|
|
753
|
+
const inner = el.contentDocument
|
|
754
|
+
?.activeElement;
|
|
755
|
+
if (!inner || inner === inner.ownerDocument.body)
|
|
756
|
+
break;
|
|
757
|
+
el = inner;
|
|
758
|
+
}
|
|
759
|
+
if (!el || el === document.body)
|
|
760
|
+
return null;
|
|
761
|
+
const doc = el.ownerDocument;
|
|
762
|
+
// A visually hidden input (React Aria's switch, checkbox, radio) stands for the control around it.
|
|
763
|
+
const tiny = (e) => {
|
|
764
|
+
const b = e.getBoundingClientRect();
|
|
765
|
+
return b.width <= 2 || b.height <= 2;
|
|
766
|
+
};
|
|
767
|
+
let shown = el;
|
|
768
|
+
while (tiny(shown) && shown.parentElement)
|
|
769
|
+
shown = shown.parentElement;
|
|
770
|
+
// Only the part that's in view: what a scroll container or the window cuts off isn't covered by anything,
|
|
771
|
+
// and a control larger than its scroller can't be shown whole.
|
|
772
|
+
const r0 = shown.getBoundingClientRect();
|
|
773
|
+
const r = { left: r0.left, top: r0.top, right: r0.right, bottom: r0.bottom };
|
|
774
|
+
const view = doc.defaultView;
|
|
775
|
+
r.left = Math.max(r.left, 0);
|
|
776
|
+
r.top = Math.max(r.top, 0);
|
|
777
|
+
r.right = Math.min(r.right, view.innerWidth);
|
|
778
|
+
r.bottom = Math.min(r.bottom, view.innerHeight);
|
|
779
|
+
for (let p = shown.parentElement; p; p = p.parentElement) {
|
|
780
|
+
const cs = view.getComputedStyle(p);
|
|
781
|
+
if (cs.overflowX === 'visible' && cs.overflowY === 'visible')
|
|
782
|
+
continue;
|
|
783
|
+
const c = p.getBoundingClientRect();
|
|
784
|
+
r.left = Math.max(r.left, c.left);
|
|
785
|
+
r.top = Math.max(r.top, c.top);
|
|
786
|
+
r.right = Math.min(r.right, c.right);
|
|
787
|
+
r.bottom = Math.min(r.bottom, c.bottom);
|
|
788
|
+
}
|
|
789
|
+
const width = r.right - r.left;
|
|
790
|
+
const height = r.bottom - r.top;
|
|
791
|
+
if (width < 1 || height < 1)
|
|
792
|
+
return null;
|
|
793
|
+
let total = 0;
|
|
794
|
+
let covered = 0;
|
|
795
|
+
for (let gx = 1; gx <= 5; gx++)
|
|
796
|
+
for (let gy = 1; gy <= 3; gy++) {
|
|
797
|
+
const x = r.left + (width * gx) / 6;
|
|
798
|
+
const y = r.top + (height * gy) / 4;
|
|
799
|
+
total++;
|
|
800
|
+
const top = doc.elementFromPoint(x, y);
|
|
801
|
+
if (!top || !(shown.contains(top) || top.contains(shown)))
|
|
802
|
+
covered++;
|
|
803
|
+
}
|
|
804
|
+
const role = el.getAttribute('role');
|
|
805
|
+
const name = (el.getAttribute('aria-label') ?? el.textContent ?? '')
|
|
806
|
+
.trim()
|
|
807
|
+
.replace(/\s+/g, ' ')
|
|
808
|
+
.slice(0, 40);
|
|
809
|
+
return {
|
|
810
|
+
key: `${el.tagName}|${el.id}|${name}|${Math.round(r0.left)}|${Math.round(r0.top)}`,
|
|
811
|
+
element: `${el.tagName.toLowerCase()}${role ? `[role=${role}]` : ''}${name ? ` "${name}"` : ''}`,
|
|
812
|
+
covered: covered / total,
|
|
813
|
+
};
|
|
814
|
+
}, undefined);
|
|
815
|
+
if (!found)
|
|
816
|
+
continue;
|
|
817
|
+
if (seen.has(found.key))
|
|
818
|
+
break;
|
|
819
|
+
seen.add(found.key);
|
|
820
|
+
if (level === 'aa' ? found.covered >= 1 : found.covered > 0)
|
|
821
|
+
out.push({ element: found.element, covered: Math.round(found.covered * 100) / 100 });
|
|
822
|
+
}
|
|
823
|
+
return out;
|
|
824
|
+
}
|
|
825
|
+
/** Throws, listing every tab stop whose focused control is hidden by other content, unless there are none. */
|
|
826
|
+
export async function expectFocusNotObscured(page, options = {}) {
|
|
827
|
+
const found = await findObscuredFocus(page, options);
|
|
828
|
+
if (!found.length)
|
|
829
|
+
return;
|
|
830
|
+
throw new Error([
|
|
831
|
+
`Focus is hidden by other content on ${found.length} control${found.length === 1 ? '' : 's'}:`,
|
|
832
|
+
...found.map((f) => ` ${f.element}: ${Math.round(f.covered * 100)}% covered`),
|
|
833
|
+
].join('\n'));
|
|
834
|
+
}
|
|
598
835
|
/**
|
|
599
836
|
* Every link in running text that people who can't tell the colours apart could not find. A link counts as
|
|
600
837
|
* in running text when it is laid out inline and the block around it has words of its own — so navigation,
|