[eric] dashboard: control/input clicks select card without yanking camera focus

This commit is contained in:
ciregenz
2026-07-19 11:18:58 -07:00
parent 9b440a3c18
commit 6652309f42
5 changed files with 25 additions and 9 deletions
@@ -66,7 +66,7 @@ interface DashboardCanvasProps {
onViewportMouseMove: (e: React.MouseEvent) => void;
onViewportMouseUp: (e: React.MouseEvent) => void;
onViewportDoubleClick: (e: React.MouseEvent) => void;
onCardSelect: (id: string, type: CardType, shiftKey: boolean) => void;
onCardSelect: (id: string, type: CardType, shiftKey: boolean, originTarget?: EventTarget | null) => void;
onDragStart: (id: string, type: CardType) => void;
onDragMove: (dx: number, dy: number, mouseX?: number, mouseY?: number) => void;
onDragEnd: (dx: number, dy: number, didDrag: boolean) => void;
@@ -51,7 +51,7 @@ interface DashboardCardLayerProps {
revealSpawnedRef: RefObject<Set<string>>;
measuredHeightsRef: RefObject<Record<string, number>>;
getCanvasState: () => { panX: number; panY: number; zoom: number };
onCardSelect: (id: string, type: CardType, shiftKey: boolean) => void;
onCardSelect: (id: string, type: CardType, shiftKey: boolean, originTarget?: EventTarget | null) => void;
onDragStart: (id: string, type: CardType) => void;
onDragMove: (dx: number, dy: number, mouseX?: number, mouseY?: number) => void;
onDragEnd: (dx: number, dy: number, didDrag: boolean) => void;
@@ -164,7 +164,7 @@ interface Props {
multiDragDelta?: { dx: number; dy: number } | null;
// Belongs to a non-active dashboard but kept mounted-hidden so its webContents + sessionStorage survive the switch.
keepAliveHidden?: boolean;
onCardSelect?: (id: string, type: 'agent' | 'view' | 'browser', shiftKey: boolean) => void;
onCardSelect?: (id: string, type: 'agent' | 'view' | 'browser', shiftKey: boolean, originTarget?: EventTarget | null) => void;
onDragStart?: (id: string, type: 'agent' | 'view' | 'browser') => void;
onDragMove?: (dx: number, dy: number, mouseX?: number, mouseY?: number) => void;
onDragEnd?: (dx: number, dy: number, didDrag: boolean) => void;
@@ -843,8 +843,8 @@ const BrowserCard: React.FC<Props> = ({
data-keepalive-hidden={keepAliveHidden ? '1' : undefined}
onPointerDownCapture={(e: React.PointerEvent) => {
onBringToFront?.(browserId, 'browser');
// Capture-phase so chrome clicks (tab strip, URL bar) the children swallow still select the card; clicks inside the guest page never reach the host at all. Shift keeps the bubbled toggle path.
if (e.button === 0 && !e.shiftKey) onCardSelect?.(browserId, 'browser', false);
// Capture-phase so chrome clicks (tab strip, URL bar) the children swallow still select the card; clicks inside the guest page never reach the host at all. Shift keeps the bubbled toggle path. Pass the target so URL-bar/tab presses select without yanking the camera.
if (e.button === 0 && !e.shiftKey) onCardSelect?.(browserId, 'browser', false, e.target);
}}
onClick={(e: React.MouseEvent) => {
if (justDraggedRef.current) return;
@@ -64,7 +64,7 @@ interface Props {
color: NoteColor;
cardZOrder?: number;
autoFocus?: boolean;
onCardSelect?: (id: string, type: 'agent' | 'view' | 'browser' | 'note', shiftKey: boolean) => void;
onCardSelect?: (id: string, type: 'agent' | 'view' | 'browser' | 'note', shiftKey: boolean, originTarget?: EventTarget | null) => void;
onDragStart?: (id: string, type: 'agent' | 'view' | 'browser' | 'note') => void;
onDragMove?: (dx: number, dy: number, mouseX?: number, mouseY?: number) => void;
onDragEnd?: (dx: number, dy: number, didDrag: boolean) => void;
@@ -261,8 +261,8 @@ const NoteCard: React.FC<Props> = ({
data-select-meta={JSON.stringify({ name: 'Note', content: content.slice(0, 60) })}
onPointerDownCapture={(e: React.PointerEvent) => {
onBringToFront?.(noteId, 'note');
// Capture-phase so a click the textarea swallows still selects the note; shift keeps the bubbled toggle path.
if (e.button === 0 && !e.shiftKey) onCardSelect?.(noteId, 'note', false);
// Capture-phase so a click the textarea swallows still selects the note; shift keeps the bubbled toggle path. Pass the target so a textarea press selects without yanking the camera.
if (e.button === 0 && !e.shiftKey) onCardSelect?.(noteId, 'note', false, e.target);
}}
onClick={(e: React.MouseEvent) => {
if (justDraggedRef.current) return;
@@ -22,6 +22,19 @@ function isCardTarget(target: EventTarget | null, boundary: EventTarget | null):
return false;
}
const CONTROL_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A', 'WEBVIEW']);
// True when the press landed on a real control (text field, button, browser URL bar/tabs, note textarea, webview) rather than the card's frame. Walk up ONLY to the card root so a button living above the card never counts.
function pressLandedOnControl(target: EventTarget | null | undefined): boolean {
let el = target as HTMLElement | null;
while (el) {
if (el.hasAttribute(SELECT_ATTR)) return false;
if (CONTROL_TAGS.has(el.tagName) || el.isContentEditable || el.getAttribute('role') === 'button') return true;
el = el.parentElement;
}
return false;
}
interface UseDashboardInteractionsArgs {
canvas: Canvas;
selection: Selection;
@@ -44,7 +57,7 @@ export function useDashboardInteractions({
// Delay single-click collapse so double-click can override
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleCardSelect = useCallback((id: string, type: CardType, shiftKey: boolean) => {
const handleCardSelect = useCallback((id: string, type: CardType, shiftKey: boolean, originTarget?: EventTarget | null) => {
report('dashboard', 'card_clicked', { card_type: type, shift: shiftKey });
if (shiftKey) {
selection.selectCard(id, type, true);
@@ -54,6 +67,9 @@ export function useDashboardInteractions({
selection.selectCard(id, type, false);
dispatch(bringToFront({ id, type }));
// Clicking a control INSIDE a card (text field, button, browser URL bar/tabs, note textarea) selects + raises it but must NOT re-center the camera onto it: yanking focus to a card just to click into its input is hostile (same reasoning as the guest-page and Workflows carve-outs). Card frame/body clicks still auto-focus.
if (pressLandedOnControl(originTarget)) return;
// The Workflows window is an app you click around inside, not a card you re-center every tap. Single-click only raises + selects it; double-click still zoom-to-fits (handleCardDoubleClick). Without this, clicking any button inside it yanked the canvas into a re-zoom.
if (type === 'workflows-hub' || type === 'workflows-monitor') return;