Skip to content

Commit 4f63a70

Browse files
authored
fix: Prevent permission request from stealing focus across cells (#1545)
## Problem ActionSelector unconditionally auto-focuses on mount, stealing focus from the active cell's editor when a permission request arrives in another command center cell. Closes #1503 ## Changes 1. Add isUserInInteractiveElement() guard to skip auto-focus when user is in an input/editor 2. Add autoFocus option to restoreStepAnswer to conditionally skip its focus call on mount 3. Guard both mount-time focus paths so only idle panels auto-focus ## How did you test this? Manually
1 parent aee3c91 commit 4f63a70

2 files changed

Lines changed: 38 additions & 4 deletions

File tree

apps/code/src/renderer/components/action-selector/useActionSelectorState.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type React from "react";
12
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
23
import {
34
filterOtherOptions,
@@ -12,6 +13,29 @@ function needsCustomInput(option: SelectorOption): boolean {
1213
return option.customInput === true || isOtherOption(option.id);
1314
}
1415

16+
function isInteractiveElementInDifferentCell(
17+
containerRef: React.RefObject<HTMLDivElement | null>,
18+
): boolean {
19+
const el = document.activeElement;
20+
if (!(el instanceof HTMLElement)) return false;
21+
22+
const isInteractive =
23+
el.tagName === "INPUT" ||
24+
el.tagName === "TEXTAREA" ||
25+
el.tagName === "SELECT" ||
26+
el.getAttribute("contenteditable") === "true";
27+
if (!isInteractive) return false;
28+
29+
const activeCell = el.closest("[data-grid-cell]");
30+
const ownCell = containerRef.current?.closest("[data-grid-cell]");
31+
32+
// Outside a grid (single-task mode): block focus steal from any interactive element.
33+
// Inside a grid: only block when the interactive element is in a different cell.
34+
if (!activeCell || !ownCell) return true;
35+
36+
return activeCell !== ownCell;
37+
}
38+
1539
interface UseActionSelectorStateProps {
1640
options: SelectorOption[];
1741
multiSelect: boolean;
@@ -51,6 +75,7 @@ export function useActionSelectorState({
5175
() => new Map(),
5276
);
5377
const containerRef = useRef<HTMLDivElement>(null);
78+
const prevActiveStepRef = useRef(currentStep);
5479

5580
const activeStep = internalStep;
5681
const hasSteps = steps !== undefined && steps.length > 1;
@@ -76,7 +101,9 @@ export function useActionSelectorState({
76101
isEditing && selectedOption && needsCustomInput(selectedOption);
77102

78103
useEffect(() => {
79-
containerRef.current?.focus();
104+
if (!isInteractiveElementInDifferentCell(containerRef)) {
105+
containerRef.current?.focus();
106+
}
80107
}, []);
81108

82109
useEffect(() => {
@@ -107,7 +134,7 @@ export function useActionSelectorState({
107134
}, [activeStep, checkedOptions, customInput, onStepAnswer]);
108135

109136
const restoreStepAnswer = useCallback(
110-
(step: number) => {
137+
(step: number, { autoFocus = true }: { autoFocus?: boolean } = {}) => {
111138
const saved = stepAnswers.get(step);
112139
if (saved) {
113140
setCheckedOptions(new Set(saved.selectedIds));
@@ -121,13 +148,19 @@ export function useActionSelectorState({
121148
}
122149
setSelectedIndex(0);
123150
setIsEditing(false);
124-
containerRef.current?.focus();
151+
if (autoFocus) {
152+
containerRef.current?.focus();
153+
}
125154
},
126155
[initialSelections, stepAnswers],
127156
);
128157

129158
useEffect(() => {
130-
restoreStepAnswer(activeStep);
159+
if (activeStep === prevActiveStepRef.current) return;
160+
prevActiveStepRef.current = activeStep;
161+
restoreStepAnswer(activeStep, {
162+
autoFocus: !isInteractiveElementInDifferentCell(containerRef),
163+
});
131164
}, [activeStep, restoreStepAnswer]);
132165

133166
useEffect(() => {

apps/code/src/renderer/features/command-center/components/CommandCenterGrid.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ function GridCell({
103103
// biome-ignore lint/a11y/useKeyWithClickEvents lint/a11y/noStaticElementInteractions: click delegates focus to ActionSelector within
104104
<div
105105
ref={cellRef}
106+
data-grid-cell
106107
className="relative overflow-hidden bg-gray-1"
107108
onClick={handleCellClick}
108109
onPointerDownCapture={handleCellPointerDownCapture}

0 commit comments

Comments
 (0)