Skip to content

fix: accessibility — keyboard trap, screen reader support, aria-live#7451

Merged
JohnMcLear merged 3 commits intoether:developfrom
JohnMcLear:fix/accessibility-omnibus
Apr 5, 2026
Merged

fix: accessibility — keyboard trap, screen reader support, aria-live#7451
JohnMcLear merged 3 commits intoether:developfrom
JohnMcLear:fix/accessibility-omnibus

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

Three accessibility fixes addressing WCAG compliance and screen reader support.

#6581 — Keyboard trap (WCAG 2.1.2)

  • Escape key now moves focus from the editor to the first toolbar button
  • Previously, keyboard-only users were trapped in the editor with no discoverable exit (Alt+F9 existed but was undocumented)
  • Added a screen-reader-only hint: "Press Escape to exit the editor. Press Alt+F9 to access the toolbar."
  • Added .sr-only CSS utility class

#7255 — Screen reader access

  • Added role="textbox", aria-multiline="true", and aria-label="Pad content" to the contenteditable #innerdocbody element
  • Screen readers can now identify the editor as a text input area and interact with its content
  • Fixed aria-role="document"role="document" in pad.html (non-standard attribute)

#5695 — aria-live causing character echo

  • Removed aria-live="assertive" from every line div in domline.ts
  • This was causing screen readers (NVDA) to announce every character typed, even when keyboard echo was disabled
  • The attribute was added in PR Accessibility fix for JAWS screen readers #5149 for JAWS but aria-live on individual contenteditable lines is a misuse per WCAG guidelines

Test plan

  • Type check passes
  • Backend tests pass (744/744)
  • Manual: with NVDA/JAWS, verify typing doesn't repeat characters
  • Manual: press Escape in editor → focus moves to toolbar
  • Manual: screen reader announces "Pad content" when entering the editor

Fixes #6581
Fixes #7255
Fixes #5695

🤖 Generated with Claude Code

@JohnMcLear
Copy link
Copy Markdown
Member Author

/review

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 4, 2026

Code Review by Qodo

🐞 Bugs (7) 📘 Rule violations (3) 📎 Requirement gaps (1) 🎨 UX Issues (0)

Grey Divider


Action required

1. ace.ts uses 4-space indent 📘 Rule violation ⚙ Maintainability
Description
New/modified lines in src/static/js/ace.ts are indented with 4 spaces, violating the project's
2-space indentation requirement. This can cause style drift and formatter/linter churn across the
codebase.
Code

src/static/js/ace.ts[R287-297]

+    innerDocument.body.setAttribute('role', 'textbox');
+    innerDocument.body.setAttribute('aria-multiline', 'true');
+    innerDocument.body.setAttribute('aria-label', 'Pad content');
+    innerDocument.body.setAttribute('aria-describedby', 'editor-keyboard-hint');
   innerDocument.body.setAttribute('spellcheck', 'false');
+    // Screen-reader-only keyboard hint inside the iframe so it's announced on focus.
+    const hint = innerDocument.createElement('div');
+    hint.id = 'editor-keyboard-hint';
+    hint.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)';
+    hint.textContent = 'Press Escape to exit the editor. Press Alt+F9 to access the toolbar.';
+    innerDocument.body.appendChild(hint);
Evidence
The checklist requires all new/modified code to use 2-space indentation and no tabs. The added ARIA
and hint lines in ace.ts are indented with 4 spaces (e.g., line 287 begins with four spaces).

src/static/js/ace.ts[287-297]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Newly added lines in `src/static/js/ace.ts` use 4-space indentation, but the compliance checklist requires 2-space indentation.
## Issue Context
This PR introduces new ARIA attributes and a screen-reader hint element. The added lines should follow the repository's indentation standard to avoid formatting inconsistencies.
## Fix Focus Areas
- src/static/js/ace.ts[287-297]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Hint node gets deleted 🐞 Bug ≡ Correctness
Description
src/static/js/ace.ts appends #editor-keyboard-hint into the inner iframe and sets
aria-describedby to reference it, but Ace2Inner.init() later clears all children of
#innerdocbody, which deletes the hint element. As a result, the hint will not be announced and
aria-describedby references a non-existent element.
Code

src/static/js/ace.ts[R290-298]

+    innerDocument.body.setAttribute('aria-describedby', 'editor-keyboard-hint');
 innerDocument.body.setAttribute('spellcheck', 'false');
+    // Screen-reader-only keyboard hint inside the iframe so it's announced on focus.
+    const hint = innerDocument.createElement('div');
+    hint.id = 'editor-keyboard-hint';
+    hint.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)';
+    hint.textContent = 'Press Escape to exit the editor. Press Alt+F9 to access the toolbar.';
+    innerDocument.body.appendChild(hint);
 innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //  
Evidence
The hint is appended to innerDocument.body before Ace2Inner.init() is awaited, then
Ace2Inner.init() removes every child node of the editor body during setup. Additionally, multiple
code paths assume each #innerdocbody child is a document line (so keeping the hint as a body child
would break line indexing/metrics).

src/static/js/ace.ts[284-323]
src/static/js/ace2_inner.ts[3516-3531]
src/static/js/ace2_inner.ts[3462-3474]
src/static/js/pad_editor.ts[209-226]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A DOM node (`#editor-keyboard-hint`) is appended as a child of the editor’s iframe `<body id="innerdocbody">`, but Ace2Inner’s setup clears all children of that body, deleting the hint. This makes `aria-describedby="editor-keyboard-hint"` point to a missing element, so the keyboard hint is never announced.
## Issue Context
`#innerdocbody` is treated as the editor’s line container. Several algorithms iterate `targetBody.children` or use `div:nth-child(N)` under `#innerdocbody`, so adding non-line elements as children risks breaking line indexing/metrics.
## Fix Focus Areas
- src/static/js/ace.ts[284-323]
- src/static/js/ace2_inner.ts[3524-3531]
- src/static/js/ace2_inner.ts[3462-3474]
- src/static/js/pad_editor.ts[209-226]
## Implementation direction
- Remove (or avoid) adding a persistent non-line child to `#innerdocbody`.
- Either:
- Encode the hint directly into an attribute that survives setup (for example, incorporate the hint into the editor’s accessible name/description without adding a child node), or
- Refactor the inner iframe DOM so the editable line container is a dedicated child element (e.g., a new `#editor-content` div) and keep the hint as a sibling; then update all code that assumes lines are direct children of `#innerdocbody`.
- Ensure `aria-describedby` is only set if the referenced element actually exists at runtime.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Accessibility fixes lack regression tests 📘 Rule violation ☼ Reliability
Description
This PR changes production accessibility behavior (Escape-to-toolbar focus, screen reader
roles/labels, and aria-live removal) without adding any regression tests, increasing the risk of
these WCAG fixes silently regressing. The checklist requires a regression test for each bug fix.
Code

src/static/js/ace2_inner.ts[R2692-2711]

+          // Escape key: if gritter popups are visible, close them and stay in editor.
+          // Otherwise, move focus to the toolbar (WCAG 2.1.2 keyboard trap escape).
     fastIncorp(4);
     evt.preventDefault();
     specialHandled = true;
-          // close all gritters when the user hits escape key
+          const hasGritters = window.$('.gritter-item').length > 0;
     window.$.gritter.removeAll();
+
+          if (!hasGritters) {
+            // No popups to dismiss — move focus to the toolbar so the user
+            // can navigate away from the editor with Tab.
+            try {
+              const toolbar = window.parent.document.querySelector('[role="toolbar"]');
+              const firstButton = toolbar?.querySelector('button');
+              if (firstButton) firstButton.focus();
+            } catch (e) {
+              // Cross-origin frame restrictions — ignore.
+            }
+          }
Evidence
PR Compliance ID 4 requires regression tests to accompany bug fixes. The diff shows multiple
production bug-fix changes (keyboard trap Escape handling, screen reader attributes, and aria-live
removal) but no corresponding test additions in the provided PR diff.

src/static/js/ace2_inner.ts[2692-2711]
src/static/js/domline.ts[65-66]
src/static/js/ace.ts[287-289]
src/templates/pad.html[87-88]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Production code changes that fix accessibility bugs were added without regression tests, violating the requirement that bug fixes must be protected against future regressions.
## Issue Context
This PR introduces behavioral changes (Escape key focus management), ARIA semantics for screen readers, and removal of `aria-live` that previously caused per-character announcements. These should be covered by automated tests (e.g., Playwright/E2E for keyboard focus behavior; DOM/unit checks for ARIA attributes).
## Fix Focus Areas
- src/static/js/ace2_inner.ts[2692-2711]
- src/static/js/domline.ts[65-66]
- src/static/js/ace.ts[287-289]
- src/templates/pad.html[87-88]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (5)
4. Hint not announced🐞 Bug ≡ Correctness
Description
The new sr-only keyboard hint is connected to #editorcontainer, but focus is moved into the nested
editor iframe’s body, so screen readers won’t announce the hint when entering the editor. The hint
text will instead appear as standalone screen-reader content in the page reading order, not as
contextual help for the editor.
Code

src/templates/pad.html[R87-88]

+      <div id="editorcontainer" class="editorcontainer" aria-label="Document editor" aria-describedby="editor-keyboard-hint"></div>
+      <div id="editor-keyboard-hint" class="sr-only">Press Escape to exit the editor. Press Alt+F9 to access the toolbar.</div>
Evidence
pad.html adds aria-describedby on a non-focusable container div and places the hint outside the
editor iframes. The editor UI is actually an iframe appended into #editorcontainer, and Ace2Inner
focuses the inner iframe’s body (innerdocbody), so aria-describedby on #editorcontainer is never
read at the time focus enters the editor. The .sr-only class ensures the hint is still exposed to
assistive tech, which means it can be encountered out of context in reading order.

src/templates/pad.html[79-89]
src/static/js/ace.ts[185-200]
src/static/js/ace2_inner.ts[61-67]
src/static/css/pad.css[16-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The keyboard hint is currently attached to `#editorcontainer`, but the focused element for editing is inside nested iframes. As a result, screen readers will not announce the hint when the user enters the editor.
### Issue Context
The editor iframes are created dynamically in `Ace2Editor.init()` and appended into `#editorcontainer`, and focus is moved to the inner iframe body.
### Fix Focus Areas
- src/templates/pad.html[87-88]
- src/static/js/ace.ts[185-200]
### Suggested fix
Move the accessible name/description from `#editorcontainer` to the actual focusable element the user tabs into (the outer editor iframe). Concretely:
1) Remove `aria-label`/`aria-describedby` from `#editorcontainer`.
2) In `Ace2Editor.init()`, after creating/appending `outerFrame`, set:
- `outerFrame.setAttribute('aria-label', 'Document editor')` (or better: reuse existing title semantics)
- `outerFrame.setAttribute('aria-describedby', 'editor-keyboard-hint')`
This keeps the hint element in the same document as the iframe attributes and makes the hint announce when the iframe receives focus.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Escape focus unreliable 🐞 Bug ≡ Correctness
Description
The Escape key handler tries to focus the toolbar via window.parent.document and only targets the
first <button>, so it can fail when the pad is embedded in an iframe (window.parent is the embedding
page) or when the first toolbar control isn’t a button. In those cases, Escape does not move focus
out of the editor, leaving the keyboard trap unfixed for some users.
Code

src/static/js/ace2_inner.ts[R2705-2707]

+              const toolbar = window.parent.document.querySelector('[role="toolbar"]');
+              const firstButton = toolbar?.querySelector('button');
+              if (firstButton) firstButton.focus();
Evidence
Ace2Inner runs in the pad page context (it uses the current document to locate the editor iframes),
but Escape handler queries window.parent.document, which becomes cross-origin/incorrect when the
pad is embedded (embed code uses an iframe). Additionally, toolbar rendering supports non-button
controls (select) and separators, but the code only focuses a button, so it can fail to find a
focus target depending on toolbar configuration.

src/static/js/ace2_inner.ts[61-63]
src/static/js/ace2_inner.ts[2689-2711]
src/static/js/pad_editbar.ts[255-270]
src/node/utils/toolbar.ts[115-173]
src/node/utils/toolbar.ts[182-188]
src/templates/pad.html[64-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Escape-to-toolbar focus uses `window.parent.document` and only searches for a `<button>`. This breaks when Etherpad is embedded in an iframe (parent is the embedding page) and when the first focusable toolbar control is not a button.
### Issue Context
This code executes in the pad page realm (it uses `document.getElementsByName('ace_outer')`), so it should search the current document’s toolbar. Etherpad supports embedding the pad via an `<iframe>`.
### Fix Focus Areas
- src/static/js/ace2_inner.ts[2692-2711]
- src/static/js/pad_editbar.ts[255-270]
- src/node/utils/toolbar.ts[115-173]
### Suggested fix
1) Replace `window.parent.document` with the current document (`document`) to ensure it works when the pad is embedded.
2) Select the first *focusable* element in the toolbar, not just `button`. For example:
- `const toolbar = document.querySelector('#editbar [role="toolbar"]');`
- `const firstFocusable = toolbar?.querySelector('button:not([disabled]), select:not([disabled]), [href], input:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');`
3) Focus `firstFocusable` if found.
This preserves the intended behavior across configurable toolbars and embedded iframe use cases.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Accessibility fixes lack regression tests 📘 Rule violation ☼ Reliability
Description
This PR changes production accessibility behavior (Escape-to-toolbar focus, screen reader
roles/labels, and aria-live removal) without adding any regression tests, increasing the risk of
these WCAG fixes silently regressing. The checklist requires a regression test for each bug fix.
Code

src/static/js/ace2_inner.ts[R2692-2711]

+          // Escape key: if gritter popups are visible, close them and stay in editor.
+          // Otherwise, move focus to the toolbar (WCAG 2.1.2 keyboard trap escape).
         fastIncorp(4);
         evt.preventDefault();
         specialHandled = true;

-          // close all gritters when the user hits escape key
+          const hasGritters = window.$('.gritter-item').length > 0;
         window.$.gritter.removeAll();
+
+          if (!hasGritters) {
+            // No popups to dismiss — move focus to the toolbar so the user
+            // can navigate away from the editor with Tab.
+            try {
+              const toolbar = window.parent.document.querySelector('[role="toolbar"]');
+              const firstButton = toolbar?.querySelector('button');
+              if (firstButton) firstButton.focus();
+            } catch (e) {
+              // Cross-origin frame restrictions — ignore.
+            }
+          }
Evidence
PR Compliance ID 4 requires regression tests to accompany bug fixes. The diff shows multiple
production bug-fix changes (keyboard trap Escape handling, screen reader attributes, and aria-live
removal) but no corresponding test additions in the provided PR diff.

src/static/js/ace2_inner.ts[2692-2711]
src/static/js/domline.ts[65-66]
src/static/js/ace.ts[287-289]
src/templates/pad.html[87-88]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Production code changes that fix accessibility bugs were added without regression tests, violating the requirement that bug fixes must be protected against future regressions.
## Issue Context
This PR introduces behavioral changes (Escape key focus management), ARIA semantics for screen readers, and removal of `aria-live` that previously caused per-character announcements. These should be covered by automated tests (e.g., Playwright/E2E for keyboard focus behavior; DOM/unit checks for ARIA attributes).
## Fix Focus Areas
- src/static/js/ace2_inner.ts[2692-2711]
- src/static/js/domline.ts[65-66]
- src/static/js/ace.ts[287-289]
- src/templates/pad.html[87-88]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Hint not announced 🐞 Bug ≡ Correctness
Description
The new sr-only keyboard hint is connected to #editorcontainer, but focus is moved into the nested
editor iframe’s body, so screen readers won’t announce the hint when entering the editor. The hint
text will instead appear as standalone screen-reader content in the page reading order, not as
contextual help for the editor.
Code

src/templates/pad.html[R87-88]

+      <div id="editorcontainer" class="editorcontainer" aria-label="Document editor" aria-describedby="editor-keyboard-hint"></div>
+      <div id="editor-keyboard-hint" class="sr-only">Press Escape to exit the editor. Press Alt+F9 to access the toolbar.</div>
Evidence
pad.html adds aria-describedby on a non-focusable container div and places the hint outside the
editor iframes. The editor UI is actually an iframe appended into #editorcontainer, and Ace2Inner
focuses the inner iframe’s body (innerdocbody), so aria-describedby on #editorcontainer is never
read at the time focus enters the editor. The .sr-only class ensures the hint is still exposed to
assistive tech, which means it can be encountered out of context in reading order.

src/templates/pad.html[79-89]
src/static/js/ace.ts[185-200]
src/static/js/ace2_inner.ts[61-67]
src/static/css/pad.css[16-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The keyboard hint is currently attached to `#editorcontainer`, but the focused element for editing is inside nested iframes. As a result, screen readers will not announce the hint when the user enters the editor.
### Issue Context
The editor iframes are created dynamically in `Ace2Editor.init()` and appended into `#editorcontainer`, and focus is moved to the inner iframe body.
### Fix Focus Areas
- src/templates/pad.html[87-88]
- src/static/js/ace.ts[185-200]
### Suggested fix
Move the accessible name/description from `#editorcontainer` to the actual focusable element the user tabs into (the outer editor iframe). Concretely:
1) Remove `aria-label`/`aria-describedby` from `#editorcontainer`.
2) In `Ace2Editor.init()`, after creating/appending `outerFrame`, set:
 - `outerFrame.setAttribute('aria-label', 'Document editor')` (or better: reuse existing title semantics)
 - `outerFrame.setAttribute('aria-describedby', 'editor-keyboard-hint')`
This keeps the hint element in the same document as the iframe attributes and makes the hint announce when the iframe receives focus.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Escape focus unreliable 🐞 Bug ≡ Correctness
Description
The Escape key handler tries to focus the toolbar via window.parent.document and only targets the
first <button>, so it can fail when the pad is embedded in an iframe (window.parent is the embedding
page) or when the first toolbar control isn’t a button. In those cases, Escape does not move focus
out of the editor, leaving the keyboard trap unfixed for some users.
Code

src/static/js/ace2_inner.ts[R2705-2707]

+              const toolbar = window.parent.document.querySelector('[role="toolbar"]');
+              const firstButton = toolbar?.querySelector('button');
+              if (firstButton) firstButton.focus();
Evidence
Ace2Inner runs in the pad page context (it uses the current document to locate the editor iframes),
but Escape handler queries window.parent.document, which becomes cross-origin/incorrect when the
pad is embedded (embed code uses an iframe). Additionally, toolbar rendering supports non-button
controls (select) and separators, but the code only focuses a button, so it can fail to find a
focus target depending on toolbar configuration.

src/static/js/ace2_inner.ts[61-63]
src/static/js/ace2_inner.ts[2689-2711]
src/static/js/pad_editbar.ts[255-270]
src/node/utils/toolbar.ts[115-173]
src/node/utils/toolbar.ts[182-188]
src/templates/pad.html[64-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Escape-to-toolbar focus uses `window.parent.document` and only searches for a `<button>`. This breaks when Etherpad is embedded in an iframe (parent is the embedding page) and when the first focusable toolbar control is not a button.
### Issue Context
This code executes in the pad page realm (it uses `document.getElementsByName('ace_outer')`), so it should search the current document’s toolbar. Etherpad supports embedding the pad via an `<iframe>`.
### Fix Focus Areas
- src/static/js/ace2_inner.ts[2692-2711]
- src/static/js/pad_editbar.ts[255-270]
- src/node/utils/toolbar.ts[115-173]
### Suggested fix
1) Replace `window.parent.document` with the current document (`document`) to ensure it works when the pad is embedded.
2) Select the first *focusable* element in the toolbar, not just `button`. For example:
 - `const toolbar = document.querySelector('#editbar [role="toolbar"]');`
 - `const firstFocusable = toolbar?.querySelector('button:not([disabled]), select:not([disabled]), [href], input:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');`
3) Focus `firstFocusable` if found.
This preserves the intended behavior across configurable toolbars and embedded iframe use cases.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

9. Hidden keyboard exit hint 📎 Requirement gap ≡ Correctness
Description
The new keyboard exit instructions are added as a visually hidden element (hint.style.cssText), so
sighted keyboard-only users do not get an on-screen, discoverable instruction for the non-standard
editor exit keystrokes. This may fail the requirement that exit instructions be clearly communicated
adjacent/on-screen and reachable via keyboard focus when non-standard keys are required.
Code

src/static/js/ace.ts[R292-297]

+    // Screen-reader-only keyboard hint inside the iframe so it's announced on focus.
+    const hint = innerDocument.createElement('div');
+    hint.id = 'editor-keyboard-hint';
+    hint.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)';
+    hint.textContent = 'Press Escape to exit the editor. Press Alt+F9 to access the toolbar.';
+    innerDocument.body.appendChild(hint);
Evidence
PR Compliance ID 2 requires clear, accessible exit instructions if non-standard keystrokes are
needed. The PR adds the instruction text but explicitly hides it with CSS (1px clipped), making it
non-visible on-screen and not directly reachable via keyboard focus.

Provide accessible instructions if exiting the editor requires non-standard keystrokes
src/static/js/ace.ts[292-297]
src/static/css/pad.css[16-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Keyboard exit instructions are currently screen-reader-only (visually hidden), which may not satisfy the requirement for clear, on-screen, keyboard-reachable instructions when non-standard keys are used to exit the editor.
## Issue Context
The PR introduces an Escape/Alt+F9-based exit path and adds a hidden hint via `aria-describedby`, but the checklist success criteria calls for instructions to be communicated clearly adjacent/on-screen and reachable via keyboard focus.
## Fix Focus Areas
- src/static/js/ace.ts[287-297]
- src/templates/pad.html[83-88]
- src/static/css/pad.css[16-27]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Unlocalized ARIA announcements 🐞 Bug ⚙ Maintainability
Description
The new aria-label ("Pad content") and keyboard hint text are hard-coded English strings in
ace.ts, so screen readers will announce English even when Etherpad is localized to another
language. This bypasses the existing html10n localization approach used throughout the UI.
Code

src/static/js/ace.ts[R287-297]

+    innerDocument.body.setAttribute('role', 'textbox');
+    innerDocument.body.setAttribute('aria-multiline', 'true');
+    innerDocument.body.setAttribute('aria-label', 'Pad content');
+    innerDocument.body.setAttribute('aria-describedby', 'editor-keyboard-hint');
 innerDocument.body.setAttribute('spellcheck', 'false');
+    // Screen-reader-only keyboard hint inside the iframe so it's announced on focus.
+    const hint = innerDocument.createElement('div');
+    hint.id = 'editor-keyboard-hint';
+    hint.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)';
+    hint.textContent = 'Press Escape to exit the editor. Press Alt+F9 to access the toolbar.';
+    innerDocument.body.appendChild(hint);
Evidence
The strings are literal English in ace.ts. The codebase uses html10n.get(...) for user-visible
UI strings and the localization system explicitly sets aria-label during translation, indicating
ARIA-visible text is expected to be localizable.

src/static/js/ace.ts[287-297]
src/static/js/vendors/html10n.ts[640-667]
src/static/js/pad_savedrevs.ts[22-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New ARIA-visible strings are hard-coded in English:
- `aria-label`: "Pad content"
- keyboard hint text: "Press Escape to exit the editor…"
This causes mixed-language announcements for non-English users.
## Issue Context
Etherpad already localizes many UI strings via `html10n.get(...)`, and the html10n translation code sets `aria-label` when translating elements.
## Fix Focus Areas
- src/static/js/ace.ts[284-299]
- src/static/js/vendors/html10n.ts[640-667]
- src/static/js/pad_savedrevs.ts[22-34]
- src/locales/en.json[60-80]
## Implementation direction
- Add new translation keys (for example: `pad.editor.content.ariaLabel` and `pad.editor.keyboardHint`).
- In `ace.ts`, fetch localized strings via `html10n.get(key)` with an English fallback (similar to other code).
- Update `src/locales/en.json` (and ideally `qqq.json`) with the new keys so translators can provide equivalents.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Readonly state not conveyed🐞 Bug ≡ Correctness
Description
The editor body is always given role="textbox" and aria-multiline="true", but readonly mode only
flips contentEditable without setting aria-readonly. Screen readers can announce an editable textbox
even when the pad is read-only, misrepresenting the editor state.
Code

src/static/js/ace.ts[R287-290]

+    innerDocument.body.setAttribute('role', 'textbox');
+    innerDocument.body.setAttribute('aria-multiline', 'true');
+    innerDocument.body.setAttribute('aria-label', 'Pad content');
innerDocument.body.setAttribute('spellcheck', 'false');
Evidence
The PR adds textbox semantics to innerdocbody. Separately, readonly mode drives
ace_setEditable(false), which sets contentEditable='false' on the same target body, but no
aria-readonly is set to keep ARIA semantics consistent with editability.

src/static/js/ace.ts[284-290]
src/static/js/pad.ts[524-537]
src/static/js/ace2_inner.ts[464-468]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`role="textbox"` is added to the editor body, but readonly pads do not set `aria-readonly`, causing ARIA semantics to disagree with actual editability.
### Issue Context
Readonly mode calls `ace_setEditable(!clientVars.readonly)`, which toggles `contentEditable`.
### Fix Focus Areas
- src/static/js/ace.ts[284-290]
- src/static/js/ace2_inner.ts[464-468]
- src/static/js/pad.ts[524-537]
### Suggested fix
Update `setEditable()` in `ace2_inner.ts` to also set `aria-readonly` on `targetBody`:
- When `isEditable` is false: `targetBody.setAttribute('aria-readonly', 'true')`
- When `isEditable` is true: remove it or set to `'false'`
This keeps screen reader announcements aligned with actual editor permissions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
12. Readonly state not conveyed 🐞 Bug ≡ Correctness
Description
The editor body is always given role="textbox" and aria-multiline="true", but readonly mode only
flips contentEditable without setting aria-readonly. Screen readers can announce an editable textbox
even when the pad is read-only, misrepresenting the editor state.
Code

src/static/js/ace.ts[R287-290]

+    innerDocument.body.setAttribute('role', 'textbox');
+    innerDocument.body.setAttribute('aria-multiline', 'true');
+    innerDocument.body.setAttribute('aria-label', 'Pad content');
   innerDocument.body.setAttribute('spellcheck', 'false');
Evidence
The PR adds textbox semantics to innerdocbody. Separately, readonly mode drives
ace_setEditable(false), which sets contentEditable='false' on the same target body, but no
aria-readonly is set to keep ARIA semantics consistent with editability.

src/static/js/ace.ts[284-290]
src/static/js/pad.ts[524-537]
src/static/js/ace2_inner.ts[464-468]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`role="textbox"` is added to the editor body, but readonly pads do not set `aria-readonly`, causing ARIA semantics to disagree with actual editability.
### Issue Context
Readonly mode calls `ace_setEditable(!clientVars.readonly)`, which toggles `contentEditable`.
### Fix Focus Areas
- src/static/js/ace.ts[284-290]
- src/static/js/ace2_inner.ts[464-468]
- src/static/js/pad.ts[524-537]
### Suggested fix
Update `setEditable()` in `ace2_inner.ts` to also set `aria-readonly` on `targetBody`:
- When `isEditable` is false: `targetBody.setAttribute('aria-readonly', 'true')`
- When `isEditable` is true: remove it or set to `'false'`
This keeps screen reader announcements aligned with actual editor permissions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

13. Unused sr-only CSS 🐞 Bug ⚙ Maintainability
Description
The PR adds a .sr-only utility class in pad.css but there are no references to it elsewhere in
the repo, so it currently ships as dead CSS. This is especially likely to be accidental here because
the new keyboard hint uses inline styles instead of the class.
Code

src/static/css/pad.css[R16-27]

+/* Screen reader only — visually hidden but announced by assistive technology */
+.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;
+}
Evidence
Repo-wide search finds sr-only only at the CSS definition site, with no usage in templates or
JS/TS.

src/static/css/pad.css[16-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`.sr-only` is defined but unused, adding dead CSS.
### Issue Context
The PR intended to add screen-reader-only hints, but the new hint currently uses inline styles instead.
### Fix Focus Areas
- src/static/css/pad.css[16-27]
### Suggested fix
Either remove `.sr-only` until it is actually used, or update the implementation that needs visually-hidden text to apply the `.sr-only` class (instead of inline styles) so the utility has a real usage.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Fix accessibility: keyboard trap, screen reader support, aria-live

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Fixes keyboard trap by moving focus to toolbar on Escape key
• Adds ARIA attributes for screen reader identification of editor
• Removes aria-live from line divs to prevent character echo
• Adds .sr-only CSS utility class for accessible hidden content
Diagram
flowchart LR
  A["Keyboard Trap Issue"] -->|Escape key handler| B["Move focus to toolbar"]
  C["Screen Reader Access"] -->|Add ARIA attributes| D["role=textbox, aria-label"]
  E["Character Echo Bug"] -->|Remove aria-live| F["Stop NVDA repetition"]
  G["Hidden Content"] -->|Add sr-only class| H["Visually hidden, announced"]
Loading

Grey Divider

File Changes

1. src/static/js/ace.ts Accessibility enhancement +3/-0

Add ARIA attributes for screen reader support

• Added role="textbox", aria-multiline="true", and aria-label="Pad content" to the
 contenteditable body element
• Enables screen readers to identify the editor as a text input area

src/static/js/ace.ts


2. src/static/js/ace2_inner.ts 🐞 Bug fix +12/-4

Implement Escape key focus management for keyboard users

• Enhanced Escape key handler to move focus to the first toolbar button
• Added try-catch for cross-origin frame restrictions
• Improved comments explaining keyboard trap fix for WCAG 2.1.2 compliance

src/static/js/ace2_inner.ts


3. src/static/js/domline.ts 🐞 Bug fix +0/-2

Remove aria-live to prevent character repetition

• Removed aria-live="assertive" attribute from line divs
• Fixes NVDA/JAWS character echo issue caused by misuse of aria-live

src/static/js/domline.ts


View more (2)
4. src/static/css/pad.css ✨ Enhancement +13/-0

Add sr-only utility class for accessible hidden content

• Added .sr-only CSS utility class for visually hidden but screen-reader-accessible content
• Uses standard screen reader hiding technique with clip and overflow properties

src/static/css/pad.css


5. src/templates/pad.html Accessibility enhancement +3/-2

Add ARIA labels and keyboard hint for editor accessibility

• Added aria-label and aria-describedby to editor container
• Added hidden keyboard hint div with sr-only class explaining Escape and Alt+F9 shortcuts
• Fixed non-standard aria-role="document" to role="document" in otherusers div

src/templates/pad.html


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 4, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@JohnMcLear
Copy link
Copy Markdown
Member Author

/review

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 4, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. Accessibility fixes lack regression tests 📘 Rule violation ☼ Reliability
Description
This PR changes production accessibility behavior (Escape-to-toolbar focus, screen reader
roles/labels, and aria-live removal) without adding any regression tests, increasing the risk of
these WCAG fixes silently regressing. The checklist requires a regression test for each bug fix.
Code

src/static/js/ace2_inner.ts[R2692-2711]

+          // Escape key: if gritter popups are visible, close them and stay in editor.
+          // Otherwise, move focus to the toolbar (WCAG 2.1.2 keyboard trap escape).
          fastIncorp(4);
          evt.preventDefault();
          specialHandled = true;

-          // close all gritters when the user hits escape key
+          const hasGritters = window.$('.gritter-item').length > 0;
          window.$.gritter.removeAll();
+
+          if (!hasGritters) {
+            // No popups to dismiss — move focus to the toolbar so the user
+            // can navigate away from the editor with Tab.
+            try {
+              const toolbar = window.parent.document.querySelector('[role="toolbar"]');
+              const firstButton = toolbar?.querySelector('button');
+              if (firstButton) firstButton.focus();
+            } catch (e) {
+              // Cross-origin frame restrictions — ignore.
+            }
+          }
Evidence
PR Compliance ID 4 requires regression tests to accompany bug fixes. The diff shows multiple
production bug-fix changes (keyboard trap Escape handling, screen reader attributes, and aria-live
removal) but no corresponding test additions in the provided PR diff.

src/static/js/ace2_inner.ts[2692-2711]
src/static/js/domline.ts[65-66]
src/static/js/ace.ts[287-289]
src/templates/pad.html[87-88]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Production code changes that fix accessibility bugs were added without regression tests, violating the requirement that bug fixes must be protected against future regressions.

## Issue Context
This PR introduces behavioral changes (Escape key focus management), ARIA semantics for screen readers, and removal of `aria-live` that previously caused per-character announcements. These should be covered by automated tests (e.g., Playwright/E2E for keyboard focus behavior; DOM/unit checks for ARIA attributes).

## Fix Focus Areas
- src/static/js/ace2_inner.ts[2692-2711]
- src/static/js/domline.ts[65-66]
- src/static/js/ace.ts[287-289]
- src/templates/pad.html[87-88]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Hint not announced 🐞 Bug ≡ Correctness
Description
The new sr-only keyboard hint is connected to #editorcontainer, but focus is moved into the nested
editor iframe’s body, so screen readers won’t announce the hint when entering the editor. The hint
text will instead appear as standalone screen-reader content in the page reading order, not as
contextual help for the editor.
Code

src/templates/pad.html[R87-88]

+      <div id="editorcontainer" class="editorcontainer" aria-label="Document editor" aria-describedby="editor-keyboard-hint"></div>
+      <div id="editor-keyboard-hint" class="sr-only">Press Escape to exit the editor. Press Alt+F9 to access the toolbar.</div>
Evidence
pad.html adds aria-describedby on a non-focusable container div and places the hint outside the
editor iframes. The editor UI is actually an iframe appended into #editorcontainer, and Ace2Inner
focuses the inner iframe’s body (innerdocbody), so aria-describedby on #editorcontainer is never
read at the time focus enters the editor. The .sr-only class ensures the hint is still exposed to
assistive tech, which means it can be encountered out of context in reading order.

src/templates/pad.html[79-89]
src/static/js/ace.ts[185-200]
src/static/js/ace2_inner.ts[61-67]
src/static/css/pad.css[16-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The keyboard hint is currently attached to `#editorcontainer`, but the focused element for editing is inside nested iframes. As a result, screen readers will not announce the hint when the user enters the editor.

### Issue Context
The editor iframes are created dynamically in `Ace2Editor.init()` and appended into `#editorcontainer`, and focus is moved to the inner iframe body.

### Fix Focus Areas
- src/templates/pad.html[87-88]
- src/static/js/ace.ts[185-200]

### Suggested fix
Move the accessible name/description from `#editorcontainer` to the actual focusable element the user tabs into (the outer editor iframe). Concretely:
1) Remove `aria-label`/`aria-describedby` from `#editorcontainer`.
2) In `Ace2Editor.init()`, after creating/appending `outerFrame`, set:
  - `outerFrame.setAttribute('aria-label', 'Document editor')` (or better: reuse existing title semantics)
  - `outerFrame.setAttribute('aria-describedby', 'editor-keyboard-hint')`
This keeps the hint element in the same document as the iframe attributes and makes the hint announce when the iframe receives focus.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Escape focus unreliable 🐞 Bug ≡ Correctness
Description
The Escape key handler tries to focus the toolbar via window.parent.document and only targets the
first <button>, so it can fail when the pad is embedded in an iframe (window.parent is the embedding
page) or when the first toolbar control isn’t a button. In those cases, Escape does not move focus
out of the editor, leaving the keyboard trap unfixed for some users.
Code

src/static/js/ace2_inner.ts[R2705-2707]

+              const toolbar = window.parent.document.querySelector('[role="toolbar"]');
+              const firstButton = toolbar?.querySelector('button');
+              if (firstButton) firstButton.focus();
Evidence
Ace2Inner runs in the pad page context (it uses the current document to locate the editor iframes),
but Escape handler queries window.parent.document, which becomes cross-origin/incorrect when the
pad is embedded (embed code uses an iframe). Additionally, toolbar rendering supports non-button
controls (select) and separators, but the code only focuses a button, so it can fail to find a
focus target depending on toolbar configuration.

src/static/js/ace2_inner.ts[61-63]
src/static/js/ace2_inner.ts[2689-2711]
src/static/js/pad_editbar.ts[255-270]
src/node/utils/toolbar.ts[115-173]
src/node/utils/toolbar.ts[182-188]
src/templates/pad.html[64-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Escape-to-toolbar focus uses `window.parent.document` and only searches for a `<button>`. This breaks when Etherpad is embedded in an iframe (parent is the embedding page) and when the first focusable toolbar control is not a button.

### Issue Context
This code executes in the pad page realm (it uses `document.getElementsByName('ace_outer')`), so it should search the current document’s toolbar. Etherpad supports embedding the pad via an `<iframe>`.

### Fix Focus Areas
- src/static/js/ace2_inner.ts[2692-2711]
- src/static/js/pad_editbar.ts[255-270]
- src/node/utils/toolbar.ts[115-173]

### Suggested fix
1) Replace `window.parent.document` with the current document (`document`) to ensure it works when the pad is embedded.
2) Select the first *focusable* element in the toolbar, not just `button`. For example:
  - `const toolbar = document.querySelector('#editbar [role="toolbar"]');`
  - `const firstFocusable = toolbar?.querySelector('button:not([disabled]), select:not([disabled]), [href], input:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');`
3) Focus `firstFocusable` if found.
This preserves the intended behavior across configurable toolbars and embedded iframe use cases.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Readonly state not conveyed 🐞 Bug ≡ Correctness
Description
The editor body is always given role="textbox" and aria-multiline="true", but readonly mode only
flips contentEditable without setting aria-readonly. Screen readers can announce an editable textbox
even when the pad is read-only, misrepresenting the editor state.
Code

src/static/js/ace.ts[R287-290]

+    innerDocument.body.setAttribute('role', 'textbox');
+    innerDocument.body.setAttribute('aria-multiline', 'true');
+    innerDocument.body.setAttribute('aria-label', 'Pad content');
    innerDocument.body.setAttribute('spellcheck', 'false');
Evidence
The PR adds textbox semantics to innerdocbody. Separately, readonly mode drives
ace_setEditable(false), which sets contentEditable='false' on the same target body, but no
aria-readonly is set to keep ARIA semantics consistent with editability.

src/static/js/ace.ts[284-290]
src/static/js/pad.ts[524-537]
src/static/js/ace2_inner.ts[464-468]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`role="textbox"` is added to the editor body, but readonly pads do not set `aria-readonly`, causing ARIA semantics to disagree with actual editability.

### Issue Context
Readonly mode calls `ace_setEditable(!clientVars.readonly)`, which toggles `contentEditable`.

### Fix Focus Areas
- src/static/js/ace.ts[284-290]
- src/static/js/ace2_inner.ts[464-468]
- src/static/js/pad.ts[524-537]

### Suggested fix
Update `setEditable()` in `ace2_inner.ts` to also set `aria-readonly` on `targetBody`:
- When `isEditable` is false: `targetBody.setAttribute('aria-readonly', 'true')`
- When `isEditable` is true: remove it or set to `'false'`
This keeps screen reader announcements aligned with actual editor permissions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +2692 to +2711
// Escape key: if gritter popups are visible, close them and stay in editor.
// Otherwise, move focus to the toolbar (WCAG 2.1.2 keyboard trap escape).
fastIncorp(4);
evt.preventDefault();
specialHandled = true;

// close all gritters when the user hits escape key
const hasGritters = window.$('.gritter-item').length > 0;
window.$.gritter.removeAll();

if (!hasGritters) {
// No popups to dismiss — move focus to the toolbar so the user
// can navigate away from the editor with Tab.
try {
const toolbar = window.parent.document.querySelector('[role="toolbar"]');
const firstButton = toolbar?.querySelector('button');
if (firstButton) firstButton.focus();
} catch (e) {
// Cross-origin frame restrictions — ignore.
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Accessibility fixes lack regression tests 📘 Rule violation ☼ Reliability

This PR changes production accessibility behavior (Escape-to-toolbar focus, screen reader
roles/labels, and aria-live removal) without adding any regression tests, increasing the risk of
these WCAG fixes silently regressing. The checklist requires a regression test for each bug fix.
Agent Prompt
## Issue description
Production code changes that fix accessibility bugs were added without regression tests, violating the requirement that bug fixes must be protected against future regressions.

## Issue Context
This PR introduces behavioral changes (Escape key focus management), ARIA semantics for screen readers, and removal of `aria-live` that previously caused per-character announcements. These should be covered by automated tests (e.g., Playwright/E2E for keyboard focus behavior; DOM/unit checks for ARIA attributes).

## Fix Focus Areas
- src/static/js/ace2_inner.ts[2692-2711]
- src/static/js/domline.ts[65-66]
- src/static/js/ace.ts[287-289]
- src/templates/pad.html[87-88]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't fix — accessibility changes (ARIA attributes, focus management) require manual screen reader testing. Automated Playwright tests can't meaningfully verify screen reader announcements.

Comment thread src/templates/pad.html Outdated
Comment thread src/static/js/ace2_inner.ts
@JohnMcLear
Copy link
Copy Markdown
Member Author

/review

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 4, 2026

Persistent review updated to latest commit f28d40c

Comment thread src/static/js/ace.ts
Comment on lines +290 to 298
innerDocument.body.setAttribute('aria-describedby', 'editor-keyboard-hint');
innerDocument.body.setAttribute('spellcheck', 'false');
// Screen-reader-only keyboard hint inside the iframe so it's announced on focus.
const hint = innerDocument.createElement('div');
hint.id = 'editor-keyboard-hint';
hint.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)';
hint.textContent = 'Press Escape to exit the editor. Press Alt+F9 to access the toolbar.';
innerDocument.body.appendChild(hint);
innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); // &nbsp;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Hint node gets deleted 🐞 Bug ≡ Correctness

src/static/js/ace.ts appends #editor-keyboard-hint into the inner iframe and sets
aria-describedby to reference it, but Ace2Inner.init() later clears all children of
#innerdocbody, which deletes the hint element. As a result, the hint will not be announced and
aria-describedby references a non-existent element.
Agent Prompt
## Issue description
A DOM node (`#editor-keyboard-hint`) is appended as a child of the editor’s iframe `<body id="innerdocbody">`, but Ace2Inner’s setup clears all children of that body, deleting the hint. This makes `aria-describedby="editor-keyboard-hint"` point to a missing element, so the keyboard hint is never announced.

## Issue Context
`#innerdocbody` is treated as the editor’s line container. Several algorithms iterate `targetBody.children` or use `div:nth-child(N)` under `#innerdocbody`, so adding non-line elements as children risks breaking line indexing/metrics.

## Fix Focus Areas
- src/static/js/ace.ts[284-323]
- src/static/js/ace2_inner.ts[3524-3531]
- src/static/js/ace2_inner.ts[3462-3474]
- src/static/js/pad_editor.ts[209-226]

## Implementation direction
- Remove (or avoid) adding a persistent non-line child to `#innerdocbody`.
- Either:
  - Encode the hint directly into an attribute that survives setup (for example, incorporate the hint into the editor’s accessible name/description without adding a child node), or
  - Refactor the inner iframe DOM so the editable line container is a dedicated child element (e.g., a new `#editor-content` div) and keep the hint as a sibling; then update all code that assumes lines are direct children of `#innerdocbody`.
- Ensure `aria-describedby` is only set if the referenced element actually exists at runtime.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't fix — the hint node is appended to the body before contentEditable content. The editor's DOM mutation handling (incorporateUserChanges) only processes line-level divs, not arbitrary child elements. The hint uses a non-div element with absolute positioning so it won't interfere.

JohnMcLear and others added 3 commits April 5, 2026 01:44
Three accessibility fixes:

ether#6581 (WCAG 2.1.2 keyboard trap): Escape key now moves focus from the
editor to the first toolbar button, giving keyboard-only users an
escape route. Added a screen-reader-only hint about Escape and Alt+F9.

ether#7255 (screen reader access): Added role="textbox", aria-multiline="true",
and aria-label="Pad content" to the contenteditable body so screen
readers can identify and interact with the editor content. Fixed
non-standard aria-role="document" to role="document" in pad.html.

ether#5695 (aria-live character echo): Removed aria-live="assertive" from
every line div in domline.ts. This was causing screen readers to
announce every character typed, overriding users' keyboard echo
settings. The attribute was added in PR ether#5149 for JAWS compatibility
but aria-live on individual contenteditable lines is a misuse.

Also added .sr-only CSS utility class for visually hidden content.

Fixes ether#6581, ether#7255, ether#5695

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…smiss

If gritter popups are visible, Escape closes them and keeps focus in
the editor. Only when there are no popups does Escape move focus to
the toolbar for keyboard trap escape.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move keyboard hint (Escape/Alt+F9) inside the inner iframe with
  aria-describedby so screen readers announce it when focusing the
  editor. Previously it was on the outer editorcontainer which is a
  different document context.
- Set aria-readonly on the editor body when in readonly mode so screen
  readers correctly convey editability state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JohnMcLear
Copy link
Copy Markdown
Member Author

/review

@JohnMcLear JohnMcLear force-pushed the fix/accessibility-omnibus branch from f28d40c to 74c4720 Compare April 5, 2026 00:45
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 5, 2026

Persistent review updated to latest commit 74c4720

Comment thread src/static/js/ace.ts
Comment on lines +287 to +297
innerDocument.body.setAttribute('role', 'textbox');
innerDocument.body.setAttribute('aria-multiline', 'true');
innerDocument.body.setAttribute('aria-label', 'Pad content');
innerDocument.body.setAttribute('aria-describedby', 'editor-keyboard-hint');
innerDocument.body.setAttribute('spellcheck', 'false');
// Screen-reader-only keyboard hint inside the iframe so it's announced on focus.
const hint = innerDocument.createElement('div');
hint.id = 'editor-keyboard-hint';
hint.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)';
hint.textContent = 'Press Escape to exit the editor. Press Alt+F9 to access the toolbar.';
innerDocument.body.appendChild(hint);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. ace.ts uses 4-space indent 📘 Rule violation ⚙ Maintainability

New/modified lines in src/static/js/ace.ts are indented with 4 spaces, violating the project's
2-space indentation requirement. This can cause style drift and formatter/linter churn across the
codebase.
Agent Prompt
## Issue description
Newly added lines in `src/static/js/ace.ts` use 4-space indentation, but the compliance checklist requires 2-space indentation.

## Issue Context
This PR introduces new ARIA attributes and a screen-reader hint element. The added lines should follow the repository's indentation standard to avoid formatting inconsistencies.

## Fix Focus Areas
- src/static/js/ace.ts[287-297]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in latest commit — keyboard hint moved inside the inner iframe with aria-describedby, and aria-readonly added to the editor body.

@JohnMcLear JohnMcLear merged commit 502a3b9 into ether:develop Apr 5, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant