@setsquare/ui 0.2.4 → 0.5.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 +132 -0
- package/NOTICE +1 -1
- package/README.md +17 -7
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/page-items.d.ts +10 -0
- package/dist/lib/page-items.d.ts.map +1 -0
- package/dist/lib/page-items.js +26 -0
- package/dist/lib/page-items.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 +84 -3
- package/dist/lint/index.js.map +1 -1
- package/dist/shell/sidebar.d.ts +23 -0
- package/dist/shell/sidebar.d.ts.map +1 -1
- package/dist/shell/sidebar.js +22 -3
- package/dist/shell/sidebar.js.map +1 -1
- package/dist/styles/theme.css +550 -0
- package/dist/styles/tokens.css +17 -0
- package/dist/testing/index.d.ts +57 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +276 -11
- package/dist/testing/index.js.map +1 -1
- package/dist/tokens.json +45 -0
- package/dist/ui/breadcrumbs.d.ts +9 -1
- package/dist/ui/breadcrumbs.d.ts.map +1 -1
- package/dist/ui/breadcrumbs.js +3 -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/combo-box.d.ts +6 -1
- package/dist/ui/combo-box.d.ts.map +1 -1
- package/dist/ui/combo-box.js +2 -2
- package/dist/ui/combo-box.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/feature-card.d.ts.map +1 -1
- package/dist/ui/feature-card.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/pagination.d.ts +69 -14
- package/dist/ui/pagination.d.ts.map +1 -1
- package/dist/ui/pagination.js +177 -32
- package/dist/ui/pagination.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 +82 -15
- package/dist/ui/tabs.d.ts.map +1 -1
- package/dist/ui/tabs.js +274 -16
- package/dist/ui/tabs.js.map +1 -1
- package/dist/ui/time-of-day-icon.d.ts.map +1 -1
- package/dist/ui/time-of-day-icon.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 +98 -0
- package/dist/ui/tree.d.ts.map +1 -0
- package/dist/ui/tree.js +75 -0
- package/dist/ui/tree.js.map +1 -0
- package/package.json +21 -25
- package/templates/next/page-transition.tsx +1 -1
|
@@ -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,
|
|
@@ -490,6 +533,25 @@ export async function findInvisibleFocus(page, options = {}) {
|
|
|
490
533
|
margin = 0;
|
|
491
534
|
others = all.filter((x) => apart(x.r, r1));
|
|
492
535
|
}
|
|
536
|
+
// Moving focus away can scroll the page (React Aria scrolls a focused row into view), taking this control
|
|
537
|
+
// out of the frame the screenshots compare. Put every scroll position back once focus has moved.
|
|
538
|
+
const scrolled = [
|
|
539
|
+
document.scrollingElement,
|
|
540
|
+
...(function* () {
|
|
541
|
+
for (let p = el.parentElement; p; p = p.parentElement)
|
|
542
|
+
yield p;
|
|
543
|
+
})(),
|
|
544
|
+
].filter((x) => !!x &&
|
|
545
|
+
(x.scrollTop !== 0 ||
|
|
546
|
+
x.scrollLeft !== 0 ||
|
|
547
|
+
x.scrollHeight > x.clientHeight ||
|
|
548
|
+
x.scrollWidth > x.clientWidth));
|
|
549
|
+
const positions = scrolled.map((x) => [x, x.scrollLeft, x.scrollTop]);
|
|
550
|
+
const restore = async () => {
|
|
551
|
+
for (const [x, left, top] of positions)
|
|
552
|
+
x.scrollTo({ left, top, behavior: 'instant' });
|
|
553
|
+
await frames();
|
|
554
|
+
};
|
|
493
555
|
let away = false;
|
|
494
556
|
for (const { o } of others.slice(0, 8)) {
|
|
495
557
|
o.focus({ preventScroll: true });
|
|
@@ -504,6 +566,7 @@ export async function findInvisibleFocus(page, options = {}) {
|
|
|
504
566
|
await frames();
|
|
505
567
|
away = document.activeElement !== el && !shown.contains(document.activeElement);
|
|
506
568
|
}
|
|
569
|
+
await restore();
|
|
507
570
|
const r2 = shown.getBoundingClientRect();
|
|
508
571
|
const left = Math.min(r1.left, r2.left) - margin;
|
|
509
572
|
const top = Math.min(r1.top, r2.top) - margin;
|
|
@@ -523,20 +586,41 @@ export async function findInvisibleFocus(page, options = {}) {
|
|
|
523
586
|
height,
|
|
524
587
|
trapped: !away,
|
|
525
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
|
+
})(),
|
|
526
604
|
};
|
|
527
605
|
}, { i, pad });
|
|
528
606
|
if (!box)
|
|
529
607
|
continue;
|
|
530
608
|
if (box.trapped) {
|
|
531
|
-
|
|
609
|
+
skipped({ element: box.described, reason: 'trapped' });
|
|
532
610
|
continue;
|
|
533
611
|
}
|
|
534
612
|
if (box.width < 1 || box.height < 1) {
|
|
535
|
-
|
|
613
|
+
skipped({ element: box.described, reason: 'offscreen' });
|
|
536
614
|
continue;
|
|
537
615
|
}
|
|
538
616
|
const clip = { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
539
|
-
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();
|
|
540
624
|
// 2. Focus this one. If it hands focus to something inside it (a grid to its first row), that element is
|
|
541
625
|
// probed on its own turn.
|
|
542
626
|
const kept = await page.evaluate(async (n) => {
|
|
@@ -549,10 +633,8 @@ export async function findInvisibleFocus(page, options = {}) {
|
|
|
549
633
|
}, i);
|
|
550
634
|
if (!kept)
|
|
551
635
|
continue;
|
|
552
|
-
const here = await
|
|
553
|
-
|
|
554
|
-
if (same)
|
|
555
|
-
out.push({ element: box.described, reason: 'unchanged' });
|
|
636
|
+
const here = await shoot();
|
|
637
|
+
await compare({ box, away, here });
|
|
556
638
|
}
|
|
557
639
|
await page.evaluate(() => {
|
|
558
640
|
document.querySelector('style[data-setsquare-focus-probe-style]')?.remove();
|
|
@@ -561,7 +643,6 @@ export async function findInvisibleFocus(page, options = {}) {
|
|
|
561
643
|
el.removeAttribute('data-setsquare-focus-skip');
|
|
562
644
|
}
|
|
563
645
|
}, undefined);
|
|
564
|
-
return out;
|
|
565
646
|
}
|
|
566
647
|
/** Throws, listing every control whose focus cannot be seen, unless there are none. `trapped` ones are skipped. */
|
|
567
648
|
export async function expectFocusVisible(page, options = {}) {
|
|
@@ -575,6 +656,182 @@ export async function expectFocusVisible(page, options = {}) {
|
|
|
575
656
|
: ` ${f.element}: took focus but could not be seen`),
|
|
576
657
|
].join('\n'));
|
|
577
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
|
+
}
|
|
578
835
|
/**
|
|
579
836
|
* Every link in running text that people who can't tell the colours apart could not find. A link counts as
|
|
580
837
|
* in running text when it is laid out inline and the block around it has words of its own — so navigation,
|
|
@@ -984,8 +1241,9 @@ export async function expectPageReflows(page, options = {}) {
|
|
|
984
1241
|
* reports every box whose content now runs out of it further than it did before. Measuring the difference
|
|
985
1242
|
* leaves alone anything that was already clipped on purpose (visually hidden text, a decorative bleed).
|
|
986
1243
|
* Scroll containers are left out too, since their content can still be scrolled to, and so is text that
|
|
987
|
-
* spills from a box inside a scroller that scrolls that way (code in a code block)
|
|
988
|
-
*
|
|
1244
|
+
* spills from a box inside a scroller that scrolls that way (code in a code block), or inside a pane marked
|
|
1245
|
+
* `data-setsquare-pannable`: one people move with pan controls instead of scrollbars, such as a React Flow
|
|
1246
|
+
* diagram, which lays itself out again once its nodes have grown. Only the innermost box is reported. Takes the spacing off again before returning. Returns [] when everything still fits.
|
|
989
1247
|
*/
|
|
990
1248
|
export function findClippedText(options = {}, root = document) {
|
|
991
1249
|
const describe = (el) => {
|
|
@@ -1014,6 +1272,8 @@ export function findClippedText(options = {}, root = document) {
|
|
|
1014
1272
|
return false;
|
|
1015
1273
|
if (/(auto|scroll)/.test(cs.overflowX + cs.overflowY))
|
|
1016
1274
|
return false;
|
|
1275
|
+
if (el.hasAttribute('data-setsquare-pannable'))
|
|
1276
|
+
return false;
|
|
1017
1277
|
const r = el.getBoundingClientRect();
|
|
1018
1278
|
// 1px visually hidden boxes clip on purpose.
|
|
1019
1279
|
return r.width > 2 && r.height > 2 && !!el.textContent?.trim();
|
|
@@ -1030,7 +1290,12 @@ export function findClippedText(options = {}, root = document) {
|
|
|
1030
1290
|
document.head.append(style);
|
|
1031
1291
|
/** Which ways the nearest ancestor that doesn't show its overflow scrolls: text spilled that way is reachable. */
|
|
1032
1292
|
const scrollsTo = (el) => {
|
|
1293
|
+
// Anything inside a pannable pane can be panned to, whatever clips it on the way.
|
|
1294
|
+
if (el.parentElement?.closest('[data-setsquare-pannable]'))
|
|
1295
|
+
return { x: true, y: true };
|
|
1033
1296
|
for (let p = el.parentElement; p && p !== document.body; p = p.parentElement) {
|
|
1297
|
+
if (p.hasAttribute('data-setsquare-pannable'))
|
|
1298
|
+
return { x: true, y: true };
|
|
1034
1299
|
const cs = getComputedStyle(p);
|
|
1035
1300
|
if (cs.overflowX === 'visible' && cs.overflowY === 'visible')
|
|
1036
1301
|
continue;
|