Improve keyboard navigation in PDF reader tab (#2395)

This commit is contained in:
Martynas Bagdonas 2022-03-12 09:22:13 +02:00 committed by GitHub
parent 6b6c27029b
commit c7972b3d38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 245 additions and 11 deletions

View file

@ -349,6 +349,25 @@
</body>
</method>
<method name="focusFirst">
<body>
<![CDATA[
(async () => {
let n = 0;
while (!this._editorInstance && n++ < 100) {
await Zotero.Promise.delay(10);
}
await this._editorInstance._initPromise;
this._iframe.focus();
try {
this._editorInstance._iframeWindow.document.querySelector('.toolbar-button-return').focus();
} catch(e) {
}
})();
]]>
</body>
</method>
<method name="_id">
<parameter name="id"/>
<body>

View file

@ -28,9 +28,15 @@ import cx from 'classnames';
const MAX_UNEXPANDED_ALL_NOTES = 7;
const NoteRow = memo(({ id, title, body, date, onClick, onContextMenu, parentItemType, parentTitle }) => {
const NoteRow = memo(({ id, title, body, date, onClick, onKeyDown, onContextMenu, parentItemType, parentTitle }) => {
return (
<div className={cx('note-row', { 'standalone-note-row': !parentItemType })} onClick={() => onClick(id)} onContextMenu={(event) => onContextMenu(id, event)}>
<div
tabIndex={-1}
className={cx('note-row', { 'standalone-note-row': !parentItemType })}
onClick={() => onClick(id)}
onContextMenu={(event) => onContextMenu(id, event)}
onKeyDown={onKeyDown}
>
<div className="inner">
{ parentItemType
? <div className="parent-line">
@ -77,6 +83,54 @@ const NotesList = forwardRef(({ onClick, onContextMenu, onAddChildButtonDown, on
_setExpanded(true);
}
function handleButtonKeydown(event) {
if (event.key === 'Tab' && !event.shiftKey) {
let node = event.target.parentElement.parentElement.querySelector('[tabindex="-1"]');
if (node) {
node.focus();
event.preventDefault();
}
}
else if (event.key === 'Tab' && event.shiftKey) {
let prevSection = event.target.parentElement.parentElement.previousElementSibling;
if (prevSection) {
let node = prevSection.querySelector('[tabindex="-1"]:last-child');
if (node) {
node.focus();
event.preventDefault();
}
}
}
}
function handleRowKeyDown(event) {
if (['Enter', 'Space'].includes(event.key)) {
// Focus the previous row, because "more-row" will disappear
if (event.target.classList.contains('more-row')) {
let node = event.target.previousElementSibling;
if (node) {
node.focus();
event.preventDefault();
}
}
event.target.click();
}
else if (event.key === 'ArrowUp') {
let node = event.target.previousElementSibling;
if (node) {
node.focus();
event.preventDefault();
}
}
else if (event.key === 'ArrowDown') {
let node = event.target.nextElementSibling;
if (node) {
node.focus();
event.preventDefault();
}
}
}
let childNotes = notes.filter(x => x.isCurrentChild);
let allNotes = notes.filter(x => !x.isCurrentChild);
let visibleNotes = allNotes.slice(0, expanded ? numVisible : MAX_UNEXPANDED_ALL_NOTES);
@ -85,22 +139,22 @@ const NotesList = forwardRef(({ onClick, onContextMenu, onAddChildButtonDown, on
{hasParent && <section>
<div className="header-row">
<h2>{Zotero.getString('pane.context.itemNotes')}</h2>
<button onMouseDown={onAddChildButtonDown}>+</button>
<button onMouseDown={onAddChildButtonDown} onClick={onAddChildButtonDown} onKeyDown={handleButtonKeydown}>+</button>
</div>
{!childNotes.length && <div className="empty-row">{Zotero.getString('pane.context.noNotes')}</div>}
{childNotes.map(note => <NoteRow key={note.id} {...note}
onClick={onClick} onContextMenu={onContextMenu}/>)}
onClick={onClick} onKeyDown={handleRowKeyDown} onContextMenu={onContextMenu}/>)}
</section>}
<section>
<div className="header-row">
<h2>{Zotero.getString('pane.context.allNotes')}</h2>
<button onMouseDown={onAddStandaloneButtonDown}>+</button>
<button onMouseDown={onAddStandaloneButtonDown} onClick={onAddStandaloneButtonDown} onKeyDown={handleButtonKeydown}>+</button>
</div>
{!allNotes.length && <div className="empty-row">{Zotero.getString('pane.context.noNotes')}</div>}
{visibleNotes.map(note => <NoteRow key={note.id} {...note}
onClick={onClick} onContextMenu={onContextMenu}/>)}
onClick={onClick} onKeyDown={handleRowKeyDown} onContextMenu={onContextMenu}/>)}
{allNotes.length > visibleNotes.length
&& <div className="more-row" onClick={handleClickMore}>{
&& <div className="more-row" tabIndex={-1} onClick={handleClickMore} onKeyDown={handleRowKeyDown}>{
Zotero.getString('general.numMore', Zotero.Utilities.numberFormat(
[allNotes.length - visibleNotes.length], 0))
}</div>

View file

@ -57,6 +57,7 @@ var ZoteroContextPane = new function () {
this.update = _update;
this.getActiveEditor = _getActiveEditor;
this.focus = _focus;
this.init = function () {
if (!Zotero) {
@ -247,6 +248,36 @@ var ZoteroContextPane = new function () {
}
}
function _focus() {
var splitter;
if (Zotero.Prefs.get('layout') == 'stacked') {
splitter = _contextPaneSplitterStacked;
}
else {
splitter = _contextPaneSplitter;
}
if (splitter.getAttribute('state') != 'collapsed') {
if (_panesDeck.selectedIndex == 0) {
var node = _itemPaneDeck.selectedPanel;
node.querySelector('tab[selected]').focus();
return true;
}
else {
var node = _notesPaneDeck.selectedPanel;
if (node.selectedIndex == 0) {
node.querySelector('textbox').focus();
return true;
}
else {
node.querySelector('zoteronoteeditor').focusFirst();
return true;
}
}
}
return false;
}
function _updateAddToNote() {
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
@ -358,6 +389,10 @@ var ZoteroContextPane = new function () {
splitter.setAttribute('state', hide ? 'collapsed' : 'open');
_update();
if (!hide) {
ZoteroContextPane.focus();
}
}
function _getCurrentAttachment() {
@ -455,6 +490,8 @@ var ZoteroContextPane = new function () {
listBox.setAttribute('flex', '1');
var listInner = document.createElementNS(HTML_NS, 'div');
listInner.className = 'notes-list-container';
// Otherwise it can be focused with tab
listInner.tabIndex = -1;
listBox.append(listInner);
list.append(head, listBox);

View file

@ -159,6 +159,22 @@ class ReaderInstance {
this._postMessage({ action: 'setSidebarOpen', open });
}
focusLastToolbarButton() {
this._iframeWindow.focus();
this._postMessage({ action: 'focusLastToolbarButton' });
}
tabToolbar(reverse) {
this._postMessage({ action: 'tabToolbar', reverse });
// Avoid toolbar find button being focused for a short moment
setTimeout(() => this._iframeWindow.focus());
}
focusFirst() {
this._postMessage({ action: 'focusFirst' });
setTimeout(() => this._iframeWindow.focus());
}
async setBottomPlaceholderHeight(height) {
await this._initPromise;
this._postMessage({ action: 'setBottomPlaceholderHeight', height });
@ -738,6 +754,22 @@ class ReaderInstance {
}
return;
}
case 'focusSplitButton': {
let win = Zotero.getMainWindow();
if (win) {
win.document.getElementById('zotero-tb-toggle-item-pane').focus();
}
return;
}
case 'focusContextPane': {
let win = Zotero.getMainWindow();
if (win) {
if (!this._window.ZoteroContextPane.focus()) {
this.focusFirst();
}
}
return;
}
}
}
catch (e) {
@ -822,6 +854,7 @@ class ReaderTab extends ReaderInstance {
this._tabContainer = container;
this._iframe = this._window.document.createElement('browser');
this._iframe.setAttribute('class', 'reader');
this._iframe.setAttribute('flex', '1');
this._iframe.setAttribute('type', 'content');
this._iframe.setAttribute('src', 'resource://zotero/pdf-reader/viewer.html');

View file

@ -515,6 +515,79 @@ var ZoteroPane = new function()
* Trigger actions based on keyboard shortcuts
*/
function handleKeyDown(event, from) {
if (Zotero_Tabs.selectedIndex > 0) {
let itemPaneToggle = document.getElementById('zotero-tb-toggle-item-pane');
let notesPaneToggle = document.getElementById('zotero-tb-toggle-notes-pane');
// Using ArrowDown and ArrowUp to be consistent with pdf-reader
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
if (event.target === itemPaneToggle) {
notesPaneToggle.focus();
}
}
else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
if (event.target === notesPaneToggle) {
itemPaneToggle.focus();
}
else if (event.target === itemPaneToggle) {
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
reader.focusLastToolbarButton();
}
}
}
else if (event.key === 'Tab'
&& [itemPaneToggle, notesPaneToggle].includes(event.target)) {
if (event.shiftKey) {
ZoteroContextPane.focus();
}
else {
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
reader.tabToolbar();
}
}
event.preventDefault();
event.stopPropagation();
}
else if (event.key === 'Escape') {
if (!document.activeElement.classList.contains('reader')) {
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
reader.focus();
event.preventDefault();
event.stopPropagation();
}
}
}
else if (event.key === 'Tab' && event.shiftKey) {
let node = document.activeElement;
if (node && node.nodeType === Node.ELEMENT_NODE && (
node.parentNode.classList.contains('zotero-editpane-tabs')
|| node.getAttribute('type') === 'search'
|| node.getAttribute('anonid') === 'editor-view'
&& node.contentWindow.document.activeElement.classList.contains('toolbar-button-return'))) {
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
reader.focus();
}
event.preventDefault();
event.stopPropagation();
}
}
else if (event.key === 'Tab') {
if (!document.activeElement.classList.contains('reader')) {
setTimeout(() => {
if (document.activeElement.classList.contains('reader')) {
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
reader.focusFirst();
}
}
});
}
}
}
const cmdOrCtrlOnly = Zotero.isMac
? (event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey)
: (event.ctrlKey && !event.shiftKey && !event.altKey);

View file

@ -84,8 +84,8 @@
</box>
<box id="zotero-tab-toolbar" class="toolbar" hidden="true">
<div id="zotero-tb-split" xmlns="http://www.w3.org/1999/xhtml" class="split-button">
<div id="zotero-tb-toggle-item-pane" class="toolbarButton item" title="&zotero.toolbar.context.item;"><span/></div>
<div id="zotero-tb-toggle-notes-pane" class="toolbarButton notes" title="&zotero.toolbar.context.notes;"><span/></div>
<button id="zotero-tb-toggle-item-pane" class="toolbarButton item" title="&zotero.toolbar.context.item;" tabindex="-1"><span/></button>
<button id="zotero-tb-toggle-notes-pane" class="toolbarButton notes" title="&zotero.toolbar.context.notes;" tabindex="-1"><span/></button>
</div>
</box>
<deck id="tabs-deck" flex="1">

@ -1 +1 @@
Subproject commit b97d62e02bdba96ef95a74dcefeab23ce2520eae
Subproject commit cc517ae53bd849e7dc3a958b156552117d209e53

View file

@ -189,6 +189,11 @@ $toolbar-btn-icon-active-offset: 0;
}
}
// Remove strange inner dotted outline that appears on focus
&::-moz-focus-inner {
border: 0;
}
&:hover {
background: $toolbar-btn-hover-bg;
border-color: $toolbar-btn-border-hover-color;

View file

@ -136,3 +136,16 @@
padding-top: 6px !important;
}
}
.note-row, .more-row {
outline: 0;
&:-moz-focusring {
box-shadow: $toolbar-btn-focus-box-shadow;
z-index: 1;
@include retina {
box-shadow: $toolbar-btn-focus-box-shadow-2x;
}
}
}