Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
e3d26d7
Fix table page header scrolling
KJ21-ENG Jun 17, 2026
6c116b3
Fix table sticky header initial load
KJ21-ENG Jun 18, 2026
881019c
Harden table sticky header timing
KJ21-ENG Jun 18, 2026
39a3c36
Address table review style findings
KJ21-ENG Jun 18, 2026
fc98458
Simplify workspace header rendering
KJ21-ENG Jun 19, 2026
991a2f6
Fix sticky table header background
KJ21-ENG Jun 19, 2026
57b38bf
Merge remote-tracking branch 'upstream/main' into KJ21-ENG/92930-rais…
KJ21-ENG Jun 19, 2026
58b94e0
Merge remote-tracking branch 'upstream/main' into KJ21-ENG/92930-rais…
KJ21-ENG Jun 22, 2026
05f9b6b
Fix native table sticky header behavior
KJ21-ENG Jun 23, 2026
8c7f7e5
Merge remote-tracking branch 'upstream/main' into KJ21-ENG/92930-rais…
KJ21-ENG Jun 25, 2026
ee2cc99
Fix table sticky header flow
KJ21-ENG Jun 25, 2026
48776b9
Address table sticky header review feedback
KJ21-ENG Jun 30, 2026
bf21245
Merge upstream main into 92930 raising flow
KJ21-ENG Jun 30, 2026
07fb1e5
Merge upstream main into 92930 raising flow
KJ21-ENG Jul 2, 2026
08528b2
Cover missed table pages in scroll flow
KJ21-ENG Jul 2, 2026
f81ecff
Avoid empty table page-header rows
KJ21-ENG Jul 2, 2026
ec561c7
Centralize table header composition
KJ21-ENG Jul 2, 2026
1979820
Fix sticky table header background
KJ21-ENG Jul 3, 2026
549388a
Merge upstream main into 92930 raising flow
KJ21-ENG Jul 3, 2026
3489b80
Fix Oxfmt formatting
KJ21-ENG Jul 3, 2026
fa531c6
Address table review feedback
KJ21-ENG Jul 6, 2026
8c1cb9a
Fix table helper knip export
KJ21-ENG Jul 6, 2026
b0557dc
Merge remote-tracking branch 'upstream/main' into KJ21-ENG/92930-rais…
KJ21-ENG Jul 7, 2026
0775e98
Merge remote-tracking branch 'upstream/main' into KJ21-ENG/92930-rais…
KJ21-ENG Jul 11, 2026
54ea891
Render table empty states inside the scrolling table list
KJ21-ENG Jul 11, 2026
26d7ce9
Scroll focused table search input above the keyboard
KJ21-ENG Jul 11, 2026
b73dbbe
Cover in-list table empty states with UI tests
KJ21-ENG Jul 11, 2026
c10e338
Re-apply table header scroll pattern onto the empty-state redesign
KJ21-ENG Jul 11, 2026
a277414
Scroll Agents page controls with the agents table
KJ21-ENG Jul 11, 2026
479e2c3
Justify requireActual type imports in table test mocks
KJ21-ENG Jul 11, 2026
17b98a6
Scroll Expensify Card and report participants page controls with thei…
KJ21-ENG Jul 13, 2026
8ca41b5
Merge remote-tracking branch 'upstream/main' into KJ21-ENG/92930-rais…
KJ21-ENG Jul 13, 2026
3db8b82
Expose composeTableHeaderComponent in company cards table test mock
KJ21-ENG Jul 13, 2026
d8b3800
Merge remote-tracking branch 'upstream/main' into KJ21-ENG/92930-rais…
KJ21-ENG Jul 13, 2026
4718441
Add agents and policy rooms loaded flags to safe Onyx export keys
KJ21-ENG Jul 13, 2026
5f27b5f
Fix sticky table header initialization
KJ21-ENG Jul 15, 2026
3adf17a
Merge upstream/main into KJ21-ENG/92930-raising-flow
KJ21-ENG Jul 15, 2026
7e47821
Fix table page compiler compatibility
KJ21-ENG Jul 15, 2026
9f910d5
Remove orphaned members search ref
KJ21-ENG Jul 15, 2026
92de38f
Fix TableBody compiler divergence
KJ21-ENG Jul 15, 2026
6102778
Fix TableBody compiler divergence
KJ21-ENG Jul 15, 2026
922478c
Merge validated compiler fix history
KJ21-ENG Jul 15, 2026
1c0d498
Merge upstream/main into KJ21-ENG/92930-raising-flow
KJ21-ENG Jul 15, 2026
a99a865
Fix sticky table header lifecycle
KJ21-ENG Jul 16, 2026
ace477e
Merge remote-tracking branch 'upstream/main' into KJ21-ENG/92930-rais…
KJ21-ENG Jul 16, 2026
059f5d8
Fix sticky table validation issues
KJ21-ENG Jul 16, 2026
9083f70
Merge remote-tracking branch 'upstream/main' into KJ21-ENG/92930-rais…
KJ21-ENG Jul 22, 2026
b976f00
Fix remaining table review feedback
KJ21-ENG Jul 22, 2026
2a7168d
Fix table synthetic row return type
KJ21-ENG Jul 22, 2026
19b3ecc
Refactor table empty states outside FlashList
KJ21-ENG Jul 23, 2026
70019b0
Harden table empty-state behavior
KJ21-ENG Jul 23, 2026
9dace1a
Merge remote-tracking branch 'upstream/main' into KJ21-ENG/92930-rais…
KJ21-ENG Jul 23, 2026
a62df5e
Preserve table search across empty layouts
KJ21-ENG Jul 23, 2026
bc4e62b
Separate table list and sticky lifecycle resets
KJ21-ENG Jul 23, 2026
8299f00
Fix table empty-result layout and search focus
KJ21-ENG Jul 27, 2026
c3e6c53
Fix true-empty table centering
KJ21-ENG Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 55 additions & 5 deletions src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import MenuItem from '@components/MenuItem';
import Modal from '@components/Modal';
import useScrollToFocusedInput from '@components/SelectionList/hooks/useScrollToFocusedInput';

import useKeyboardState from '@hooks/useKeyboardState';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
Expand All @@ -11,18 +13,22 @@ import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import CONST from '@src/CONST';

import type {FlashListRef} from '@shopify/flash-list';
import type {ReactElement} from 'react';

import React, {useImperativeHandle, useRef} from 'react';

import type {TableContextValue} from './TableContext';
import type {TableData, TableHandle, TableMethods, TableProps, TableRow} from './types';

import {getTableListMetadata} from './buildTableListData';
import useFiltering from './middlewares/filtering';
import useHighlighting from './middlewares/highlight';
import useSearching from './middlewares/searching';
import useSelection from './middlewares/selection';
import useSorting from './middlewares/sorting';
import TableContext from './TableContext';
import TableEmptyState from './TableEmptyStates/TableEmptyState';
import TableNoResultsState from './TableEmptyStates/TableNoResultsState';

/**
* Builds the Proxy exposed through the Table's ref, forwarding to `tableMethods` first and
Expand All @@ -38,6 +44,7 @@ function createTableHandle<DataType extends TableData, ColumnKey extends string
tableMethods: TableMethods<ColumnKey, FilterKey>,
listRef: React.RefObject<FlashListRef<DataType> | null>,
getProcessedData: () => Array<TableRow<DataType>>,
listDataRowOffset: number,
): TableHandle<DataType, ColumnKey, FilterKey> {
return new Proxy(tableMethods, {
get: (target, property) => {
Expand All @@ -49,6 +56,17 @@ function createTableHandle<DataType extends TableData, ColumnKey extends string
return getProcessedData;
}

if (property === 'scrollToIndex') {
const scrollToIndex = listRef.current?.scrollToIndex;
if (listDataRowOffset === 0 || !scrollToIndex) {
return scrollToIndex;
}

return (params: Parameters<FlashListRef<DataType>['scrollToIndex']>[0]) => {
scrollToIndex({...params, index: params.index + listDataRowOffset});
};
}

return listRef.current?.[property as keyof FlashListRef<DataType>];
},
}) as TableHandle<DataType, ColumnKey, FilterKey>;
Expand Down Expand Up @@ -186,6 +204,8 @@ function Table<DataType extends TableData, ColumnKey extends string = string, Fi
isItemInSearch,
initialSortColumn,
narrowLayoutSortColumn,
headerComponent,
shouldUseStickyColumnHeader = false,
children,
selectionEnabled,
shouldEnableSelectionInNarrowPaneModal,
Expand Down Expand Up @@ -234,6 +254,10 @@ function Table<DataType extends TableData, ColumnKey extends string = string, Fi
const processedData = highlightMiddleware(selectionData);

const listRef = useRef<FlashListRef<DataType>>(null);
// Keeps the table search input visible above the keyboard when it is focused inside the
// scrolling list (native only; the web variant of the hook is a no-op).
const {isKeyboardShown} = useKeyboardState();
const {containerRef: listContainerRef, trackScrollOffset, scrollInputIntoView} = useScrollToFocusedInput(listRef, isKeyboardShown);

const tableMethods: TableMethods<ColumnKey, FilterKey> = {
...filterMethods,
Expand All @@ -243,14 +267,32 @@ function Table<DataType extends TableData, ColumnKey extends string = string, Fi
...highlightingMethods,
};

const originalDataLength = data?.length ?? 0;
const isEmptyResult = processedData.length === 0 && originalDataLength > 0 && (hasActiveSearchString || hasActiveFilters);
const shouldRenderStickyHeader = shouldUseStickyColumnHeader && !(shouldUseNarrowTableLayout && !title);

// When the page header scrolls with the table, TableBody owns the list footer used for empty
// states. They are extracted from the direct children here so they don't render a second time
// as siblings of the body.
const childrenArray = React.Children.toArray(children);
const emptyStateElement = childrenArray.find((child): child is ReactElement => React.isValidElement(child) && child.type === TableEmptyState);
const noResultsStateElement = childrenArray.find((child): child is ReactElement => React.isValidElement(child) && child.type === TableNoResultsState);

const tableListMetadata = getTableListMetadata({
headerComponent,
listHeaderComponent: listProps.ListHeaderComponent,
isEmptyResult,
shouldRenderStickyHeader,
});
const renderedChildren = tableListMetadata.hasPageHeader
? childrenArray.filter((child) => !(React.isValidElement(child) && (child.type === TableEmptyState || child.type === TableNoResultsState)))
: children;

/**
* Exposes table control methods through the ref.
* Uses a Proxy to also forward FlashList methods (like scrollToIndex).
*/
useImperativeHandle(ref, () => createTableHandle(tableMethods, listRef, () => processedData));

const originalDataLength = data?.length ?? 0;
const isEmptyResult = processedData.length === 0 && originalDataLength > 0 && (hasActiveSearchString || hasActiveFilters);
useImperativeHandle(ref, () => createTableHandle(tableMethods, listRef, () => processedData, tableListMetadata.listDataRowOffset));

const handleMobileSelectionPress = () => {
turnOnMobileSelectionMode();
Expand All @@ -264,7 +306,13 @@ function Table<DataType extends TableData, ColumnKey extends string = string, Fi
// eslint-disable-next-line react/jsx-no-constructed-context-values
const contextValue: TableContextValue<DataType, ColumnKey, FilterKey> = {
title,
headerComponent,
emptyStateElement,
noResultsStateElement,
listRef,
listContainerRef,
trackScrollOffset,
scrollInputIntoView,
listProps,
processedData,
originalDataLength,
Expand All @@ -276,6 +324,8 @@ function Table<DataType extends TableData, ColumnKey extends string = string, Fi
tableMethods,
hasActiveFilters,
hasSearchString: hasActiveSearchString,
shouldRenderStickyHeader,
tableListMetadata,
isEmptyResult,
shouldUseNarrowTableLayout,
selectionEnabled,
Expand All @@ -286,7 +336,7 @@ function Table<DataType extends TableData, ColumnKey extends string = string, Fi

return (
<TableContext.Provider value={contextValue as unknown as TableContextValue<TableData, string, string>}>
{children}
{renderedChildren}

<Modal
shouldPreventScrollOnFocus
Expand Down
Loading
Loading