diff --git a/locales/en.json b/locales/en.json index 9148ec8..0c626d7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,5 +1,7 @@ { "ep_set_title_on_pad.ok" : "OK", - "ep_set_title_on_pad.show_title" : "Show Title" + "ep_set_title_on_pad.show_title" : "Show Title", + "ep_set_title_on_pad.input_aria" : "Pad title", + "ep_set_title_on_pad.edit_aria" : "Edit pad title", + "ep_set_title_on_pad.save_aria" : "Save pad title" } - diff --git a/package.json b/package.json index ef72ca7..5935bc9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ep_set_title_on_pad", "description": "Set the title on a pad in Etherpad, also includes real time updates to the UI", - "version": "0.7.5", + "version": "0.7.6", "license": "Apache-2.0", "author": "johnyma22 (John McLear) ", "keywords": [ diff --git a/static/js/index.js b/static/js/index.js index b2463f2..bd5cd9a 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -20,18 +20,33 @@ const titleToggle = padToggle({ // when another user toggles the pad-wide checkbox. exports.handleClientMessage_CLIENT_MESSAGE = titleToggle.handleClientMessage_CLIENT_MESSAGE; +// Centralised so the three call sites that swap the loading text for a real +// title can't drift apart on a11y cleanup. html10n auto-populates aria-label +// (etherpad PR #7584) when it translates a data-l10n-id node; if we leave +// those attributes behind, screen readers keep announcing the stale value +// (e.g. "Loading..." or the previous title). See ether/etherpad#7255. +const applyLiveTitle = (text) => { + const titleTag = $('#title > h1 > a'); + titleTag.text(text); + titleTag.removeAttr('data-l10n-id'); + // Defensive: html10n now places its auto-aria-label on the inner + // that holds data-l10n-id, but .text() above replaces all children with a + // single text node, so neither the span nor any stale attributes survive + // — still clean these in case an older template version is in play. + titleTag.removeAttr('aria-label'); + titleTag.removeAttr('data-l10n-aria-label'); +}; + // Existing CUSTOM message handler for the live title-text broadcast (separate // from the padToggle show/hide checkbox). exports.handleClientMessage_CUSTOM = (hook, context, cb) => { if (context.payload.action === 'recieveTitleMessage') { const message = context.payload.message; const padTitleElement = $('#input_title'); - const titleTag = $('#title > h1 > a'); if (!padTitleElement.is(':visible')) { // if we're not editing.. if (message) { window.document.title = message; - titleTag.text(message); - titleTag.removeAttr('data-l10n-id'); + applyLiveTitle(message); padTitleElement.val(message); clientVars.ep_set_title_on_pad = {}; clientVars.ep_set_title_on_pad.title = message; @@ -74,8 +89,7 @@ exports.documentReady = () => { }); if (!clientVars.ep_set_title_on_pad) { - $('#title > h1 > a').text(clientVars.padId); - $('#title > h1 > a').removeAttr('data-l10n-id'); + applyLiveTitle(clientVars.padId); } if (!$('#editorcontainerbox').hasClass('flex-layout')) { @@ -98,8 +112,7 @@ exports.documentReady = () => { $('#save_title').click(() => { sendTitle(); window.document.title = $('#input_title').val(); - $('#title > h1 > a').text($('#input_title').val()); - $('#title > h1 > a').removeAttr('data-l10n-id'); + applyLiveTitle($('#input_title').val()); $('#title, #edit_title').show(); $('#input_title, #save_title').hide(); }); diff --git a/static/tests/frontend-new/specs/title.spec.ts b/static/tests/frontend-new/specs/title.spec.ts index 3daf59a..69684fc 100644 --- a/static/tests/frontend-new/specs/title.spec.ts +++ b/static/tests/frontend-new/specs/title.spec.ts @@ -13,4 +13,34 @@ test.describe('Set Title On Pad', () => { await expect(page.locator('#pad_title > #title > h1 > a')).toHaveText('JohnMcLear'); }); + + test('Title-bar elements have accessible names (ether/etherpad#7255)', async ({page}) => { + // Edit / save buttons + input each carry an aria-labelledby pointing at a + // visually-hidden translated label. Verify the resolved accessible name + // so a future template tweak that drops the labelledby (or the label + // element) fails this assertion instead of silently regressing AT UX. + const edit = page.locator('#edit_title'); + await expect(edit).toHaveAttribute('aria-labelledby', 'edit_title_label'); + await expect(edit).toHaveRole('button'); + + const input = page.locator('#input_title'); + await expect(input).toHaveAttribute('aria-labelledby', 'input_title_label'); + + const save = page.locator('#save_title button'); + await expect(save).toHaveAttribute('aria-labelledby', 'save_title_label'); + + // Loading-state aria-label leak: html10n auto-populates aria-label on + // the element carrying data-l10n-id (PR #7584). If that element was the + // outer , the stale "Loading..." aria-label would survive the title + // swap. By scoping data-l10n-id to a child and replacing the + // anchor's contents on save, no aria-label should remain on the . + await edit.click(); + await page.locator('#input_title').fill('Test'); + await page.locator('#save_title').click(); + const anchor = page.locator('#pad_title > #title > h1 > a'); + await expect(anchor).toHaveText('Test'); + await expect(anchor).not.toHaveAttribute('aria-label', /.+/); + await expect(anchor).not.toHaveAttribute('data-l10n-aria-label', /.+/); + await expect(anchor).not.toHaveAttribute('data-l10n-id', /.+/); + }); }); diff --git a/templates/title.ejs b/templates/title.ejs index a40df3b..a5f481e 100644 --- a/templates/title.ejs +++ b/templates/title.ejs @@ -1,8 +1,27 @@
- - -
-
+ + Pad title + Edit pad title + Save pad title + + + + + +
+ +