diff --git a/src/static/js/ace.ts b/src/static/js/ace.ts index c140e8a072d..1fabb63fa14 100644 --- a/src/static/js/ace.ts +++ b/src/static/js/ace.ts @@ -301,8 +301,12 @@ const Ace2Editor = function () { // tag innerDocument.body.id = 'innerdocbody'; innerDocument.body.classList.add('innerdocbody'); - innerDocument.body.setAttribute('role', 'textbox'); - innerDocument.body.setAttribute('aria-multiline', 'true'); + // Deliberately no role="textbox" / aria-multiline: those put NVDA/JAWS + // into focus mode (the whole pad becomes one flat edit field), which + // hides links and headings from the rotor and suppresses arrow-key + // line navigation. contenteditable=true already tells AT this is + // editable; without textbox semantics AT can browse the content as a + // document. See #7778 / #7255. innerDocument.body.setAttribute('aria-label', 'Pad content'); innerDocument.body.setAttribute('aria-describedby', 'editor-keyboard-hint'); innerDocument.body.setAttribute('spellcheck', 'false'); diff --git a/src/tests/frontend-new/specs/a11y_dialogs.spec.ts b/src/tests/frontend-new/specs/a11y_dialogs.spec.ts index e9389fdfb7e..b7a71aa5a71 100644 --- a/src/tests/frontend-new/specs/a11y_dialogs.spec.ts +++ b/src/tests/frontend-new/specs/a11y_dialogs.spec.ts @@ -280,6 +280,22 @@ test('editor-keyboard-hint exists in the editor iframe with localized text (#725 expect(text).toContain('Escape'); }); +test('innerdocbody does not advertise role=textbox / aria-multiline (#7778)', async ({page}) => { + // role=textbox + aria-multiline force NVDA/JAWS into focus mode for the + // whole pad, which hides links/headings from the rotor and stops + // arrow-key line navigation. Keep these attributes absent so AT browses + // the editor as document content. The aria-label / aria-describedby + // (#editor-keyboard-hint) stay — they don't change AT mode. + const innerFrame = page.frameLocator('iframe[name="ace_outer"]') + .frameLocator('iframe[name="ace_inner"]'); + const body = innerFrame.locator('body#innerdocbody'); + await expect(body).toHaveCount(1); + expect(await body.getAttribute('role')).toBeNull(); + expect(await body.getAttribute('aria-multiline')).toBeNull(); + await expect(body).toHaveAttribute('aria-label', 'Pad content'); + await expect(body).toHaveAttribute('aria-describedby', 'editor-keyboard-hint'); +}); + test('line-number sidediv is hidden from screen readers (#7255)', async ({page}) => { // sidediv lives in the outer ace iframe (ace_outer) — query the frame. const outerFrame = page.frameLocator('iframe[name="ace_outer"]');