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.
This commit is contained in:
abaevbog 2024-10-10 14:08:32 -07:00 committed by GitHub
parent 15ccf28fb4
commit c14896a640
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 72 additions and 19 deletions

View file

@ -103,6 +103,15 @@ var Scaffold = new function () {
}); });
document.getElementById('tabpanels').addEventListener('select', event => Scaffold.handleTabSelect(event)); 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'); let lastTranslatorID = Zotero.Prefs.get('scaffold.lastTranslatorID');
if (lastTranslatorID) { if (lastTranslatorID) {
@ -157,6 +166,10 @@ var Scaffold = new function () {
this.initCodeEditor(); this.initCodeEditor();
this.initTestsEditor(); this.initTestsEditor();
this.addEditorKeydownHandlers(_editors.import);
this.addEditorKeydownHandlers(_editors.code);
this.addEditorKeydownHandlers(_editors.tests);
// Set font size from general pref // Set font size from general pref
Zotero.UIProperties.registerRoot(document.getElementById('scaffold-pane')); Zotero.UIProperties.registerRoot(document.getElementById('scaffold-pane'));
@ -864,24 +877,31 @@ var Scaffold = new function () {
return; return;
} }
// Focus editor when switching to tab
var tab = document.getElementById('tabs').selectedItem.id.match(/^tab-(.+)$/)[1]; var tab = document.getElementById('tabs').selectedItem.id.match(/^tab-(.+)$/)[1];
switch (tab) { let tabPanel = document.getElementById("left-tabbox").selectedPanel;
case 'import': // The select event's default behavior is to focus the selected tab.
case 'code': // we don't want to prevent *all* of the event's default behavior,
case 'tests': // but we do want to focus an element inside of tabpanel instead of the tab
// the select event's default behavior is to focus the selected tab. // (unless tabs are being navigated via keyboard)
// we don't want to prevent *all* of the event's default behavior, // so this stupid hack focuses the desired element after skipping a tick
// but we do want to focus the editor instead of the tab. if (document.getElementById('tabs').getAttribute("clicked")) {
// so this stupid hack waits 10 ms for event processing to finish setTimeout(() => {
// before focusing the editor. let toFocus = tabPanel.querySelector("[focus-on-tab-select]");
setTimeout(() => { if (toFocus) {
document.getElementById(`editor-${tab}`).focus(); toFocus.focus();
_editors[tab].focus(); // activate editor that is being focused, if any
}, 10); if (toFocus.src.includes("monaco.html")) {
break; _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'); let codeTabBroadcaster = document.getElementById('code-tab-only');
if (tab == 'code') { if (tab == 'code') {
codeTabBroadcaster.removeAttribute('disabled'); 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) { this.listFieldsForItemType = function (itemType) {
var outputObject = {}; var outputObject = {};
outputObject.itemType = Zotero.ItemTypes.getName(itemType); outputObject.itemType = Zotero.ItemTypes.getName(itemType);

View file

@ -468,7 +468,7 @@
</tabpanel> </tabpanel>
<tabpanel flex="1" id="tabpanel-code"> <tabpanel flex="1" id="tabpanel-code">
<vbox flex="1"> <vbox flex="1">
<iframe src="monaco/monaco.html" id="editor-code" flex="1" onmousedown="this.focus()"/> <iframe src="monaco/monaco.html" id="editor-code" focus-on-tab-select="true" flex="1" onmousedown="this.focus()"/>
</vbox> </vbox>
</tabpanel> </tabpanel>
<tabpanel flex="1" id="tabpanel-tests"> <tabpanel flex="1" id="tabpanel-tests">
@ -490,6 +490,7 @@
seltype="multiple" seltype="multiple"
onselect="Scaffold.handleTestSelect(event)" onselect="Scaffold.handleTestSelect(event)"
context="testing-context-menu" context="testing-context-menu"
focus-on-tab-select="true"
/> />
</vbox> </vbox>
<hbox id="testing-buttons"> <hbox id="testing-buttons">
@ -533,7 +534,7 @@
<button observes="validate-tests" label="&scaffold.testing.create.import;" tooltiptext="Create a new test from the current import data" oncommand="Scaffold.saveTestFromCurrent('import')" /> <button observes="validate-tests" label="&scaffold.testing.create.import;" tooltiptext="Create a new test from the current import data" oncommand="Scaffold.saveTestFromCurrent('import')" />
<button observes="validate-tests" label="&scaffold.testing.create.search;" tooltiptext="Create a new test from the current search data" oncommand="Scaffold.saveTestFromCurrent('search')" /> <button observes="validate-tests" label="&scaffold.testing.create.search;" tooltiptext="Create a new test from the current search data" oncommand="Scaffold.saveTestFromCurrent('search')" />
</hbox> </hbox>
<iframe src="monaco/monaco.html" id="editor-import" flex="1" onmousedown="this.focus()"/> <iframe src="monaco/monaco.html" id="editor-import" flex="1" focus-on-tab-select="true" onmousedown="this.focus()"/>
</vbox> </vbox>
</tabpanel> </tabpanel>
</tabpanels> </tabpanels>

View file

@ -82,7 +82,8 @@ browser,
#left-tabbox { #left-tabbox {
flex: 1; flex: 1;
min-width: 500px; min-width: 500px;
margin: 5px; margin: 0 5px 5px 5px;
padding-top: 5px; // padding at the top for focus-ring of tabs to not cutoff tabs focusring
overflow: clip; overflow: clip;
tabpanel { tabpanel {
@ -212,3 +213,9 @@ browser,
} }
} }
} }
// show focusring only during keyboard navigation
#tabs:not([clicked]) tab:focus-visible {
outline: none;
box-shadow: 0 0 0 var(--width-focus-border) var(--color-focus-border);
}