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
1 change: 1 addition & 0 deletions .changelog/next/changed-issue-4156.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Instance and peer settings now have clear screen-reader labels.
17 changes: 17 additions & 0 deletions client/src/a11yConventions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,23 @@ describe('a11y conventions', () => {
expect(offenders, `Icon-only <button> with no aria-label/aria-labelledby — title alone isn't touch-discoverable and isn't reliably read as the accessible name; see media/MediaCard.jsx's Annotate button for the convention:\n${offenders.join('\n')}`).toEqual([]);
});

it('gives the inline Instances form controls accessible names', () => {
// These controls live in compact peer-management rows where visible labels
// would break the layout. Keep explicit names on each input so placeholders
// never become their only screen-reader context.
const file = 'src/pages/Instances.jsx';
const src = readFileSync(join(CLIENT_ROOT, file), 'utf8');
const offenders = [];
const re = /<input\b/g;
let m;
while ((m = re.exec(src))) {
const tag = openingTagAt(src, m.index, '<input'.length);
if (!tag || /\baria-label\s*=|\baria-labelledby\s*=/.test(tag)) continue;
offenders.push(`${file}:${lineOf(src, m.index)}`);
}
expect(offenders, `Instances input without an accessible name:\n${offenders.join('\n')}`).toEqual([]);
});

it('meets the 44px touch-target minimum on Close buttons', () => {
// Close buttons keep shipping sized to their bare icon (w-4 h-4, p-1,
// p-1.5) instead of a real tap target. components/Drawer.jsx:106 is the
Expand Down
14 changes: 12 additions & 2 deletions client/src/pages/Instances.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ function SelfCard({ self, onUpdate, syncStatus, tailnetInfo }) {
{editing ? (
<div className="flex items-center gap-2 flex-1">
<input
aria-label="Instance name"
value={name}
onChange={e => setName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && saveName()}
Expand Down Expand Up @@ -369,6 +370,7 @@ function AddPeerForm({ onAdd }) {
</h3>
<div className="flex flex-wrap gap-2">
<input
aria-label="Peer address"
value={address}
onChange={e => setAddress(e.target.value)}
placeholder="100.64.x.x"
Expand All @@ -377,6 +379,7 @@ function AddPeerForm({ onAdd }) {
className="bg-port-bg border border-port-border rounded px-3 py-2 text-sm text-white placeholder-gray-600 focus:outline-hidden focus:border-port-accent flex-1 min-w-[140px]"
/>
<input
aria-label="Peer port"
value={port}
onChange={e => setPort(e.target.value)}
placeholder="5554"
Expand All @@ -386,6 +389,7 @@ function AddPeerForm({ onAdd }) {
className="bg-port-bg border border-port-border rounded px-3 py-2 text-sm text-white placeholder-gray-600 focus:outline-hidden focus:border-port-accent w-20"
/>
<input
aria-label="Peer name"
value={name}
onChange={e => setName(e.target.value)}
placeholder="Name (optional)"
Expand All @@ -411,13 +415,15 @@ function AddPeerForm({ onAdd }) {
{showAuth && (
<div className="flex flex-wrap gap-2 mt-2">
<input
aria-label="Peer username"
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="Username (optional)"
autoComplete="off"
className="bg-port-bg border border-port-border rounded px-3 py-2 text-sm text-white placeholder-gray-600 focus:outline-hidden focus:border-port-accent flex-1 min-w-[120px]"
/>
<input
aria-label="Peer password"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="Password"
Expand Down Expand Up @@ -931,14 +937,15 @@ function PeerHostEditor({ peer, onRefresh, tailnetInfo }) {
return (
<div className="mt-1 flex items-center gap-1 flex-wrap">
<input
aria-label="Peer hostname"
value={value}
onChange={e => setValue(e.target.value)}
onKeyDown={e => e.key === 'Enter' && save()}
placeholder="host.tailnet.ts.net (empty to clear)"
className="bg-port-bg border border-port-border rounded px-2 py-0.5 text-xs text-white font-mono focus:outline-hidden focus:border-port-accent flex-1 min-w-[180px]"
autoFocus
/>
<button onClick={save} disabled={saving} aria-label="Save name" className="text-port-success hover:text-port-success/80 disabled:opacity-50"><Check size={14} /></button>
<button onClick={save} disabled={saving} aria-label="Save hostname" className="text-port-success hover:text-port-success/80 disabled:opacity-50"><Check size={14} /></button>
<button onClick={() => setEditing(false)} aria-label="Cancel" className="text-gray-500 hover:text-white"><X size={14} /></button>
{suggestion && suggestion !== value && (
<button
Expand Down Expand Up @@ -1036,13 +1043,15 @@ function PeerAuthEditor({ peer, onRefresh }) {
return (
<div className="mt-1 flex items-center gap-1 flex-wrap">
<input
aria-label="Peer username"
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="user (optional)"
autoComplete="off"
className="bg-port-bg border border-port-border rounded px-2 py-0.5 text-xs text-white focus:outline-hidden focus:border-port-accent w-28"
/>
<input
aria-label="Peer password"
value={password}
onChange={e => setPassword(e.target.value)}
onKeyDown={e => e.key === 'Enter' && save()}
Expand All @@ -1052,7 +1061,7 @@ function PeerAuthEditor({ peer, onRefresh }) {
className="bg-port-bg border border-port-border rounded px-2 py-0.5 text-xs text-white focus:outline-hidden focus:border-port-accent flex-1 min-w-[120px]"
autoFocus
/>
<button onClick={save} disabled={saving || !password} aria-label="Save name" className="text-port-success hover:text-port-success/80 disabled:opacity-50"><Check size={14} /></button>
<button onClick={save} disabled={saving || !password} aria-label="Save credentials" className="text-port-success hover:text-port-success/80 disabled:opacity-50"><Check size={14} /></button>
<button onClick={() => setEditing(false)} aria-label="Cancel" className="text-gray-500 hover:text-white"><X size={14} /></button>
{hasAuth && (
<button onClick={clear} disabled={saving} className="text-[10px] text-gray-500 hover:text-port-error underline disabled:opacity-50">remove</button>
Expand Down Expand Up @@ -1249,6 +1258,7 @@ function PeerCard({ peer, onRefresh, syncStatus, tailnetInfo }) {
{editingName ? (
<div className="flex items-center gap-1">
<input
aria-label="Peer name"
value={name}
onChange={e => setName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && saveName()}
Expand Down