Skip to content

Commit 987bb3a

Browse files
JohnMcLearclaude
andcommitted
fix(a11y): label #online_count for AT (#7255 - "number next to the user icon")
Murphy's 2026-05-16 follow-up cut off mid-bullet ("It's not clear what the number next to the …"). Best guess: the user-count badge in the showusers toolbar button. Currently it exposes a bare digit to AT — "5" with no context — because the visible badge text is also the entire accessible content. Append a localized aria-label generated from a new pad.userlist.onlineCount key (plural macro for one / other) whenever the count updates, so AT announces "5 connected users" instead of the bare digit. Add role=status and aria-live=polite so the count change is announced inline without forcing the user to refocus the button. Visible badge digit unchanged. html10n.get is null-safe (falls back to an English template so AT never gets back "undefined" before the locale bundle has loaded). Adds a Playwright spec verifying role/aria-live and that the aria-label contains "connected user". Refs #7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 09d0db6 commit 987bb3a

3 files changed

Lines changed: 35 additions & 2 deletions

File tree

src/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@
401401
"pad.savedrevs.timeslider": "You can see saved revisions by visiting the timeslider",
402402
"pad.userlist.entername": "Enter your name",
403403
"pad.userlist.unnamed": "unnamed",
404+
"pad.userlist.onlineCount": "{[ plural(count) one: {{count}} connected user, other: {{count}} connected users ]}",
404405
"pad.editbar.clearcolors": "Clear authorship colors on entire document? This cannot be undone",
405406

406407
"pad.impexp.importbutton": "Import Now",

src/static/js/pad_userlist.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,12 @@ const paduserlist = (() => {
354354
self.setMyUserInfo(myInitialUserInfo);
355355

356356
if ($('#online_count').length === 0) {
357-
$('#editbar [data-key=showusers] > a').append('<span id="online_count">1</span>');
357+
// role="status" + aria-live="polite" announces the count when it
358+
// changes; the localized aria-label (set in updateNumberOfOnlineUsers)
359+
// turns the bare badge digit into "N connected users" so AT users
360+
// get context, not a stray "5". See ether/etherpad#7255.
361+
$('#editbar [data-key=showusers] > a').append(
362+
'<span id="online_count" role="status" aria-live="polite">1</span>');
358363
}
359364

360365
$('#otheruserstable tr').remove();
@@ -547,7 +552,15 @@ const paduserlist = (() => {
547552
localStorage.setItem('recentPads', JSON.stringify(recentPadsList));
548553
}
549554

550-
$('#online_count').text(online);
555+
// Set both visible text (the badge digit) and the accessible name in
556+
// one place so they can't drift. html10n.get returns undefined if the
557+
// locale bundle hasn't loaded yet — fall back to an English template
558+
// so AT never reads back "undefined".
559+
const $count = $('#online_count');
560+
$count.text(online);
561+
const label = html10n.get('pad.userlist.onlineCount', {count: online})
562+
|| `${online} connected user${online === 1 ? '' : 's'}`;
563+
$count.attr('aria-label', label);
551564

552565
return online;
553566
},

src/tests/frontend-new/specs/a11y_dialogs.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,25 @@ test('toolbar <li>/<a> wrappers are presentational (Lighthouse listitem rule, #7
196196
}
197197
});
198198

199+
test('online_count badge has a localized accessible label (#7255)', async ({page}) => {
200+
// The user-count badge in the showusers toolbar button used to expose a
201+
// bare digit ("5") to AT, with no clue it was a user count. Now the badge
202+
// carries an aria-label generated from pad.userlist.onlineCount that
203+
// updates whenever the count changes. role=status + aria-live=polite
204+
// means AT announces the change without the user having to refocus.
205+
const badge = page.locator('#online_count');
206+
await expect(badge).toHaveAttribute('role', 'status');
207+
await expect(badge).toHaveAttribute('aria-live', 'polite');
208+
// toHaveText polls so the assertion survives the initial userlist update
209+
// that pad_userlist.ts schedules after the connection completes.
210+
await expect(badge).toHaveText(/^\d+$/);
211+
const label = await badge.getAttribute('aria-label');
212+
expect(label).toBeTruthy();
213+
// English plural form contains "connected user" — covers both singular
214+
// and plural without baking the exact count into the test.
215+
expect(label).toMatch(/connected user/);
216+
});
217+
199218
test('linemetricsdiv is hidden from screen readers (#7255)', async ({page}) => {
200219
// The "Ether X" announcement in the issue's a11y-inspector screenshot was
201220
// the outer iframe (titled "Ether") plus a single "x" text leaf from

0 commit comments

Comments
 (0)