Skip to content

Commit 1c106fb

Browse files
committed
fix: refocus search input on repeated Cmd+F
When the search bar was already open, Cmd+F was a no-op because setting the isOpen atom to true again had no effect. Fix uses a per-instance focusInput atom (a monotonic counter) on SearchAtoms: activateSearch increments it for the focused block's search instance, and SearchComponent watches it to call focus()+select() on its own inputRef — avoiding a global DOM query that would target the wrong block when multiple searches are open simultaneously. The counter is reset to 0 on close so re-opening doesn't re-trigger the focus effect.
1 parent 0c7392e commit 1c106fb

File tree

3 files changed

+26
-2
lines changed

3 files changed

+26
-2
lines changed

frontend/app/element/search.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const SearchComponent = ({
2626
caseSensitive: caseSensitiveAtom,
2727
wholeWord: wholeWordAtom,
2828
isOpen: isOpenAtom,
29+
focusInput: focusInputAtom,
2930
anchorRef,
3031
offsetX = 10,
3132
offsetY = 10,
@@ -37,6 +38,8 @@ const SearchComponent = ({
3738
const [search, setSearch] = useAtom<string>(searchAtom);
3839
const [index, setIndex] = useAtom<number>(indexAtom);
3940
const [numResults, setNumResults] = useAtom<number>(numResultsAtom);
41+
const [focusInputCounter, setFocusInputCounter] = useAtom<number>(focusInputAtom);
42+
const inputRef = useRef<HTMLInputElement>(null);
4043

4144
const handleOpenChange = useCallback((open: boolean) => {
4245
setIsOpen(open);
@@ -47,6 +50,7 @@ const SearchComponent = ({
4750
setSearch("");
4851
setIndex(0);
4952
setNumResults(0);
53+
setFocusInputCounter(0);
5054
}
5155
}, [isOpen]);
5256

@@ -56,6 +60,15 @@ const SearchComponent = ({
5660
onSearch?.(search);
5761
}, [search]);
5862

63+
// When activateSearch fires while already open, it increments focusInputCounter
64+
// to signal this specific instance to grab focus (avoids global DOM queries).
65+
useEffect(() => {
66+
if (focusInputCounter > 0 && isOpen) {
67+
inputRef.current?.focus();
68+
inputRef.current?.select();
69+
}
70+
}, [focusInputCounter]);
71+
5972
const middleware: Middleware[] = [];
6073
const offsetCallback = useCallback(
6174
({ rects }) => {
@@ -146,6 +159,7 @@ const SearchComponent = ({
146159
<FloatingPortal>
147160
<div className="search-container" style={{ ...floatingStyles }} ref={refs.setFloating}>
148161
<Input
162+
ref={inputRef}
149163
placeholder="Search"
150164
value={search}
151165
onChange={setSearch}
@@ -197,6 +211,7 @@ export function useSearch(options?: SearchOptions): SearchProps {
197211
resultsIndex: atom(0),
198212
resultsCount: atom(0),
199213
isOpen: atom(false),
214+
focusInput: atom(0),
200215
regex: options?.regex !== undefined ? atom(options.regex) : undefined,
201216
caseSensitive: options?.caseSensitive !== undefined ? atom(options.caseSensitive) : undefined,
202217
wholeWord: options?.wholeWord !== undefined ? atom(options.wholeWord) : undefined,

frontend/app/store/keymodel.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,15 @@ function registerGlobalKeys() {
695695
return false;
696696
}
697697
if (bcm.viewModel.searchAtoms) {
698-
globalStore.set(bcm.viewModel.searchAtoms.isOpen, true);
698+
if (globalStore.get(bcm.viewModel.searchAtoms.isOpen)) {
699+
// Already open — increment the focusInput counter so this block's
700+
// SearchComponent focuses its own input (avoids a global DOM query
701+
// that could target the wrong block when multiple searches are open).
702+
const cur = globalStore.get(bcm.viewModel.searchAtoms.focusInput) as number;
703+
globalStore.set(bcm.viewModel.searchAtoms.focusInput, cur + 1);
704+
} else {
705+
globalStore.set(bcm.viewModel.searchAtoms.isOpen, true);
706+
}
699707
return true;
700708
}
701709
return false;

frontend/types/custom.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Copyright 2026, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import type { WaveEnv } from "@/app/waveenv/waveenv";
45
import { type Placement } from "@floating-ui/react";
56
import type * as jotai from "jotai";
67
import type * as rxjs from "rxjs";
7-
import type { WaveEnv } from "@/app/waveenv/waveenv";
88

99
declare global {
1010
type GlobalAtomsType = {
@@ -276,6 +276,7 @@ declare global {
276276
resultsIndex: PrimitiveAtom<number>;
277277
resultsCount: PrimitiveAtom<number>;
278278
isOpen: PrimitiveAtom<boolean>;
279+
focusInput: PrimitiveAtom<number>;
279280
regex?: PrimitiveAtom<boolean>;
280281
caseSensitive?: PrimitiveAtom<boolean>;
281282
wholeWord?: PrimitiveAtom<boolean>;

0 commit comments

Comments
 (0)