diff --git a/src/common/components/context-menu.js b/src/common/components/context-menu.js index 4e5fe26c..a7e01368 100644 --- a/src/common/components/context-menu.js +++ b/src/common/components/context-menu.js @@ -96,6 +96,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({}); @@ -134,6 +136,33 @@ function ContextMenu({ params, onClose }) { } } + // Select a menuitem from typing, similar to native context menus + 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 = ''; + }, 2000); + + // 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)); + + // Focus the first match + if (candidates.length) { + candidates[0].focus(); + } + } + function handleClick(event, item) { onClose(); event.preventDefault(); @@ -143,7 +172,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 336f7a98..455dfb01 100644 --- a/src/common/components/sidebar/annotations-view.js +++ b/src/common/components/sidebar/annotations-view.js @@ -175,7 +175,7 @@ const AnnotationsView = memo(React.forwardRef((props, ref) => { props.onUpdateAnnotations([annotation]); }, []); - function handlePointerDown() { + function handlePointerDown(event) { pointerDownRef.current = true; } diff --git a/src/common/focus-manager.js b/src/common/focus-manager.js index fdf7e615..4e08e6ac 100644 --- a/src/common/focus-manager.js +++ b/src/common/focus-manager.js @@ -135,6 +135,18 @@ export class FocusManager { e.preventDefault(); this.tabToItem(true); } + // If context menu is opened and a character is typed, forward the event to context menu + // so it can select a menuitem, similar to how native menus do it. + let contextMenu = document.querySelector('.context-menu'); + if (contextMenu && e.key.length == 1 && !e.forwardedToContextMenu && !contextMenu.contains(e.target)) { + let eventCopy = new KeyboardEvent('keydown', { + key: e.key, + bubbles: true + }); + // Mark the event to skip it when it gets captured here to avoid infinite loop + eventCopy.forwardedToContextMenu = true; + contextMenu.dispatchEvent(eventCopy); + } } tabToGroup(reverse) {