Skip to content

Commit 2fbcf50

Browse files
committed
Add onKeyDown prop to ListBoxItem for custom keyboard handling
1 parent 93d39fd commit 2fbcf50

File tree

3 files changed

+89
-2
lines changed

3 files changed

+89
-2
lines changed

packages/react-aria-components/docs/ListBox.mdx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,46 @@ By default, link items in a ListBox are not selectable, and only perform navigat
409409

410410
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.
411411

412+
## Custom keyboard handling
413+
414+
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.
415+
416+
```tsx example
417+
import {useListData} from 'react-stately';
418+
419+
function Example() {
420+
let list = useListData({
421+
initialItems: [
422+
{id: 1, name: 'Item 1'},
423+
{id: 2, name: 'Item 2'},
424+
{id: 3, name: 'Item 3'},
425+
{id: 4, name: 'Item 4'},
426+
{id: 5, name: 'Item 5'}
427+
]
428+
});
429+
430+
let handleKeyDown = (key: React.Key) => (e: React.KeyboardEvent) => {
431+
if (e.key === 'Delete' || e.key === 'Backspace') {
432+
e.preventDefault();
433+
list.remove(key);
434+
}
435+
};
436+
437+
return (
438+
<ListBox
439+
aria-label="ListBox with delete support"
440+
selectionMode="single"
441+
items={list.items}>
442+
{item => (
443+
<ListBoxItem onKeyDown={handleKeyDown(item.id)}>
444+
{item.name}
445+
</ListBoxItem>
446+
)}
447+
</ListBox>
448+
);
449+
}
450+
```
451+
412452
## Sections
413453

414454
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.

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,9 @@ export interface ListBoxItemProps<T = object> extends RenderProps<ListBoxItemRen
356356
* Handler that is called when a user performs an action on the item. The exact user event depends on
357357
* the collection's `selectionBehavior` prop and the interaction modality.
358358
*/
359-
onAction?: () => void
359+
onAction?: () => void,
360+
/** Handler that is called when a key is pressed on the item. */
361+
onKeyDown?: (e: React.KeyboardEvent) => void
360362
}
361363

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

384+
let keyDownProps = props.onKeyDown ? {onKeyDown: props.onKeyDown} : undefined;
385+
382386
let draggableItem: DraggableItemResult | null = null;
383387
if (dragState && dragAndDropHooks) {
384388
draggableItem = dragAndDropHooks.useDraggableItem!({key: item.key, hasAction: states.hasAction}, dragState);
@@ -422,7 +426,7 @@ export const ListBoxItem = /*#__PURE__*/ createLeafComponent(ItemNode, function
422426

423427
return (
424428
<ElementType
425-
{...mergeProps(DOMProps, renderProps, optionProps, hoverProps, draggableItem?.dragProps, droppableItem?.dropProps)}
429+
{...mergeProps(DOMProps, renderProps, optionProps, hoverProps, keyDownProps, draggableItem?.dragProps, droppableItem?.dropProps)}
426430
ref={ref}
427431
data-allows-dragging={!!dragState || undefined}
428432
data-selected={states.isSelected || undefined}

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

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

813+
export const ListBoxWithKeyboardDelete: ListBoxStory = () => {
814+
let initialItems = [
815+
{id: 1, name: 'Item 1'},
816+
{id: 2, name: 'Item 2'},
817+
{id: 3, name: 'Item 3'},
818+
{id: 4, name: 'Item 4'},
819+
{id: 5, name: 'Item 5'}
820+
];
821+
822+
let list = useListData({
823+
initialItems
824+
});
825+
826+
let handleKeyDown = (key: Key) => (e: React.KeyboardEvent) => {
827+
if (e.key === 'Delete' || e.key === 'Backspace') {
828+
e.preventDefault();
829+
list.remove(key);
830+
action('onDelete')(key);
831+
}
832+
};
833+
834+
return (
835+
<div style={{display: 'flex', flexDirection: 'column',alignItems: 'center', gap: 12}}>
836+
<div style={{padding: 12, background: '#f0f0f0', borderRadius: 4}}>
837+
Press <Keyboard>Delete</Keyboard> or <Keyboard>Backspace</Keyboard> to remove the focused item.
838+
</div>
839+
<ListBox
840+
className={styles.menu}
841+
aria-label="ListBox with delete support"
842+
selectionMode="single"
843+
items={list.items}>
844+
{item => (
845+
<MyListBoxItem onKeyDown={handleKeyDown(item.id)}>
846+
{item.name}
847+
</MyListBoxItem>
848+
)}
849+
</ListBox>
850+
</div>
851+
);
852+
};
853+

0 commit comments

Comments
 (0)