diff --git a/src/common/components/context-menu.js b/src/common/components/context-menu.js index 00999186..6b0bf59d 100644 --- a/src/common/components/context-menu.js +++ b/src/common/components/context-menu.js @@ -109,6 +109,8 @@ function ContextMenu({ params, onClose }) { const [position, setPosition] = useState({ style: {} }); const [update, setUpdate] = useState(); const containerRef = useRef(); + const searchStringRef = useRef(''); + const searchTimeoutRef = useRef(null); useEffect(() => { setUpdate({}); @@ -147,6 +149,36 @@ function ContextMenu({ params, onClose }) { } } + function handleKeyDown(event) { + let { key } = event; + // Ignore non-characters + if (key.length !== 1 || !key.match(/\S/)) return; + + // Clear search string after 3 seconds of no typing + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + searchTimeoutRef.current = setTimeout(() => { + searchStringRef.current = ''; + }, 3000); + + // Keep track of what has been typed so far + searchStringRef.current += key.toLowerCase(); + + // Find all buttons with text that start with what has been typed + let menuOptions = [...document.querySelectorAll(".context-menu button:not([disabled])")]; + let candidates = menuOptions.filter(option => option.textContent.toLowerCase().startsWith(searchStringRef.current)); + + // If there is only one candidate, click it right away + if (candidates.length == 1) { + candidates[0].click(); + } + // If there are multiple - focus the first one + else if (candidates.length > 1) { + candidates[0].focus(); + } + } + function handleClick(event, item) { onClose(); event.preventDefault(); @@ -156,7 +188,7 @@ function ContextMenu({ params, onClose }) { return (
-
+
{params.itemGroups.map((items, i) => (
{items.map((item, i) => { diff --git a/src/common/components/sidebar/annotations-view.js b/src/common/components/sidebar/annotations-view.js index 9169abe6..34ec5271 100644 --- a/src/common/components/sidebar/annotations-view.js +++ b/src/common/components/sidebar/annotations-view.js @@ -175,7 +175,11 @@ const AnnotationsView = memo(React.forwardRef((props, ref) => { props.onUpdateAnnotations([annotation]); }, []); - function handlePointerDown() { + function handlePointerDown(event) { + // Clicking on the rendered content when a contextmenu is open will + // lead to pointerup event not firing and the annotation becoming not-selectable + // via keyboard until it is clicked. + if (event.target.classList.contains("context-menu-overlay")) return; pointerDownRef.current = true; } diff --git a/src/common/keyboard-manager.js b/src/common/keyboard-manager.js index 077fbd61..63dec15d 100644 --- a/src/common/keyboard-manager.js +++ b/src/common/keyboard-manager.js @@ -71,6 +71,11 @@ export class KeyboardManager { // Escape must be pressed alone. We basically want to prevent // Option-Escape (speak text on macOS) deselecting text if (key === 'Escape') { + // If context menu is opened, close it and let focus return to last active element + if (document.querySelector(".context-menu")) { + this._reader.closeContextMenu(); + return; + } this._reader._lastView.focus(); this._reader.abortPrint(); this._reader._updateState({ diff --git a/src/common/reader.js b/src/common/reader.js index 31ecc2fa..910fdc16 100644 --- a/src/common/reader.js +++ b/src/common/reader.js @@ -626,8 +626,7 @@ class Reader { this._onBringReaderToFront?.(true); this._updateState({ contextMenu: params }); setTimeout(() => { - window.focus(); - document.activeElement.blur(); + document.querySelector(".context-menu button:not([disabled])").focus(); }); }