'use client' import Link from '../../../embed-shims/next-link' import React, { memo, useCallback, useRef, type ReactNode } from 'react' import { flexRender, type Row } from '@tanstack/react-table' import { cn } from '../../../utils/cn' import { ROW_HEIGHT_DESKTOP, ROW_SHELL_CLASSES } from './data-table-skeleton' import { alignJustify, getHideClasses } from './utils' export interface DataTableRowProps { row: Row onClick?: (item: T) => void href?: string | null /** Dense row height. */ compact?: boolean /** * Treat the design row height as a minimum: a cell rendering multi-line * content (e.g. a line-clamped description) grows the row instead of being * clipped. Default keeps the fixed height. */ autoHeight?: boolean className?: string /** Expandable content rendered below the cells, inside the same card. */ subRow?: ReactNode } /** * Click-bubbling protocol: any element inside a cell that should NOT trigger * `onRowClick` / row navigation must carry the `data-no-row-click` attribute. * The row checks `target.closest('[data-no-row-click]')` and short-circuits: * in `onClick` mode it skips the consumer's handler; in link mode (when * `href` is set) it calls `e.preventDefault()` so `` does not navigate. * * Clicks originating from portaled descendants (e.g. `FloatingTooltip`, * dropdown menus rendered through `FloatingPortal`) bubble through React's * component tree and reach this handler, but their DOM target lives outside * the row subtree. The handler ignores any click whose target is not * physically contained within the row element — no `stopPropagation` * required at the source. * * In link mode the row IS the `` — content lives inside it, not under * an absolute overlay — so native browser link behaviour works: hover, * right-click "Open in new tab", middle-click, `Cmd+click`, focus outlines, * `:visited` styles, etc. * * Example column with action buttons: * ```tsx * { * id: 'actions', * cell: ({ row }) => ( *
* *
* ), * enableSorting: false, * meta: { width: 'w-[160px] shrink-0', align: 'right' }, * } * ``` * * Wrapped in `React.memo` — for best perf in long lists, pass stable references * for `onClick` and `className` (if function) via `useCallback` / `useMemo` in * the consumer. `href` and string `className` are compared by value. */ function DataTableRowImpl({ row, onClick, href, compact, autoHeight, className, subRow, }: DataTableRowProps) { const hasSubRow = subRow != null && subRow !== false // A sub-row carries its own interactive controls, so it must not live inside the // row-level ; when present, the link wraps only the cells. const isLinkMode = Boolean(href) && !onClick const isWholeCardLink = isLinkMode && !hasSubRow const containerRef = useRef(null) const handleClick = useCallback( (e: React.MouseEvent) => { const target = e.target as HTMLElement // React-bubbled events from portaled descendants (tooltips, dropdowns, etc.) // reach this handler even though their DOM target lives outside the row. // Suppress them — and in link mode, preventDefault so `` does not navigate. if (!containerRef.current?.contains(target)) { if (isLinkMode) e.preventDefault() return } if (target.closest('[data-no-row-click]')) { if (isLinkMode) e.preventDefault() return } onClick?.(row.original) }, [onClick, row.original, isLinkMode], ) const containerClassName = cn( 'block rounded-md bg-ods-card border border-ods-border overflow-hidden no-underline text-inherit', // With a sub-row the link wraps only the cells, so keep the clickable affordance off the whole card. (onClick || isWholeCardLink) && 'cursor-pointer hover:bg-ods-bg-active transition-colors', className, ) const cells = (
{row.getVisibleCells().map(cell => { const meta = cell.column.columnDef.meta return (
{flexRender(cell.column.columnDef.cell, cell.getContext())}
) })}
) if (isWholeCardLink && href) { return ( } className={containerClassName} onClick={handleClick} > {cells} ) } return (
} className={containerClassName} onClick={onClick ? handleClick : undefined} > {isLinkMode && href ? ( {cells} ) : ( cells )} {hasSubRow && subRow}
) } export const DataTableRow = memo(DataTableRowImpl) as typeof DataTableRowImpl /** Wraps primitive string/number cell values in the default text style. */ function CellContent({ children }: { children: ReactNode }) { if (typeof children === 'string' || typeof children === 'number') { return ( {children} ) } return <>{children} }