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('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);

View file

@ -468,7 +468,7 @@
</tabpanel>
<tabpanel flex="1" id="tabpanel-code">
<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>
</tabpanel>
<tabpanel flex="1" id="tabpanel-tests">
@ -490,6 +490,7 @@
seltype="multiple"
onselect="Scaffold.handleTestSelect(event)"
context="testing-context-menu"
focus-on-tab-select="true"
/>
</vbox>
<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.search;" tooltiptext="Create a new test from the current search data" oncommand="Scaffold.saveTestFromCurrent('search')" />
</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>
</tabpanel>
</tabpanels>

View file

@ -82,7 +82,8 @@ browser,
#left-tabbox {
flex: 1;
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;
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);
}