mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-17 16:47:05 +00:00
fix(a11y): localized aria-label + title on export-as links (#7697)
* fix(a11y): localized aria-label + title on export-as links The six export anchors in the import/export dialog had no aria-label or title; their accessible name relied on the inner icon span's translated text (e.g. "Etherpad"). That announces just the format name with no "export" verb context, and the icon span doubles as the visible glyph which screen readers also pick up. Add `data-l10n-id="pad.importExport.export<format>a.title"` to each anchor — html10n populates both the `title` attribute and `aria-label` from the same key, so screen readers announce e.g. "Export as Etherpad" and sighted users get a tooltip. Mark the inner icon span `aria-hidden="true"` so SR doesn't double-read the format name. Strengthens the existing `a11y_dialogs.spec.ts` "export links" test to assert aria-label/title are populated and inner spans are aria-hidden, and only checks the three formats that are present without soffice. * Address Qodo review - Add rel="noopener" to each export anchor: closes the reverse-tabnabbing window-opener gap. Other target="_blank" links in pad.html (e.g. the "Powered by Etherpad" link) already follow this hardening pattern. - Pin Playwright locale to en-US for the a11y_dialogs spec: this file already asserts specific English strings (e.g. "Close chat", "Active users on this pad"); without the pin, translatewiki updates would break those tests. Tighten the export-anchor assertions to exact-match the English aria-label/title now that the locale is stable, and assert rel="noopener" too.
This commit is contained in:
parent
bcf5c91c15
commit
c7ac1cce6f
3 changed files with 43 additions and 28 deletions
|
|
@ -141,6 +141,12 @@
|
|||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.exportetherpada.title": "Export as Etherpad",
|
||||
"pad.importExport.exporthtmla.title": "Export as HTML",
|
||||
"pad.importExport.exportplaina.title": "Export as plain text",
|
||||
"pad.importExport.exportworda.title": "Export as Microsoft Word",
|
||||
"pad.importExport.exportpdfa.title": "Export as PDF",
|
||||
"pad.importExport.exportopena.title": "Export as ODF (Open Document Format)",
|
||||
"pad.importExport.noConverter.innerHTML": "You can only import from plain text or HTML formats. For more advanced import features, please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">install LibreOffice</a>.",
|
||||
|
||||
"pad.modals.connected": "Connected.",
|
||||
|
|
|
|||
|
|
@ -323,23 +323,23 @@
|
|||
<div id="exportColumn">
|
||||
<h2 data-l10n-id="pad.importExport.export"></h2>
|
||||
<% e.begin_block("exportColumn"); %>
|
||||
<a id="exportetherpada" target="_blank" class="exportlink">
|
||||
<span class="exporttype buttonicon buttonicon-file-powerpoint" id="exportetherpad" data-l10n-id="pad.importExport.exportetherpad"></span>
|
||||
<a id="exportetherpada" data-l10n-id="pad.importExport.exportetherpada.title" target="_blank" rel="noopener" class="exportlink">
|
||||
<span class="exporttype buttonicon buttonicon-file-powerpoint" id="exportetherpad" data-l10n-id="pad.importExport.exportetherpad" aria-hidden="true"></span>
|
||||
</a>
|
||||
<a id="exporthtmla" target="_blank" class="exportlink">
|
||||
<span class="exporttype buttonicon buttonicon-file-code" id="exporthtml" data-l10n-id="pad.importExport.exporthtml"></span>
|
||||
<a id="exporthtmla" data-l10n-id="pad.importExport.exporthtmla.title" target="_blank" rel="noopener" class="exportlink">
|
||||
<span class="exporttype buttonicon buttonicon-file-code" id="exporthtml" data-l10n-id="pad.importExport.exporthtml" aria-hidden="true"></span>
|
||||
</a>
|
||||
<a id="exportplaina" target="_blank" class="exportlink">
|
||||
<span class="exporttype buttonicon buttonicon-file" id="exportplain" data-l10n-id="pad.importExport.exportplain"></span>
|
||||
<a id="exportplaina" data-l10n-id="pad.importExport.exportplaina.title" target="_blank" rel="noopener" class="exportlink">
|
||||
<span class="exporttype buttonicon buttonicon-file" id="exportplain" data-l10n-id="pad.importExport.exportplain" aria-hidden="true"></span>
|
||||
</a>
|
||||
<a id="exportworda" target="_blank" class="exportlink">
|
||||
<span class="exporttype buttonicon buttonicon-file-word" id="exportword" data-l10n-id="pad.importExport.exportword"></span>
|
||||
<a id="exportworda" data-l10n-id="pad.importExport.exportworda.title" target="_blank" rel="noopener" class="exportlink">
|
||||
<span class="exporttype buttonicon buttonicon-file-word" id="exportword" data-l10n-id="pad.importExport.exportword" aria-hidden="true"></span>
|
||||
</a>
|
||||
<a id="exportpdfa" target="_blank" class="exportlink">
|
||||
<span class="exporttype buttonicon buttonicon-file-pdf" id="exportpdf" data-l10n-id="pad.importExport.exportpdf"></span>
|
||||
<a id="exportpdfa" data-l10n-id="pad.importExport.exportpdfa.title" target="_blank" rel="noopener" class="exportlink">
|
||||
<span class="exporttype buttonicon buttonicon-file-pdf" id="exportpdf" data-l10n-id="pad.importExport.exportpdf" aria-hidden="true"></span>
|
||||
</a>
|
||||
<a id="exportopena" target="_blank" class="exportlink">
|
||||
<span class="exporttype buttonicon buttonicon-file-alt" id="exportopen" data-l10n-id="pad.importExport.exportopen"></span>
|
||||
<a id="exportopena" data-l10n-id="pad.importExport.exportopena.title" target="_blank" rel="noopener" class="exportlink">
|
||||
<span class="exporttype buttonicon buttonicon-file-alt" id="exportopen" data-l10n-id="pad.importExport.exportopen" aria-hidden="true"></span>
|
||||
</a>
|
||||
<% e.end_block(); %>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import {expect, test} from '@playwright/test';
|
||||
import {goToNewPad} from '../helper/padHelper';
|
||||
|
||||
// Pin browser locale so html10n picks the English bundle. Several
|
||||
// assertions in this file compare against specific English strings
|
||||
// (e.g. "Close chat", "Export as Etherpad"); without this, translatewiki
|
||||
// updates would localise those strings and break the suite.
|
||||
test.use({locale: 'en-US'});
|
||||
|
||||
test.beforeEach(async ({page}) => {
|
||||
await goToNewPad(page);
|
||||
});
|
||||
|
|
@ -65,28 +71,31 @@ test('users popup closes on Escape even when focus is outside the popup', async
|
|||
await expect(dialog).not.toHaveClass(/popup-show/);
|
||||
});
|
||||
|
||||
test('export links have an accessible name from their localized content', async ({page}) => {
|
||||
test('export links have a localized aria-label and matching title', async ({page}) => {
|
||||
await page.locator('button[data-l10n-id="pad.toolbar.import_export.title"]').click();
|
||||
// The Word/PDF/ODF export links are removed client-side by pad_impexp.ts
|
||||
// when soffice is not configured, so only assert on links that the
|
||||
// environment actually renders. For the ones that are present, their
|
||||
// accessible name comes from the localized child span (data-l10n-id
|
||||
// pad.importExport.exportetherpad etc.), not a hard-coded English
|
||||
// aria-label. Assert the visible text is non-empty, which is what a
|
||||
// screen reader will announce.
|
||||
const ids = [
|
||||
'#exportetherpada',
|
||||
'#exporthtmla',
|
||||
'#exportplaina',
|
||||
'#exportworda',
|
||||
'#exportpdfa',
|
||||
'#exportopena',
|
||||
// environment actually renders. Each anchor carries
|
||||
// data-l10n-id="pad.importExport.export<format>a.title", which html10n
|
||||
// expands into both `title` and `aria-label` from the same translation
|
||||
// (e.g. "Export as Etherpad"). The inner icon span is aria-hidden so a
|
||||
// screen reader announces the anchor's label once, not twice.
|
||||
const cases: Array<[string, string]> = [
|
||||
['#exportetherpada', 'Export as Etherpad'],
|
||||
['#exporthtmla', 'Export as HTML'],
|
||||
['#exportplaina', 'Export as plain text'],
|
||||
['#exportworda', 'Export as Microsoft Word'],
|
||||
['#exportpdfa', 'Export as PDF'],
|
||||
['#exportopena', 'Export as ODF (Open Document Format)'],
|
||||
];
|
||||
for (const id of ids) {
|
||||
for (const [id, expected] of cases) {
|
||||
const locator = page.locator(id);
|
||||
if ((await locator.count()) === 0) continue;
|
||||
const text = (await locator.innerText()).trim();
|
||||
expect(text.length).toBeGreaterThan(0);
|
||||
await expect(locator).toHaveAttribute('aria-label', expected);
|
||||
await expect(locator).toHaveAttribute('title', expected);
|
||||
await expect(locator).toHaveAttribute('rel', 'noopener');
|
||||
const innerSpan = locator.locator('span.exporttype');
|
||||
await expect(innerSpan).toHaveAttribute('aria-hidden', 'true');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue