Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
d2b7a2c
Add `onKeyDown` prop to ListBoxItem for custom keyboard handling
hasegawa-101 Nov 13, 2025
b404954
Update type annotation in ListBox example and fix style formatting in…
hasegawa-101 Nov 13, 2025
dfd80e1
fix: remove custom keyboard handling example from ListBox docs and story
hasegawa-101 Nov 14, 2025
49e754d
test: add unit tests for `onKeyDown` prop in ListBox component
hasegawa-101 Nov 14, 2025
330d9a3
Merge branch 'main' into issue-8732
hasegawa-101 Nov 14, 2025
00f7be5
test: remove redundant assertions for `onKeyDown` in ListBox tests
hasegawa-101 Nov 14, 2025
e7356f7
test: update `onKeyDown` tests in ListBox to use user-event for bette…
hasegawa-101 Nov 15, 2025
2fbcf50
Add `onKeyDown` prop to ListBoxItem for custom keyboard handling
hasegawa-101 Nov 13, 2025
139f601
Update type annotation in ListBox example and fix style formatting in…
hasegawa-101 Nov 13, 2025
f1d2493
fix: remove custom keyboard handling example from ListBox docs and story
hasegawa-101 Nov 14, 2025
299d78c
test: add unit tests for `onKeyDown` prop in ListBox component
hasegawa-101 Nov 14, 2025
0c8979b
test: remove redundant assertions for `onKeyDown` in ListBox tests
hasegawa-101 Nov 14, 2025
81ccc92
test: update `onKeyDown` tests in ListBox to use user-event for bette…
hasegawa-101 Nov 15, 2025
416ceee
Merge remote-tracking branch 'origin/issue-8732' into issue-8732
hasegawa-101 Nov 15, 2025
cded5a0
chore: remove unnecessary blank lines in ListBox story
hasegawa-101 Nov 15, 2025
175fc31
chore: remove trailing blank line in ListBox story
hasegawa-101 Nov 15, 2025
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
41 changes: 41 additions & 0 deletions packages/react-aria-components/docs/ListBox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,47 @@ By default, link items in a ListBox are not selectable, and only perform navigat

The `<ListBoxItem>` component works with frameworks and client side routers like [Next.js](https://nextjs.org/) and [React Router](https://reactrouter.com/en/main). As with other React Aria components that support links, this works via the <TypeLink links={docs.links} type={docs.exports.RouterProvider} /> component at the root of your app. See the [client side routing guide](routing.html) to learn how to set this up.

## Custom keyboard handling
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to document this, it'll be in the props table and is a pretty common pattern


ListBox supports custom keyboard event handling on individual items via the `onKeyDown` prop on `ListBoxItem`. This enables you to implement additional keyboard interactions beyond the built-in selection and navigation behavior, such as deleting items with the <Keyboard>Backspace</Keyboard> or <Keyboard>Delete</Keyboard> keys.

```tsx example
import {useListData} from 'react-stately';
import type {Key} from 'react-aria-components';

function Example() {
let list = useListData({
initialItems: [
{id: 1, name: 'Item 1'},
{id: 2, name: 'Item 2'},
{id: 3, name: 'Item 3'},
{id: 4, name: 'Item 4'},
{id: 5, name: 'Item 5'}
]
});

let handleKeyDown = (key: Key) => (e: React.KeyboardEvent) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
list.remove(key);
}
};

return (
<ListBox
aria-label="ListBox with delete support"
selectionMode="single"
items={list.items}>
{item => (
<ListBoxItem onKeyDown={handleKeyDown(item.id)}>
{item.name}
</ListBoxItem>
)}
</ListBox>
);
}
```

## Sections

ListBox supports sections in order to group options. Sections can be used by wrapping groups of items in a `ListBoxSection` element. A `<Header>` element may also be included to label the section.
Expand Down
8 changes: 6 additions & 2 deletions packages/react-aria-components/src/ListBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,9 @@ export interface ListBoxItemProps<T = object> extends RenderProps<ListBoxItemRen
* Handler that is called when a user performs an action on the item. The exact user event depends on
* the collection's `selectionBehavior` prop and the interaction modality.
*/
onAction?: () => void
onAction?: () => void,
/** Handler that is called when a key is pressed on the item. */
onKeyDown?: (e: React.KeyboardEvent) => void
}

/**
Expand All @@ -379,6 +381,8 @@ export const ListBoxItem = /*#__PURE__*/ createLeafComponent(ItemNode, function
onHoverEnd: item.props.onHoverEnd
});

let keyDownProps = props.onKeyDown ? {onKeyDown: props.onKeyDown} : undefined;

let draggableItem: DraggableItemResult | null = null;
if (dragState && dragAndDropHooks) {
draggableItem = dragAndDropHooks.useDraggableItem!({key: item.key, hasAction: states.hasAction}, dragState);
Expand Down Expand Up @@ -422,7 +426,7 @@ export const ListBoxItem = /*#__PURE__*/ createLeafComponent(ItemNode, function

return (
<ElementType
{...mergeProps(DOMProps, renderProps, optionProps, hoverProps, draggableItem?.dragProps, droppableItem?.dropProps)}
{...mergeProps(DOMProps, renderProps, optionProps, hoverProps, keyDownProps, draggableItem?.dragProps, droppableItem?.dropProps)}
ref={ref}
data-allows-dragging={!!dragState || undefined}
data-selected={states.isSelected || undefined}
Expand Down
43 changes: 43 additions & 0 deletions packages/react-aria-components/stories/ListBox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

import {action} from '@storybook/addon-actions';
import {Collection, DropIndicator, GridLayout, Header, ListBox, ListBoxItem, ListBoxProps, ListBoxSection, ListLayout, Separator, Text, useDragAndDrop, Virtualizer, WaterfallLayout} from 'react-aria-components';
import {Key} from '@react-types/shared';
import {Keyboard} from '@react-spectrum/text';
import {ListBoxLoadMoreItem} from '../';
import {LoadingSpinner, MyListBoxItem} from './utils';
import {Meta, StoryFn, StoryObj} from '@storybook/react';
Expand Down Expand Up @@ -808,3 +810,44 @@ export let VirtualizedListBoxDndOnAction: ListBoxStory = () => {
);
};

export const ListBoxWithKeyboardDelete: ListBoxStory = () => {
let initialItems = [
{id: 1, name: 'Item 1'},
{id: 2, name: 'Item 2'},
{id: 3, name: 'Item 3'},
{id: 4, name: 'Item 4'},
{id: 5, name: 'Item 5'}
];

let list = useListData({
initialItems
});

let handleKeyDown = (key: Key) => (e: React.KeyboardEvent) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
list.remove(key);
action('onDelete')(key);
}
};

return (
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12}}>
<div style={{padding: 12, background: '#f0f0f0', borderRadius: 4}}>
Press <Keyboard>Delete</Keyboard> or <Keyboard>Backspace</Keyboard> to remove the focused item.
</div>
<ListBox
className={styles.menu}
aria-label="ListBox with delete support"
selectionMode="single"
items={list.items}>
{item => (
<MyListBoxItem onKeyDown={handleKeyDown(item.id)}>
{item.name}
</MyListBoxItem>
)}
</ListBox>
</div>
);
};