Skip to content

Commit 9ed86e9

Browse files
authored
fix: retry giveFocus on next animation frame for newly created blocks (#3100)
Fixes #2926 ## Problem Keyboard scrolling (arrow keys) does not work in newly opened blocks until the user mouse-clicks. This is a race condition in BlockFull's focus handling. When `isFocused` becomes true for a newly created block, `setFocusTarget()` calls `viewModel.giveFocus()`. But the view's DOM element (terminal, Monaco editor, webview) may not be mounted yet, so `giveFocus()` returns false and focus falls back to the hidden dummy `<input>` element — which cannot handle arrow key scrolling. ## Fix After falling back to the dummy focus element, schedule a `requestAnimationFrame` callback that retries `viewModel.giveFocus()`. This gives React one more frame to flush pending renders and mount the view's DOM, so focus transfers to the real element once it's ready. ## Test Plan - Open a new terminal block — verify arrow keys scroll immediately without clicking - Open a file preview — verify arrow keys scroll the file content - Open a webview — verify keyboard scrolling works - Switch focus between blocks using keyboard shortcuts — verify scrolling works in each - `npx tsc --noEmit` passes clean --------- Signed-off-by: majiayu000 <1835304752@qq.com>
1 parent 4805c59 commit 9ed86e9

File tree

1 file changed

+19
-0
lines changed

1 file changed

+19
-0
lines changed

frontend/app/block/block.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
144144
const focusElemRef = useRef<HTMLInputElement>(null);
145145
const blockRef = useRef<HTMLDivElement>(null);
146146
const contentRef = useRef<HTMLDivElement>(null);
147+
const pendingFocusRafRef = useRef<number | null>(null);
147148
const [blockClicked, setBlockClicked] = useState(false);
148149
const blockView = useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "view")) ?? "";
149150
const isFocused = useAtomValue(nodeModel.isFocused);
@@ -156,6 +157,14 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
156157
const innerRect = useDebouncedNodeInnerRect(nodeModel);
157158
const noPadding = useAtomValueSafe(viewModel.noPadding);
158159

160+
useEffect(() => {
161+
return () => {
162+
if (pendingFocusRafRef.current != null) {
163+
cancelAnimationFrame(pendingFocusRafRef.current);
164+
}
165+
};
166+
}, []);
167+
159168
useLayoutEffect(() => {
160169
setBlockClicked(isFocused);
161170
}, [isFocused]);
@@ -221,11 +230,21 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
221230
);
222231

223232
const setFocusTarget = useCallback(() => {
233+
if (pendingFocusRafRef.current != null) {
234+
cancelAnimationFrame(pendingFocusRafRef.current);
235+
pendingFocusRafRef.current = null;
236+
}
224237
const ok = viewModel?.giveFocus?.();
225238
if (ok) {
226239
return;
227240
}
228241
focusElemRef.current?.focus({ preventScroll: true });
242+
pendingFocusRafRef.current = requestAnimationFrame(() => {
243+
pendingFocusRafRef.current = null;
244+
if (blockRef.current?.contains(document.activeElement)) {
245+
viewModel?.giveFocus?.();
246+
}
247+
});
229248
}, [viewModel]);
230249

231250
const focusFromPointerEnter = useCallback(

0 commit comments

Comments
 (0)