From c14896a64089ed3aed0d8bf04d1e9b3d24e4e3e4 Mon Sep 17 00:00:00 2001 From: abaevbog Date: Thu, 10 Oct 2024 14:08:32 -0700 Subject: [PATCH] vpat 44: scaffold keyboard tab selection focus (#4069) Do not move focus from the tab onto the editor/input during keyboard navigation to not change context per https://www.w3.org/WAI/WCAG21/Understanding/on-input. Focus will still shift if tab selection changed on mouse click. Also: - added focus ring to tabs. Additional mouseup handling to prevent the focus ring from briefly appearing on click. - on Escape from within the editor, focus the current tab. - on shift-tab from the beginning of the editor, tab out of the editor to previous element. --- chrome/content/scaffold/scaffold.js | 77 ++++++++++++++++++++------ chrome/content/scaffold/scaffold.xhtml | 5 +- scss/scaffold.scss | 9 ++- 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/chrome/content/scaffold/scaffold.js b/chrome/content/scaffold/scaffold.js index 08b79a1948..f5d7d1f8dc 100644 --- a/chrome/content/scaffold/scaffold.js +++ b/chrome/content/scaffold/scaffold.js @@ -103,6 +103,15 @@ var Scaffold = new function () { }); document.getElementById('tabpanels').addEventListener('select', event => Scaffold.handleTabSelect(event)); + document.getElementById('tabs').addEventListener('mousedown', (event) => { + // Record if tab selection will happen due to a mouse click vs keyboard nav. + if (event.clientX === 0 && event.clientY === 0) return; + document.getElementById('tabs').setAttribute("clicked", true); + }, true); + // Record that click has happened for better focus-ring handling in the stylesheet + document.addEventListener("mouseup", (_) => { + document.getElementById('tabs').removeAttribute("clicked"); + }); let lastTranslatorID = Zotero.Prefs.get('scaffold.lastTranslatorID'); if (lastTranslatorID) { @@ -157,6 +166,10 @@ var Scaffold = new function () { this.initCodeEditor(); this.initTestsEditor(); + this.addEditorKeydownHandlers(_editors.import); + this.addEditorKeydownHandlers(_editors.code); + this.addEditorKeydownHandlers(_editors.tests); + // Set font size from general pref Zotero.UIProperties.registerRoot(document.getElementById('scaffold-pane')); @@ -864,24 +877,31 @@ var Scaffold = new function () { return; } - // Focus editor when switching to tab var tab = document.getElementById('tabs').selectedItem.id.match(/^tab-(.+)$/)[1]; - switch (tab) { - case 'import': - case 'code': - case 'tests': - // the select event's default behavior is to focus the selected tab. - // we don't want to prevent *all* of the event's default behavior, - // but we do want to focus the editor instead of the tab. - // so this stupid hack waits 10 ms for event processing to finish - // before focusing the editor. - setTimeout(() => { - document.getElementById(`editor-${tab}`).focus(); - _editors[tab].focus(); - }, 10); - break; + let tabPanel = document.getElementById("left-tabbox").selectedPanel; + // The select event's default behavior is to focus the selected tab. + // we don't want to prevent *all* of the event's default behavior, + // but we do want to focus an element inside of tabpanel instead of the tab + // (unless tabs are being navigated via keyboard) + // so this stupid hack focuses the desired element after skipping a tick + if (document.getElementById('tabs').getAttribute("clicked")) { + setTimeout(() => { + let toFocus = tabPanel.querySelector("[focus-on-tab-select]"); + if (toFocus) { + toFocus.focus(); + // activate editor that is being focused, if any + if (toFocus.src.includes("monaco.html")) { + _editors[tab].focus(); + } + } + else { + // if no specific element set, just tab into the panel + setTimeout(() => { + Services.focus.moveFocus(window, document.getElementById('tabs').selectedItem, Services.focus.MOVEFOCUS_FORWARD, 0); + }); + } + }); } - let codeTabBroadcaster = document.getElementById('code-tab-only'); if (tab == 'code') { codeTabBroadcaster.removeAttribute('disabled'); @@ -907,6 +927,31 @@ var Scaffold = new function () { } }; + // Add special keydown handling for the editors + this.addEditorKeydownHandlers = function (editor) { + let doc = editor.getDomNode().ownerDocument; + let tabbox = document.getElementById("left-tabbox"); + // On shift-tab from the start of the first line, tab out of the editor. + // Use capturing listener, since Shift-Tab keydown events do not propagate to the document. + doc.addEventListener("keydown", (event) => { + if (event.key == "Tab" && event.shiftKey) { + let position = editor.getPosition(); + if (position.column == 1 && position.lineNumber == 1) { + Services.focus.moveFocus(window, event.target, Services.focus.MOVEFOCUS_BACKWARD, 0); + event.preventDefault(); + } + } + }, true); + // On Escape, focus the selected tab. Use non-capturing listener to not + // do anything on Escape events handled by the editor (e.g. to dismiss autocomplete popup) + doc.addEventListener("keydown", (event) => { + if (event.key == "Escape") { + tabbox.selectedTab.focus(); + } + }); + }; + + this.listFieldsForItemType = function (itemType) { var outputObject = {}; outputObject.itemType = Zotero.ItemTypes.getName(itemType); diff --git a/chrome/content/scaffold/scaffold.xhtml b/chrome/content/scaffold/scaffold.xhtml index ce9224326f..e2b8ae08c0 100644 --- a/chrome/content/scaffold/scaffold.xhtml +++ b/chrome/content/scaffold/scaffold.xhtml @@ -468,7 +468,7 @@ -