Skip to content
Merged
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
19 changes: 10 additions & 9 deletions packages/react-aria-components/src/GridList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface GridListProps<T> extends Omit<AriaGridListProps<T>, 'children'>
/** The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the GridList. */
dragAndDropHooks?: DragAndDropHooks,
/** Provides content to display when there are no items in the list. */
renderEmptyState?: () => ReactNode
renderEmptyState?: (props: GridListRenderProps) => ReactNode
}


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

let {focusProps, isFocused, isFocusVisible} = useFocusRing();
let renderValues = {
isDropTarget: isRootDropTarget,
isEmpty: state.collection.size === 0,
isFocused,
isFocusVisible,
state
};
let renderProps = useRenderProps({
className: props.className,
style: props.style,
defaultClassName: 'react-aria-GridList',
values: {
isDropTarget: isRootDropTarget,
isEmpty: state.collection.size === 0,
isFocused,
isFocusVisible,
state
}
values: renderValues
});

let emptyState: ReactNode = null;
Expand All @@ -176,7 +177,7 @@ function GridListInner<T extends object>({props, collection, gridListRef: ref}:
// they don't affect the layout of the children. However, WebKit currently has
// a bug that makes grid elements with display: contents hidden to screen readers.
// https://bugs.webkit.org/show_bug.cgi?id=239479
let content = props.renderEmptyState();
let content = props.renderEmptyState(renderValues);
if (isWebKit()) {
// For now, when in an empty state, swap the role to group in webkit.
emptyStatePropOverrides = {
Expand Down
21 changes: 11 additions & 10 deletions packages/react-aria-components/src/ListBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export interface ListBoxProps<T> extends Omit<AriaListBoxProps<T>, 'children' |
/** The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the ListBox. */
dragAndDropHooks?: DragAndDropHooks,
/** Provides content to display when there are no items in the list. */
renderEmptyState?: () => ReactNode,
renderEmptyState?: (props: ListBoxRenderProps) => ReactNode,
/**
* Whether the items are arranged in a stack or grid.
* @default 'stack'
Expand Down Expand Up @@ -213,18 +213,19 @@ function ListBoxInner<T>({state, props, listBoxRef}: ListBoxInnerProps<T>) {
}

let {focusProps, isFocused, isFocusVisible} = useFocusRing();
let renderValues = {
isDropTarget: isRootDropTarget,
isEmpty: state.collection.size === 0,
isFocused,
isFocusVisible,
layout: props.layout || 'stack',
state
};
let renderProps = useRenderProps({
className: props.className,
style: props.style,
defaultClassName: 'react-aria-ListBox',
values: {
isDropTarget: isRootDropTarget,
isEmpty: state.collection.size === 0,
isFocused,
isFocusVisible,
layout: props.layout || 'stack',
state
}
values: renderValues
});

let emptyState: JSX.Element | null = null;
Expand All @@ -234,7 +235,7 @@ function ListBoxInner<T>({state, props, listBoxRef}: ListBoxInnerProps<T>) {
// eslint-disable-next-line
role="option"
style={{display: 'contents'}}>
{props.renderEmptyState()}
{props.renderEmptyState(renderValues)}
</div>
);
}
Expand Down
34 changes: 23 additions & 11 deletions packages/react-aria-components/src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -575,12 +575,17 @@ export interface TableBodyRenderProps {
* Whether the table body has no rows and should display its empty state.
* @selector [data-empty]
*/
isEmpty: boolean
isEmpty: boolean,
/**
* Whether the Table is currently the active drop target.
* @selector [data-drop-target]
*/
isDropTarget: boolean
}

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

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

function TableBodyRowGroup<T>({collection, isDroppable}: {collection: TableCollection<T>, isDroppable: boolean}) {
function TableBodyRowGroup<T>(props: {collection: TableCollection<T>, isDroppable: boolean}) {
let {collection, isDroppable} = props;
let bodyRows = useCachedChildren({
items: collection.rows,
children: useCallback((item: Node<T>) => {
Expand All @@ -706,23 +712,29 @@ function TableBodyRowGroup<T>({collection, isDroppable}: {collection: TableColle
}, [])
});

let props: TableBodyProps<T> = collection.body.props;
let state = useContext(TableStateContext);
let {dropState} = useContext(DragAndDropContext);
let isRootDropTarget = isDroppable && !!dropState && (dropState.isDropTarget({type: 'root'}) ?? false);

let bodyProps: TableBodyProps<T> = collection.body.props;
let renderValues = {
isDropTarget: isRootDropTarget,
isEmpty: collection.size === 0
};
let renderProps = useRenderProps({
...props,
...bodyProps,
id: undefined,
children: undefined,
defaultClassName: 'react-aria-TableBody',
values: {
isEmpty: collection.size === 0
}
values: renderValues
});

let emptyState;
if (collection.size === 0 && props.renderEmptyState) {
if (collection.size === 0 && bodyProps.renderEmptyState && state) {
emptyState = (
<tr role="row">
<td role="gridcell" colSpan={collection.columnCount}>
{props.renderEmptyState()}
{bodyProps.renderEmptyState(renderValues)}
</td>
</tr>
);
Expand All @@ -731,7 +743,7 @@ function TableBodyRowGroup<T>({collection, isDroppable}: {collection: TableColle
let {rowGroupProps} = useTableRowGroup();
return (
<tbody
{...mergeProps(filterDOMProps(props as any), rowGroupProps)}
{...mergeProps(filterDOMProps(bodyProps as any), rowGroupProps)}
{...renderProps}
ref={collection.body.props.ref}
data-empty={collection.size === 0 || undefined}>
Expand Down
24 changes: 15 additions & 9 deletions packages/react-aria-components/src/TagGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import {ContextValue, DOMProps, forwardRefType, Provider, RenderProps, SlotProps
import {filterDOMProps, mergeProps, useObjectRef} from '@react-aria/utils';
import {LabelContext} from './Label';
import {LinkDOMProps} from '@react-types/shared';
import {ListState, Node, useListState} from 'react-stately';
import {ListStateContext} from './ListBox';
import {Node, useListState} from 'react-stately';
import React, {createContext, ForwardedRef, forwardRef, Key, ReactNode, useContext, useEffect, useRef} from 'react';
import {TextContext} from './Text';

Expand All @@ -39,12 +39,16 @@ export interface TagListRenderProps {
* Whether the tag list is currently keyboard focused.
* @selector [data-focus-visible]
*/
isFocusVisible: boolean
isFocusVisible: boolean,
/**
* State of the TagGroup.
*/
state: ListState<unknown>
}

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

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

let {focusProps, isFocused, isFocusVisible} = useFocusRing();
let renderValues = {
isEmpty: state.collection.size === 0,
isFocused,
isFocusVisible,
state
};
let renderProps = useRenderProps({
className: props.className,
style: props.style,
defaultClassName: 'react-aria-TagList',
values: {
isEmpty: state.collection.size === 0,
isFocused,
isFocusVisible
}
values: renderValues
});

return (
Expand All @@ -161,7 +167,7 @@ function TagListInner<T extends object>({props, forwardedRef}: TagListInnerProps
data-empty={state.collection.size === 0 || undefined}
data-focused={isFocused || undefined}
data-focus-visible={isFocusVisible || undefined}>
{state.collection.size === 0 && props.renderEmptyState ? props.renderEmptyState() : children}
{state.collection.size === 0 && props.renderEmptyState ? props.renderEmptyState(renderValues) : children}
</div>
);
}
Expand Down
78 changes: 77 additions & 1 deletion packages/react-aria-components/stories/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {action} from '@storybook/addon-actions';
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';
import {classNames} from '@react-spectrum/utils';
import clsx from 'clsx';
import {FocusRing, mergeProps, useButton, useClipboard, useDrag} from 'react-aria';
import {FocusRing, isTextDropItem, mergeProps, useButton, useClipboard, useDrag} from 'react-aria';
import React, {useRef, useState} from 'react';
import {RouterProvider} from '@react-aria/utils';
import styles from '../example/index.css';
Expand Down Expand Up @@ -713,6 +713,82 @@ export const TabsRenderProps = () => {
);
};

const ReorderableTable = ({initialItems}: {initialItems: {id: string, name: string}[]}) => {
let list = useListData({initialItems});

const {dragAndDropHooks} = useDragAndDrop({
getItems: keys => {
return [...keys].map(k => {
const item = list.getItem(k);
return {
'text/plain': item.id,
item: JSON.stringify(item)
};
});
},
getDropOperation: () => 'move',
onReorder: e => {
if (e.target.dropPosition === 'before') {
list.moveBefore(e.target.key, e.keys);
} else if (e.target.dropPosition === 'after') {
list.moveAfter(e.target.key, e.keys);
}
},
onInsert: async e => {
const processedItems = await Promise.all(
e.items.filter(isTextDropItem).map(async item => JSON.parse(await item.getText('item')))
);
if (e.target.dropPosition === 'before') {
list.insertBefore(e.target.key, ...processedItems);
} else if (e.target.dropPosition === 'after') {
list.insertAfter(e.target.key, ...processedItems);
}
},

onDragEnd: e => {
if (e.dropOperation === 'move' && !e.isInternal) {
list.remove(...e.keys);
}
},

onRootDrop: async e => {
const processedItems = await Promise.all(
e.items.filter(isTextDropItem).map(async item => JSON.parse(await item.getText('item')))
);

list.append(...processedItems);
}
});

return (
<Table aria-label="Reorderable table" dragAndDropHooks={dragAndDropHooks}>
<TableHeader>
<MyColumn isRowHeader defaultWidth="50%">Id</MyColumn>
<MyColumn>Name</MyColumn>
</TableHeader>
<TableBody items={list.items} renderEmptyState={({isDropTarget}) => <span style={{color: isDropTarget ? 'red' : 'black'}}>Drop items here</span>}>
{item => (
<Row>
<Cell>{item.id}</Cell>
<Cell>{item.name}</Cell>
</Row>
)}
</TableBody>
</Table>
);
};

export const ReorderableTableExample = () => (
<>
<ResizableTableContainer style={{width: 300, overflow: 'auto'}}>
<ReorderableTable initialItems={[{id: '1', name: 'Bob'}]} />
</ResizableTableContainer>
<ResizableTableContainer style={{width: 300, overflow: 'auto'}}>
<ReorderableTable initialItems={[{id: '2', name: 'Alex'}]} />
</ResizableTableContainer>
</>
);

export const TableExample = () => {
let list = useListData({
initialItems: [
Expand Down