Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/preview/cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,24 @@ const A11Y_AUDIT = `(() => {
if (r.width > 0 && (r.width < 24 || r.height < 24)) tiny++;
}
if (tiny > 0) out.push(tiny + ' interactive element(s) smaller than 24x24px');
// Target spacing (WCAG 2.5.8): small adjacent targets need 24px of
// clearance between centers, or fingers hit the wrong one.
const targets = [...document.querySelectorAll('a[href], button, [role="button"], input:not([type=hidden])')]
.map((el) => el.getBoundingClientRect())
.filter((r) => r.width > 0 && r.height > 0 && (r.width < 24 || r.height < 24));
let cramped = 0;
for (let i = 0; i < targets.length && cramped < 1; i++) {
for (let j = i + 1; j < targets.length; j++) {
const a = targets[i], b = targets[j];
const dx = Math.abs((a.left + a.right) / 2 - (b.left + b.right) / 2);
const dy = Math.abs((a.top + a.bottom) / 2 - (b.top + b.bottom) / 2);
if (dx < 24 && dy < 24) {
out.push('target spacing: small tap targets sit closer than 24px apart (WCAG 2.5.8) — enlarge them or add spacing');
cramped++;
break;
}
}
}
for (const el of document.querySelectorAll('[tabindex]')) {
if (Number(el.getAttribute('tabindex')) > 0) out.push('positive tabindex ' + short(el));
}
Expand Down
2 changes: 2 additions & 0 deletions test/cdp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ describe.skipIf(!chrome || !hasWebSocket())('cdpCapture (requires Chrome + WebSo
<input type="search" aria-labelledby="renamed-label" aria-describedby="real-desc" />
<button popovertarget="no-such-popover">open</button>
<button></button><button></button><button></button><button></button>
<div style="position:absolute;top:400px;left:10px"><a href="#" style="display:inline-block;width:16px;height:16px">a</a><a href="#" style="display:inline-block;width:16px;height:16px">b</a></div>
<div onclick="void 0">x</div><div onclick="void 0">y</div><img onerror="void 0" src="x" />
<form id="silent-form"><input type="email" required /><button type="submit">Sign up</button></form>
<input id="offname" name="fullname" autocomplete="off" />
Expand Down Expand Up @@ -174,6 +175,7 @@ describe.skipIf(!chrome || !hasWebSocket())('cdpCapture (requires Chrome + WebSo
expect(findings).toContain('autocomplete="off" on an identity field')
expect(findings).toContain('top-layer: <div.modal-overlay> is a hand-rolled full-screen modal')
expect(findings).toMatch(/semantic gap: \d+ of \d+ interactive elements have no accessible name/)
expect(findings).toContain('target spacing: small tap targets sit closer than 24px apart')
expect(findings).toContain('autocomplete="firstname" ends in "firstname", not a valid field token')
expect(findings).not.toContain('autofill: input#goodtok')
expect(findings).not.toContain('real-desc')
Expand Down