import { useCallback, useMemo, useRef, useState } from 'react'; import type { LayoutChangeEvent, View } from 'react-native'; import { useReducedMotion } from '../../../hooks/use-reduced-motion'; import type { RowEntry, SortState, TableProps } from './table-types'; import { computeColumnWidths, nextSort, sortRows } from './table-utils'; import { useColumnReorder } from './use-column-reorder'; // Returns all state, derived values, and stable callbacks for . // Kept JSX-free so it can live in a .ts file. // biome-ignore lint/complexity/noExcessiveLinesPerFunction: all table state (sort, filter, select, edit, delete) lives in one hook to share derived values export function useTable(props: TableProps) { const { data, columns, getRowId, selectable = false, selectedRowIds, defaultSelectedRowIds, onSelectionChange, sortable = true, sort: sortProp, defaultSort = null, onSortChange, onColumnOrderChange, onCellEdit, onInsertRow, onDeleteRow, onInsertColumn, onDeleteColumn, rowHeight = 48, height = 440, loading = false, loadingMore = false, skeletonRows = 3, mode, hasMore = false, onEndReached, onEndReachedThreshold = 0.2, onLoadMore, loadMoreLabel = 'Load more', page = 1, pageSize = 10, total = 0, onPageChange, paginationLabel, emptyState, emptyIcon, emptyTitle, emptyDescription, striped = false, stripedStyle, testID, } = props; const reduce = useReducedMotion(); // ── Container width + page-x ─────────────────────────────────────────────── const containerRef = useRef(null); const containerPageX = useRef(0); const [containerWidth, setContainerWidth] = useState(0); const onContainerLayout = useCallback((e: LayoutChangeEvent) => { setContainerWidth(e.nativeEvent.layout.width); containerRef.current?.measureInWindow?.((x) => { containerPageX.current = x; }); }, []); const colWidths = useMemo( () => (containerWidth > 0 ? computeColumnWidths(columns, containerWidth, selectable) : {}), [columns, containerWidth, selectable], ); // ── Column reorder ───────────────────────────────────────────────────────── const { orderedColumns, dragKey, indicatorX, gripHandlers } = useColumnReorder({ columns, colWidths, selectable, containerWidth, containerRef, containerPageX, onColumnOrderChange, }); // ── Row entries ──────────────────────────────────────────────────────────── const rows: RowEntry[] = useMemo( () => data.map((row, index) => ({ row, id: getRowId ? getRowId(row, index) : String(index) })), [data, getRowId], ); // ── Sort state (controllable) ────────────────────────────────────────────── const isControlledSort = sortProp !== undefined; const [internalSort, setInternalSort] = useState(defaultSort ?? null); const activeSort = isControlledSort ? (sortProp ?? null) : internalSort; const toggleSort = useCallback( (key: string) => { const next = nextSort(activeSort, key); if (!isControlledSort) setInternalSort(next); onSortChange?.(next); }, [activeSort, isControlledSort, onSortChange], ); // Pass columns so sortRows can use getSortValue when present. const sortedRows = useMemo(() => sortRows(rows, activeSort, columns), [rows, activeSort, columns]); // ── Selection state (controllable) ──────────────────────────────────────── const isControlledSelection = selectedRowIds !== undefined; const [internalSelected, setInternalSelected] = useState>(() => new Set(defaultSelectedRowIds ?? [])); const selected: Set = isControlledSelection ? new Set(selectedRowIds) : internalSelected; const allSelected = sortedRows.length > 0 && sortedRows.every(({ id }) => selected.has(id)); const someSelected = !allSelected && sortedRows.some(({ id }) => selected.has(id)); const toggleAll = useCallback(() => { const next = allSelected ? new Set() : new Set(sortedRows.map(({ id }) => id)); if (!isControlledSelection) setInternalSelected(next); onSelectionChange?.(Array.from(next)); }, [allSelected, sortedRows, isControlledSelection, onSelectionChange]); const toggleRow = useCallback( (id: string) => { const next = new Set(selected); if (next.has(id)) next.delete(id); else next.add(id); if (!isControlledSelection) setInternalSelected(next); onSelectionChange?.(Array.from(next)); }, [selected, isControlledSelection, onSelectionChange], ); // ── Row / col action menus ───────────────────────────────────────────────── const [pressedRowId, setPressedRowId] = useState(null); const [pressedColKey, setPressedColKey] = useState(null); const hasRowMenu = Boolean(onInsertRow || onDeleteRow); const hasColMenu = Boolean(onInsertColumn || onDeleteColumn); const keyExtractor = useCallback((item: RowEntry) => item.id, []); const getItemLayout = useCallback( (_: unknown, index: number) => ({ length: rowHeight, offset: rowHeight * index, index }), [rowHeight], ); // ── Pagination ───────────────────────────────────────────────────────────── const totalPages = Math.max(1, Math.ceil(total / pageSize)); const goToPreviousPage = useCallback(() => onPageChange?.(page - 1), [onPageChange, page]); const goToNextPage = useCallback(() => onPageChange?.(page + 1), [onPageChange, page]); return { // layout containerRef, containerWidth, onContainerLayout, colWidths, // reorder orderedColumns, dragKey, indicatorX, gripHandlers, // sort sortable, activeSort, toggleSort, sortedRows, // selection selected, allSelected, someSelected, toggleAll, toggleRow, // menus pressedRowId, setPressedRowId, pressedColKey, setPressedColKey, hasRowMenu, hasColMenu, // rendering reduce, keyExtractor, getItemLayout, // loading state loading, loadingMore, skeletonRows, mode, hasMore, onEndReached, onEndReachedThreshold, onLoadMore, loadMoreLabel, // pagination page, pageSize, total, totalPages, onPageChange, paginationLabel, goToPreviousPage, goToNextPage, // empty state emptyState, emptyIcon, emptyTitle, emptyDescription, // striped striped, stripedStyle, // pass-through convenience flatListHeight: height - rowHeight, rowHeight, selectable, onCellEdit, onInsertRow, onDeleteRow, testID, }; }