Skip to content

Commit 6d78539

Browse files
fix: resolve table cell click issue by adding interactive click guard
1 parent 8859bf2 commit 6d78539

11 files changed

Lines changed: 115 additions & 65 deletions

File tree

web/oss/src/components/EvaluationRunsTablePOC/actions/navigationActions.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type {MouseEvent} from "react"
2-
31
import {message} from "@agenta/ui/app-message"
42
import {getDefaultStore} from "jotai"
53
import Router from "next/router"
@@ -23,14 +21,6 @@ const getUrlState = (): URLState => store.get(urlAtom) as URLState
2321

2422
const getActiveAppId = (): string | null => store.get(routerAppIdAtom)
2523

26-
export const shouldIgnoreRowClick = (event: MouseEvent<HTMLElement>) => {
27-
const target = event.target as HTMLElement | null
28-
if (!target) return false
29-
const interactiveSelector =
30-
"button, a, input, textarea, select, [role='button'], [role='menuitem'], [role='checkbox'], .ant-checkbox, .ant-checkbox-input, .ant-checkbox-inner, .ant-checkbox-wrapper, .ant-btn, .ant-select, .ant-dropdown-trigger"
31-
return Boolean(target.closest(interactiveSelector))
32-
}
33-
3424
interface NavigateToRunParams {
3525
record: EvaluationRunTableRow
3626
scope: "app" | "project"

web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsTable/index.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {Key, MouseEvent, ReactNode} from "react"
1+
import type {Key, ReactNode} from "react"
22
import {useCallback, useEffect, useMemo, useRef, useState} from "react"
33

44
import {useQueryClient} from "@tanstack/react-query"
@@ -34,7 +34,6 @@ import {
3434
} from "@/oss/lib/onboarding"
3535
import {useQueryParamState} from "@/oss/state/appState"
3636

37-
import {shouldIgnoreRowClick} from "../../actions/navigationActions"
3837
import {
3938
evaluationRunsDeleteContextAtom,
4039
evaluationRunsTableFetchEnabledAtom,
@@ -333,8 +332,7 @@ const EvaluationRunsTableActive = ({
333332
const runId = record.preview?.id ?? record.runId
334333
const isNavigable = Boolean(!record.__isSkeleton && runId)
335334
return {
336-
onClick: (event: MouseEvent<HTMLTableRowElement>) => {
337-
if (shouldIgnoreRowClick(event)) return
335+
onClick: () => {
338336
if (!isNavigable) return
339337
handleOpenRun(record)
340338
},

web/oss/src/components/InfiniteVirtualTable/columns/createStandardColumns.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ function createActionsColumn<T extends InfiniteTableRowBase>(
203203
align: "center",
204204
// Lock actions column from being toggled in visibility menu
205205
columnVisibilityLocked: true as any,
206+
onCell: () => ({className: "ag-table-actions-cell"}),
206207
render: (_, record) => {
207208
if (record.__isSkeleton) return null
208209

@@ -294,7 +295,10 @@ function createActionsColumn<T extends InfiniteTableRowBase>(
294295
}
295296

296297
return (
297-
<div className="h-full flex items-center justify-center">
298+
<div
299+
className="w-full h-full flex items-center justify-center"
300+
onClick={(e) => e.stopPropagation()}
301+
>
298302
<Dropdown
299303
trigger={["click"]}
300304
styles={{root: {width: 200}}}

web/oss/src/components/InfiniteVirtualTable/columns/createTableColumns.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {ReactNode} from "react"
1+
import type {MouseEvent, ReactNode} from "react"
22

33
import type {ColumnsType} from "antd/es/table"
44
import clsx from "clsx"
@@ -132,6 +132,24 @@ const buildColumn = <Row extends object>(
132132
;(column as any).exportMetadata = config.exportMetadata
133133
}
134134

135+
// Auto-stop click propagation in action columns so clicks on empty cell area
136+
// don't bubble to the row navigation handler.
137+
if (config.key === "actions") {
138+
const prevOnCell = column.onCell as ((record: Row, index?: number) => any) | undefined
139+
column.onCell = (record: Row, index?: number) => {
140+
const base = prevOnCell ? prevOnCell(record, index) : {}
141+
const prevClick = (base as any)?.onClick
142+
return {
143+
...base,
144+
className: clsx((base as any)?.className, "ag-table-actions-cell"),
145+
onClick: (e: MouseEvent<HTMLTableDataCellElement>) => {
146+
e.stopPropagation()
147+
prevClick?.(e)
148+
},
149+
}
150+
}
151+
}
152+
135153
return column
136154
}
137155

web/oss/src/components/InfiniteVirtualTable/components/InfiniteVirtualTableInner.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import useInfiniteScroll from "../hooks/useInfiniteScroll"
3131
import useScrollContainer from "../hooks/useScrollContainer"
3232
import useSmartResizableColumns from "../hooks/useSmartResizableColumns"
3333
import useTableKeyboardShortcuts from "../hooks/useTableKeyboardShortcuts"
34+
import {shouldIgnoreRowClick} from "../hooks/useTableManager"
3435
import useTableRowSelection from "../hooks/useTableRowSelection"
3536
import ColumnVisibilityProvider from "../providers/ColumnVisibilityProvider"
3637
import type {InfiniteVirtualTableProps} from "../types"
@@ -69,6 +70,7 @@ const InfiniteVirtualTableInnerBase = <RecordType extends object>({
6970
keyboardShortcuts,
7071
expandable,
7172
tableRef,
73+
disableInteractiveClickGuard = false,
7274
}: InfiniteVirtualTableInnerProps<RecordType>) => {
7375
const generatedScopeId = useId()
7476
const resolvedScopeId = useMemo(
@@ -500,28 +502,43 @@ const InfiniteVirtualTableInnerBase = <RecordType extends object>({
500502
const shortcutProps = getShortcutRowProps
501503
? (getShortcutRowProps(record, index) ?? {})
502504
: {}
503-
if (!shortcutProps || Object.keys(shortcutProps).length === 0) {
504-
return baseProps
505+
506+
const baseOnClick = baseProps?.onClick
507+
const guardedOnClick =
508+
!disableInteractiveClickGuard && baseOnClick
509+
? (event: React.MouseEvent<HTMLTableRowElement>) => {
510+
if (shouldIgnoreRowClick(event)) return
511+
baseOnClick(event)
512+
}
513+
: baseOnClick
514+
515+
const hasShortcuts = shortcutProps && Object.keys(shortcutProps).length > 0
516+
if (!hasShortcuts) {
517+
if (guardedOnClick === baseOnClick) return baseProps
518+
return {...baseProps, onClick: guardedOnClick}
505519
}
506520
return {
507521
...baseProps,
508522
...shortcutProps,
509523
className: clsx(baseProps?.className, shortcutProps?.className),
510524
onMouseEnter: mergeHandlers(baseProps?.onMouseEnter, shortcutProps?.onMouseEnter),
525+
onClick: guardedOnClick,
511526
}
512527
},
513-
[finalTableProps.onRow, getShortcutRowProps],
528+
[finalTableProps.onRow, getShortcutRowProps, disableInteractiveClickGuard],
514529
)
515530

516531
const tablePropsWithShortcuts = useMemo<TableProps<RecordType>>(() => {
517-
if (!getShortcutRowProps) {
532+
const needsMerge =
533+
getShortcutRowProps || (Boolean(finalTableProps.onRow) && !disableInteractiveClickGuard)
534+
if (!needsMerge) {
518535
return finalTableProps
519536
}
520537
return {
521538
...finalTableProps,
522539
onRow: mergedOnRow,
523540
}
524-
}, [finalTableProps, getShortcutRowProps, mergedOnRow])
541+
}, [finalTableProps, getShortcutRowProps, mergedOnRow, disableInteractiveClickGuard])
525542

526543
const tableRowSelection = useTableRowSelection(rowSelection)
527544

web/oss/src/components/InfiniteVirtualTable/hooks/useTableManager.tsx

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,18 @@ import useTableExport from "./useTableExport"
2626
/** Stable no-op atom used when no external search atom is provided (hooks can't be conditional) */
2727
const dummySearchAtom = atom("")
2828

29+
const INTERACTIVE_SELECTOR =
30+
"button, a, input, textarea, select, [role='button'], [role='menuitem'], [role='checkbox'], " +
31+
".ant-btn, .ant-checkbox, .ant-checkbox-input, .ant-checkbox-inner, .ant-checkbox-wrapper, " +
32+
".ant-select, .ant-dropdown-trigger, .ant-table-selection-column, .ag-table-actions-cell"
33+
2934
/**
30-
* Helper to detect if a click event should be ignored for row navigation
31-
* Returns true if the click was on an interactive element (button, link, dropdown, etc.)
35+
* Returns true when the click originated from an interactive element (button, link,
36+
* dropdown, checkbox, etc.) and should not bubble up to the row navigation handler.
3237
*/
3338
export const shouldIgnoreRowClick = (event: MouseEvent<HTMLElement>): boolean => {
34-
const target = event.target as HTMLElement
35-
36-
// Check if clicking on interactive elements
37-
if (
38-
target.closest("button") ||
39-
target.closest("a") ||
40-
target.closest(".ant-dropdown-trigger") ||
41-
target.closest(".ant-checkbox-wrapper") ||
42-
target.closest(".ant-select") ||
43-
target.closest("input") ||
44-
target.closest("textarea")
45-
) {
46-
return true
47-
}
48-
49-
return false
39+
const target = event.target as HTMLElement | null
40+
return Boolean(target?.closest(INTERACTIVE_SELECTOR))
5041
}
5142

5243
/** Configuration for built-in search. When provided, the hook manages search state internally. */

web/oss/src/components/InfiniteVirtualTable/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,12 @@ export interface InfiniteVirtualTableProps<RecordType, ExpandedChildType = unkno
276276
rowSelection?: InfiniteVirtualTableRowSelection<RecordType>
277277
resizableColumns?: boolean | ResizableColumnsConfig
278278
columnVisibility?: ColumnVisibilityConfig<RecordType>
279+
/**
280+
* When true, disables the built-in guard that prevents row-click navigation
281+
* from firing when the click originates from an interactive element (button,
282+
* checkbox, dropdown, etc.). Defaults to false — the guard is on by default.
283+
*/
284+
disableInteractiveClickGuard?: boolean
279285
onColumnToggle?: (payload: {
280286
scopeId: string | null
281287
columnKey: string

web/packages/agenta-ui/src/InfiniteVirtualTable/columns/createStandardColumns.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ function createActionsColumn<T extends InfiniteTableRowBase>(
244244
columnVisibilityLocked: true,
245245
// Exclude actions column from CSV export
246246
exportEnabled: false,
247+
onCell: () => ({className: "ag-table-actions-cell"}),
247248
render: (_, record) => {
248249
if (record.__isSkeleton) return null
249250

@@ -342,7 +343,10 @@ function createActionsColumn<T extends InfiniteTableRowBase>(
342343
}
343344

344345
return (
345-
<div className="h-full flex items-center justify-center">
346+
<div
347+
className="w-full h-full flex items-center justify-center"
348+
onClick={(e) => e.stopPropagation()}
349+
>
346350
<Dropdown
347351
trigger={["click"]}
348352
styles={{root: {width: 200}}}

web/packages/agenta-ui/src/InfiniteVirtualTable/components/InfiniteVirtualTableInner.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import useInfiniteScroll from "../hooks/useInfiniteScroll"
3333
import useScrollContainer from "../hooks/useScrollContainer"
3434
import useSmartResizableColumns from "../hooks/useSmartResizableColumns"
3535
import useTableKeyboardShortcuts from "../hooks/useTableKeyboardShortcuts"
36+
import {shouldIgnoreRowClick} from "../hooks/useTableManager"
3637
import useTableRowSelection from "../hooks/useTableRowSelection"
3738
import {useTypeChipColumns} from "../hooks/useTypeChipColumns"
3839
import {useTypeChipFeature} from "../hooks/useTypeChipFeature"
@@ -74,6 +75,7 @@ const InfiniteVirtualTableInnerBase = <RecordType extends object>({
7475
expandable,
7576
tableRef,
7677
typeChips,
78+
disableInteractiveClickGuard = false,
7779
}: InfiniteVirtualTableInnerProps<RecordType>) => {
7880
const generatedScopeId = useId()
7981
const resolvedScopeId = useMemo(
@@ -612,7 +614,16 @@ const InfiniteVirtualTableInnerBase = <RecordType extends object>({
612614
}
613615
: {}
614616

615-
const allProps = {
617+
const rawOnClick = mergeHandlers(baseProps?.onClick, selectionProps?.onClick)
618+
const onClick =
619+
!disableInteractiveClickGuard && rawOnClick
620+
? (event: MouseEvent<HTMLElement>) => {
621+
if (shouldIgnoreRowClick(event)) return
622+
rawOnClick(event as MouseEvent<HTMLTableRowElement>)
623+
}
624+
: rawOnClick
625+
626+
return {
616627
...baseProps,
617628
...shortcutProps,
618629
...selectionProps,
@@ -622,23 +633,37 @@ const InfiniteVirtualTableInnerBase = <RecordType extends object>({
622633
selectionProps?.className,
623634
),
624635
onMouseEnter: mergeHandlers(baseProps?.onMouseEnter, shortcutProps?.onMouseEnter),
625-
onClick: mergeHandlers(baseProps?.onClick, selectionProps?.onClick),
636+
onClick,
626637
}
627-
628-
return allProps
629638
},
630-
[finalTableProps.onRow, getShortcutRowProps, selectOnRowClick, handleSelectionRowClick],
639+
[
640+
finalTableProps.onRow,
641+
getShortcutRowProps,
642+
selectOnRowClick,
643+
handleSelectionRowClick,
644+
disableInteractiveClickGuard,
645+
],
631646
)
632647

633648
const tablePropsWithShortcuts = useMemo<TableProps<RecordType>>(() => {
634-
if (!getShortcutRowProps && !selectOnRowClick) {
649+
const needsMerge =
650+
getShortcutRowProps ||
651+
selectOnRowClick ||
652+
(Boolean(finalTableProps.onRow) && !disableInteractiveClickGuard)
653+
if (!needsMerge) {
635654
return finalTableProps
636655
}
637656
return {
638657
...finalTableProps,
639658
onRow: mergedOnRow,
640659
}
641-
}, [finalTableProps, getShortcutRowProps, selectOnRowClick, mergedOnRow])
660+
}, [
661+
finalTableProps,
662+
getShortcutRowProps,
663+
selectOnRowClick,
664+
mergedOnRow,
665+
disableInteractiveClickGuard,
666+
])
642667

643668
const tableRowSelection = useTableRowSelection(rowSelection)
644669

web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useTableManager.tsx

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,18 @@ import useTableExport from "./useTableExport"
2727
/** Stable no-op atom used when no external search atom is provided (hooks can't be conditional) */
2828
const dummySearchAtom = atom("")
2929

30+
const INTERACTIVE_SELECTOR =
31+
"button, a, input, textarea, select, [role='button'], [role='menuitem'], [role='checkbox'], " +
32+
".ant-btn, .ant-checkbox, .ant-checkbox-input, .ant-checkbox-inner, .ant-checkbox-wrapper, " +
33+
".ant-select, .ant-dropdown-trigger, .ant-table-selection-column, .ag-table-actions-cell"
34+
3035
/**
31-
* Helper to detect if a click event should be ignored for row navigation
32-
* Returns true if the click was on an interactive element (button, link, dropdown, etc.)
36+
* Returns true when the click originated from an interactive element (button, link,
37+
* dropdown, checkbox, etc.) and should not bubble up to the row navigation handler.
3338
*/
3439
export const shouldIgnoreRowClick = (event: MouseEvent<HTMLElement>): boolean => {
35-
const target = event.target as HTMLElement
36-
37-
// Check if clicking on interactive elements
38-
if (
39-
target.closest("button") ||
40-
target.closest("a") ||
41-
target.closest(".ant-dropdown-trigger") ||
42-
target.closest(".ant-checkbox-wrapper") ||
43-
target.closest(".ant-select") ||
44-
target.closest("input") ||
45-
target.closest("textarea")
46-
) {
47-
return true
48-
}
49-
50-
return false
40+
const target = event.target as HTMLElement | null
41+
return Boolean(target?.closest(INTERACTIVE_SELECTOR))
5142
}
5243

5344
/** Configuration for built-in search. When provided, the hook manages search state internally. */

0 commit comments

Comments
 (0)