From fe0d9f904277172246570d2e035fef3fce8ca545 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 17 Apr 2026 15:10:05 -0400 Subject: [PATCH 1/3] first pass at adding hotkeys for moving focus prev or next --- .../guide/components/Toolbar/V2/FocusChin.tsx | 147 ++++++++++++------ .../guide/components/Toolbar/V2/Kbd.tsx | 17 ++ .../guide/components/Toolbar/V2/V2.tsx | 17 +- 3 files changed, 116 insertions(+), 65 deletions(-) create mode 100644 packages/react/src/modules/guide/components/Toolbar/V2/Kbd.tsx diff --git a/packages/react/src/modules/guide/components/Toolbar/V2/FocusChin.tsx b/packages/react/src/modules/guide/components/Toolbar/V2/FocusChin.tsx index a043433a6..af87c6314 100644 --- a/packages/react/src/modules/guide/components/Toolbar/V2/FocusChin.tsx +++ b/packages/react/src/modules/guide/components/Toolbar/V2/FocusChin.tsx @@ -7,6 +7,7 @@ import { ChevronLeft, ChevronRight, X } from "lucide-react"; import * as React from "react"; import { GUIDE_ROW_DATA_SELECTOR } from "./GuideRow"; +import { Kbd } from "./Kbd"; import { sharedTooltipProps } from "./helpers"; import { InspectionResultOk } from "./useInspectGuideClientStore"; @@ -14,6 +15,9 @@ import { InspectionResultOk } from "./useInspectGuideClientStore"; // reducing how often consecutive next/prev clicks trigger a scroll. const SCROLL_OVERSHOOT = 60; +const FOCUS_PREV_HOTKEY = ","; +const FOCUS_NEXT_HOTKEY = "/"; + const maybeScrollGuideIntoView = (container: HTMLElement, guideKey: string) => { requestAnimationFrame(() => { const el = container.querySelector( @@ -56,14 +60,81 @@ export const FocusChin = ({ guides, guideListRef }: Props) => { })); const focusedKeys = Object.keys(debugSettings?.focusedGuideKeys || {}); - const isFocused = focusedKeys.length > 0; + const currentKey = focusedKeys[0]; + + const focusGuide = React.useCallback( + (guideKey: string) => { + client.setDebug({ + ...client.store.state.debug, + focusedGuideKeys: { [guideKey]: true }, + }); + if (guideListRef.current) { + maybeScrollGuideIntoView(guideListRef.current, guideKey); + } + }, + [client, guideListRef], + ); + + const focusPrevious = React.useCallback(() => { + const selectableGuides = guides.filter( + (g) => !!g.annotation.selectable.status, + ); + if (selectableGuides.length === 0) return; + + if (!currentKey) { + focusGuide(selectableGuides[selectableGuides.length - 1]!.key); + return; + } + + const currIndex = selectableGuides.findIndex((g) => g.key === currentKey); + const prevGuide = + currIndex <= 0 ? undefined : selectableGuides[currIndex - 1]; + if (!prevGuide) return; + focusGuide(prevGuide.key); + }, [guides, currentKey, focusGuide]); + + const focusNext = React.useCallback(() => { + const selectableGuides = guides.filter( + (g) => !!g.annotation.selectable.status, + ); + if (selectableGuides.length === 0) return; + + if (!currentKey) { + focusGuide(selectableGuides[0]!.key); + return; + } + + const currIndex = selectableGuides.findIndex((g) => g.key === currentKey); + const nextGuide = + currIndex < 0 || currIndex + 1 > selectableGuides.length - 1 + ? undefined + : selectableGuides[currIndex + 1]; + if (!nextGuide) return; + focusGuide(nextGuide.key); + }, [guides, currentKey, focusGuide]); + + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!e.ctrlKey || e.repeat) return; + if (e.key === FOCUS_PREV_HOTKEY) { + e.preventDefault(); + focusPrevious(); + } else if (e.key === FOCUS_NEXT_HOTKEY) { + e.preventDefault(); + focusNext(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [focusPrevious, focusNext]); + if (!isFocused) { return null; } - const currentKey = focusedKeys[0]!; - return ( { Focus mode: {currentKey} - + + Focus previous guide + + ctrl + , + + + } + {...sharedTooltipProps} + >