Skip to content

Commit a4261c2

Browse files
authored
fix(a11y): drop role=textbox / aria-multiline from innerdocbody (#7778) (#7782)
Murphy's 2026-05-16 re-test of #7255 reported "you still can't cycle through the text properly line by line to press links and such". The narrower toolbar/measurement fixes in #7777 don't address this — it's caused by the editor body advertising textbox semantics. role="textbox" + aria-multiline="true" pin NVDA/JAWS into focus mode for the whole pad. In focus mode arrow keys move the caret one character at a time, the P/H/K rotor shortcuts are suppressed, and links don't surface in the links list. That matches Murphy's symptoms exactly. contenteditable="true" by itself is enough to tell AT this is editable. Without the textbox role, NVDA/JAWS browse the content as document-mode HTML — line-by-line arrow nav, headings rotor, links list all return. aria-label / aria-describedby stay so the pad is still announced as "Pad content" with the keyboard hint on focus. This is the lighter alternative to the AT-only read mirror originally sketched in #7778 — ARIA-only, no DOM restructuring, no plugin impact. Refs #7255 #7777
1 parent 278acb1 commit a4261c2

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

src/static/js/ace.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,12 @@ const Ace2Editor = function () {
301301
// <body> tag
302302
innerDocument.body.id = 'innerdocbody';
303303
innerDocument.body.classList.add('innerdocbody');
304-
innerDocument.body.setAttribute('role', 'textbox');
305-
innerDocument.body.setAttribute('aria-multiline', 'true');
304+
// Deliberately no role="textbox" / aria-multiline: those put NVDA/JAWS
305+
// into focus mode (the whole pad becomes one flat edit field), which
306+
// hides links and headings from the rotor and suppresses arrow-key
307+
// line navigation. contenteditable=true already tells AT this is
308+
// editable; without textbox semantics AT can browse the content as a
309+
// document. See #7778 / #7255.
306310
innerDocument.body.setAttribute('aria-label', 'Pad content');
307311
innerDocument.body.setAttribute('aria-describedby', 'editor-keyboard-hint');
308312
innerDocument.body.setAttribute('spellcheck', 'false');

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,22 @@ test('editor-keyboard-hint exists in the editor iframe with localized text (#725
280280
expect(text).toContain('Escape');
281281
});
282282

283+
test('innerdocbody does not advertise role=textbox / aria-multiline (#7778)', async ({page}) => {
284+
// role=textbox + aria-multiline force NVDA/JAWS into focus mode for the
285+
// whole pad, which hides links/headings from the rotor and stops
286+
// arrow-key line navigation. Keep these attributes absent so AT browses
287+
// the editor as document content. The aria-label / aria-describedby
288+
// (#editor-keyboard-hint) stay — they don't change AT mode.
289+
const innerFrame = page.frameLocator('iframe[name="ace_outer"]')
290+
.frameLocator('iframe[name="ace_inner"]');
291+
const body = innerFrame.locator('body#innerdocbody');
292+
await expect(body).toHaveCount(1);
293+
expect(await body.getAttribute('role')).toBeNull();
294+
expect(await body.getAttribute('aria-multiline')).toBeNull();
295+
await expect(body).toHaveAttribute('aria-label', 'Pad content');
296+
await expect(body).toHaveAttribute('aria-describedby', 'editor-keyboard-hint');
297+
});
298+
283299
test('line-number sidediv is hidden from screen readers (#7255)', async ({page}) => {
284300
// sidediv lives in the outer ace iframe (ace_outer) — query the frame.
285301
const outerFrame = page.frameLocator('iframe[name="ace_outer"]');

0 commit comments

Comments
 (0)