Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 10 additions & 21 deletions packages/@react-spectrum/s2/src/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ import {GridNode} from 'react-stately/private/grid/GridCollection';
import {IconContext} from './Icon';
import intlMessages from '../intl/*.json';
import {Key} from '@react-types/shared';
import {LayoutInfo, Rect, TableLayout, Virtualizer} from 'react-aria-components/Virtualizer';
import {LayoutInfo, TableLayout, Virtualizer} from 'react-aria-components/Virtualizer';
import {LayoutNode} from 'react-stately/useVirtualizerState';
import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu';
import Nubbin from '../ui-icons/S2_MoveHorizontalTableWidget.svg';
Expand Down Expand Up @@ -257,6 +257,8 @@ const table = style<
minWidth: 0,
fontFamily: 'sans',
fontWeight: 'normal',
display: 'flex',
flexDirection: 'column',
overflow: 'auto',
backgroundColor: {
default: 'gray-25',
Expand Down Expand Up @@ -343,13 +345,6 @@ export class S2TableLayout<T> extends TableLayout<T> {
if (rowGroups.length < 2) {
return [];
}
let {layoutInfo} = rowGroups[1];
// TableLayout's buildCollection always sets the body width to the max width between the header width, but
// we want the body to be sticky and only as wide as the table so it is always in view if loading/empty
let isEmptyOrLoading = this.virtualizer?.collection.size === 0;
if (isEmptyOrLoading) {
layoutInfo.rect.width = this.virtualizer!.size.width - 80;
}

return rowGroups;
}
Expand All @@ -359,11 +354,6 @@ export class S2TableLayout<T> extends TableLayout<T> {
let {layoutInfo} = layoutNode;
layoutInfo.allowOverflow = true;
layoutInfo.rect.width = this.virtualizer!.size.width;
// If performing first load or empty, the body will be sticky so we don't want to apply sticky to the loader, otherwise it will
// affect the positioning of the empty state renderer
let collection = this.virtualizer!.collection;
let isEmptyOrLoading = collection?.size === 0;
layoutInfo.isSticky = !isEmptyOrLoading;
return layoutNode;
}

Expand All @@ -375,16 +365,15 @@ export class S2TableLayout<T> extends TableLayout<T> {
let {layoutInfo} = layoutNode;
// Needs overflow for sticky loader
layoutInfo.allowOverflow = true;
// If loading or empty, we'll want the body to be sticky and centered
// The base TableLayout.buildRowGroup sizes the empty/loading body from the virtualizer's
// current on-screen size, which is circular when no explicit height is set (the empty-state
// content is rendered outside this virtualized rect entirely, see Table.tsx's TableInner).
// Reset it back to the real (near-zero) computed height so it doesn't feed back into the
// ResizeObserver that measures that same on-screen size.
let isEmptyOrLoading = this.virtualizer?.collection.size === 0;
if (isEmptyOrLoading) {
layoutInfo.rect = new Rect(
40,
40,
this.virtualizer!.size.width - 80,
this.virtualizer!.size.height - 80
);
layoutInfo.isSticky = true;
let maxY = layoutNode.children?.reduce((max, child) => Math.max(max, child.layoutInfo.rect.maxY), layoutInfo.rect.y) ?? layoutInfo.rect.y;
layoutInfo.rect.height = maxY - layoutInfo.rect.y;
}

return {...layoutNode, layoutInfo};
Expand Down
27 changes: 26 additions & 1 deletion packages/@react-spectrum/s2/stories/TableView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ function renderEmptyState() {
}

const EmptyStateTable = (args: TableViewProps): ReactElement => (
<TableView aria-label="Empty state" {...args} styles={style({height: 320, width: 320})}>
<TableView aria-label="Empty state" {...args} styles={style({ width: 320})}>
<TableHeader columns={columns}>
{column => (
<Column minWidth={200} width={200} isRowHeader={column.isRowHeader}>
Expand All @@ -459,6 +459,31 @@ export const EmptyState: StoryObj<typeof EmptyStateTable> = {
}
};

// No explicit height set at all: https://github.com/adobe/react-spectrum/issues/10235
// The table should still auto-size to show the column headers and the empty state.
const EmptyStateTableNoHeight = (args: TableViewProps): ReactElement => (
<TableView aria-label="Empty state, no height" {...args} styles={style({width: 320})}>
<TableHeader columns={columns}>
{column => (
<Column minWidth={200} width={200} isRowHeader={column.isRowHeader}>
{column.name}
</Column>
)}
</TableHeader>
<TableBody items={[]} renderEmptyState={renderEmptyState}>
{[]}
</TableBody>
</TableView>
);

export const EmptyStateNoHeight: StoryObj<typeof EmptyStateTableNoHeight> = {
render: EmptyStateTableNoHeight,
args: {
...Example.args
},
name: 'empty state, no explicit height'
};

export const LoadingStateNoItems: StoryObj<typeof EmptyStateTable> = {
render: EmptyStateTable,
args: {
Expand Down
64 changes: 46 additions & 18 deletions packages/react-aria-components/src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,21 @@ function TableInner({props, forwardedRef: ref, selectionState, collection}: Tabl

let DOMProps = filterDOMProps(props, {global: true});

let bodyNode = filteredState.collection.body;
let isBodyEmpty = filteredState.collection.size === 0;
let renderEmptyState = bodyNode?.props?.renderEmptyState;
let {rowGroupProps: emptyRowGroupProps} = useTableRowGroup();
let emptyStateRowGroup = isVirtualized && isBodyEmpty && renderEmptyState && (
<TableBodyElementType {...emptyRowGroupProps} data-empty="true">
{renderTableEmptyState(
renderEmptyState,
{isDropTarget: isRootDropTarget, isEmpty: isBodyEmpty},
filteredState.collection.columnCount,
isVirtualized
)}
</TableBodyElementType>
);

return (
<Provider
values={[
Expand Down Expand Up @@ -921,6 +936,7 @@ function TableInner({props, forwardedRef: ref, selectionState, collection}: Tabl
scrollRef={tableContainerContext?.scrollRef ?? ref}
persistedKeys={useDndPersistedKeys(selectionManager, dragAndDropHooks, dropState)}
/>
{emptyStateRowGroup}
</SharedElementTransition>
</TableElementType>
</FocusScope>
Expand Down Expand Up @@ -1500,6 +1516,31 @@ let TableBodyElementType = forwardRef(function TableBodyElementType(
return <dom.tbody {...props} ref={ref} />;
});

function renderTableEmptyState(
renderEmptyState: (props: TableBodyRenderProps) => ReactNode,
renderValues: TableBodyRenderProps,
numColumns: number,
isVirtualized: boolean
) {
let rowProps = {};
let rowHeaderProps = {};
let style = {};
if (isVirtualized) {
rowHeaderProps['aria-colspan'] = numColumns;
style = {display: 'contents'};
} else {
rowHeaderProps['colSpan'] = numColumns;
}

return (
<TableRowElementType role="row" {...rowProps} style={style}>
<TableCellElementType role="rowheader" {...rowHeaderProps} style={style}>
{renderEmptyState(renderValues)}
</TableCellElementType>
</TableRowElementType>
);
}

/**
* The body of a `<Table>`, containing the table rows.
*/
Expand Down Expand Up @@ -1535,24 +1576,11 @@ export const TableBody = /*#__PURE__*/ createBranchComponent(
let emptyState;
let numColumns = collection.columnCount;

if (isEmpty && props.renderEmptyState && state) {
let rowProps = {};
let rowHeaderProps = {};
let style = {};
if (isVirtualized) {
rowHeaderProps['aria-colspan'] = numColumns;
style = {display: 'contents'};
} else {
rowHeaderProps['colSpan'] = numColumns;
}

emptyState = (
<TableRowElementType role="row" {...rowProps} style={style}>
<TableCellElementType role="rowheader" {...rowHeaderProps} style={style}>
{props.renderEmptyState(renderValues)}
</TableCellElementType>
</TableRowElementType>
);
// When virtualized, the empty state is rendered as a sibling of the CollectionRoot
// in TableInner instead, so that it can be sized by its own content rather than being
// trapped in a `contain: size` virtualizer-positioned box. See TableInner.
if (!isVirtualized && isEmpty && props.renderEmptyState && state) {
emptyState = renderTableEmptyState(props.renderEmptyState, renderValues, numColumns, isVirtualized);
}

let {rowGroupProps} = useTableRowGroup();
Expand Down