mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] dashboard: control/input clicks select card without yanking camera focus
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user