Skip to content

Commit 778eaa4

Browse files
authored
Add isRootDropTarget parameter to renderEmptyState in GridList component (#5211)
* Add renderProps to the function for renderEmptyState
1 parent 4cb6dd3 commit 778eaa4

File tree

5 files changed

+136
-40
lines changed

5 files changed

+136
-40
lines changed

packages/react-aria-components/src/GridList.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export interface GridListProps<T> extends Omit<AriaGridListProps<T>, 'children'>
5454
/** The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the GridList. */
5555
dragAndDropHooks?: DragAndDropHooks,
5656
/** Provides content to display when there are no items in the list. */
57-
renderEmptyState?: () => ReactNode
57+
renderEmptyState?: (props: GridListRenderProps) => ReactNode
5858
}
5959

6060

@@ -156,17 +156,18 @@ function GridListInner<T extends object>({props, collection, gridListRef: ref}:
156156
}
157157

158158
let {focusProps, isFocused, isFocusVisible} = useFocusRing();
159+
let renderValues = {
160+
isDropTarget: isRootDropTarget,
161+
isEmpty: state.collection.size === 0,
162+
isFocused,
163+
isFocusVisible,
164+
state
165+
};
159166
let renderProps = useRenderProps({
160167
className: props.className,
161168
style: props.style,
162169
defaultClassName: 'react-aria-GridList',
163-
values: {
164-
isDropTarget: isRootDropTarget,
165-
isEmpty: state.collection.size === 0,
166-
isFocused,
167-
isFocusVisible,
168-
state
169-
}
170+
values: renderValues
170171
});
171172

172173
let emptyState: ReactNode = null;
@@ -176,7 +177,7 @@ function GridListInner<T extends object>({props, collection, gridListRef: ref}:
176177
// they don't affect the layout of the children. However, WebKit currently has
177178
// a bug that makes grid elements with display: contents hidden to screen readers.
178179
// https://bugs.webkit.org/show_bug.cgi?id=239479
179-
let content = props.renderEmptyState();
180+
let content = props.renderEmptyState(renderValues);
180181
if (isWebKit()) {
181182
// For now, when in an empty state, swap the role to group in webkit.
182183
emptyStatePropOverrides = {

packages/react-aria-components/src/ListBox.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export interface ListBoxProps<T> extends Omit<AriaListBoxProps<T>, 'children' |
5959
/** The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the ListBox. */
6060
dragAndDropHooks?: DragAndDropHooks,
6161
/** Provides content to display when there are no items in the list. */
62-
renderEmptyState?: () => ReactNode,
62+
renderEmptyState?: (props: ListBoxRenderProps) => ReactNode,
6363
/**
6464
* Whether the items are arranged in a stack or grid.
6565
* @default 'stack'
@@ -213,18 +213,19 @@ function ListBoxInner<T>({state, props, listBoxRef}: ListBoxInnerProps<T>) {
213213
}
214214

215215
let {focusProps, isFocused, isFocusVisible} = useFocusRing();
216+
let renderValues = {
217+
isDropTarget: isRootDropTarget,
218+
isEmpty: state.collection.size === 0,
219+
isFocused,
220+
isFocusVisible,
221+
layout: props.layout || 'stack',
222+
state
223+
};
216224
let renderProps = useRenderProps({
217225
className: props.className,
218226
style: props.style,
219227
defaultClassName: 'react-aria-ListBox',
220-
values: {
221-
isDropTarget: isRootDropTarget,
222-
isEmpty: state.collection.size === 0,
223-
isFocused,
224-
isFocusVisible,
225-
layout: props.layout || 'stack',
226-
state
227-
}
228+
values: renderValues
228229
});
229230

230231
let emptyState: JSX.Element | null = null;
@@ -234,7 +235,7 @@ function ListBoxInner<T>({state, props, listBoxRef}: ListBoxInnerProps<T>) {
234235
// eslint-disable-next-line
235236
role="option"
236237
style={{display: 'contents'}}>
237-
{props.renderEmptyState()}
238+
{props.renderEmptyState(renderValues)}
238239
</div>
239240
);
240241
}

packages/react-aria-components/src/Table.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -575,12 +575,17 @@ export interface TableBodyRenderProps {
575575
* Whether the table body has no rows and should display its empty state.
576576
* @selector [data-empty]
577577
*/
578-
isEmpty: boolean
578+
isEmpty: boolean,
579+
/**
580+
* Whether the Table is currently the active drop target.
581+
* @selector [data-drop-target]
582+
*/
583+
isDropTarget: boolean
579584
}
580585

581586
export interface TableBodyProps<T> extends CollectionProps<T>, StyleRenderProps<TableBodyRenderProps> {
582587
/** Provides content to display when there are no rows in the table. */
583-
renderEmptyState?: () => ReactNode
588+
renderEmptyState?: (props: TableBodyRenderProps) => ReactNode
584589
}
585590

586591
function TableBody<T extends object>(props: TableBodyProps<T>, ref: ForwardedRef<HTMLTableSectionElement>): JSX.Element | null {
@@ -693,7 +698,8 @@ function TableHeaderRowGroup<T>({collection}: {collection: TableCollection<T>})
693698
);
694699
}
695700

696-
function TableBodyRowGroup<T>({collection, isDroppable}: {collection: TableCollection<T>, isDroppable: boolean}) {
701+
function TableBodyRowGroup<T>(props: {collection: TableCollection<T>, isDroppable: boolean}) {
702+
let {collection, isDroppable} = props;
697703
let bodyRows = useCachedChildren({
698704
items: collection.rows,
699705
children: useCallback((item: Node<T>) => {
@@ -706,23 +712,29 @@ function TableBodyRowGroup<T>({collection, isDroppable}: {collection: TableColle
706712
}, [])
707713
});
708714

709-
let props: TableBodyProps<T> = collection.body.props;
715+
let state = useContext(TableStateContext);
716+
let {dropState} = useContext(DragAndDropContext);
717+
let isRootDropTarget = isDroppable && !!dropState && (dropState.isDropTarget({type: 'root'}) ?? false);
718+
719+
let bodyProps: TableBodyProps<T> = collection.body.props;
720+
let renderValues = {
721+
isDropTarget: isRootDropTarget,
722+
isEmpty: collection.size === 0
723+
};
710724
let renderProps = useRenderProps({
711-
...props,
725+
...bodyProps,
712726
id: undefined,
713727
children: undefined,
714728
defaultClassName: 'react-aria-TableBody',
715-
values: {
716-
isEmpty: collection.size === 0
717-
}
729+
values: renderValues
718730
});
719731

720732
let emptyState;
721-
if (collection.size === 0 && props.renderEmptyState) {
733+
if (collection.size === 0 && bodyProps.renderEmptyState && state) {
722734
emptyState = (
723735
<tr role="row">
724736
<td role="gridcell" colSpan={collection.columnCount}>
725-
{props.renderEmptyState()}
737+
{bodyProps.renderEmptyState(renderValues)}
726738
</td>
727739
</tr>
728740
);
@@ -731,7 +743,7 @@ function TableBodyRowGroup<T>({collection, isDroppable}: {collection: TableColle
731743
let {rowGroupProps} = useTableRowGroup();
732744
return (
733745
<tbody
734-
{...mergeProps(filterDOMProps(props as any), rowGroupProps)}
746+
{...mergeProps(filterDOMProps(bodyProps as any), rowGroupProps)}
735747
{...renderProps}
736748
ref={collection.body.props.ref}
737749
data-empty={collection.size === 0 || undefined}>

packages/react-aria-components/src/TagGroup.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import {ContextValue, DOMProps, forwardRefType, Provider, RenderProps, SlotProps
1717
import {filterDOMProps, mergeProps, useObjectRef} from '@react-aria/utils';
1818
import {LabelContext} from './Label';
1919
import {LinkDOMProps} from '@react-types/shared';
20+
import {ListState, Node, useListState} from 'react-stately';
2021
import {ListStateContext} from './ListBox';
21-
import {Node, useListState} from 'react-stately';
2222
import React, {createContext, ForwardedRef, forwardRef, Key, ReactNode, useContext, useEffect, useRef} from 'react';
2323
import {TextContext} from './Text';
2424

@@ -39,12 +39,16 @@ export interface TagListRenderProps {
3939
* Whether the tag list is currently keyboard focused.
4040
* @selector [data-focus-visible]
4141
*/
42-
isFocusVisible: boolean
42+
isFocusVisible: boolean,
43+
/**
44+
* State of the TagGroup.
45+
*/
46+
state: ListState<unknown>
4347
}
4448

4549
export interface TagListProps<T> extends Omit<CollectionProps<T>, 'disabledKeys'>, StyleRenderProps<TagListRenderProps> {
4650
/** Provides content to display when there are no items in the tag list. */
47-
renderEmptyState?: () => ReactNode
51+
renderEmptyState?: (props: TagListRenderProps) => ReactNode
4852
}
4953

5054
export const TagGroupContext = createContext<ContextValue<TagGroupProps, HTMLDivElement>>(null);
@@ -142,15 +146,17 @@ function TagListInner<T extends object>({props, forwardedRef}: TagListInnerProps
142146
});
143147

144148
let {focusProps, isFocused, isFocusVisible} = useFocusRing();
149+
let renderValues = {
150+
isEmpty: state.collection.size === 0,
151+
isFocused,
152+
isFocusVisible,
153+
state
154+
};
145155
let renderProps = useRenderProps({
146156
className: props.className,
147157
style: props.style,
148158
defaultClassName: 'react-aria-TagList',
149-
values: {
150-
isEmpty: state.collection.size === 0,
151-
isFocused,
152-
isFocusVisible
153-
}
159+
values: renderValues
154160
});
155161

156162
return (
@@ -161,7 +167,7 @@ function TagListInner<T extends object>({props, forwardedRef}: TagListInnerProps
161167
data-empty={state.collection.size === 0 || undefined}
162168
data-focused={isFocused || undefined}
163169
data-focus-visible={isFocusVisible || undefined}>
164-
{state.collection.size === 0 && props.renderEmptyState ? props.renderEmptyState() : children}
170+
{state.collection.size === 0 && props.renderEmptyState ? props.renderEmptyState(renderValues) : children}
165171
</div>
166172
);
167173
}

packages/react-aria-components/stories/index.stories.tsx

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {action} from '@storybook/addon-actions';
1414
import {Button, Calendar, CalendarCell, CalendarGrid, Cell, Checkbox, Column, ColumnResizer, ComboBox, DateField, DateInput, DatePicker, DateRangePicker, DateSegment, Dialog, DialogTrigger, DropZone, FileTrigger, Group, Header, Heading, Input, Item, Keyboard, Label, Link, ListBox, ListBoxProps, Menu, MenuTrigger, Modal, ModalOverlay, NumberField, OverlayArrow, Popover, Radio, RadioGroup, RangeCalendar, ResizableTableContainer, Row, SearchField, Section, Select, SelectValue, Separator, Slider, SliderOutput, SliderThumb, SliderTrack, Switch, Tab, Table, TableBody, TableHeader, TabList, TabPanel, Tabs, TabsProps, Tag, TagGroup, TagList, Text, TextField, TimeField, ToggleButton, Toolbar, Tooltip, TooltipTrigger, useDragAndDrop} from 'react-aria-components';
1515
import {classNames} from '@react-spectrum/utils';
1616
import clsx from 'clsx';
17-
import {FocusRing, mergeProps, useButton, useClipboard, useDrag} from 'react-aria';
17+
import {FocusRing, isTextDropItem, mergeProps, useButton, useClipboard, useDrag} from 'react-aria';
1818
import React, {useRef, useState} from 'react';
1919
import {RouterProvider} from '@react-aria/utils';
2020
import styles from '../example/index.css';
@@ -713,6 +713,82 @@ export const TabsRenderProps = () => {
713713
);
714714
};
715715

716+
const ReorderableTable = ({initialItems}: {initialItems: {id: string, name: string}[]}) => {
717+
let list = useListData({initialItems});
718+
719+
const {dragAndDropHooks} = useDragAndDrop({
720+
getItems: keys => {
721+
return [...keys].map(k => {
722+
const item = list.getItem(k);
723+
return {
724+
'text/plain': item.id,
725+
item: JSON.stringify(item)
726+
};
727+
});
728+
},
729+
getDropOperation: () => 'move',
730+
onReorder: e => {
731+
if (e.target.dropPosition === 'before') {
732+
list.moveBefore(e.target.key, e.keys);
733+
} else if (e.target.dropPosition === 'after') {
734+
list.moveAfter(e.target.key, e.keys);
735+
}
736+
},
737+
onInsert: async e => {
738+
const processedItems = await Promise.all(
739+
e.items.filter(isTextDropItem).map(async item => JSON.parse(await item.getText('item')))
740+
);
741+
if (e.target.dropPosition === 'before') {
742+
list.insertBefore(e.target.key, ...processedItems);
743+
} else if (e.target.dropPosition === 'after') {
744+
list.insertAfter(e.target.key, ...processedItems);
745+
}
746+
},
747+
748+
onDragEnd: e => {
749+
if (e.dropOperation === 'move' && !e.isInternal) {
750+
list.remove(...e.keys);
751+
}
752+
},
753+
754+
onRootDrop: async e => {
755+
const processedItems = await Promise.all(
756+
e.items.filter(isTextDropItem).map(async item => JSON.parse(await item.getText('item')))
757+
);
758+
759+
list.append(...processedItems);
760+
}
761+
});
762+
763+
return (
764+
<Table aria-label="Reorderable table" dragAndDropHooks={dragAndDropHooks}>
765+
<TableHeader>
766+
<MyColumn isRowHeader defaultWidth="50%">Id</MyColumn>
767+
<MyColumn>Name</MyColumn>
768+
</TableHeader>
769+
<TableBody items={list.items} renderEmptyState={({isDropTarget}) => <span style={{color: isDropTarget ? 'red' : 'black'}}>Drop items here</span>}>
770+
{item => (
771+
<Row>
772+
<Cell>{item.id}</Cell>
773+
<Cell>{item.name}</Cell>
774+
</Row>
775+
)}
776+
</TableBody>
777+
</Table>
778+
);
779+
};
780+
781+
export const ReorderableTableExample = () => (
782+
<>
783+
<ResizableTableContainer style={{width: 300, overflow: 'auto'}}>
784+
<ReorderableTable initialItems={[{id: '1', name: 'Bob'}]} />
785+
</ResizableTableContainer>
786+
<ResizableTableContainer style={{width: 300, overflow: 'auto'}}>
787+
<ReorderableTable initialItems={[{id: '2', name: 'Alex'}]} />
788+
</ResizableTableContainer>
789+
</>
790+
);
791+
716792
export const TableExample = () => {
717793
let list = useListData({
718794
initialItems: [

0 commit comments

Comments
 (0)