// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file useTableRowExpansion.tsx * @input React, StyleX, Icon, Table types, i18n (useTranslator) * @output Exports useTableRowExpansion hook + config type * @position Row-expansion plugin (detail panel); consumed by Table via plugins prop * * Expands a full-width detail panel below a row, rendered by the consumer's * `renderExpanded(item)`. For hierarchical/tree tables (child rows that reuse * the parent columns) use `useTableTreeData` + `useTableTreeState` instead. * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/Table/index.ts (exports) * - /packages/core/src/Table/useTableRowExpansion.doc.mjs */ import { useMemo, useRef, type ReactNode } from 'react'; import * as stylex from "../../../style-runtime.js"; import "../../../theme/tokens.stylex.js"; import { spacingVars, colorVars, radiusVars } from "../../../theme/tokens.stylex.js"; import { Icon } from "../../../Icon/index.js"; import { VisuallyHidden } from "../../../VisuallyHidden/index.js"; import { resolveContextActions } from "../../tableContextMenu.js"; import { useTranslator } from "../../../i18n/index.js"; import { rtlStyles } from "../../../utils/index.js"; import type { TablePlugin, TableColumn, BodyRowRenderProps, BodyCellRenderProps } from "../../types.js"; // ============================================================================= // Config // ============================================================================= /** * Configuration for useTableRowExpansion (detail-panel mode). * * The consumer owns expansion state; the plugin provides the chevron UI, the * full-width detail panel rendered below an expanded row, and a right-click * "Expand/Collapse row" action. */ export interface UseTableRowExpansionConfig> { /** Set of currently-expanded row keys. */ expandedKeys: Set; /** Called with a row key when its expansion is toggled. */ onToggle: (key: string) => void; /** Derive a stable unique key from a row item. */ getRowKey: (item: T) => string; /** * Render the detail content shown in a full-width panel below the row when * it is expanded. Receives the row's item. */ renderExpanded: (item: T) => ReactNode; /** * Control which rows are expandable. Non-expandable rows show no chevron, no * context-menu action, and never render a panel. @default all rows expandable */ getIsItemExpandable?: (item: T) => boolean; } // ============================================================================= // Styles // ============================================================================= const EXPANSION_COLUMN_WIDTH = { type: 'pixel' as const, value: 40 }; // ============================================================================= // Chevron Cell // ============================================================================= function ExpansionChevron({ isExpanded, onToggle }: { isExpanded: boolean; onToggle: () => void; }) { const t = useTranslator(); return ; } // ============================================================================= // Hook // ============================================================================= /** * Returns a TablePlugin that expands a full-width detail panel below a row. * * The consumer owns the `expandedKeys` set (via `useState`); the plugin adds * a leading chevron column, a right-click expand/collapse action, and renders * `renderExpanded(item)` in a full-width panel below each expanded row. * * For hierarchical data (child rows sharing the parent columns) use * `useTableTreeData` + `useTableTreeState` instead. * * @example * ``` * const [expandedKeys, setExpandedKeys] = useState>(new Set()); * const expansion = useTableRowExpansion({ * expandedKeys, * onToggle: key => * setExpandedKeys(prev => { * const next = new Set(prev); * next.has(key) ? next.delete(key) : next.add(key); * return next; * }), * getRowKey: item => item.id, * renderExpanded: item => , * }); * ; * ``` */ export function useTableRowExpansion>(config: UseTableRowExpansionConfig): TablePlugin { const { expandedKeys, onToggle, getRowKey, renderExpanded, getIsItemExpandable } = config; const t = useTranslator(); // Final rendered column count, captured in transformColumns (pipeline step // 1) and read in transformBodyRow for the detail panel's colSpan. const columnCountRef = useRef(1); const expansionColumn = useMemo((): TableColumn => ({ key: '__expansion', // A `; return { ...props, afterRow: props.afterRow ? <> {props.afterRow} {panel} : panel }; } }), [expandedKeys, getRowKey, renderExpanded, getIsItemExpandable, onToggle, t, expansionColumn]); }
` with no discernible text is announced as an unlabelled // column; the label is hidden because the column shows only chevrons. header: {t('@astryx.tableRowExpansion.columnHeader')} , width: EXPANSION_COLUMN_WIDTH, resizable: false, renderCell: (item: T) => { const expandable = getIsItemExpandable ? getIsItemExpandable(item) : true; if (!expandable) { return null; } const key = getRowKey(item); return onToggle(key)} />; } }), [expandedKeys, onToggle, getRowKey, getIsItemExpandable, t]); return useMemo((): TablePlugin => ({ transformColumns(columns: TableColumn[]) { const withExpansion = [expansionColumn, ...columns]; columnCountRef.current = withExpansion.length; return withExpansion; }, transformBodyCell(props: BodyCellRenderProps, _column: TableColumn, item: T): BodyCellRenderProps { // Contribute the expand/collapse action on every cell; BaseTable // aggregates them into one menu per row. Skip non-expandable rows. const expandable = getIsItemExpandable ? getIsItemExpandable(item) : true; if (!expandable) { return props; } const key = getRowKey(item); const isExpanded = expandedKeys.has(key); return { ...props, contextMenuActions: () => [...resolveContextActions(props.contextMenuActions), { id: 'row-expansion-toggle', group: 'row-expansion', label: isExpanded ? t('@astryx.tableRowExpansion.collapseRow') : t('@astryx.tableRowExpansion.expandRow'), icon: , onSelect: () => onToggle(key) }] }; }, transformBodyRow(props: BodyRowRenderProps, item: T): BodyRowRenderProps { const expandable = getIsItemExpandable ? getIsItemExpandable(item) : true; if (!expandable) { return props; } const key = getRowKey(item); if (!expandedKeys.has(key)) { return props; } const panel =
{renderExpanded(item)}