import React, { forwardRef, useMemo, useState, useCallback, ReactNode } from 'react'; import { View, ScrollView, Text, TouchableOpacity, Pressable } from 'react-native'; import { tableStyles } from './Table.styles'; import type { TableProps, TableColumn, TableType, TableSizeVariant, TableAlignVariant, SortDirection } from './types'; import type { MenuItem } from '../Menu/types'; import { getNativeAccessibilityProps } from '../utils/accessibility'; import Icon from '../Icon/Icon.native'; import Menu from '../Menu/Menu.native'; // ============================================================================ // Sub-component Props // ============================================================================ interface TRProps { children: ReactNode; size?: TableSizeVariant; type?: TableType; clickable?: boolean; dividers?: boolean; even?: boolean; onPress?: () => void; testID?: string; } interface THProps { children: ReactNode; size?: TableSizeVariant; type?: TableType; align?: TableAlignVariant; width?: number | string; sortable?: boolean; sortDirection?: SortDirection; onSort?: () => void; options?: MenuItem[]; } interface TDProps { children: ReactNode; size?: TableSizeVariant; type?: TableType; align?: TableAlignVariant; width?: number | string; } // ============================================================================ // TR Component // ============================================================================ function TR({ children, size = 'md', type = 'standard', clickable = false, dividers = false, even = false, onPress, testID, }: TRProps) { tableStyles.useVariants({ size, type, clickable, dividers, even, }); const rowStyle = (tableStyles.row as any)({}); const RowComponent = clickable ? TouchableOpacity : View; return ( {children} ); } // ============================================================================ // TH Component // ============================================================================ function TH({ children, size = 'md', type = 'standard', align = 'left', width, sortable, sortDirection, onSort, options, }: THProps) { const [menuOpen, setMenuOpen] = useState(false); tableStyles.useVariants({ size, type, align, sortable: !!sortable, sortActive: sortDirection != null, }); const headerCellStyle = (tableStyles.headerCell as any)({}); const sortIndicatorStyle = (tableStyles.sortIndicator as any)({ sortActive: sortDirection != null }); const optionsButtonStyle = (tableStyles.optionsButton as any)({}); const sortIconName = sortDirection === 'asc' ? 'arrow-up' : sortDirection === 'desc' ? 'arrow-down' : 'arrow-up-down'; const content = ( {typeof children === 'string' ? ( {children} ) : ( children )} {sortable && ( )} {options && options.length > 0 && ( )} ); if (sortable) { return ( {content} ); } return content; } // ============================================================================ // TD Component // ============================================================================ function TD({ children, size = 'md', type = 'standard', align = 'left', width, }: TDProps) { tableStyles.useVariants({ size, type, align, }); const cellStyle = (tableStyles.cell as any)({}); return ( {children} ); } // ============================================================================ // TF Component (Footer Cell) // ============================================================================ interface TFProps { children: ReactNode; size?: TableSizeVariant; type?: TableType; align?: TableAlignVariant; width?: number | string; } function TF({ children, size = 'md', type = 'standard', align = 'left', width, }: TFProps) { tableStyles.useVariants({ size, type, align, }); const footerCellStyle = (tableStyles.footerCell as any)({}); return ( {typeof children === 'string' ? ( {children} ) : ( children )} ); } // ============================================================================ // Main Table Component // ============================================================================ function TableInner({ columns, data, type = 'standard', size = 'md', stickyHeader: _stickyHeader = false, onRowPress, onSort, dividers = false, emptyState, // Spacing variants from ContainerStyleProps gap, padding, paddingVertical, paddingHorizontal, margin, marginVertical, marginHorizontal, style, testID, id, // Accessibility props accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityHidden, }: TableProps, ref: React.Ref) { // Sort state const [sortColumn, setSortColumn] = useState(null); const [sortDirection, setSortDirection] = useState(null); const handleSort = useCallback((columnKey: string) => { let newDir: SortDirection; if (sortColumn !== columnKey) { newDir = 'asc'; } else if (sortDirection === 'asc') { newDir = 'desc'; } else { setSortColumn(null); setSortDirection(null); onSort?.(columnKey, null); return; } setSortColumn(columnKey); setSortDirection(newDir); onSort?.(columnKey, newDir); }, [sortColumn, sortDirection, onSort]); // Generate native accessibility props const nativeA11yProps = useMemo(() => { return getNativeAccessibilityProps({ accessibilityLabel, accessibilityHint, accessibilityRole: accessibilityRole ?? 'grid', accessibilityHidden, }); }, [accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityHidden]); // Apply variants for container tableStyles.useVariants({ type, size, gap, padding, paddingVertical, paddingHorizontal, margin, marginVertical, marginHorizontal, }); // Call styles as functions to get theme-reactive styles const containerStyle = (tableStyles.container as any)({}); const tableStyle = (tableStyles.table as any)({}); const theadStyle = (tableStyles.thead as any)({}); const tbodyStyle = (tableStyles.tbody as any)({}); const cellStyle = (tableStyles.cell as any)({}); // Helper to get cell value const getCellValue = (column: TableColumn, row: T, rowIndex: number) => { if (column.render) { const value = column.dataIndex ? (row as any)[column.dataIndex] : row; return column.render(value, row, rowIndex); } const value = column.dataIndex ? (row as any)[column.dataIndex] : ''; return {String(value)}; }; const isClickable = !!onRowPress; const hasFooter = columns.some((col) => col.footer !== undefined); // Helper to resolve footer content const getFooterContent = (column: TableColumn) => { if (typeof column.footer === 'function') { return column.footer(data); } return column.footer; }; // Split columns into sticky left, scrollable, and sticky right const leftCols = useMemo(() => columns.filter((c) => c.sticky === true || c.sticky === 'left'), [columns]); const rightCols = useMemo(() => columns.filter((c) => c.sticky === 'right'), [columns]); const scrollCols = useMemo(() => columns.filter((c) => !c.sticky), [columns]); const hasStickyColumns = leftCols.length > 0 || rightCols.length > 0; // Renders a column group (header + body + footer) for a set of columns const renderColumnGroup = (cols: TableColumn[]) => ( {/* Header */} {cols.map((column) => ( handleSort(column.key) : undefined} options={column.options} > {column.title} ))} {/* Body */} {data.length === 0 && emptyState ? ( {emptyState} ) : ( data.map((row, rowIndex) => ( onRowPress?.(row, rowIndex)} testID={testID ? `${testID}-row-${rowIndex}` : undefined} > {cols.map((column) => ( {getCellValue(column, row, rowIndex)} ))} )) )} {/* Footer */} {hasFooter && ( {cols.map((column) => ( {getFooterContent(column) ?? null} ))} )} ); // When there are sticky columns, render as: [left sticky] [scrollable] [right sticky] if (hasStickyColumns) { return ( {leftCols.length > 0 && ( {renderColumnGroup(leftCols)} )} {renderColumnGroup(scrollCols)} {rightCols.length > 0 && ( {renderColumnGroup(rightCols)} )} ); } // No sticky columns — original simple layout return ( {renderColumnGroup(columns)} ); } const Table = forwardRef(TableInner) as ( props: TableProps & { ref?: React.Ref } ) => React.ReactElement; (Table as any).displayName = 'Table'; export default Table;