// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; declare const process: {env: {NODE_ENV?: string}}; /** * @file useTableRowStatus.tsx * @input React, StyleX, Icon, i18n, dev warnings, VisuallyHidden, Table types * @output Exports useTableRowStatus hook + config type * @position Row-status plugin; consumed by Table via plugins prop * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/Table/index.ts (exports) * - /packages/core/src/Table/useTableRowStatus.doc.mjs * - /packages/core/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx * - /apps/storybook/stories/TableRowStatus.stories.tsx * - /packages/cli/assets/templates/blocks/components/Table/TableRowStatusTable.tsx */ import { useMemo } from 'react'; import * as stylex from "../../../style-runtime.js"; import { Icon, type IconColor, type IconName } from "../../../Icon/index.js"; import { Tooltip } from "../../../Tooltip/index.js"; import { useTranslator } from "../../../i18n/index.js"; import { warnOnce } from "../../../utils/devWarning.js"; import { VisuallyHidden } from "../../../VisuallyHidden/index.js"; import type { TableRowStatus, TableSemanticRowStatus } from "../../index.js"; import type { TableColumn, TablePlugin } from "../../types.js"; /** * Named colors supported by the custom row-status marker. These resolve to the * design system's icon color tokens; raw CSS colors remain an escape hatch. */ export type TableRowStatusColor = 'accent' | 'success' | 'error' | 'warning' | 'red' | 'orange' | 'green' | 'yellow' | 'blue' | 'gray'; type TableRowSemanticStatus = TableSemanticRowStatus['status']; const NAMED_COLORS: Record = { accent: 'var(--color-icon-accent)', success: 'var(--color-icon-green)', error: 'var(--color-icon-red)', warning: 'var(--color-icon-orange)', red: 'var(--color-icon-red)', orange: 'var(--color-icon-orange)', green: 'var(--color-icon-green)', yellow: 'var(--color-icon-yellow)', blue: 'var(--color-icon-blue)', gray: 'var(--color-icon-gray)' }; /** Preserve the released Icon color mapping for named custom-marker colors. */ const ICON_COLOR_BY_NAMED_COLOR: Record = { accent: 'accent', success: 'success', error: 'error', warning: 'warning', red: 'red', orange: 'warning', green: 'green', yellow: 'warning', blue: 'blue', gray: 'gray' }; /** Configuration for {@link useTableRowStatus}. */ export interface UseTableRowStatusConfig> { /** * Derive the status indicator for a row. Return `null` for no indicator. * Memoize with `useCallback` for a stable plugin identity across renders. * * @example * ``` * getStatus: row => * row.hasError ? {status: 'error', label: 'Error'} : null * ``` */ getStatus: (item: T) => (TableRowStatus & { status?: never; }) | TableSemanticRowStatus | null; } type TableRowStatusResult = Exclude>['getStatus']>, null>; // The status column holds a small centered dot or icon. A fixed narrow width // keeps every row's indicator aligned in one gutter. const STATUS_COLUMN_WIDTH = { type: 'pixel' as const, value: 28 }; /** Resolve a named color to a token, or pass a raw CSS color through. */ function resolveColor(color: string): string { return (NAMED_COLORS as Record)[color] ?? color; } function isTableRowSemanticStatus(value: unknown): value is TableRowSemanticStatus { return value === 'success' || value === 'warning' || value === 'error'; } type ResolvedTableRowStatus = { variant: 'dot'; color: string; } | { variant: 'icon'; icon: IconName; iconColor: IconColor; } | { variant: 'icon'; icon: IconName; customColor: string; }; function resolveTableRowStatus(value: TableRowStatusResult): ResolvedTableRowStatus | null { const untypedValue = value as { status?: unknown; color?: unknown; icon?: unknown; }; if (isTableRowSemanticStatus(untypedValue.status)) { if (process.env.NODE_ENV !== 'production' && (Object.prototype.hasOwnProperty.call(untypedValue, 'color') || Object.prototype.hasOwnProperty.call(untypedValue, 'icon'))) { warnOnce('useTableRowStatus:semantic-custom-conflict', 'useTableRowStatus', 'status cannot be combined with color or icon. The semantic status takes precedence and the custom marker fields are ignored.'); } return { variant: 'icon', icon: untypedValue.status, iconColor: untypedValue.status }; } if (typeof untypedValue.color !== 'string') { return null; } if (typeof untypedValue.icon === 'string') { const namedIconColor = (ICON_COLOR_BY_NAMED_COLOR as Record)[untypedValue.color]; return namedIconColor == null ? { variant: 'icon', icon: untypedValue.icon as IconName, customColor: untypedValue.color } : { variant: 'icon', icon: untypedValue.icon as IconName, iconColor: namedIconColor }; } return { variant: 'dot', color: resolveColor(untypedValue.color) }; } const _temp = { kzqmXN: "x1xc55vz", kZKoxP: "xdk7pt", kaIpWk: "x16rqkct", kmuXW: "x2lah0s", "$$css": true }; const styles = { wrap: { k1xSpc: "x78zum5", kGNEyG: "x6s0dn4", kjj79g: "xl56j7k", $$css: true as const }, customIcon: (color: string) => [{ kMwMTN: color != null ? "x14rh7hd" : color, $$css: true as const }, { "--x-color": color != null ? color : undefined }], dot: (color: string) => [_temp, { kWkggS: color != null ? "xl8spv7" : color, $$css: true as const }, { "--x-backgroundColor": color != null ? color : undefined }] }; /** * Returns a {@link TablePlugin} that prepends a narrow column signaling per-row * status. A semantic `status` resolves a themed glyph and tone; a custom * `color` renders the stable dot unless the caller also provides an `icon`. * * @example * ``` * const rowStatus = useTableRowStatus({ * getStatus: row => * row.state === 'error' * ? {status: 'error', label: 'Error'} * : {color: 'blue', icon: 'info', label: 'Informational'}, * }); * ; * ``` */ export function useTableRowStatus>(config: UseTableRowStatusConfig): TablePlugin { const t = useTranslator(); const { getStatus } = config; return useMemo((): TablePlugin => { const statusColumn: TableColumn = { key: '__rowStatus', // The gutter stays visually blank, but the th needs a discernible // name for assistive technology (axe: empty-table-header). header: {t('@astryx.table.rowStatus.columnHeader')} , width: STATUS_COLUMN_WIDTH, resizable: false, renderCell: (item: T) => { const status = getStatus(item); if (!status) { return null; } const resolvedStatus = resolveTableRowStatus(status); if (!resolvedStatus) { return null; } const signifier = resolvedStatus.variant === 'dot' ? : ; return {signifier} ; } }; return { transformColumns(columns) { return [statusColumn, ...columns]; } }; }, [getStatus, t]); }