Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const DrawerHeader: FC<DrawerHeaderProps> = ({ 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',
Expand Down
11 changes: 8 additions & 3 deletions packages/design-system/src/components/Table/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
METHOD_COLORS,
multiplySecurityEvents,
renderSecurityPreview,
renderSecurityPreviewHeader,
renderSecurityPreviewWithTitle,
type SecurityEvent,
type SecurityHeaderEntry,
securityColumnHelper,
Expand Down Expand Up @@ -611,8 +613,11 @@ export const MasterCellWithActions: StoryFn<typeof meta> = () => {
onSortingChange={setSorting}
columnSizing={columnSizing}
onColumnSizingChange={setColumnSizing}
previewTrigger='button'
renderPreviewContent={renderSecurityPreview}
preview={{
trigger: 'button',
renderHeader: renderSecurityPreviewHeader,
renderContent: renderSecurityPreview,
}}
/>
);
};
Expand Down Expand Up @@ -655,7 +660,7 @@ export const MasterCellWithPreviewDrawer: StoryFn<typeof meta> = () => {
getRowId={row => row.id}
sorting={sorting}
onSortingChange={setSorting}
renderPreviewContent={renderSecurityPreview}
preview={{ renderContent: renderSecurityPreviewWithTitle }}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -39,20 +40,29 @@ export const TableBodyCell = <T,>({
const { canDnd, setNodeRef, dndStyle } = useColumnDnd(column);
const pinningStyles = getPinningStyles(column);
const lastLeft = isLastPinnedLeft(column, allLeafColumns, column.id);
const { isMasterTrigger, isButtonTrigger, isActive, togglePreview } = usePreviewCell<T>(
column.id,
cell.row.id,
);
const { isMasterTrigger, isButtonTrigger, isActive, togglePreview, tooltipText } =
usePreviewCell<T>(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 ? (
<Tooltip>
<TooltipTrigger asChild>
<span className='min-w-0 [&>*]:block [&>*]:truncate'>{content}</span>
</TooltipTrigger>
<TooltipContent>{tooltipText}</TooltipContent>
</Tooltip>
) : (
<span className='min-w-0 [&>*]:block [&>*]:truncate'>{content}</span>
);

if (isCut && hasActions) {
return (
<div className='flex items-center justify-between gap-2'>
<span className='min-w-0 [&>*]:block [&>*]:truncate'>{content}</span>
{wrappedContent}
<TableMasterCellActions>
{isButtonTrigger && <TablePreviewToggle active={isActive} onClick={togglePreview} />}
{meta?.renderPreviewAction?.(cell.row)}
Expand All @@ -65,12 +75,12 @@ export const TableBodyCell = <T,>({
if (isCut) {
return (
<div className='overflow-hidden' style={{ minWidth: column.columnDef.size }}>
{content}
{tooltipText ? wrappedContent : content}
</div>
);
}

return content;
return tooltipText ? wrappedContent : content;
};

return (
Expand All @@ -80,7 +90,7 @@ export const TableBodyCell = <T,>({
getAlignClass(meta),
getExpandBorderClass(isExpandColumn, cell.row.depth),
isCut && 'pr-0',
isMasterTrigger && 'cursor-pointer',
(isMasterTrigger || tooltipText) && 'cursor-pointer',
meta?.cellClassName,
className,
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,16 @@ export const TableProvider = <T,>(props: TableProviderProps<T>) => {
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;
Expand Down Expand Up @@ -329,10 +333,14 @@ export const TableProvider = <T,>(props: TableProviderProps<T>) => {
containerRef,
onEndReached,
onEndReachedThreshold,
previewRowId,
setPreviewRowId,
renderPreviewContent: renderPreviewContent as ((row: Row<T>) => ReactNode) | undefined,
previewTrigger,
preview: {
rowId: previewRowId,
setRowId: setPreviewRowId,
renderHeader: renderPreviewHeader as ((row: Row<T>) => ReactNode) | undefined,
renderContent: renderPreviewContent as ((row: Row<T>) => ReactNode) | undefined,
trigger: previewTrigger,
tooltipText: previewTooltipText,
},
}),
[
table,
Expand All @@ -359,8 +367,11 @@ export const TableProvider = <T,>(props: TableProviderProps<T>) => {
onEndReached,
onEndReachedThreshold,
previewRowId,
setPreviewRowId,
renderPreviewHeader,
renderPreviewContent,
previewTrigger,
previewTooltipText,
],
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ export interface TableContextValue<T> {
onEndReachedThreshold?: number;

// Preview drawer
previewRowId: string | null;
setPreviewRowId: (id: string | null) => void;
renderPreviewContent?: (row: Row<T>) => ReactNode;
previewTrigger: 'master' | 'button';
preview: {
rowId: string | null;
setRowId: (id: string | null) => void;
renderHeader?: (row: Row<T>) => ReactNode;
renderContent?: (row: Row<T>) => ReactNode;
trigger: 'master' | 'button';
tooltipText: string;
};
}

export interface TableProviderProps<T> extends Omit<TableProps<T>, 'children' | 'aria-label'> {
Expand Down
30 changes: 18 additions & 12 deletions packages/design-system/src/components/Table/TablePreviewDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,40 @@ 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<ReactNode>(null);
if (preview) lastPreviewRef.current = preview;
const displayPreview = preview ?? lastPreviewRef.current;
const lastHeaderRef = useRef<ReactNode>(null);
const lastContentRef = useRef<ReactNode>(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 (
<Drawer
open={!!row}
onOpenChange={open => {
if (!open) setPreviewRowId(null);
if (!open) previewCtx.setRowId(null);
}}
modal={false}
overlay={false}
closeOnOutsideClick={false}
width={960}
>
<DrawerContent>
<DrawerHeader>
<span />
</DrawerHeader>
<DrawerBody>{displayPreview}</DrawerBody>
{displayHeader ?? (
<DrawerHeader>
<span />
</DrawerHeader>
)}
<DrawerBody>{displayContent}</DrawerBody>
</DrawerContent>
</Drawer>
);
Expand Down
4 changes: 2 additions & 2 deletions packages/design-system/src/components/Table/TableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ interface TableRowProps<T> {
}

const TableRowInner = <T,>({ row, ref, 'data-index': dataIndex }: TableRowProps<T>) => {
const { expandingEnabled, previewRowId } = useTableContext<T>();
const { expandingEnabled, preview } = useTableContext<T>();
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T>(columnId: string, rowId: string) => {
const { masterColumnId, previewRowId, setPreviewRowId, renderPreviewContent, previewTrigger } =
useTableContext<T>();
const { masterColumnId, preview } = useTableContext<T>();

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 */
Expand All @@ -28,5 +27,7 @@ export const usePreviewCell = <T>(columnId: string, rowId: string) => {
isActive,
/** Toggle preview for this row (open/close) */
togglePreview,
/** Tooltip text for master cell hover */
tooltipText: hasPreview ? preview.tooltipText : undefined,
};
};
1 change: 1 addition & 0 deletions packages/design-system/src/components/Table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type {
TableExpandedState,
TableGroupingState,
TableOnChangeFn,
TablePreview,
TableProps,
TableRow,
TableRowSelectionState,
Expand Down
36 changes: 33 additions & 3 deletions packages/design-system/src/components/Table/mocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 }) => (
<VStack gap={16}>
/** Shared preview drawer header for security events */
export const renderSecurityPreviewHeader = (row: { original: SecurityEvent }) => (
<DrawerHeader>
<Text size='lg' weight='medium'>
{row.original.objectName}
</Text>
</DrawerHeader>
);

/** Preview drawer content for security events */
const SecurityPreviewContent = ({
row,
showTitle,
}: {
row: { original: SecurityEvent };
showTitle?: boolean;
}) => (
<VStack gap={16}>
{showTitle && (
<Text size='lg' weight='medium'>
{row.original.objectName}
</Text>
)}
<HStack gap={8}>
<Badge
variant='dotted'
Expand Down Expand Up @@ -645,6 +663,18 @@ export const renderSecurityPreview = (row: { original: SecurityEvent }) => (
</VStack>
);

SecurityPreviewContent.displayName = 'SecurityPreviewContent';

/** Preview content for use with header (objectName shown in header) */
export const renderSecurityPreview = (row: { original: SecurityEvent }) => (
<SecurityPreviewContent row={row} />
);

/** Preview content for use without header (includes objectName) */
export const renderSecurityPreviewWithTitle = (row: { original: SecurityEvent }) => (
<SecurityPreviewContent row={row} showTitle />
);

/** Duplicate securityEvents N times with unique IDs */
export const multiplySecurityEvents = (times = 4): SecurityEvent[] =>
Array.from({ length: times }, (_, batch) =>
Expand Down
20 changes: 15 additions & 5 deletions packages/design-system/src/components/Table/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,24 @@ export interface TableProps<T> extends TestableProps {
onEndReachedThreshold?: number;

// --- Preview drawer ---
/** Render preview drawer content for a row. */
renderPreviewContent?: (row: TableRow<T>) => ReactNode;
/** Preview drawer configuration */
preview?: TablePreview<T>;
}

/** Preview drawer configuration */
export interface TablePreview<T> {
/** Render drawer header for a row */
renderHeader?: (row: TableRow<T>) => ReactNode;
/** Render drawer content for a row */
renderContent: (row: TableRow<T>) => 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;
}
Loading