ListView
Lists are containers for displaying information. They allow users to quickly scan, sort, compare, and take action on large amounts of data.
| install | yarn add @react-spectrum/list |
|---|---|
| version | 3.0.0-alpha.11 |
| usage | import {Item, ListView} from '@react-spectrum/list' |
Example#
<ListView
selectionMode="multiple"
aria-label="Static ListView items example"
maxWidth="size-6000"
>
<Item>Adobe Photoshop</Item>
<Item>Adobe InDesign</Item>
<Item>Adobe AfterEffects</Item>
<Item>Adobe Illustrator</Item>
<Item>Adobe Lightroom</Item>
</ListView>
<ListView
selectionMode="multiple"
aria-label="Static ListView items example"
maxWidth="size-6000"
>
<Item>Adobe Photoshop</Item>
<Item>Adobe InDesign</Item>
<Item>Adobe AfterEffects</Item>
<Item>Adobe Illustrator</Item>
<Item>Adobe Lightroom</Item>
</ListView>
<ListView
selectionMode="multiple"
aria-label="Static ListView items example"
maxWidth="size-6000"
>
<Item>
Adobe Photoshop
</Item>
<Item>
Adobe InDesign
</Item>
<Item>
Adobe AfterEffects
</Item>
<Item>
Adobe Illustrator
</Item>
<Item>
Adobe Lightroom
</Item>
</ListView>
Content#
ListView is a collection component that provides users with a way to view, select, navigate, or drag and drop items in a list.
Basic usage of ListView, seen in the example above, shows the use of a static collection where the contents of the ListView is hard coded. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API, or update over time. Providing the data dynamically allows ListView to automatically cache the rendering of each item, which dramatically improves performance.
Each item has a unique key defined by the data. In the example below, the key of each row element is implicitly defined by the id property of the row object.
See collections to learn more keys in dynamic collections.
const items = [
{ id: 1, name: 'Adobe Photoshop' },
{ id: 2, name: 'Adobe XD' },
{ id: 3, name: 'Adobe InDesign' },
{ id: 4, name: 'Adobe AfterEffects' },
{ id: 5, name: 'Adobe Illustrator' },
{ id: 6, name: 'Adobe Lightroom' },
{ id: 7, name: 'Adobe Premiere Pro' },
{ id: 8, name: 'Adobe Fresco' },
{ id: 9, name: 'Adobe Dreamweaver' }
];
<ListView
items={items}
selectionMode="multiple"
maxWidth="size-6000"
height="250px"
aria-label="Dynamic ListView items example"
>
{(item) => (
<Item key={item.key}>
{item.name}
</Item>
)}
</ListView>
const items = [
{ id: 1, name: 'Adobe Photoshop' },
{ id: 2, name: 'Adobe XD' },
{ id: 3, name: 'Adobe InDesign' },
{ id: 4, name: 'Adobe AfterEffects' },
{ id: 5, name: 'Adobe Illustrator' },
{ id: 6, name: 'Adobe Lightroom' },
{ id: 7, name: 'Adobe Premiere Pro' },
{ id: 8, name: 'Adobe Fresco' },
{ id: 9, name: 'Adobe Dreamweaver' }
];
<ListView
items={items}
selectionMode="multiple"
maxWidth="size-6000"
height="250px"
aria-label="Dynamic ListView items example"
>
{(item) => (
<Item key={item.key}>
{item.name}
</Item>
)}
</ListView>
const items = [
{
id: 1,
name:
'Adobe Photoshop'
},
{
id: 2,
name: 'Adobe XD'
},
{
id: 3,
name:
'Adobe InDesign'
},
{
id: 4,
name:
'Adobe AfterEffects'
},
{
id: 5,
name:
'Adobe Illustrator'
},
{
id: 6,
name:
'Adobe Lightroom'
},
{
id: 7,
name:
'Adobe Premiere Pro'
},
{
id: 8,
name: 'Adobe Fresco'
},
{
id: 9,
name:
'Adobe Dreamweaver'
}
];
<ListView
items={items}
selectionMode="multiple"
maxWidth="size-6000"
height="250px"
aria-label="Dynamic ListView items example"
>
{(item) => (
<Item
key={item.key}
>
{item.name}
</Item>
)}
</ListView>
Internationalization#
To internationalize a ListView, all text content within the ListView should be localized. This includes the aria-label provided to the ListView if any.
For languages that are read right-to-left (e.g. Hebrew and Arabic), the layout of ListView is automatically flipped.
Labeling#
Accessibility#
An aria-label must be provided to the ListView for accessibility. If the ListView is labeled by a separate element, an aria-labelledby prop must be provided using the id of the labeling element instead.
Asynchronous loading#
ListView supports loading data asynchronously, and will display a progress circle reflecting the current load state,
set by the loadingState prop. It also supports infinite scrolling to load more data on demand as the user scrolls, via the onLoadMore prop.
This example uses the useAsyncList hook to handle loading the data. See the docs for more information.
import {useAsyncList} from '@react-stately/data';
function AsyncList() {
let list = useAsyncList({
async load({ signal, cursor }) {
if (cursor) {
cursor = cursor.replace(/^http:\/\//i, 'https://');
}
let res = await fetch(
cursor || `https://swapi.py4e.com/api/people/?search=`,
{ signal }
);
let json = await res.json();
return {
items: json.results,
cursor: json.next
};
}
});
return (
<ListView
selectionMode="multiple"
aria-label="Async loading ListView example"
maxWidth="size-6000"
height="size-3000"
items={list.items}
loadingState={list.loadingState}
onLoadMore={list.loadMore}
>
{(item) => <Item key={item.name}>{item.name}</Item>}
</ListView>
);
}
import {useAsyncList} from '@react-stately/data';
function AsyncList() {
let list = useAsyncList({
async load({ signal, cursor }) {
if (cursor) {
cursor = cursor.replace(/^http:\/\//i, 'https://');
}
let res = await fetch(
cursor ||
`https://swapi.py4e.com/api/people/?search=`,
{ signal }
);
let json = await res.json();
return {
items: json.results,
cursor: json.next
};
}
});
return (
<ListView
selectionMode="multiple"
aria-label="Async loading ListView example"
maxWidth="size-6000"
height="size-3000"
items={list.items}
loadingState={list.loadingState}
onLoadMore={list.loadMore}
>
{(item) => <Item key={item.name}>{item.name}</Item>}
</ListView>
);
}
import {useAsyncList} from '@react-stately/data';
function AsyncList() {
let list =
useAsyncList({
async load(
{
signal,
cursor
}
) {
if (cursor) {
cursor = cursor
.replace(
/^http:\/\//i,
'https://'
);
}
let res =
await fetch(
cursor ||
`https://swapi.py4e.com/api/people/?search=`,
{ signal }
);
let json =
await res
.json();
return {
items:
json.results,
cursor:
json.next
};
}
});
return (
<ListView
selectionMode="multiple"
aria-label="Async loading ListView example"
maxWidth="size-6000"
height="size-3000"
items={list.items}
loadingState={list
.loadingState}
onLoadMore={list
.loadMore}
>
{(item) => (
<Item
key={item.name}
>
{item.name}
</Item>
)}
</ListView>
);
}
Complex items#
Items within a ListView also allow for additional content used to add context or provide additional actions to items. Descriptions, icons, and thumbnails can be added to
the children of <Item> as shown in the example below. If a description is added, the prop slot="description" must be used to distinguish the different <Text> elements.
Additionally, components such as <ActionButton>, <ActionGroup>, and <ActionMenu> will be styled appropriately if included within an item. Providing the hasChildItems prop
to an <Item> will add a chevron icon to the end of the row to visually indicate that the row has children.
<ListView
selectionMode="multiple"
maxWidth="size-6000"
aria-label="ListView example with complex items"
onAction={(key) => alert(`Triggering action on item `)}
>
<Item key="1" textValue="Utilities" hasChildItems>
<Folder />
<Text>Utilities</Text>
<Text slot="description">16 items</Text>
<ActionMenu>
<Item key="edit" textValue="Edit">
<Edit />
<Text>Edit</Text>
</Item>
<Item key="delete" textValue="Delete">
<Delete />
<Text>Delete</Text>
</Item>
</ActionMenu>
</Item>
<Item key="2" textValue="Glasses Dog">
<Image
src="https://random.dog/1a0535a6-ca89-4059-9b3a-04a554c0587b.jpg"
alt="Shiba Inu with glasses"
/>
<Text>Glasses Dog</Text>
<Text slot="description">JPG</Text>
<ActionMenu>
<Item key="edit" textValue="Edit">
<Edit />
<Text>Edit</Text>
</Item>
<Item key="delete" textValue="Delete">
<Delete />
<Text>Delete</Text>
</Item>
</ActionMenu>
</Item>
<Item key="3" textValue="readme">
<Text>readme.txt</Text>
<Text slot="description">TXT</Text>
<ActionMenu>
<Item key="edit" textValue="Edit">
<Edit />
<Text>Edit</Text>
</Item>
<Item key="delete" textValue="Delete">
<Delete />
<Text>Delete</Text>
</Item>
</ActionMenu>
</Item>
<Item key="4" textValue="Onboarding">
<Text>Onboarding</Text>
<Text slot="description">PDF</Text>
<ActionMenu>
<Item key="edit" textValue="Edit">
<Edit />
<Text>Edit</Text>
</Item>
<Item key="delete" textValue="Delete">
<Delete />
<Text>Delete</Text>
</Item>
</ActionMenu>
</Item>
</ListView>
<ListView
selectionMode="multiple"
maxWidth="size-6000"
aria-label="ListView example with complex items"
onAction={(key) =>
alert(`Triggering action on item `)}
>
<Item key="1" textValue="Utilities" hasChildItems>
<Folder />
<Text>Utilities</Text>
<Text slot="description">16 items</Text>
<ActionMenu>
<Item key="edit" textValue="Edit">
<Edit />
<Text>Edit</Text>
</Item>
<Item key="delete" textValue="Delete">
<Delete />
<Text>Delete</Text>
</Item>
</ActionMenu>
</Item>
<Item key="2" textValue="Glasses Dog">
<Image
src="https://random.dog/1a0535a6-ca89-4059-9b3a-04a554c0587b.jpg"
alt="Shiba Inu with glasses"
/>
<Text>Glasses Dog</Text>
<Text slot="description">JPG</Text>
<ActionMenu>
<Item key="edit" textValue="Edit">
<Edit />
<Text>Edit</Text>
</Item>
<Item key="delete" textValue="Delete">
<Delete />
<Text>Delete</Text>
</Item>
</ActionMenu>
</Item>
<Item key="3" textValue="readme">
<Text>readme.txt</Text>
<Text slot="description">TXT</Text>
<ActionMenu>
<Item key="edit" textValue="Edit">
<Edit />
<Text>Edit</Text>
</Item>
<Item key="delete" textValue="Delete">
<Delete />
<Text>Delete</Text>
</Item>
</ActionMenu>
</Item>
<Item key="4" textValue="Onboarding">
<Text>Onboarding</Text>
<Text slot="description">PDF</Text>
<ActionMenu>
<Item key="edit" textValue="Edit">
<Edit />
<Text>Edit</Text>
</Item>
<Item key="delete" textValue="Delete">
<Delete />
<Text>Delete</Text>
</Item>
</ActionMenu>
</Item>
</ListView>
<ListView
selectionMode="multiple"
maxWidth="size-6000"
aria-label="ListView example with complex items"
onAction={(key) =>
alert(
`Triggering action on item `
)}
>
<Item
key="1"
textValue="Utilities"
hasChildItems
>
<Folder />
<Text>
Utilities
</Text>
<Text slot="description">
16 items
</Text>
<ActionMenu>
<Item
key="edit"
textValue="Edit"
>
<Edit />
<Text>
Edit
</Text>
</Item>
<Item
key="delete"
textValue="Delete"
>
<Delete />
<Text>
Delete
</Text>
</Item>
</ActionMenu>
</Item>
<Item
key="2"
textValue="Glasses Dog"
>
<Image
src="https://random.dog/1a0535a6-ca89-4059-9b3a-04a554c0587b.jpg"
alt="Shiba Inu with glasses"
/>
<Text>
Glasses Dog
</Text>
<Text slot="description">
JPG
</Text>
<ActionMenu>
<Item
key="edit"
textValue="Edit"
>
<Edit />
<Text>
Edit
</Text>
</Item>
<Item
key="delete"
textValue="Delete"
>
<Delete />
<Text>
Delete
</Text>
</Item>
</ActionMenu>
</Item>
<Item
key="3"
textValue="readme"
>
<Text>
readme.txt
</Text>
<Text slot="description">
TXT
</Text>
<ActionMenu>
<Item
key="edit"
textValue="Edit"
>
<Edit />
<Text>
Edit
</Text>
</Item>
<Item
key="delete"
textValue="Delete"
>
<Delete />
<Text>
Delete
</Text>
</Item>
</ActionMenu>
</Item>
<Item
key="4"
textValue="Onboarding"
>
<Text>
Onboarding
</Text>
<Text slot="description">
PDF
</Text>
<ActionMenu>
<Item
key="edit"
textValue="Edit"
>
<Edit />
<Text>
Edit
</Text>
</Item>
<Item
key="delete"
textValue="Delete"
>
<Delete />
<Text>
Delete
</Text>
</Item>
</ActionMenu>
</Item>
</ListView>
Selection#
By default, ListView doesn't allow row selection, but this can be enabled using the selectionMode prop.
Use defaultSelectedKeys to provide a default set of selected rows. Note that the value of the selected keys must match the key prop of the Item.
The example below enables multiple selection mode, and uses defaultSelectedKeys to select the rows with keys "Charizard" and "Venusaur".
<ListView
maxWidth="size-6000"
selectionMode="multiple"
defaultSelectedKeys={['Charizard', 'Venusaur']}
aria-label="ListView multiple selection example"
>
<Item key="Charizard">
Charizard
</Item>
<Item key="Blastoise">
Blastoise
</Item>
<Item key="Venusaur">
Venusaur
</Item>
<Item key="Pikachu">
Pikachu
</Item>
</ListView>
<ListView
maxWidth="size-6000"
selectionMode="multiple"
defaultSelectedKeys={['Charizard', 'Venusaur']}
aria-label="ListView multiple selection example"
>
<Item key="Charizard">
Charizard
</Item>
<Item key="Blastoise">
Blastoise
</Item>
<Item key="Venusaur">
Venusaur
</Item>
<Item key="Pikachu">
Pikachu
</Item>
</ListView>
<ListView
maxWidth="size-6000"
selectionMode="multiple"
defaultSelectedKeys={[
'Charizard',
'Venusaur'
]}
aria-label="ListView multiple selection example"
>
<Item key="Charizard">
Charizard
</Item>
<Item key="Blastoise">
Blastoise
</Item>
<Item key="Venusaur">
Venusaur
</Item>
<Item key="Pikachu">
Pikachu
</Item>
</ListView>
Controlled selection#
To programmatically control row selection, use the selectedKeys prop paired with the onSelectionChange callback. The key prop from the selected rows will
be passed into the callback when the row is pressed, allowing you to update state accordingly. Note that the value of the selected keys must match the key prop of the Item.
Here is how you would control selection for the above example.
function PokemonList(props) {
let rows = [
{ id: 1, name: 'Charizard' },
{ id: 2, name: 'Blastoise' },
{ id: 3, name: 'Venusaur' },
{ id: 4, name: 'Pikachu' }
];
let [selectedKeys, setSelectedKeys] = React.useState(new Set([2]));
return (
<ListView
items={rows}
maxWidth="size-6000"
aria-label="ListView with controlled selection"
selectionMode="multiple"
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
{...props}
>
{(item) => (
<Item>
{item.name}
</Item>
)}
</ListView>
);
}
function PokemonList(props) {
let rows = [
{ id: 1, name: 'Charizard' },
{ id: 2, name: 'Blastoise' },
{ id: 3, name: 'Venusaur' },
{ id: 4, name: 'Pikachu' }
];
let [selectedKeys, setSelectedKeys] = React.useState(
new Set([2])
);
return (
<ListView
items={rows}
maxWidth="size-6000"
aria-label="ListView with controlled selection"
selectionMode="multiple"
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
{...props}
>
{(item) => (
<Item>
{item.name}
</Item>
)}
</ListView>
);
}
function PokemonList(
props
) {
let rows = [
{
id: 1,
name: 'Charizard'
},
{
id: 2,
name: 'Blastoise'
},
{
id: 3,
name: 'Venusaur'
},
{
id: 4,
name: 'Pikachu'
}
];
let [
selectedKeys,
setSelectedKeys
] = React.useState(
new Set([2])
);
return (
<ListView
items={rows}
maxWidth="size-6000"
aria-label="ListView with controlled selection"
selectionMode="multiple"
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
{...props}
>
{(item) => (
<Item>
{item.name}
</Item>
)}
</ListView>
);
}
Single selection#
To limit users to selecting only a single item at a time, selectionMode can be set to single.
// Using the same list as above
<PokemonList
selectionMode="single"
selectionStyle="highlight"
aria-label="ListView with single selection"
/>
// Using the same list as above
<PokemonList
selectionMode="single"
selectionStyle="highlight"
aria-label="ListView with single selection"
/>
// Using the same list as above
<PokemonList
selectionMode="single"
selectionStyle="highlight"
aria-label="ListView with single selection"
/>
Disallow empty selection#
ListView also supports a disallowEmptySelection prop which forces the user to have at least one row in the ListView selected at all times.
In this mode, if a single row is selected and the user presses it, it will not be deselected.
// Using the same list as above
<PokemonList
disallowEmptySelection
aria-label="ListView with empty selection disallowed"
/>
// Using the same list as above
<PokemonList
disallowEmptySelection
aria-label="ListView with empty selection disallowed"
/>
// Using the same list as above
<PokemonList
disallowEmptySelection
aria-label="ListView with empty selection disallowed"
/>
Disabled rows#
You can disable specific rows by providing an array of keys to ListView via the disabledKeys prop. This will prevent rows from being selectable as shown in the example below.
// Using the same list as above
<PokemonList disabledKeys={[3]} aria-label="ListView with disabled rows" />
// Using the same list as above
<PokemonList
disabledKeys={[3]}
aria-label="ListView with disabled rows"
/>
// Using the same list as above
<PokemonList
disabledKeys={[3]}
aria-label="ListView with disabled rows"
/>
Highlight selection#
By default, ListView uses the checkbox selection style, which includes a checkbox in each row for selection. When the selectionStyle prop is set to "highlight", the checkboxes are hidden,
and the selected rows are displayed with a highlighted background instead.
In addition to changing the appearance, the selection behavior also changes depending on the selectionStyle prop. In the default checkbox selection style, clicking, tapping,
or pressing the Space or Enter keys toggles selection for the focused row. Using the arrow keys moves focus but does not change selection.
In the highlight selection style, however, clicking a row with the mouse replaces the selection with only that row. Using the arrow keys moves both focus and selection. To select multiple rows, modifier keys such as Ctrl, Cmd, and Shift can be used. To move focus without moving selection, the Ctrl key on Windows or the Option key on macOS can be held while pressing the arrow keys. Holding this modifier while pressing the Space key toggles selection for the focused row, which allows multiple selection of non-contiguous items. On touch screen devices, selection always behaves as toggle since modifier keys may not be available. This behavior emulates native platforms such as macOS and Windows.
// Using the same list as above
<PokemonList
selectionStyle="highlight"
aria-label="Highlight selection ListView"
/>
// Using the same list as above
<PokemonList
selectionStyle="highlight"
aria-label="Highlight selection ListView"
/>
// Using the same list as above
<PokemonList
selectionStyle="highlight"
aria-label="Highlight selection ListView"
/>
Row actions#
ListView may be used in use cases where users can perform actions on rows, such as navigating into items to open them or get more details. The onAction prop can be used to enable
these row actions, but the interaction to trigger the row action depends on the selection style and current selection state of the ListView. When onAction is provided to a checkbox selection style ListView,
clicking the row with a mouse or pressing the Enter key will trigger onAction. Row selection is performed by pressing on the row's checkbox itself or by pressing Space.
While any number of rows are selected, subsequent clicks on a row will toggle selection instead. During this state onAction can only be triggered via the Enter key, but deselecting all selected
rows will restore the original "click for onAction" behavior. If onAction is provided to a highlight selection ListView, double clicking the row triggers onAction and a single click selects the row instead.
On touch devices, onAction is always the primary tap interaction, and tapping on the row's checkbox or long pressing the row will select the row. In highlight selection with row actions, a long press will also shift the ListView into selection mode,
which displays checkboxes to perform selection. While the ListView is in this state, tapping the row will toggle selection instead of triggering onAction. Deselecting all items exits selection mode and hides the checkboxes.
// Checkbox selection with onAction
<Flex wrap gap="size-300">
<PokemonList
onAction={(key) => alert(`Opening item ...`)}
aria-label="Checkbox selection ListView with row actions"
width="size-2400"
/>
<PokemonList
selectionStyle="highlight"
onAction={(key) => alert(`Opening item ...`)}
aria-label="Highlight selection ListView with row actions"
width="size-2400"
/>
</Flex>
// Checkbox selection with onAction
<Flex wrap gap="size-300">
<PokemonList
onAction={(key) => alert(`Opening item ...`)}
aria-label="Checkbox selection ListView with row actions"
width="size-2400"
/>
<PokemonList
selectionStyle="highlight"
onAction={(key) => alert(`Opening item ...`)}
aria-label="Highlight selection ListView with row actions"
width="size-2400"
/>
</Flex>
// Checkbox selection with onAction
<Flex
wrap
gap="size-300"
>
<PokemonList
onAction={(key) =>
alert(
`Opening item ...`
)}
aria-label="Checkbox selection ListView with row actions"
width="size-2400"
/>
<PokemonList
selectionStyle="highlight"
onAction={(key) =>
alert(
`Opening item ...`
)}
aria-label="Highlight selection ListView with row actions"
width="size-2400"
/>
</Flex>
Drag and drop#
To enable drag and drop in a ListView, you must provide the respective drag and drop hooks sourced from useDragHooks and useDropHooks to the ListView's dragHooks and dropHooks props respectively.
See the examples below for various common drag and drop use cases. For more information on the hooks themselves and the various supported ways to perform a drag and drop interaction,
please see the drag and drop documentation.
The example below demonstrates how to create a draggable ListView and a droppable ListView.
Show code
import {useDragHooks, useDropHooks} from '@react-spectrum/dnd';
import {useListData} from '@react-stately/data';
function DragIntoListExample() {
let sourceList = useListData({
initialItems: [
{ id: '1', type: 'file', name: 'Adobe Photoshop' },
{ id: '2', type: 'file', name: 'Adobe XD' },
{ id: '3', type: 'file', name: 'Adobe InDesign' },
{ id: '4', type: 'file', name: 'Adobe AfterEffects' }
]
});
let targetList = useListData({
initialItems: [
{ id: '5', type: 'file', name: 'Adobe Dreamweaver' },
{ id: '6', type: 'file', name: 'Adobe Fresco' },
{ id: '7', type: 'file', name: 'Adobe Connect' },
{ key: '8', type: 'file', name: 'Adobe Lightroom' }
]
});
// Append a generated key to the item type so they can only be dragged between these two lists
let dragType = React.useMemo(
() => `keys-`,
[]
);
let acceptedDragTypes = `-file`;
// Update the target and source list upon successful drop
let onMove = (keys, target) => {
if (target.type === 'root') {
targetList.append(...keys.map((key) => sourceList.getItem(key)));
} else {
switch (target.dropPosition) {
case 'before':
targetList.insertBefore(
target.key,
...keys.map((key) => sourceList.getItem(key))
);
break;
case 'after':
targetList.insertAfter(
target.key,
...keys.map((key) => sourceList.getItem(key))
);
break;
}
}
sourceList.remove(...keys);
};
let dragHooks = useDragHooks({
getItems(keys) {
return [...keys].map((key) => {
let item = sourceList.getItem(key);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]: JSON.stringify(item)
};
});
}
});
let dropHooks = useDropHooks({
async onDrop(e) {
let keysToMove = [];
// Only add the dropped item to the list of keys to move
// if the dropped item has the expected types
for (let item of e.items) {
if (item.kind === 'text' && item.types.has(acceptedDragTypes)) {
let type = acceptedDragTypes;
let { id } = JSON.parse(await item.getText(type));
keysToMove.push(id);
}
}
onMove(keysToMove, e.target);
},
getDropOperation(target, types) {
let typesSet = types.types ? types.types : types;
let draggedTypes = [...typesSet.values()];
// Cancel the drop operations if any of drag items aren't a file or if attempting to drop on a item
if (
!draggedTypes.every((type) => acceptedDragTypes === type) ||
(target.type === 'item' && target.dropPosition === 'on')
) {
return 'cancel';
}
return 'move';
}
});
return (
<Flex wrap gap="size-300">
<ListView
aria-label="Draggable ListView in drag into list example"
selectionMode="multiple"
width="size-3600"
height="size-2400"
items={sourceList.items}
dragHooks={dragHooks}
>
{(item) => (
<Item textValue={item.name}>
<Text>{item.name}</Text>
</Item>
)}
</ListView>
<ListView
aria-label="Droppable ListView in drag into list example"
width="size-3600"
height="size-2400"
items={targetList.items}
dropHooks={dropHooks}
>
{(item) => (
<Item textValue={item.name}>
<Text>{item.name}</Text>
</Item>
)}
</ListView>
</Flex>
);
}
import {
useDragHooks,
useDropHooks
} from '@react-spectrum/dnd';
import {useListData} from '@react-stately/data';
function DragIntoListExample() {
let sourceList = useListData({
initialItems: [
{ id: '1', type: 'file', name: 'Adobe Photoshop' },
{ id: '2', type: 'file', name: 'Adobe XD' },
{ id: '3', type: 'file', name: 'Adobe InDesign' },
{ id: '4', type: 'file', name: 'Adobe AfterEffects' }
]
});
let targetList = useListData({
initialItems: [
{ id: '5', type: 'file', name: 'Adobe Dreamweaver' },
{ id: '6', type: 'file', name: 'Adobe Fresco' },
{ id: '7', type: 'file', name: 'Adobe Connect' },
{ key: '8', type: 'file', name: 'Adobe Lightroom' }
]
});
// Append a generated key to the item type so they can only be dragged between these two lists
let dragType = React.useMemo(
() => `keys-`,
[]
);
let acceptedDragTypes = `-file`;
// Update the target and source list upon successful drop
let onMove = (keys, target) => {
if (target.type === 'root') {
targetList.append(
...keys.map((key) => sourceList.getItem(key))
);
} else {
switch (target.dropPosition) {
case 'before':
targetList.insertBefore(
target.key,
...keys.map((key) => sourceList.getItem(key))
);
break;
case 'after':
targetList.insertAfter(
target.key,
...keys.map((key) => sourceList.getItem(key))
);
break;
}
}
sourceList.remove(...keys);
};
let dragHooks = useDragHooks({
getItems(keys) {
return [...keys].map((key) => {
let item = sourceList.getItem(key);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]: JSON.stringify(item)
};
});
}
});
let dropHooks = useDropHooks({
async onDrop(e) {
let keysToMove = [];
// Only add the dropped item to the list of keys to move
// if the dropped item has the expected types
for (let item of e.items) {
if (
item.kind === 'text' &&
item.types.has(acceptedDragTypes)
) {
let type = acceptedDragTypes;
let { id } = JSON.parse(await item.getText(type));
keysToMove.push(id);
}
}
onMove(keysToMove, e.target);
},
getDropOperation(target, types) {
let typesSet = types.types ? types.types : types;
let draggedTypes = [...typesSet.values()];
// Cancel the drop operations if any of drag items aren't a file or if attempting to drop on a item
if (
!draggedTypes.every((type) =>
acceptedDragTypes === type
) ||
(target.type === 'item' &&
target.dropPosition === 'on')
) {
return 'cancel';
}
return 'move';
}
});
return (
<Flex wrap gap="size-300">
<ListView
aria-label="Draggable ListView in drag into list example"
selectionMode="multiple"
width="size-3600"
height="size-2400"
items={sourceList.items}
dragHooks={dragHooks}
>
{(item) => (
<Item textValue={item.name}>
<Text>{item.name}</Text>
</Item>
)}
</ListView>
<ListView
aria-label="Droppable ListView in drag into list example"
width="size-3600"
height="size-2400"
items={targetList.items}
dropHooks={dropHooks}
>
{(item) => (
<Item textValue={item.name}>
<Text>{item.name}</Text>
</Item>
)}
</ListView>
</Flex>
);
}
import {
useDragHooks,
useDropHooks
} from '@react-spectrum/dnd';
import {useListData} from '@react-stately/data';
function DragIntoListExample() {
let sourceList =
useListData({
initialItems: [
{
id: '1',
type: 'file',
name:
'Adobe Photoshop'
},
{
id: '2',
type: 'file',
name:
'Adobe XD'
},
{
id: '3',
type: 'file',
name:
'Adobe InDesign'
},
{
id: '4',
type: 'file',
name:
'Adobe AfterEffects'
}
]
});
let targetList =
useListData({
initialItems: [
{
id: '5',
type: 'file',
name:
'Adobe Dreamweaver'
},
{
id: '6',
type: 'file',
name:
'Adobe Fresco'
},
{
id: '7',
type: 'file',
name:
'Adobe Connect'
},
{
key: '8',
type: 'file',
name:
'Adobe Lightroom'
}
]
});
// Append a generated key to the item type so they can only be dragged between these two lists
let dragType = React
.useMemo(
() =>
`keys-`,
[]
);
let acceptedDragTypes =
`-file`;
// Update the target and source list upon successful drop
let onMove = (
keys,
target
) => {
if (
target.type ===
'root'
) {
targetList.append(
...keys.map(
(key) =>
sourceList
.getItem(
key
)
)
);
} else {
switch (
target
.dropPosition
) {
case 'before':
targetList
.insertBefore(
target.key,
...keys
.map(
(key) =>
sourceList
.getItem(
key
)
)
);
break;
case 'after':
targetList
.insertAfter(
target.key,
...keys
.map(
(key) =>
sourceList
.getItem(
key
)
)
);
break;
}
}
sourceList.remove(
...keys
);
};
let dragHooks =
useDragHooks({
getItems(keys) {
return [...keys]
.map((key) => {
let item =
sourceList
.getItem(
key
);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]:
JSON
.stringify(
item
)
};
});
}
});
let dropHooks =
useDropHooks({
async onDrop(e) {
let keysToMove =
[];
// Only add the dropped item to the list of keys to move
// if the dropped item has the expected types
for (
let item of e
.items
) {
if (
item.kind ===
'text' &&
item.types
.has(
acceptedDragTypes
)
) {
let type =
acceptedDragTypes;
let { id } =
JSON.parse(
await item
.getText(
type
)
);
keysToMove
.push(id);
}
}
onMove(
keysToMove,
e.target
);
},
getDropOperation(
target,
types
) {
let typesSet =
types.types
? types.types
: types;
let draggedTypes =
[...typesSet
.values()];
// Cancel the drop operations if any of drag items aren't a file or if attempting to drop on a item
if (
!draggedTypes
.every(
(type) =>
acceptedDragTypes ===
type
) ||
(target
.type ===
'item' &&
target
.dropPosition ===
'on')
) {
return 'cancel';
}
return 'move';
}
});
return (
<Flex
wrap
gap="size-300"
>
<ListView
aria-label="Draggable ListView in drag into list example"
selectionMode="multiple"
width="size-3600"
height="size-2400"
items={sourceList
.items}
dragHooks={dragHooks}
>
{(item) => (
<Item
textValue={item
.name}
>
<Text>
{item.name}
</Text>
</Item>
)}
</ListView>
<ListView
aria-label="Droppable ListView in drag into list example"
width="size-3600"
height="size-2400"
items={targetList
.items}
dropHooks={dropHooks}
>
{(item) => (
<Item
textValue={item
.name}
>
<Text>
{item.name}
</Text>
</Item>
)}
</ListView>
</Flex>
);
}
<DragIntoListExample />
<DragIntoListExample />
<DragIntoListExample />
The example below replicates the previous example but demonstrates how to handle multiple item types. In the droppable ListView, folders are valid drop targets but we cancel any drop operations if any of the dragged items is a folder.
Show code
function DragIntoListFilesOnly() {
let sourceList = useListData({
initialItems: [
{ id: '1', type: 'file', name: 'Adobe Photoshop' },
{ id: '2', type: 'file', name: 'Adobe XD' },
{ id: '3', type: 'folder', name: 'Documents' },
{ id: '4', type: 'file', name: 'Adobe InDesign' },
{ id: '5', type: 'folder', name: 'Utilities' },
{ id: '6', type: 'file', name: 'Adobe AfterEffects' }
]
});
let targetList = useListData({
initialItems: [
{ id: '7', type: 'folder', name: 'Pictures', childNodes: [] },
{ id: '8', type: 'file', name: 'Adobe Fresco' },
{ id: '9', type: 'folder', name: 'Apps', childNodes: [] }
]
});
// Append a generated key to the item type so they can only be dragged between these two lists
let dragType = React.useMemo(
() => `keys-`,
[]
);
let acceptedDragTypes = `-file`;
// Update the target and source list upon successful drop
let onMove = (keys, target) => {
if (target.type === 'root') {
targetList.append(...keys.map((key) => sourceList.getItem(key)));
} else {
switch (target.dropPosition) {
case 'before':
targetList.insertBefore(
target.key,
...keys.map((key) => sourceList.getItem(key))
);
break;
case 'after':
targetList.insertAfter(
target.key,
...keys.map((key) => sourceList.getItem(key))
);
break;
case 'on': {
let targetFolder = targetList.getItem(target.key);
let draggedItems = keys.map((key) => sourceList.getItem(key));
targetList.update(target.key, {
...targetFolder,
childNodes: [...targetFolder.childNodes, ...draggedItems]
});
break;
}
}
}
sourceList.remove(...keys);
};
let dragHooks = useDragHooks({
getItems(keys) {
return [...keys].map((key) => {
let item = sourceList.getItem(key);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]: JSON.stringify(item)
};
});
}
});
let dropHooks = useDropHooks({
async onDrop(e) {
let keysToMove = [];
// Only add the dropped item to the list of keys to move
// if the dropped item has the expected types
for (let item of e.items) {
if (item.kind === 'text' && item.types.has(acceptedDragTypes)) {
let type = acceptedDragTypes;
let { id } = JSON.parse(await item.getText(type));
keysToMove.push(id);
}
}
onMove(keysToMove, e.target);
},
getDropOperation(target, types) {
let typesSet = types.types ? types.types : types;
let draggedTypes = [...typesSet.values()];
// Cancel the drop operations if any of drag items aren't a file or if attempting to drop on a non-folder item
if (
!draggedTypes.every((type) => acceptedDragTypes === type) ||
(target.type === 'item' && target.dropPosition === 'on' &&
!targetList.getItem(target.key).childNodes)
) {
return 'cancel';
}
return 'move';
}
});
return (
<Flex wrap gap="size-300">
<ListView
aria-label="Draggable ListView in drag into list files only example"
selectionMode="multiple"
width="size-3600"
height="size-3600"
items={sourceList.items}
dragHooks={dragHooks}
>
{(item) => (
<Item textValue={item.name}>
{item.type === 'folder' && <Folder />}
<Text>{item.name}</Text>
</Item>
)}
</ListView>
<ListView
aria-label="Droppable ListView in drag into list files only example"
width="size-3600"
height="size-3600"
items={targetList.items}
dropHooks={dropHooks}
>
{(item) => (
<Item textValue={item.name} hasChildItems={item.type === 'folder'}>
<Text>
{item.name}
</Text>
{item.type === 'folder' &&
(
<>
<Folder />
<Text slot="description">
{`contains dropped item(s)`}
</Text>
</>
)}
</Item>
)}
</ListView>
</Flex>
);
}
function DragIntoListFilesOnly() {
let sourceList = useListData({
initialItems: [
{ id: '1', type: 'file', name: 'Adobe Photoshop' },
{ id: '2', type: 'file', name: 'Adobe XD' },
{ id: '3', type: 'folder', name: 'Documents' },
{ id: '4', type: 'file', name: 'Adobe InDesign' },
{ id: '5', type: 'folder', name: 'Utilities' },
{ id: '6', type: 'file', name: 'Adobe AfterEffects' }
]
});
let targetList = useListData({
initialItems: [
{
id: '7',
type: 'folder',
name: 'Pictures',
childNodes: []
},
{ id: '8', type: 'file', name: 'Adobe Fresco' },
{
id: '9',
type: 'folder',
name: 'Apps',
childNodes: []
}
]
});
// Append a generated key to the item type so they can only be dragged between these two lists
let dragType = React.useMemo(
() => `keys-`,
[]
);
let acceptedDragTypes = `-file`;
// Update the target and source list upon successful drop
let onMove = (keys, target) => {
if (target.type === 'root') {
targetList.append(
...keys.map((key) => sourceList.getItem(key))
);
} else {
switch (target.dropPosition) {
case 'before':
targetList.insertBefore(
target.key,
...keys.map((key) => sourceList.getItem(key))
);
break;
case 'after':
targetList.insertAfter(
target.key,
...keys.map((key) => sourceList.getItem(key))
);
break;
case 'on': {
let targetFolder = targetList.getItem(target.key);
let draggedItems = keys.map((key) =>
sourceList.getItem(key)
);
targetList.update(target.key, {
...targetFolder,
childNodes: [
...targetFolder.childNodes,
...draggedItems
]
});
break;
}
}
}
sourceList.remove(...keys);
};
let dragHooks = useDragHooks({
getItems(keys) {
return [...keys].map((key) => {
let item = sourceList.getItem(key);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]: JSON.stringify(item)
};
});
}
});
let dropHooks = useDropHooks({
async onDrop(e) {
let keysToMove = [];
// Only add the dropped item to the list of keys to move
// if the dropped item has the expected types
for (let item of e.items) {
if (
item.kind === 'text' &&
item.types.has(acceptedDragTypes)
) {
let type = acceptedDragTypes;
let { id } = JSON.parse(await item.getText(type));
keysToMove.push(id);
}
}
onMove(keysToMove, e.target);
},
getDropOperation(target, types) {
let typesSet = types.types ? types.types : types;
let draggedTypes = [...typesSet.values()];
// Cancel the drop operations if any of drag items aren't a file or if attempting to drop on a non-folder item
if (
!draggedTypes.every((type) =>
acceptedDragTypes === type
) ||
(target.type === 'item' &&
target.dropPosition === 'on' &&
!targetList.getItem(target.key).childNodes)
) {
return 'cancel';
}
return 'move';
}
});
return (
<Flex wrap gap="size-300">
<ListView
aria-label="Draggable ListView in drag into list files only example"
selectionMode="multiple"
width="size-3600"
height="size-3600"
items={sourceList.items}
dragHooks={dragHooks}
>
{(item) => (
<Item textValue={item.name}>
{item.type === 'folder' && <Folder />}
<Text>{item.name}</Text>
</Item>
)}
</ListView>
<ListView
aria-label="Droppable ListView in drag into list files only example"
width="size-3600"
height="size-3600"
items={targetList.items}
dropHooks={dropHooks}
>
{(item) => (
<Item
textValue={item.name}
hasChildItems={item.type === 'folder'}
>
<Text>{item.name}</Text>
{item.type === 'folder' &&
(
<>
<Folder />
<Text slot="description">
{`contains dropped item(s)`}
</Text>
</>
)}
</Item>
)}
</ListView>
</Flex>
);
}
function DragIntoListFilesOnly() {
let sourceList =
useListData({
initialItems: [
{
id: '1',
type: 'file',
name:
'Adobe Photoshop'
},
{
id: '2',
type: 'file',
name:
'Adobe XD'
},
{
id: '3',
type: 'folder',
name:
'Documents'
},
{
id: '4',
type: 'file',
name:
'Adobe InDesign'
},
{
id: '5',
type: 'folder',
name:
'Utilities'
},
{
id: '6',
type: 'file',
name:
'Adobe AfterEffects'
}
]
});
let targetList =
useListData({
initialItems: [
{
id: '7',
type: 'folder',
name:
'Pictures',
childNodes: []
},
{
id: '8',
type: 'file',
name:
'Adobe Fresco'
},
{
id: '9',
type: 'folder',
name: 'Apps',
childNodes: []
}
]
});
// Append a generated key to the item type so they can only be dragged between these two lists
let dragType = React
.useMemo(
() =>
`keys-`,
[]
);
let acceptedDragTypes =
`-file`;
// Update the target and source list upon successful drop
let onMove = (
keys,
target
) => {
if (
target.type ===
'root'
) {
targetList.append(
...keys.map(
(key) =>
sourceList
.getItem(
key
)
)
);
} else {
switch (
target
.dropPosition
) {
case 'before':
targetList
.insertBefore(
target.key,
...keys
.map(
(key) =>
sourceList
.getItem(
key
)
)
);
break;
case 'after':
targetList
.insertAfter(
target.key,
...keys
.map(
(key) =>
sourceList
.getItem(
key
)
)
);
break;
case 'on': {
let targetFolder =
targetList
.getItem(
target
.key
);
let draggedItems =
keys.map((
key
) =>
sourceList
.getItem(
key
)
);
targetList
.update(
target.key,
{
...targetFolder,
childNodes:
[
...targetFolder
.childNodes,
...draggedItems
]
}
);
break;
}
}
}
sourceList.remove(
...keys
);
};
let dragHooks =
useDragHooks({
getItems(keys) {
return [...keys]
.map((key) => {
let item =
sourceList
.getItem(
key
);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]:
JSON
.stringify(
item
)
};
});
}
});
let dropHooks =
useDropHooks({
async onDrop(e) {
let keysToMove =
[];
// Only add the dropped item to the list of keys to move
// if the dropped item has the expected types
for (
let item of e
.items
) {
if (
item.kind ===
'text' &&
item.types
.has(
acceptedDragTypes
)
) {
let type =
acceptedDragTypes;
let { id } =
JSON.parse(
await item
.getText(
type
)
);
keysToMove
.push(id);
}
}
onMove(
keysToMove,
e.target
);
},
getDropOperation(
target,
types
) {
let typesSet =
types.types
? types.types
: types;
let draggedTypes =
[...typesSet
.values()];
// Cancel the drop operations if any of drag items aren't a file or if attempting to drop on a non-folder item
if (
!draggedTypes
.every(
(type) =>
acceptedDragTypes ===
type
) ||
(target
.type ===
'item' &&
target
.dropPosition ===
'on' &&
!targetList
.getItem(
target
.key
).childNodes)
) {
return 'cancel';
}
return 'move';
}
});
return (
<Flex
wrap
gap="size-300"
>
<ListView
aria-label="Draggable ListView in drag into list files only example"
selectionMode="multiple"
width="size-3600"
height="size-3600"
items={sourceList
.items}
dragHooks={dragHooks}
>
{(item) => (
<Item
textValue={item
.name}
>
{item
.type ===
'folder' &&
<Folder />}
<Text>
{item.name}
</Text>
</Item>
)}
</ListView>
<ListView
aria-label="Droppable ListView in drag into list files only example"
width="size-3600"
height="size-3600"
items={targetList
.items}
dropHooks={dropHooks}
>
{(item) => (
<Item
textValue={item
.name}
hasChildItems={item
.type ===
'folder'}
>
<Text>
{item.name}
</Text>
{item
.type ===
'folder' &&
(
<>
<Folder />
<Text slot="description">
{`contains dropped item(s)`}
</Text>
</>
)}
</Item>
)}
</ListView>
</Flex>
);
}
<DragIntoListFilesOnly />
<DragIntoListFilesOnly />
<DragIntoListFilesOnly />
The example below demonstrates how to make a ListView draggable and droppable at the same time. The ListView below supports reordering its own rows via drag and drop.
Show code
function ReorderableList() {
let list = useListData({
initialItems: [
{ id: '1', type: 'file', name: 'Adobe Photoshop' },
{ id: '2', type: 'file', name: 'Adobe XD' },
{ id: '3', type: 'folder', name: 'Documents', childNodes: [] },
{ id: '4', type: 'file', name: 'Adobe InDesign' },
{ id: '5', type: 'folder', name: 'Utilities', childNodes: [] },
{ id: '6', type: 'file', name: 'Adobe AfterEffects' }
]
});
// Append a generated key to the item type so they can only be reordered within this list and not dragged elsewhere.
let dragType = React.useMemo(
() => `keys-`,
[]
);
let acceptedDragTypes = [`-folder`, `-file`];
// Update the list order based on the position of the drop and the set of rows that were dropped
let onMove = (keys, target) => {
switch (target.dropPosition) {
case 'before':
list.moveBefore(target.key, keys);
break;
case 'after':
list.moveAfter(target.key, keys);
break;
case 'on': {
let targetFolder = list.getItem(target.key);
let draggedItems = keys.map((key) => list.getItem(key));
list.update(target.key, {
...targetFolder,
childNodes: [...targetFolder.childNodes, ...draggedItems]
});
list.remove(...keys);
break;
}
}
};
let dragHooks = useDragHooks({
getItems(keys) {
return [...keys].map((key) => {
let item = list.getItem(key);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]: JSON.stringify(item)
};
});
}
});
let dropHooks = useDropHooks({
async onDrop(e) {
let keysToMove = [];
// Only add the dropped item to the list of keys to move
// if the dropped item has the expected types
for (let item of e.items) {
for (let acceptedType of acceptedDragTypes) {
if (item.kind === 'text' && item.types.has(acceptedType)) {
let { id } = JSON.parse(await item.getText(acceptedType));
keysToMove.push(id);
}
}
}
onMove(keysToMove, e.target);
},
getDropOperation(target, types) {
let typesSet = types.types ? types.types : types;
let draggedTypes = [...typesSet.values()];
// Cancel the drop operations if any of drag items aren't of the acceptedTypes, if the drop target is root,
// or if attempting to drop on a non-folder item
if (
!draggedTypes.every((type) => acceptedDragTypes.includes(type)) ||
target.type === 'root' ||
(target.type === 'item' && target.dropPosition === 'on' &&
!list.getItem(target.key).childNodes)
) {
return 'cancel';
}
return 'move';
}
});
return (
<ListView
aria-label="Reorderable ListView"
selectionMode="multiple"
width="size-3600"
height="size-3600"
items={list.items}
dragHooks={dragHooks}
dropHooks={dropHooks}
>
{(item) => (
<Item textValue={item.name} hasChildItems={item.type === 'folder'}>
<Text>
{item.name}
</Text>
{item.type === 'folder' &&
(
<>
<Folder />
<Text slot="description">
{`contains dropped item(s)`}
</Text>
</>
)}
</Item>
)}
</ListView>
);
}
function ReorderableList() {
let list = useListData({
initialItems: [
{ id: '1', type: 'file', name: 'Adobe Photoshop' },
{ id: '2', type: 'file', name: 'Adobe XD' },
{
id: '3',
type: 'folder',
name: 'Documents',
childNodes: []
},
{ id: '4', type: 'file', name: 'Adobe InDesign' },
{
id: '5',
type: 'folder',
name: 'Utilities',
childNodes: []
},
{ id: '6', type: 'file', name: 'Adobe AfterEffects' }
]
});
// Append a generated key to the item type so they can only be reordered within this list and not dragged elsewhere.
let dragType = React.useMemo(
() => `keys-`,
[]
);
let acceptedDragTypes = [
`-folder`,
`-file`
];
// Update the list order based on the position of the drop and the set of rows that were dropped
let onMove = (keys, target) => {
switch (target.dropPosition) {
case 'before':
list.moveBefore(target.key, keys);
break;
case 'after':
list.moveAfter(target.key, keys);
break;
case 'on': {
let targetFolder = list.getItem(target.key);
let draggedItems = keys.map((key) =>
list.getItem(key)
);
list.update(target.key, {
...targetFolder,
childNodes: [
...targetFolder.childNodes,
...draggedItems
]
});
list.remove(...keys);
break;
}
}
};
let dragHooks = useDragHooks({
getItems(keys) {
return [...keys].map((key) => {
let item = list.getItem(key);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]: JSON.stringify(item)
};
});
}
});
let dropHooks = useDropHooks({
async onDrop(e) {
let keysToMove = [];
// Only add the dropped item to the list of keys to move
// if the dropped item has the expected types
for (let item of e.items) {
for (let acceptedType of acceptedDragTypes) {
if (
item.kind === 'text' &&
item.types.has(acceptedType)
) {
let { id } = JSON.parse(
await item.getText(acceptedType)
);
keysToMove.push(id);
}
}
}
onMove(keysToMove, e.target);
},
getDropOperation(target, types) {
let typesSet = types.types ? types.types : types;
let draggedTypes = [...typesSet.values()];
// Cancel the drop operations if any of drag items aren't of the acceptedTypes, if the drop target is root,
// or if attempting to drop on a non-folder item
if (
!draggedTypes.every((type) =>
acceptedDragTypes.includes(type)
) ||
target.type === 'root' ||
(target.type === 'item' &&
target.dropPosition === 'on' &&
!list.getItem(target.key).childNodes)
) {
return 'cancel';
}
return 'move';
}
});
return (
<ListView
aria-label="Reorderable ListView"
selectionMode="multiple"
width="size-3600"
height="size-3600"
items={list.items}
dragHooks={dragHooks}
dropHooks={dropHooks}
>
{(item) => (
<Item
textValue={item.name}
hasChildItems={item.type === 'folder'}
>
<Text>{item.name}</Text>
{item.type === 'folder' &&
(
<>
<Folder />
<Text slot="description">
{`contains dropped item(s)`}
</Text>
</>
)}
</Item>
)}
</ListView>
);
}
function ReorderableList() {
let list = useListData(
{
initialItems: [
{
id: '1',
type: 'file',
name:
'Adobe Photoshop'
},
{
id: '2',
type: 'file',
name:
'Adobe XD'
},
{
id: '3',
type: 'folder',
name:
'Documents',
childNodes: []
},
{
id: '4',
type: 'file',
name:
'Adobe InDesign'
},
{
id: '5',
type: 'folder',
name:
'Utilities',
childNodes: []
},
{
id: '6',
type: 'file',
name:
'Adobe AfterEffects'
}
]
}
);
// Append a generated key to the item type so they can only be reordered within this list and not dragged elsewhere.
let dragType = React
.useMemo(
() =>
`keys-`,
[]
);
let acceptedDragTypes =
[
`-folder`,
`-file`
];
// Update the list order based on the position of the drop and the set of rows that were dropped
let onMove = (
keys,
target
) => {
switch (
target.dropPosition
) {
case 'before':
list.moveBefore(
target.key,
keys
);
break;
case 'after':
list.moveAfter(
target.key,
keys
);
break;
case 'on': {
let targetFolder =
list.getItem(
target.key
);
let draggedItems =
keys.map((
key
) =>
list.getItem(
key
)
);
list.update(
target.key,
{
...targetFolder,
childNodes: [
...targetFolder
.childNodes,
...draggedItems
]
}
);
list.remove(
...keys
);
break;
}
}
};
let dragHooks =
useDragHooks({
getItems(keys) {
return [...keys]
.map((key) => {
let item =
list
.getItem(
key
);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]:
JSON
.stringify(
item
)
};
});
}
});
let dropHooks =
useDropHooks({
async onDrop(e) {
let keysToMove =
[];
// Only add the dropped item to the list of keys to move
// if the dropped item has the expected types
for (
let item of e
.items
) {
for (
let acceptedType
of acceptedDragTypes
) {
if (
item
.kind ===
'text' &&
item.types
.has(
acceptedType
)
) {
let {
id
} = JSON
.parse(
await item
.getText(
acceptedType
)
);
keysToMove
.push(
id
);
}
}
}
onMove(
keysToMove,
e.target
);
},
getDropOperation(
target,
types
) {
let typesSet =
types.types
? types.types
: types;
let draggedTypes =
[...typesSet
.values()];
// Cancel the drop operations if any of drag items aren't of the acceptedTypes, if the drop target is root,
// or if attempting to drop on a non-folder item
if (
!draggedTypes
.every(
(type) =>
acceptedDragTypes
.includes(
type
)
) ||
target.type ===
'root' ||
(target
.type ===
'item' &&
target
.dropPosition ===
'on' &&
!list
.getItem(
target
.key
).childNodes)
) {
return 'cancel';
}
return 'move';
}
});
return (
<ListView
aria-label="Reorderable ListView"
selectionMode="multiple"
width="size-3600"
height="size-3600"
items={list.items}
dragHooks={dragHooks}
dropHooks={dropHooks}
>
{(item) => (
<Item
textValue={item
.name}
hasChildItems={item
.type ===
'folder'}
>
<Text>
{item.name}
</Text>
{item.type ===
'folder' &&
(
<>
<Folder />
<Text slot="description">
{`contains dropped item(s)`}
</Text>
</>
)}
</Item>
)}
</ListView>
);
}
<ReorderableList />
<ReorderableList />
<ReorderableList />
The example below demonstrates how to create a pair of ListViews that supports dragging and dropping any items between each list, but disables the ability to drop into a folder and onto the root of the list.
Show code
function DragBetweenListsExample() {
let list1 = useListData({
initialItems: [
{ id: '1', type: 'file', name: 'Adobe Photoshop' },
{ id: '2', type: 'file', name: 'Adobe XD' },
{ id: '3', type: 'folder', name: 'Documents' },
{ id: '4', type: 'file', name: 'Adobe InDesign' },
{ id: '5', type: 'folder', name: 'Utilities' },
{ id: '6', type: 'file', name: 'Adobe AfterEffects' }
]
});
let list2 = useListData({
initialItems: [
{ id: '7', type: 'folder', name: 'Pictures' },
{ id: '8', type: 'file', name: 'Adobe Fresco' },
{ id: '9', type: 'folder', name: 'Apps' },
{ id: '10', type: 'file', name: 'Adobe Illustrator' },
{ id: '11', type: 'file', name: 'Adobe Lightroom' },
{ id: '12', type: 'file', name: 'Adobe Dreamweaver' }
]
});
// Append a generated key to the item type so they can only be reordered within this list and not dragged elsewhere.
let dragType = React.useMemo(
() => `keys-`,
[]
);
let acceptedDragTypes = [`-folder`, `-file`];
// Update the list order based on the position of the drop and the set of rows that were dropped
let onMove = (keys, target) => {
let sourceList = list1.getItem(keys[0]) ? list1 : list2;
let destinationList = list1.getItem(target.key) ? list1 : list2;
if (sourceList === destinationList) {
// Handle dragging within same list
if (target.dropPosition === 'before') {
sourceList.moveBefore(target.key, keys);
} else {
sourceList.moveAfter(target.key, keys);
}
} else {
// Handle dragging between lists
if (target.dropPosition === 'before') {
destinationList.insertBefore(
target.key,
...keys.map((key) => sourceList.getItem(key))
);
} else {
destinationList.insertAfter(
target.key,
...keys.map((key) => sourceList.getItem(key))
);
}
sourceList.remove(...keys);
}
};
let dragHooksList1 = useDragHooks({
getItems(keys) {
return [...keys].map((key) => {
let item = list1.getItem(key);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]: JSON.stringify(item)
};
});
}
});
let dragHooksList2 = useDragHooks({
getItems(keys) {
return [...keys].map((key) => {
let item = list2.getItem(key);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]: JSON.stringify(item)
};
});
}
});
let dropHooks = useDropHooks({
async onDrop(e) {
let keysToMove = [];
// Only add the dropped item to the list of keys to move
// if the dropped item has the expected types
for (let item of e.items) {
for (let acceptedType of acceptedDragTypes) {
if (item.kind === 'text' && item.types.has(acceptedType)) {
let { id } = JSON.parse(await item.getText(acceptedType));
keysToMove.push(id);
}
}
}
onMove(keysToMove, e.target);
},
getDropOperation(target, types) {
let typesSet = types.types ? types.types : types;
let draggedTypes = [...typesSet.values()];
// Cancel the drop operations if any of drag items aren't of the acceptedTypes or if
// the drop is done on the root of the ListView or on the ListView items themselves
if (
!draggedTypes.every((type) => acceptedDragTypes.includes(type)) ||
target.type === 'root' || target.dropPosition === 'on'
) {
return 'cancel';
}
return 'move';
}
});
return (
<Flex wrap gap="size-300">
<ListView
aria-label="First ListView in drag between list example"
selectionMode="multiple"
width="size-3600"
height="size-3600"
items={list1.items}
dragHooks={dragHooksList1}
dropHooks={dropHooks}
>
{(item) => (
<Item textValue={item.name}>
{item.type === 'folder' && <Folder />}
<Text>{item.name}</Text>
</Item>
)}
</ListView>
<ListView
aria-label="Second ListView in drag between list example"
selectionMode="multiple"
width="size-3600"
height="size-3600"
items={list2.items}
dragHooks={dragHooksList2}
dropHooks={dropHooks}
>
{(item) => (
<Item textValue={item.name}>
{item.type === 'folder' && <Folder />}
<Text>{item.name}</Text>
</Item>
)}
</ListView>
</Flex>
);
}
function DragBetweenListsExample() {
let list1 = useListData({
initialItems: [
{ id: '1', type: 'file', name: 'Adobe Photoshop' },
{ id: '2', type: 'file', name: 'Adobe XD' },
{ id: '3', type: 'folder', name: 'Documents' },
{ id: '4', type: 'file', name: 'Adobe InDesign' },
{ id: '5', type: 'folder', name: 'Utilities' },
{ id: '6', type: 'file', name: 'Adobe AfterEffects' }
]
});
let list2 = useListData({
initialItems: [
{ id: '7', type: 'folder', name: 'Pictures' },
{ id: '8', type: 'file', name: 'Adobe Fresco' },
{ id: '9', type: 'folder', name: 'Apps' },
{ id: '10', type: 'file', name: 'Adobe Illustrator' },
{ id: '11', type: 'file', name: 'Adobe Lightroom' },
{ id: '12', type: 'file', name: 'Adobe Dreamweaver' }
]
});
// Append a generated key to the item type so they can only be reordered within this list and not dragged elsewhere.
let dragType = React.useMemo(
() => `keys-`,
[]
);
let acceptedDragTypes = [
`-folder`,
`-file`
];
// Update the list order based on the position of the drop and the set of rows that were dropped
let onMove = (keys, target) => {
let sourceList = list1.getItem(keys[0]) ? list1 : list2;
let destinationList = list1.getItem(target.key)
? list1
: list2;
if (sourceList === destinationList) {
// Handle dragging within same list
if (target.dropPosition === 'before') {
sourceList.moveBefore(target.key, keys);
} else {
sourceList.moveAfter(target.key, keys);
}
} else {
// Handle dragging between lists
if (target.dropPosition === 'before') {
destinationList.insertBefore(
target.key,
...keys.map((key) => sourceList.getItem(key))
);
} else {
destinationList.insertAfter(
target.key,
...keys.map((key) => sourceList.getItem(key))
);
}
sourceList.remove(...keys);
}
};
let dragHooksList1 = useDragHooks({
getItems(keys) {
return [...keys].map((key) => {
let item = list1.getItem(key);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]: JSON.stringify(item)
};
});
}
});
let dragHooksList2 = useDragHooks({
getItems(keys) {
return [...keys].map((key) => {
let item = list2.getItem(key);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]: JSON.stringify(item)
};
});
}
});
let dropHooks = useDropHooks({
async onDrop(e) {
let keysToMove = [];
// Only add the dropped item to the list of keys to move
// if the dropped item has the expected types
for (let item of e.items) {
for (let acceptedType of acceptedDragTypes) {
if (
item.kind === 'text' &&
item.types.has(acceptedType)
) {
let { id } = JSON.parse(
await item.getText(acceptedType)
);
keysToMove.push(id);
}
}
}
onMove(keysToMove, e.target);
},
getDropOperation(target, types) {
let typesSet = types.types ? types.types : types;
let draggedTypes = [...typesSet.values()];
// Cancel the drop operations if any of drag items aren't of the acceptedTypes or if
// the drop is done on the root of the ListView or on the ListView items themselves
if (
!draggedTypes.every((type) =>
acceptedDragTypes.includes(type)
) || target.type === 'root' ||
target.dropPosition === 'on'
) {
return 'cancel';
}
return 'move';
}
});
return (
<Flex wrap gap="size-300">
<ListView
aria-label="First ListView in drag between list example"
selectionMode="multiple"
width="size-3600"
height="size-3600"
items={list1.items}
dragHooks={dragHooksList1}
dropHooks={dropHooks}
>
{(item) => (
<Item textValue={item.name}>
{item.type === 'folder' && <Folder />}
<Text>{item.name}</Text>
</Item>
)}
</ListView>
<ListView
aria-label="Second ListView in drag between list example"
selectionMode="multiple"
width="size-3600"
height="size-3600"
items={list2.items}
dragHooks={dragHooksList2}
dropHooks={dropHooks}
>
{(item) => (
<Item textValue={item.name}>
{item.type === 'folder' && <Folder />}
<Text>{item.name}</Text>
</Item>
)}
</ListView>
</Flex>
);
}
function DragBetweenListsExample() {
let list1 =
useListData({
initialItems: [
{
id: '1',
type: 'file',
name:
'Adobe Photoshop'
},
{
id: '2',
type: 'file',
name:
'Adobe XD'
},
{
id: '3',
type: 'folder',
name:
'Documents'
},
{
id: '4',
type: 'file',
name:
'Adobe InDesign'
},
{
id: '5',
type: 'folder',
name:
'Utilities'
},
{
id: '6',
type: 'file',
name:
'Adobe AfterEffects'
}
]
});
let list2 =
useListData({
initialItems: [
{
id: '7',
type: 'folder',
name:
'Pictures'
},
{
id: '8',
type: 'file',
name:
'Adobe Fresco'
},
{
id: '9',
type: 'folder',
name: 'Apps'
},
{
id: '10',
type: 'file',
name:
'Adobe Illustrator'
},
{
id: '11',
type: 'file',
name:
'Adobe Lightroom'
},
{
id: '12',
type: 'file',
name:
'Adobe Dreamweaver'
}
]
});
// Append a generated key to the item type so they can only be reordered within this list and not dragged elsewhere.
let dragType = React
.useMemo(
() =>
`keys-`,
[]
);
let acceptedDragTypes =
[
`-folder`,
`-file`
];
// Update the list order based on the position of the drop and the set of rows that were dropped
let onMove = (
keys,
target
) => {
let sourceList =
list1.getItem(
keys[0]
)
? list1
: list2;
let destinationList =
list1.getItem(
target.key
)
? list1
: list2;
if (
sourceList ===
destinationList
) {
// Handle dragging within same list
if (
target
.dropPosition ===
'before'
) {
sourceList
.moveBefore(
target.key,
keys
);
} else {
sourceList
.moveAfter(
target.key,
keys
);
}
} else {
// Handle dragging between lists
if (
target
.dropPosition ===
'before'
) {
destinationList
.insertBefore(
target.key,
...keys.map(
(key) =>
sourceList
.getItem(
key
)
)
);
} else {
destinationList
.insertAfter(
target.key,
...keys.map(
(key) =>
sourceList
.getItem(
key
)
)
);
}
sourceList.remove(
...keys
);
}
};
let dragHooksList1 =
useDragHooks({
getItems(keys) {
return [...keys]
.map((key) => {
let item =
list1
.getItem(
key
);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]:
JSON
.stringify(
item
)
};
});
}
});
let dragHooksList2 =
useDragHooks({
getItems(keys) {
return [...keys]
.map((key) => {
let item =
list2
.getItem(
key
);
// Setup the info for each item that will be propagated upon drag
return {
[`-`]:
JSON
.stringify(
item
)
};
});
}
});
let dropHooks =
useDropHooks({
async onDrop(e) {
let keysToMove =
[];
// Only add the dropped item to the list of keys to move
// if the dropped item has the expected types
for (
let item of e
.items
) {
for (
let acceptedType
of acceptedDragTypes
) {
if (
item
.kind ===
'text' &&
item.types
.has(
acceptedType
)
) {
let {
id
} = JSON
.parse(
await item
.getText(
acceptedType
)
);
keysToMove
.push(
id
);
}
}
}
onMove(
keysToMove,
e.target
);
},
getDropOperation(
target,
types
) {
let typesSet =
types.types
? types.types
: types;
let draggedTypes =
[...typesSet
.values()];
// Cancel the drop operations if any of drag items aren't of the acceptedTypes or if
// the drop is done on the root of the ListView or on the ListView items themselves
if (
!draggedTypes
.every(
(type) =>
acceptedDragTypes
.includes(
type
)
) ||
target.type ===
'root' ||
target
.dropPosition ===
'on'
) {
return 'cancel';
}
return 'move';
}
});
return (
<Flex
wrap
gap="size-300"
>
<ListView
aria-label="First ListView in drag between list example"
selectionMode="multiple"
width="size-3600"
height="size-3600"
items={list1
.items}
dragHooks={dragHooksList1}
dropHooks={dropHooks}
>
{(item) => (
<Item
textValue={item
.name}
>
{item
.type ===
'folder' &&
<Folder />}
<Text>
{item.name}
</Text>
</Item>
)}
</ListView>
<ListView
aria-label="Second ListView in drag between list example"
selectionMode="multiple"
width="size-3600"
height="size-3600"
items={list2
.items}
dragHooks={dragHooksList2}
dropHooks={dropHooks}
>
{(item) => (
<Item
textValue={item
.name}
>
{item
.type ===
'folder' &&
<Folder />}
<Text>
{item.name}
</Text>
</Item>
)}
</ListView>
</Flex>
);
}
<DragBetweenListsExample />
<DragBetweenListsExample />
<DragBetweenListsExample />
Props#
| Name | Type | Default | Description |
children | CollectionChildren<object> | — | The contents of the collection. |
density | 'compact'
| 'regular'
| 'spacious' | 'regular' | Sets the amount of vertical padding within each cell. |
isQuiet | boolean | — | Whether the ListView should be displayed with a quiet style. |
loadingState | LoadingState | — | The current loading state of the ListView. Determines whether or not the progress circle should be shown. |
overflowMode | 'truncate' | 'wrap' | 'truncate' | Sets the text behavior for the row contents. |
renderEmptyState | () => JSX.Element | — | Sets what the ListView should render when there is no content to display. |
disabledBehavior | DisabledBehavior | — | Whether disabledKeys applies to all interactions, or only selection. |
dragHooks | DragHooks | — | The drag hooks returned by |
dropHooks | DropHooks | — | The drag hooks returned by |
items | Iterable<object> | — | Item objects in the collection. |
disabledKeys | Iterable<Key> | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. |
selectionMode | SelectionMode | — | The type of selection that is allowed in the collection. |
disallowEmptySelection | boolean | — | Whether the collection allows empty selection. |
selectedKeys | 'all' | Iterable<Key> | — | The currently selected keys in the collection (controlled). |
defaultSelectedKeys | 'all' | Iterable<Key> | — | The initial selected keys in the collection (uncontrolled). |
selectionStyle | 'checkbox' | 'highlight' | — | How selection should be displayed. |
Events
| Name | Type | Default | Description |
onAction | (
(key: string
)) => void | — | Handler that is called when a user performs an action on an item. The exact user event depends on
the collection's |
onSelectionChange | (
(keys: Selection
)) => any | — | Handler that is called when the selection changes. |
onLoadMore | () => any | — | Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. |
Layout
| Name | Type | Default | Description |
flex | Responsive<string
| number
| boolean> | — | When used in a flex layout, specifies how the element will grow or shrink to fit the space available. See MDN. |
flexGrow | Responsive<number> | — | When used in a flex layout, specifies how the element will grow to fit the space available. See MDN. |
flexShrink | Responsive<number> | — | When used in a flex layout, specifies how the element will shrink to fit the space available. See MDN. |
flexBasis | Responsive<number | string> | — | When used in a flex layout, specifies the initial main size of the element. See MDN. |
alignSelf | Responsive<'auto'
| 'normal'
| 'start'
| 'end'
| 'center'
| 'flex-start'
| 'flex-end'
| 'self-start'
| 'self-end'
| 'stretch'> | — | Overrides the alignItems property of a flex or grid container. See MDN. |
justifySelf | Responsive<'auto'
| 'normal'
| 'start'
| 'end'
| 'flex-start'
| 'flex-end'
| 'self-start'
| 'self-end'
| 'center'
| 'left'
| 'right'
| 'stretch'> | — | Specifies how the element is justified inside a flex or grid container. See MDN. |
order | Responsive<number> | — | The layout order for the element within a flex or grid container. See MDN. |
gridArea | Responsive<string> | — | When used in a grid layout, specifies the named grid area that the element should be placed in within the grid. See MDN. |
gridColumn | Responsive<string> | — | When used in a grid layout, specifies the column the element should be placed in within the grid. See MDN. |
gridRow | Responsive<string> | — | When used in a grid layout, specifies the row the element should be placed in within the grid. See MDN. |
gridColumnStart | Responsive<string> | — | When used in a grid layout, specifies the starting column to span within the grid. See MDN. |
gridColumnEnd | Responsive<string> | — | When used in a grid layout, specifies the ending column to span within the grid. See MDN. |
gridRowStart | Responsive<string> | — | When used in a grid layout, specifies the starting row to span within the grid. See MDN. |
gridRowEnd | Responsive<string> | — | When used in a grid layout, specifies the ending row to span within the grid. See MDN. |
Spacing
| Name | Type | Default | Description |
margin | Responsive<DimensionValue> | — | The margin for all four sides of the element. See MDN. |
marginTop | Responsive<DimensionValue> | — | The margin for the top side of the element. See MDN. |
marginBottom | Responsive<DimensionValue> | — | The margin for the bottom side of the element. See MDN. |
marginStart | Responsive<DimensionValue> | — | The margin for the logical start side of the element, depending on layout direction. See MDN. |
marginEnd | Responsive<DimensionValue> | — | The margin for the logical end side of an element, depending on layout direction. See MDN. |
marginX | Responsive<DimensionValue> | — | The margin for both the left and right sides of the element. See MDN. |
marginY | Responsive<DimensionValue> | — | The margin for both the top and bottom sides of the element. See MDN. |
Sizing
| Name | Type | Default | Description |
width | Responsive<DimensionValue> | — | The width of the element. See MDN. |
minWidth | Responsive<DimensionValue> | — | The minimum width of the element. See MDN. |
maxWidth | Responsive<DimensionValue> | — | The maximum width of the element. See MDN. |
height | Responsive<DimensionValue> | — | The height of the element. See MDN. |
minHeight | Responsive<DimensionValue> | — | The minimum height of the element. See MDN. |
maxHeight | Responsive<DimensionValue> | — | The maximum height of the element. See MDN. |
Positioning
| Name | Type | Default | Description |
position | Responsive<'static'
| 'relative'
| 'absolute'
| 'fixed'
| 'sticky'> | — | Specifies how the element is positioned. See MDN. |
top | Responsive<DimensionValue> | — | The top position for the element. See MDN. |
bottom | Responsive<DimensionValue> | — | The bottom position for the element. See MDN. |
left | Responsive<DimensionValue> | — | The left position for the element. See MDN. Consider using start instead for RTL support. |
right | Responsive<DimensionValue> | — | The right position for the element. See MDN. Consider using start instead for RTL support. |
start | Responsive<DimensionValue> | — | The logical start position for the element, depending on layout direction. See MDN. |
end | Responsive<DimensionValue> | — | The logical end position for the element, depending on layout direction. See MDN. |
zIndex | Responsive<number> | — | The stacking order for the element. See MDN. |
isHidden | Responsive<boolean> | — | Hides the element. |
Accessibility
| Name | Type | Default | Description |
id | string | — | The element's unique identifier. See MDN. |
aria-label | string | — | Defines a string value that labels the current element. |
aria-labelledby | string | — | Identifies the element (or elements) that labels the current element. |
aria-describedby | string | — | Identifies the element (or elements) that describes the object. |
aria-details | string | — | Identifies the element (or elements) that provide a detailed, extended description for the object. |
Advanced
| Name | Type | Default | Description |
UNSAFE_className | string | — | Sets the CSS className for the element. Only use as a last resort. Use style props instead. |
UNSAFE_style | CSSProperties | — | Sets inline style for the element. Only use as a last resort. Use style props instead. |
Visual options#
Quiet#
function ListExample(props) {
return (
<ListView
selectionMode="multiple"
aria-label="Quiet ListView example"
width="size-3000"
{...props}
>
<Item>Adobe AfterEffects</Item>
<Item>Adobe Dreamweaver</Item>
<Item>Adobe Acrobat</Item>
</ListView>
);
}
<ListExample isQuiet />
function ListExample(props) {
return (
<ListView
selectionMode="multiple"
aria-label="Quiet ListView example"
width="size-3000"
{...props}
>
<Item>Adobe AfterEffects</Item>
<Item>Adobe Dreamweaver</Item>
<Item>Adobe Acrobat</Item>
</ListView>
);
}
<ListExample isQuiet />
function ListExample(
props
) {
return (
<ListView
selectionMode="multiple"
aria-label="Quiet ListView example"
width="size-3000"
{...props}
>
<Item>
Adobe
AfterEffects
</Item>
<Item>
Adobe Dreamweaver
</Item>
<Item>Adobe Acrobat
</Item>
</ListView>
);
}
<ListExample isQuiet />
Density#
The amount of vertical padding that each row contains can be modified by providing the density prop.
<Flex wrap gap="size-300">
<ListExample density="compact" aria-label="Compact ListView example" />
<ListExample density="spacious" aria-label="Spacious ListView example" />
</Flex>
<Flex wrap gap="size-300">
<ListExample
density="compact"
aria-label="Compact ListView example"
/>
<ListExample
density="spacious"
aria-label="Spacious ListView example"
/>
</Flex>
<Flex
wrap
gap="size-300"
>
<ListExample
density="compact"
aria-label="Compact ListView example"
/>
<ListExample
density="spacious"
aria-label="Spacious ListView example"
/>
</Flex>
Overflow mode#
By default, text content that overflows its row will be truncated. You can have it wrap instead by passing overflowMode="wrap"
to the ListView.
<ListExample
overflowMode="wrap"
aria-label="Text wrapping ListView example"
width="size-2000"
/>
<ListExample
overflowMode="wrap"
aria-label="Text wrapping ListView example"
width="size-2000"
/>
<ListExample
overflowMode="wrap"
aria-label="Text wrapping ListView example"
width="size-2000"
/>
Empty state#
Use the renderEmptyState prop to customize what the ListView will display if there are no rows provided.
import {Content} from '@react-spectrum/view';
import {IllustratedMessage} from '@react-spectrum/illustratedmessage';
import NotFound from '@spectrum-icons/illustrations/NotFound';
import {Heading} from '@react-spectrum/text';
function renderEmptyState() {
return (
<IllustratedMessage>
<NotFound />
<Heading>No results</Heading>
<Content>No results found</Content>
</IllustratedMessage>
);
}
<ListView
selectionMode="multiple"
aria-label="Example ListView for empty state"
maxWidth="size-6000"
height="size-3000"
renderEmptyState={renderEmptyState}>
{[]}
</ListView>
import {Content} from '@react-spectrum/view';
import {IllustratedMessage} from '@react-spectrum/illustratedmessage';
import NotFound from '@spectrum-icons/illustrations/NotFound';
import {Heading} from '@react-spectrum/text';
function renderEmptyState() {
return (
<IllustratedMessage>
<NotFound />
<Heading>No results</Heading>
<Content>No results found</Content>
</IllustratedMessage>
);
}
<ListView
selectionMode="multiple"
aria-label="Example ListView for empty state"
maxWidth="size-6000"
height="size-3000"
renderEmptyState={renderEmptyState}
>
{[]}
</ListView>
import {Content} from '@react-spectrum/view';
import {IllustratedMessage} from '@react-spectrum/illustratedmessage';
import NotFound from '@spectrum-icons/illustrations/NotFound';
import {Heading} from '@react-spectrum/text';
function renderEmptyState() {
return (
<IllustratedMessage>
<NotFound />
<Heading>
No results
</Heading>
<Content>
No results found
</Content>
</IllustratedMessage>
);
}
<ListView
selectionMode="multiple"
aria-label="Example ListView for empty state"
maxWidth="size-6000"
height="size-3000"
renderEmptyState={renderEmptyState}
>
{[]}
</ListView>