diff --git a/packages/design-system/src/components/Drawer/DrawerHeader.tsx b/packages/design-system/src/components/Drawer/DrawerHeader.tsx index 5f61770b..e0701ccf 100644 --- a/packages/design-system/src/components/Drawer/DrawerHeader.tsx +++ b/packages/design-system/src/components/Drawer/DrawerHeader.tsx @@ -19,7 +19,7 @@ export const DrawerHeader: FC = ({ children, ref }) => { className={cn( 'relative shrink-0 w-full', 'bg-bg-surface-2', - 'flex items-center justify-between gap-12', + 'flex items-start justify-between gap-12', 'pt-16 pb-12 pl-24 pr-16', 'rounded-t-12', 'outline-none', diff --git a/packages/design-system/src/components/Table/Table.stories.tsx b/packages/design-system/src/components/Table/Table.stories.tsx index fc1fe8a9..5ddc390c 100644 --- a/packages/design-system/src/components/Table/Table.stories.tsx +++ b/packages/design-system/src/components/Table/Table.stories.tsx @@ -28,6 +28,8 @@ import { METHOD_COLORS, multiplySecurityEvents, renderSecurityPreview, + renderSecurityPreviewHeader, + renderSecurityPreviewWithTitle, type SecurityEvent, type SecurityHeaderEntry, securityColumnHelper, @@ -611,8 +613,11 @@ export const MasterCellWithActions: StoryFn = () => { onSortingChange={setSorting} columnSizing={columnSizing} onColumnSizingChange={setColumnSizing} - previewTrigger='button' - renderPreviewContent={renderSecurityPreview} + preview={{ + trigger: 'button', + renderHeader: renderSecurityPreviewHeader, + renderContent: renderSecurityPreview, + }} /> ); }; @@ -655,7 +660,7 @@ export const MasterCellWithPreviewDrawer: StoryFn = () => { getRowId={row => row.id} sorting={sorting} onSortingChange={setSorting} - renderPreviewContent={renderSecurityPreview} + preview={{ renderContent: renderSecurityPreviewWithTitle }} /> ); }; diff --git a/packages/design-system/src/components/Table/TableBody/TableBodyCell.tsx b/packages/design-system/src/components/Table/TableBody/TableBodyCell.tsx index 9f70033d..56d74ba2 100644 --- a/packages/design-system/src/components/Table/TableBody/TableBodyCell.tsx +++ b/packages/design-system/src/components/Table/TableBody/TableBodyCell.tsx @@ -1,6 +1,7 @@ import { type Cell, flexRender } from '@tanstack/react-table'; import { cn } from '../../../utils/cn'; import { useTestId } from '../../../utils/testId'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../../Tooltip'; import { usePreviewCell } from '../hooks'; import { getAlignClass, @@ -39,20 +40,29 @@ export const TableBodyCell = ({ const { canDnd, setNodeRef, dndStyle } = useColumnDnd(column); const pinningStyles = getPinningStyles(column); const lastLeft = isLastPinnedLeft(column, allLeafColumns, column.id); - const { isMasterTrigger, isButtonTrigger, isActive, togglePreview } = usePreviewCell( - column.id, - cell.row.id, - ); + const { isMasterTrigger, isButtonTrigger, isActive, togglePreview, tooltipText } = + usePreviewCell(column.id, cell.row.id); const isCut = column.id === masterColumnId || meta?.resizeType === 'cut'; const content = flexRender(cell.column.columnDef.cell, cell.getContext()); const hasActions = isButtonTrigger || !!meta?.renderPreviewAction || !!meta?.renderMenuAction; const renderContent = () => { + const wrappedContent = tooltipText ? ( + + + {content} + + {tooltipText} + + ) : ( + {content} + ); + if (isCut && hasActions) { return (
- {content} + {wrappedContent} {isButtonTrigger && } {meta?.renderPreviewAction?.(cell.row)} @@ -65,12 +75,12 @@ export const TableBodyCell = ({ if (isCut) { return (
- {content} + {tooltipText ? wrappedContent : content}
); } - return content; + return tooltipText ? wrappedContent : content; }; return ( @@ -80,7 +90,7 @@ export const TableBodyCell = ({ getAlignClass(meta), getExpandBorderClass(isExpandColumn, cell.row.depth), isCut && 'pr-0', - isMasterTrigger && 'cursor-pointer', + (isMasterTrigger || tooltipText) && 'cursor-pointer', meta?.cellClassName, className, )} diff --git a/packages/design-system/src/components/Table/TableContext/TableProvider.tsx b/packages/design-system/src/components/Table/TableContext/TableProvider.tsx index 6c7d1daf..75dcfef3 100644 --- a/packages/design-system/src/components/Table/TableContext/TableProvider.tsx +++ b/packages/design-system/src/components/Table/TableContext/TableProvider.tsx @@ -80,12 +80,16 @@ export const TableProvider = (props: TableProviderProps) => { overscan = TABLE_VIRTUALIZATION_OVERSCAN, onEndReached, onEndReachedThreshold, - renderPreviewContent, - previewTrigger = 'master', - previewRowId: previewRowIdProp, - onPreviewRowChange, + preview, } = props; + const renderPreviewHeader = preview?.renderHeader; + const renderPreviewContent = preview?.renderContent; + const previewTrigger = preview?.trigger ?? 'master'; + const previewTooltipText = preview?.tooltipText ?? 'Open preview'; + const previewRowIdProp = preview?.rowId; + const onPreviewRowChange = preview?.onRowChange; + // Feature detection const sortingEnabled = !!onSortingChange; const selectionEnabled = !!onRowSelectionChange; @@ -329,10 +333,14 @@ export const TableProvider = (props: TableProviderProps) => { containerRef, onEndReached, onEndReachedThreshold, - previewRowId, - setPreviewRowId, - renderPreviewContent: renderPreviewContent as ((row: Row) => ReactNode) | undefined, - previewTrigger, + preview: { + rowId: previewRowId, + setRowId: setPreviewRowId, + renderHeader: renderPreviewHeader as ((row: Row) => ReactNode) | undefined, + renderContent: renderPreviewContent as ((row: Row) => ReactNode) | undefined, + trigger: previewTrigger, + tooltipText: previewTooltipText, + }, }), [ table, @@ -359,8 +367,11 @@ export const TableProvider = (props: TableProviderProps) => { onEndReached, onEndReachedThreshold, previewRowId, + setPreviewRowId, + renderPreviewHeader, renderPreviewContent, previewTrigger, + previewTooltipText, ], ); diff --git a/packages/design-system/src/components/Table/TableContext/types.ts b/packages/design-system/src/components/Table/TableContext/types.ts index 41aee505..32927448 100644 --- a/packages/design-system/src/components/Table/TableContext/types.ts +++ b/packages/design-system/src/components/Table/TableContext/types.ts @@ -54,10 +54,14 @@ export interface TableContextValue { onEndReachedThreshold?: number; // Preview drawer - previewRowId: string | null; - setPreviewRowId: (id: string | null) => void; - renderPreviewContent?: (row: Row) => ReactNode; - previewTrigger: 'master' | 'button'; + preview: { + rowId: string | null; + setRowId: (id: string | null) => void; + renderHeader?: (row: Row) => ReactNode; + renderContent?: (row: Row) => ReactNode; + trigger: 'master' | 'button'; + tooltipText: string; + }; } export interface TableProviderProps extends Omit, 'children' | 'aria-label'> { diff --git a/packages/design-system/src/components/Table/TablePreviewDrawer.tsx b/packages/design-system/src/components/Table/TablePreviewDrawer.tsx index f9ce5c41..0eec4fe1 100644 --- a/packages/design-system/src/components/Table/TablePreviewDrawer.tsx +++ b/packages/design-system/src/components/Table/TablePreviewDrawer.tsx @@ -4,23 +4,27 @@ import { Drawer, DrawerBody, DrawerContent, DrawerHeader } from '../Drawer'; import { useTableContext } from './TableContext'; export const TablePreviewDrawer: FC = () => { - const { table, previewRowId, setPreviewRowId, renderPreviewContent } = useTableContext(); + const { table, preview: previewCtx } = useTableContext(); - const row = previewRowId ? table.getRowModel().rowsById[previewRowId] : undefined; - const preview = row && renderPreviewContent ? renderPreviewContent(row) : undefined; + const row = previewCtx.rowId ? table.getRowModel().rowsById[previewCtx.rowId] : undefined; + const header = row && previewCtx.renderHeader ? previewCtx.renderHeader(row) : undefined; + const content = row && previewCtx.renderContent ? previewCtx.renderContent(row) : undefined; // Keep the last valid preview so drawer content doesn't flash empty during close animation - const lastPreviewRef = useRef(null); - if (preview) lastPreviewRef.current = preview; - const displayPreview = preview ?? lastPreviewRef.current; + const lastHeaderRef = useRef(null); + const lastContentRef = useRef(null); + if (header) lastHeaderRef.current = header; + if (content) lastContentRef.current = content; + const displayHeader = header ?? lastHeaderRef.current; + const displayContent = content ?? lastContentRef.current; - if (!renderPreviewContent) return null; + if (!previewCtx.renderContent) return null; return ( { - if (!open) setPreviewRowId(null); + if (!open) previewCtx.setRowId(null); }} modal={false} overlay={false} @@ -28,10 +32,12 @@ export const TablePreviewDrawer: FC = () => { width={960} > - - - - {displayPreview} + {displayHeader ?? ( + + + + )} + {displayContent} ); diff --git a/packages/design-system/src/components/Table/TableRow.tsx b/packages/design-system/src/components/Table/TableRow.tsx index fca44651..b0d3bf3d 100644 --- a/packages/design-system/src/components/Table/TableRow.tsx +++ b/packages/design-system/src/components/Table/TableRow.tsx @@ -17,11 +17,11 @@ interface TableRowProps { } const TableRowInner = ({ row, ref, 'data-index': dataIndex }: TableRowProps) => { - const { expandingEnabled, previewRowId } = useTableContext(); + const { expandingEnabled, preview } = useTableContext(); const testId = useTestId('row'); const isGroupParent = row.subRows.length > 0; const isSelected = isGroupParent ? row.getIsAllSubRowsSelected() : row.getIsSelected(); - const isPreviewActive = previewRowId === row.id; + const isPreviewActive = preview.rowId === row.id; if (isGroupParent) { const cells = row.getVisibleCells(); diff --git a/packages/design-system/src/components/Table/hooks/usePreviewCell.ts b/packages/design-system/src/components/Table/hooks/usePreviewCell.ts index f272bcd7..8aaa6b5a 100644 --- a/packages/design-system/src/components/Table/hooks/usePreviewCell.ts +++ b/packages/design-system/src/components/Table/hooks/usePreviewCell.ts @@ -3,21 +3,20 @@ import { useTableContext } from '../TableContext'; /** * Encapsulates preview drawer logic for a body cell. - * Returns flags and a click handler based on `previewTrigger` mode. + * Returns flags and a click handler based on `preview.trigger` mode. */ export const usePreviewCell = (columnId: string, rowId: string) => { - const { masterColumnId, previewRowId, setPreviewRowId, renderPreviewContent, previewTrigger } = - useTableContext(); + const { masterColumnId, preview } = useTableContext(); const isMasterColumn = columnId === masterColumnId; - const hasPreview = isMasterColumn && !!renderPreviewContent; - const isMasterTrigger = hasPreview && previewTrigger === 'master'; - const isButtonTrigger = hasPreview && previewTrigger === 'button'; - const isActive = previewRowId === rowId; + const hasPreview = isMasterColumn && !!preview.renderContent; + const isMasterTrigger = hasPreview && preview.trigger === 'master'; + const isButtonTrigger = hasPreview && preview.trigger === 'button'; + const isActive = preview.rowId === rowId; const togglePreview = useCallback(() => { - setPreviewRowId(isActive ? null : rowId); - }, [setPreviewRowId, isActive, rowId]); + preview.setRowId(isActive ? null : rowId); + }, [preview.setRowId, isActive, rowId]); return { /** Preview opens by clicking the master cell */ @@ -28,5 +27,7 @@ export const usePreviewCell = (columnId: string, rowId: string) => { isActive, /** Toggle preview for this row (open/close) */ togglePreview, + /** Tooltip text for master cell hover */ + tooltipText: hasPreview ? preview.tooltipText : undefined, }; }; diff --git a/packages/design-system/src/components/Table/index.ts b/packages/design-system/src/components/Table/index.ts index 0bb64758..563a1038 100644 --- a/packages/design-system/src/components/Table/index.ts +++ b/packages/design-system/src/components/Table/index.ts @@ -17,6 +17,7 @@ export type { TableExpandedState, TableGroupingState, TableOnChangeFn, + TablePreview, TableProps, TableRow, TableRowSelectionState, diff --git a/packages/design-system/src/components/Table/mocks.tsx b/packages/design-system/src/components/Table/mocks.tsx index 52509056..0350d9ae 100644 --- a/packages/design-system/src/components/Table/mocks.tsx +++ b/packages/design-system/src/components/Table/mocks.tsx @@ -4,6 +4,7 @@ import { abbreviateNumber } from '../../utils/abbreviateNumber'; import { Badge } from '../Badge'; import { InlineCodeSnippet } from '../CodeSnippet'; import type { CountryCode } from '../Country'; +import { DrawerHeader } from '../Drawer'; import { DropdownMenu, DropdownMenuContent, @@ -611,12 +612,29 @@ export const createLargeGroupedData = ( // Large flat data — for a Virtualized story // --------------------------------------------------------------------------- -/** Shared preview drawer content for security events */ -export const renderSecurityPreview = (row: { original: SecurityEvent }) => ( - +/** Shared preview drawer header for security events */ +export const renderSecurityPreviewHeader = (row: { original: SecurityEvent }) => ( + {row.original.objectName} + +); + +/** Preview drawer content for security events */ +const SecurityPreviewContent = ({ + row, + showTitle, +}: { + row: { original: SecurityEvent }; + showTitle?: boolean; +}) => ( + + {showTitle && ( + + {row.original.objectName} + + )} ( ); +SecurityPreviewContent.displayName = 'SecurityPreviewContent'; + +/** Preview content for use with header (objectName shown in header) */ +export const renderSecurityPreview = (row: { original: SecurityEvent }) => ( + +); + +/** Preview content for use without header (includes objectName) */ +export const renderSecurityPreviewWithTitle = (row: { original: SecurityEvent }) => ( + +); + /** Duplicate securityEvents N times with unique IDs */ export const multiplySecurityEvents = (times = 4): SecurityEvent[] => Array.from({ length: times }, (_, batch) => diff --git a/packages/design-system/src/components/Table/types.ts b/packages/design-system/src/components/Table/types.ts index 61aed4db..cf6aa2ae 100644 --- a/packages/design-system/src/components/Table/types.ts +++ b/packages/design-system/src/components/Table/types.ts @@ -216,14 +216,24 @@ export interface TableProps extends TestableProps { onEndReachedThreshold?: number; // --- Preview drawer --- - /** Render preview drawer content for a row. */ - renderPreviewContent?: (row: TableRow) => ReactNode; + /** Preview drawer configuration */ + preview?: TablePreview; +} + +/** Preview drawer configuration */ +export interface TablePreview { + /** Render drawer header for a row */ + renderHeader?: (row: TableRow) => ReactNode; + /** Render drawer content for a row */ + renderContent: (row: TableRow) => ReactNode; /** How the preview drawer is triggered: * - `'master'` — clicking the master cell toggles the drawer (default) * - `'button'` — a toggle button appears in master cell actions on hover */ - previewTrigger?: 'master' | 'button'; + trigger?: 'master' | 'button'; + /** Tooltip text on master cell hover (default: 'Open preview') */ + tooltipText?: string; /** Controlled preview row ID. Pass `null` to close. */ - previewRowId?: string | null; + rowId?: string | null; /** Callback when preview row changes (open/close/swap). */ - onPreviewRowChange?: (rowId: string | null) => void; + onRowChange?: (rowId: string | null) => void; }