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