Skip to content
Open
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
6 changes: 4 additions & 2 deletions locales/en.json
Original file line number Diff line number Diff line change
@@ -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"
}

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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) <john@mclear.co.uk>",
"keywords": [
Expand Down
27 changes: 20 additions & 7 deletions static/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <span>
// 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;
Expand Down Expand Up @@ -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')) {
Expand All @@ -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();
});
Expand Down
30 changes: 30 additions & 0 deletions static/tests/frontend-new/specs/title.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a>, the stale "Loading..." aria-label would survive the title
// swap. By scoping data-l10n-id to a child <span> and replacing the
// anchor's contents on save, no aria-label should remain on the <a>.
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', /.+/);
});
});
51 changes: 46 additions & 5 deletions templates/title.ejs
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
<div id='pad_title' class='flex_title'>
<div id='title'><h1><a href="" data-l10n-id="pad.loading">Loading...</a></h1></div>
<input id='input_title'>
<div id='edit_title' class='acl-write buttonicon buttonicon-pencil-alt'></div>
<div id='save_title'><button class="btn btn-primary">OK</button></div>
<!-- Visually-hidden labels referenced by aria-labelledby below. They live
inside #pad_title so the layout doesn't change, but `position: absolute`
+ clip pulls them out of the flexbox flow. See ether/etherpad#7255. -->
<span id='input_title_label' class='ep_set_title_sr_only'
data-l10n-id="ep_set_title_on_pad.input_aria">Pad title</span>
<span id='edit_title_label' class='ep_set_title_sr_only'
data-l10n-id="ep_set_title_on_pad.edit_aria">Edit pad title</span>
<span id='save_title_label' class='ep_set_title_sr_only'
data-l10n-id="ep_set_title_on_pad.save_aria">Save pad title</span>

<!-- Loading text lives in a <span> so html10n's aria-label auto-population
(introduced in etherpad PR #7584) targets the span, not the <a>. When
the live title arrives the <a>'s contents are replaced wholesale, so
there is no stale aria-label="Loading..." left behind on the anchor. -->
<div id='title'><h1><a href=""><span data-l10n-id="pad.loading">Loading...</span></a></h1></div>
<input id='input_title' type="text" aria-labelledby="input_title_label">
<button type="button" id='edit_title' class='acl-write buttonicon buttonicon-pencil-alt'
aria-labelledby="edit_title_label"></button>
<div id='save_title'>
<button type="button" class="btn btn-primary"
aria-labelledby="save_title_label"
data-l10n-id="ep_set_title_on_pad.ok">OK</button>
</div>
</div>

<style type="text/css">
Expand Down Expand Up @@ -31,9 +50,17 @@
#title h1{
line-height:160%;
}
/* Reset the native <button> chrome introduced when we promoted #edit_title
from <div> to <button> for accessibility. Keeps the pencil glyph centered
and inheriting the toolbar's color, matching pre-7255 visual behaviour. */
#edit_title{
padding-top: 5px;
appearance: none;
background: transparent;
border: 0;
padding: 5px 0 0 0;
margin-left: 10px;
color: inherit;
font: inherit;
cursor:pointer;
display:none;
}
Expand All @@ -45,6 +72,20 @@
#save_title button{
margin-right: 0 !important;
}
/* Visually hide accessible-name labels without removing them from the
accessibility tree. Same pattern as Tailwind/Bootstrap `sr-only`,
scoped so we don't collide with host-app utility classes. */
.ep_set_title_sr_only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media print{
#edit_title, #save_title{
display:none;
Expand Down
Loading