From 4da8c75355c8c8cecb81adf6adef7a8691625a16 Mon Sep 17 00:00:00 2001 From: Oksana Klimova Date: Thu, 2 Apr 2026 17:34:04 +0200 Subject: [PATCH] fix(table): click master cell to toggle preview drawer [AS-801] - Remove preview toggle button, open drawer by clicking master cell - Add controlled mode: `previewRowId` + `onPreviewRowChange` props - DrawerHeader always renders DrawerClose, DrawerTitle only with title - Add MasterCellWithPreviewDrawer story - Fix duplicate DrawerClose (DrawerHeader already includes one) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/Table/Table.stories.tsx | 59 +++++++++++++++++++ .../Table/TableBody/TableBodyCell.tsx | 26 +++----- .../Table/TableContext/TableProvider.tsx | 18 +++++- .../components/Table/TablePreviewDrawer.tsx | 8 +-- .../src/components/Table/types.ts | 6 +- 5 files changed, 91 insertions(+), 26 deletions(-) diff --git a/packages/design-system/src/components/Table/Table.stories.tsx b/packages/design-system/src/components/Table/Table.stories.tsx index 34862331..fd2e68d6 100644 --- a/packages/design-system/src/components/Table/Table.stories.tsx +++ b/packages/design-system/src/components/Table/Table.stories.tsx @@ -657,6 +657,65 @@ export const MasterCellWithActions: StoryFn = () => { ); }; +export const MasterCellWithPreviewDrawer: StoryFn = () => { + const [sorting, setSorting] = useState([]); + + const data = useMemo( + () => + Array.from({ length: 4 }, (_, batch) => + securityEvents.map(row => ({ + ...row, + id: `${row.id}-${batch}`, + objectName: batch === 0 ? row.objectName : `${row.objectName} (${batch + 1})`, + })), + ).flat(), + [], + ); + + return ( + row.id} + sorting={sorting} + onSortingChange={setSorting} + renderPreviewContent={row => ({ + title: row.original.objectName, + content: ( + + + + {row.original.status} + + + + + + Source: {row.original.sourceCountry} ยท {row.original.sourceProvider} + + + First detected: {row.original.firstDetected} + + + Last seen: {row.original.lastSeen} + + + Security: {row.original.cweId} + + + + ), + })} + /> + ); +}; + export const FullFeatured: StoryFn = () => { const data = useMemo(() => createLargeGroupedData(12, 50), []); diff --git a/packages/design-system/src/components/Table/TableBody/TableBodyCell.tsx b/packages/design-system/src/components/Table/TableBody/TableBodyCell.tsx index 639437cd..3e733b7e 100644 --- a/packages/design-system/src/components/Table/TableBody/TableBodyCell.tsx +++ b/packages/design-system/src/components/Table/TableBody/TableBodyCell.tsx @@ -12,7 +12,6 @@ import { import { Td } from '../primitives'; import { useTableContext } from '../TableContext'; import { TableMasterCellActions } from '../TableMasterCellActions'; -import { TablePreviewToggle } from '../TablePreviewToggle'; interface TableBodyCellProps { cell: Cell; @@ -45,23 +44,14 @@ export const TableBodyCell = ({ const content = flexRender(cell.column.columnDef.cell, cell.getContext()); const isMasterColumn = column.id === masterColumnId; - const hasAutoPreview = isMasterColumn && !!renderPreviewContent; - const hasActions = !!(hasAutoPreview || meta?.renderMenuAction); + const hasPreview = isMasterColumn && !!renderPreviewContent; + const hasMenuAction = !!meta?.renderMenuAction; + const hasActions = hasMenuAction; - const renderActions = () => { + const handleMasterCellClick = () => { + if (!hasPreview) return; const rowId = cell.row.id; - - return ( - <> - {hasAutoPreview && ( - setPreviewRowId(previewRowId === rowId ? null : rowId)} - /> - )} - {meta?.renderMenuAction?.(cell.row)} - - ); + setPreviewRowId(previewRowId === rowId ? null : rowId); }; const renderContent = () => { @@ -69,7 +59,7 @@ export const TableBodyCell = ({ return (
{content} - {renderActions()} + {meta?.renderMenuAction?.(cell.row)}
); } @@ -92,6 +82,7 @@ export const TableBodyCell = ({ getAlignClass(meta), getExpandBorderClass(isExpandColumn, cell.row.depth), isCut && 'pr-0', + hasPreview && 'cursor-pointer', meta?.cellClassName, className, )} @@ -99,6 +90,7 @@ export const TableBodyCell = ({ lastPinnedLeft={disablePinnedShadow ? false : lastLeft} expanded={isExpandedToggle} ref={canDnd ? setNodeRef : undefined} + onClick={hasPreview ? handleMasterCellClick : undefined} style={{ ...pinningStyles, width: cell.column.getSize(), diff --git a/packages/design-system/src/components/Table/TableContext/TableProvider.tsx b/packages/design-system/src/components/Table/TableContext/TableProvider.tsx index eaf541a8..d3161c2d 100644 --- a/packages/design-system/src/components/Table/TableContext/TableProvider.tsx +++ b/packages/design-system/src/components/Table/TableContext/TableProvider.tsx @@ -1,4 +1,4 @@ -import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { type ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'; import { closestCenter, DndContext, @@ -81,6 +81,8 @@ export const TableProvider = (props: TableProviderProps) => { onEndReached, onEndReachedThreshold, renderPreviewContent, + previewRowId: previewRowIdProp, + onPreviewRowChange, } = props; // Feature detection @@ -284,8 +286,18 @@ export const TableProvider = (props: TableProviderProps) => { const theadRef = useRef(null); const containerRef = useRef(null); - // Preview drawer state - const [previewRowId, setPreviewRowId] = useState(null); + // Preview drawer state (controlled/uncontrolled) + const [previewRowId = null, setPreviewRowIdInternal] = useControlled({ + controlled: previewRowIdProp, + default: null, + }); + const setPreviewRowId = useCallback( + (id: string | null) => { + setPreviewRowIdInternal(id); + onPreviewRowChange?.(id); + }, + [setPreviewRowIdInternal, onPreviewRowChange], + ); // Context value const contextValue: TableContextValue = useMemo( diff --git a/packages/design-system/src/components/Table/TablePreviewDrawer.tsx b/packages/design-system/src/components/Table/TablePreviewDrawer.tsx index 1eaed02d..916a6456 100644 --- a/packages/design-system/src/components/Table/TablePreviewDrawer.tsx +++ b/packages/design-system/src/components/Table/TablePreviewDrawer.tsx @@ -28,11 +28,9 @@ export const TablePreviewDrawer: FC = () => { > {displayPreview && ( - {displayPreview.title && ( - - {displayPreview.title} - - )} + + {displayPreview.title && {displayPreview.title}} + {displayPreview.content} )} diff --git a/packages/design-system/src/components/Table/types.ts b/packages/design-system/src/components/Table/types.ts index 0ef3e27c..a156c079 100644 --- a/packages/design-system/src/components/Table/types.ts +++ b/packages/design-system/src/components/Table/types.ts @@ -213,6 +213,10 @@ export interface TableProps extends TestableProps { // --- Preview drawer --- /** Render preview drawer for a row. Returns title and body content. - * When provided, a preview toggle button is automatically added to master cell actions. */ + * When provided, clicking the master cell toggles the preview drawer. */ renderPreviewContent?: (row: TableRow) => { title?: ReactNode; content: ReactNode }; + /** Controlled preview row ID. Pass `null` to close. */ + previewRowId?: string | null; + /** Callback when preview row changes (open/close/swap). */ + onPreviewRowChange?: (rowId: string | null) => void; }