// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file Table.tsx * @input React, StyleX, BaseTable, theme tokens, types, components * @output Exports Table component, TableProps, TableDensity, TableDividers types * @position Styled wrapper; the primary table API for consumers * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/Table/Table.doc.mjs (props table, features, usage examples) * - /packages/core/src/Table/Table.test.tsx (tests for new/changed behavior) * - /packages/core/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts (browser scroll behavior) * - /packages/core/src/Table/index.ts (exports if types change) * - /apps/storybook/stories/Table.stories.tsx (storybook stories) * - /packages/cli/assets/templates/blocks/components/Table/ (showcase blocks) */ import { useMemo, type ReactElement, type Ref } from 'react'; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { colorVars } from "../theme/tokens.stylex.js"; import { BaseTable } from "./BaseTable.js"; import { TableContext } from "./TableContext.js"; import { useBaseTablePlugins } from "./useBaseTablePlugins.js"; import { useScrollableArea } from "../hooks/useScrollableArea.js"; import { mergeProps } from "../utils/index.js"; import { themeProps } from "../utils/themeProps.js"; import type { BaseTableProps, TableVerticalAlign, TablePlugin, TableRenderProps } from "./types.js"; import type { StyleXStyles } from "../theme/types.js"; import { useTranslator } from "../i18n/index.js"; // ============================================================================= // Table Types // ============================================================================= /** Row density controlling padding and font size */ export type TableDensity = 'compact' | 'balanced' | 'spacious'; /** Divider style between cells */ export type TableDividers = 'rows' | 'columns' | 'grid' | 'none'; /** How body cell text behaves when it exceeds column width */ export type TableTextOverflow = 'wrap' | 'truncate'; /** * Props for the styled Table component. * Supports both data-driven mode and children mode with TableRow/Cell. * * @template T - The row data type */ export interface TableProps> extends Omit, 'plugins' | 'components'> { /** Row density. @default 'balanced' */ density?: TableDensity; /** Divider style. @default 'rows' */ dividers?: TableDividers; /** Striped even rows. @default false */ isStriped?: boolean; /** Hover highlight on rows. @default false */ hasHover?: boolean; /** * Vertical alignment for body row cells. * Controls `vertical-align` on `` elements. * * @default 'middle' * * @example * ``` * * ``` */ verticalAlign?: TableVerticalAlign; /** * How body cell text behaves when it exceeds the column width. * * - `'wrap'` — text wraps and the row grows taller. No content is hidden. * - `'truncate'` — text is clipped with an ellipsis. Default-rendered cells * show a tooltip on hover when truncated. * * Header cells always truncate regardless of this setting. * * @default 'wrap' * * @example * ``` *
* ``` */ textOverflow?: 'wrap' | 'truncate'; /** Named plugins to extend table behavior */ plugins?: Record>; } // ============================================================================= // StyleX Styles (table-level only; cell/row/header styles owned by components) // ============================================================================= const tableStyles = { base: { kMv6JI: "xjb2p0i", kMwMTN: "x1tgivj0", $$css: true as const } }; const scrollWrapperStyles = { base: { kXHlph: "xw2csxc", kuHK5b: "x5lxg6s", $$css: true as const }, containerBleed: { keTefX: "xojxgvx", k71WvV: "x1fcf3bl", kzqmXN: "xx6qvi6", keoZOQ: "xkibk3", k1K539: "xlayyun", $$css: true as const } }; function TableScrollWrapper({ children, htmlProps, xstyle: pluginStyles, beforeTable, afterTable }: { children: React.ReactNode; htmlProps?: React.HTMLAttributes & { ref?: React.Ref; }; xstyle?: StyleXStyles[]; beforeTable?: React.ReactNode; afterTable?: React.ReactNode; }) { const t = useTranslator(); const label = t('@astryx.table.label'); const { getViewportProps, getContentProps, state } = useScrollableArea({ axis: 'inline', keyboardAccess: { owner: 'content' }, overscroll: 'contain' }); const { ref, ...restHtmlProps } = htmlProps ?? {}; const viewportProps = getViewportProps({ role: 'group', 'aria-label': label, ...(state.inline.isScrollable ? { tabIndex: 0 } : {}), ...restHtmlProps, ref, ...mergeProps(themeProps('table-scroll-wrapper'), stylex.props(scrollWrapperStyles.base, scrollWrapperStyles.containerBleed, ...(pluginStyles ?? []))) }); return
()}> {beforeTable} {children} {afterTable}
; } // ============================================================================= // Table-level styling plugin (only transforms the
element) // ============================================================================= function buildTableStylePlugin>(): TablePlugin { return { transformTable(props: TableRenderProps): TableRenderProps { // The `astryx-table` class itself comes from BaseTable, which renders // the
element and now names it `table` (with `base-table` as its // legacy name). Adding it here too would put the token on twice. return { ...props, xstyle: [...props.xstyle, tableStyles.base] }; } }; } // ============================================================================= // Table Component // ============================================================================= function TableInner>({ density = 'balanced', dividers = 'rows', isStriped = false, hasHover = false, verticalAlign = 'middle', textOverflow = 'wrap', plugins: userPlugins, columns, data, ref, ...rest }: TableProps & { ref?: Ref; }): ReactElement { // Table-level styling plugin (just adds font/color to
) const tablePlugin = useMemo(() => buildTableStylePlugin(), []); const basePlugins = useMemo(() => [tablePlugin], [tablePlugin]); // Convert named plugin record to stable memoized array const mergedPlugins = useBaseTablePlugins(basePlugins, userPlugins); const contextValue = useMemo(() => ({ density, dividers, isStriped, hasHover, verticalAlign, textOverflow }), [density, dividers, isStriped, hasHover, verticalAlign, textOverflow]); return ref={ref} data={data} columns={columns} plugins={mergedPlugins} textOverflow={textOverflow} scrollWrapper={TableScrollWrapper} {...rest} /> ; } /** * Table — a styled, data-driven table component. * * Wraps BaseTable with styled components (TableRow, TableCell, * TableHeaderCell) that read appearance configuration from TableContext. * Density, dividers, striped rows, and hover effects are applied via * design tokens in the component styles. * * @compositionHint Use renderCell on columns to compose rich cell content. * Combine with Badge (status labels), StatusDot (colored indicators), * Text (formatted values), Avatar (user cells), and HStack/VStack * (multi-element cell layouts). Without renderCell, cells render as plain text. * Always set explicit width on columns using proportional() or pixel() — omitting * width skips the minimum width floor, which can cause columns to collapse on mobile. * * @example * ``` *
( * * * {u.name} * * )}, * { key: 'status', header: 'Status', width: proportional(1), renderCell: (u) => ( * * )}, * ]} * density="compact" * dividers="grid" * hasHover * /> * ``` */ export const Table = TableInner as >(props: TableProps & { ref?: Ref; }) => ReactElement; // eslint-disable-next-line @typescript-eslint/no-explicit-any (Table as any).displayName = 'Table';