diff --git a/src/static/css/pad.css b/src/static/css/pad.css index 5ef192509a7..63aefe4d72c 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -13,6 +13,19 @@ @import url("pad/loadingbox.css"); @import url("pad/form.css"); +/* 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; +} + html { font-size: 15px; color: #3e3e3e; diff --git a/src/static/js/ace.ts b/src/static/js/ace.ts index 4c062584cde..b2196f5b65b 100644 --- a/src/static/js/ace.ts +++ b/src/static/js/ace.ts @@ -284,7 +284,17 @@ const Ace2Editor = function () { // tag innerDocument.body.id = 'innerdocbody'; innerDocument.body.classList.add('innerdocbody'); + 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); innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //   /* debugLog('Ace2Editor.init() waiting for require kernel load'); diff --git a/src/static/js/ace2_inner.ts b/src/static/js/ace2_inner.ts index 714b7f50777..d0950bccc23 100644 --- a/src/static/js/ace2_inner.ts +++ b/src/static/js/ace2_inner.ts @@ -464,6 +464,7 @@ function Ace2Inner(editorInfo, cssManagers) { const setEditable = (newVal) => { isEditable = newVal; targetBody.contentEditable = isEditable ? 'true' : 'false'; + targetBody.setAttribute('aria-readonly', isEditable ? 'false' : 'true'); targetBody.classList.toggle('static', !isEditable); }; @@ -2689,15 +2690,26 @@ function Ace2Inner(editorInfo, cssManagers) { if (!specialHandled && isTypeForSpecialKey && keyCode === 27 && padShortcutEnabled.esc) { - // prevent esc key; - // in mozilla versions 14-19 avoid reconnecting pad. - + // 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. + } + } } if (!specialHandled && isTypeForCmdKey && /* Do a saved revision on ctrl S */ diff --git a/src/static/js/domline.ts b/src/static/js/domline.ts index 900f60176f4..0cdc465da69 100644 --- a/src/static/js/domline.ts +++ b/src/static/js/domline.ts @@ -62,8 +62,6 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => { if (document) { result.node = document.createElement('div'); - // JAWS and NVDA screen reader compatibility. Only needed if in a real browser. - result.node.setAttribute('aria-live', 'assertive'); } else { result.node = { innerHTML: '', diff --git a/src/templates/pad.html b/src/templates/pad.html index eb93196e266..1c6071f28ba 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -84,7 +84,7 @@ -
+
<% e.begin_block("permissionDenied"); %> @@ -363,7 +363,7 @@

-
+