/** biome-ignore-all lint/style/noExcessiveLinesPerFile: complex component */ import { memo, useCallback, useMemo } from 'react'; import type { ListRenderItemInfo } from 'react-native'; import { FlatList, ScrollView, View } from 'react-native'; import { cn } from '../../../lib/cn'; import { Checkbox } from '../../form/Checkbox/checkbox'; import { Text } from '../../typography/Text/text'; import { HeaderCell } from './table-header'; import { LoadingMoreFooter, LoadMoreFooter, PaginationFooter, SkeletonFooter, TableCard } from './table-parts'; import { SkeletonCellPulse, TableRow } from './table-row'; import type { RowEntry, TableProps } from './table-types'; import { CHECKBOX_COLUMN_WIDTH } from './table-utils'; import { useTable } from './use-table'; const PAGINATION_FOOTER_HEIGHT = 52; // footer paddingVertical (10×2) + button height (32) /** * **Right-to-left:** column *order* is deliberately left alone — the table * renders the `columns` array in the order given, and whether the first column * belongs on the right under RTL depends on what the data means, so reversing * it here would be guessing. Reverse the array yourself if it should follow the * reading direction. Cell alignment *does* follow the direction unless a column * sets `align` explicitly (see `TableColumn.align`). * * Column drag-to-reorder follows the direction too: its drop boundaries are * accumulated from column widths in column order rather than measured, so they * describe the logical axis and are reconciled with the physical pointer. */ /** biome-ignore lint/complexity/noExcessiveLinesPerFunction: the table has too many props and features to be simplified more */ // biome-ignore lint/style/useComponentExportOnlyModules: exported via memo() wrapper below — the impl/export split is the standard React.memo pattern function TableImpl(props: TableProps) { const { containerRef, containerWidth, onContainerLayout, colWidths, orderedColumns, dragKey, indicatorX, gripHandlers, sortable, activeSort, toggleSort, sortedRows, selected, allSelected, someSelected, toggleAll, toggleRow, pressedRowId, setPressedRowId, pressedColKey, setPressedColKey, hasRowMenu, hasColMenu, reduce, keyExtractor, getItemLayout, loading, loadingMore, skeletonRows, mode, hasMore, onEndReached, onEndReachedThreshold, onLoadMore, loadMoreLabel, page, totalPages, paginationLabel, goToPreviousPage, goToNextPage, emptyState, emptyIcon, emptyTitle, emptyDescription, striped, stripedStyle, flatListHeight, rowHeight, selectable, onCellEdit, onInsertRow, onDeleteRow, testID, } = useTable(props); const { reorderable = false, height = 440, onColumnRename, onInsertColumn, onDeleteColumn, style, className, headerClassName, rowClassName, cellClassName, cardClassName, footerClassName, selectedClassName, dropIndicatorClassName, skeletonClassName, emptyClassName, } = props; const { renderSmallScreen, useSmallScreen = false, cardStyle } = props; // Card mode: hide the table header and render each row as a card via renderSmallScreen. const isCardMode = useSmallScreen && Boolean(renderSmallScreen); // Reduce FlatList height when pagination footer is pinned below it. const effectiveFlatListHeight = mode === 'pagination' ? flatListHeight - PAGINATION_FOOTER_HEIGHT : flatListHeight; // Only wrap in a horizontal ScrollView when columns actually overflow. A // ScrollView that stays idle still changes the responder tree (react-native-web // claims pointer events for potential scrolling), which breaks long-press menus // and shifts the coordinate space for the column-reorder drop indicator. const totalContentWidth = useMemo(() => { let w = selectable ? CHECKBOX_COLUMN_WIDTH : 0; for (const col of orderedColumns) w += colWidths[col.key] ?? 0; return w; }, [orderedColumns, colWidths, selectable]); const needsHorizontalScroll = totalContentWidth > containerWidth; // ── Render callbacks ─────────────────────────────────────────────────────── const renderItem = useCallback( ({ item, index }: ListRenderItemInfo>) => { if (isCardMode && renderSmallScreen) return ( ); return ( ); }, [ isCardMode, renderSmallScreen, cardStyle, cardClassName, orderedColumns, selectable, selected, toggleRow, pressedRowId, hasRowMenu, onInsertRow, onDeleteRow, onCellEdit, rowHeight, striped, stripedStyle, rowClassName, cellClassName, reduce, selectedClassName, setPressedRowId, testID, colWidths, ], ); const renderSkeletonRows = useCallback( (count: number) => ( {Array.from({ length: count }, (_, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: static placeholder rows, fixed length, never reordered {selectable ? ( ) : null} {orderedColumns.map((col) => ( ))} ))} ), [orderedColumns, selectable, rowHeight, reduce, colWidths, skeletonClassName], ); const renderSkeletonCards = useCallback( (count: number) => ( {Array.from({ length: count }, (_, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: static placeholder cards, fixed length, never reordered {selectable ? : null} ))} ), [selectable, cardStyle, cardClassName], ); const ListEmptyComponent = useMemo(() => { if (loading) return isCardMode ? renderSkeletonCards(skeletonRows) : renderSkeletonRows(skeletonRows); // Rich empty state: render icon + title + description when text props are provided. if (!emptyState && (emptyTitle || emptyIcon || emptyDescription)) return ( {emptyIcon ? {emptyIcon} : null} {emptyTitle ? ( {emptyTitle} ) : null} {emptyDescription ? {emptyDescription} : null} ); return ( {typeof emptyState === 'string' ? {emptyState} : (emptyState ?? null)} ); }, [ loading, isCardMode, skeletonRows, emptyState, emptyIcon, emptyTitle, emptyDescription, emptyClassName, renderSkeletonRows, renderSkeletonCards, ]); // loadingMore spinner + loadMore button — rendered inside FlatList footer so they scroll with content. const ListFooterComponent = useMemo(() => { const skeletonProps = { loading, hasRows: sortedRows.length > 0, isCardMode, renderSkeletonCards, renderSkeletonRows, skeletonRows, }; if (loadingMore) return ( <> ); if (mode === 'loadMore' && hasMore) return ( <> ); return ; }, [ loading, loadingMore, mode, hasMore, onLoadMore, loadMoreLabel, isCardMode, sortedRows.length, skeletonRows, renderSkeletonRows, renderSkeletonCards, footerClassName, ]); const paginationFooter = mode === 'pagination' ? ( ) : null; // FlatList body — shared between card and table mode. In table mode the // header row and this FlatList are siblings inside a horizontal ScrollView // so they scroll together when columns overflow. Card mode fills the width // naturally — no horizontal wrapper needed. const flatListBody = ( ); // Header row + FlatList body together — conditionally wrapped in a horizontal // ScrollView when columns overflow below. const tableBody = ( <> {/* Sticky header row */} {selectable ? ( ) : null} {orderedColumns.map((col, colIndex) => ( ))} {/* Drop indicator: a line at the insertion boundary while dragging. `left` is physical, so the position is resolved for the direction upstream (see `dropIndicatorX`). */} {dragKey && indicatorX !== null ? ( ) : null} {/* Body FlatList — vertical scroll, fixed height */} {flatListBody} ); const renderTableBody = useCallback(() => { if (!needsHorizontalScroll) return tableBody; return ( {/* A horizontal ScrollView lays its children out in a row, so the header row and the body FlatList must be wrapped in a single column — left as direct siblings the FlatList sits to the right of the header and the body reads as empty. The explicit width keeps the header cells and body cells on the same column edges and gives the ScrollView a definite content width to scroll. */} {tableBody} ); }, [needsHorizontalScroll, tableBody, totalContentWidth, testID]); return ( {/* ── Card mode: full-width cards, no horizontal scroll ── */} {isCardMode ? flatListBody : renderTableBody()} {/* ── Pagination footer (fixed, outside ScrollView / FlatList) ── */} {paginationFooter} ); } // biome-ignore lint/plugin: memo loses the generic type parameter; the cast is the type-safe way to restore it — the runtime identity is exactly TableImpl export const Table = memo(TableImpl) as typeof TableImpl; export type { SortDirection, SortState, TableColumn, TableMode, TableProps } from './table-types';