From d2b7a2cd568141be7f108627b13fae3e185ff3ed Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 01:59:48 +0900 Subject: [PATCH 01/15] Add `onKeyDown` prop to ListBoxItem for custom keyboard handling --- .../react-aria-components/docs/ListBox.mdx | 40 +++++++++++++++++ .../react-aria-components/src/ListBox.tsx | 8 +++- .../stories/ListBox.stories.tsx | 43 +++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/packages/react-aria-components/docs/ListBox.mdx b/packages/react-aria-components/docs/ListBox.mdx index dae009ba866..8dd6b940bfe 100644 --- a/packages/react-aria-components/docs/ListBox.mdx +++ b/packages/react-aria-components/docs/ListBox.mdx @@ -409,6 +409,46 @@ By default, link items in a ListBox are not selectable, and only perform navigat The `` 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 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 + +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 Backspace or Delete keys. + +```tsx example +import {useListData} from 'react-stately'; + +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: React.Key) => (e: React.KeyboardEvent) => { + if (e.key === 'Delete' || e.key === 'Backspace') { + e.preventDefault(); + list.remove(key); + } + }; + + return ( + + {item => ( + + {item.name} + + )} + + ); +} +``` + ## Sections ListBox supports sections in order to group options. Sections can be used by wrapping groups of items in a `ListBoxSection` element. A `
` element may also be included to label the section. diff --git a/packages/react-aria-components/src/ListBox.tsx b/packages/react-aria-components/src/ListBox.tsx index 636b0141c43..9e5424a7834 100644 --- a/packages/react-aria-components/src/ListBox.tsx +++ b/packages/react-aria-components/src/ListBox.tsx @@ -356,7 +356,9 @@ export interface ListBoxItemProps extends RenderProps void + onAction?: () => void, + /** Handler that is called when a key is pressed on the item. */ + onKeyDown?: (e: React.KeyboardEvent) => void } /** @@ -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); @@ -422,7 +426,7 @@ export const ListBoxItem = /*#__PURE__*/ createLeafComponent(ItemNode, function return ( { ); }; +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 ( +
+
+ Press Delete or Backspace to remove the focused item. +
+ + {item => ( + + {item.name} + + )} + +
+ ); +}; + From b404954bcbfe3ba45edec122594128c9cbd29951 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 02:29:02 +0900 Subject: [PATCH 02/15] Update type annotation in ListBox example and fix style formatting in ListBox story --- packages/react-aria-components/docs/ListBox.mdx | 3 ++- packages/react-aria-components/stories/ListBox.stories.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-aria-components/docs/ListBox.mdx b/packages/react-aria-components/docs/ListBox.mdx index 8dd6b940bfe..011bded3f63 100644 --- a/packages/react-aria-components/docs/ListBox.mdx +++ b/packages/react-aria-components/docs/ListBox.mdx @@ -415,6 +415,7 @@ ListBox supports custom keyboard event handling on individual items via the `onK ```tsx example import {useListData} from 'react-stately'; +import type {Key} from 'react-aria-components'; function Example() { let list = useListData({ @@ -427,7 +428,7 @@ function Example() { ] }); - let handleKeyDown = (key: React.Key) => (e: React.KeyboardEvent) => { + let handleKeyDown = (key: Key) => (e: React.KeyboardEvent) => { if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); list.remove(key); diff --git a/packages/react-aria-components/stories/ListBox.stories.tsx b/packages/react-aria-components/stories/ListBox.stories.tsx index f7dfce2881e..33b90446f89 100644 --- a/packages/react-aria-components/stories/ListBox.stories.tsx +++ b/packages/react-aria-components/stories/ListBox.stories.tsx @@ -832,7 +832,7 @@ export const ListBoxWithKeyboardDelete: ListBoxStory = () => { }; return ( -
+
Press Delete or Backspace to remove the focused item.
From dfd80e104b3b3fbf598bd1b4c0e732403c466fd1 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 11:42:25 +0900 Subject: [PATCH 03/15] fix: remove custom keyboard handling example from ListBox docs and story --- .../react-aria-components/docs/ListBox.mdx | 41 ------------------ .../stories/ListBox.stories.tsx | 42 ------------------- 2 files changed, 83 deletions(-) diff --git a/packages/react-aria-components/docs/ListBox.mdx b/packages/react-aria-components/docs/ListBox.mdx index 011bded3f63..dae009ba866 100644 --- a/packages/react-aria-components/docs/ListBox.mdx +++ b/packages/react-aria-components/docs/ListBox.mdx @@ -409,47 +409,6 @@ By default, link items in a ListBox are not selectable, and only perform navigat The `` 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 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 - -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 Backspace or Delete 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 ( - - {item => ( - - {item.name} - - )} - - ); -} -``` - ## Sections ListBox supports sections in order to group options. Sections can be used by wrapping groups of items in a `ListBoxSection` element. A `
` element may also be included to label the section. diff --git a/packages/react-aria-components/stories/ListBox.stories.tsx b/packages/react-aria-components/stories/ListBox.stories.tsx index 33b90446f89..e9f33b3ba2a 100644 --- a/packages/react-aria-components/stories/ListBox.stories.tsx +++ b/packages/react-aria-components/stories/ListBox.stories.tsx @@ -12,8 +12,6 @@ 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'; @@ -810,44 +808,4 @@ 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 ( -
-
- Press Delete or Backspace to remove the focused item. -
- - {item => ( - - {item.name} - - )} - -
- ); -}; From 49e754d61d34db5edb243fecac719a7616713470 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 13:27:50 +0900 Subject: [PATCH 04/15] test: add unit tests for `onKeyDown` prop in ListBox component --- .../react-aria-components/test/ListBox.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js index a46b9fc71aa..ae754298691 100644 --- a/packages/react-aria-components/test/ListBox.test.js +++ b/packages/react-aria-components/test/ListBox.test.js @@ -1861,4 +1861,20 @@ describe('ListBox', () => { expect(onClick).toHaveBeenCalledTimes(1); }); }); + + describe('onKeyDown', () => { + it('should call onKeyDown handler when key is pressed on item', () => { + let onKeyDown = jest.fn(); + let {getAllByRole} = renderListbox({}, {onKeyDown}); + let options = getAllByRole('option'); + + fireEvent.keyDown(options[0], {key: 'Delete'}); + expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(onKeyDown.mock.calls[0][0]).toHaveProperty('key', 'Delete'); + + fireEvent.keyDown(options[1], {key: 'Backspace'}); + expect(onKeyDown).toHaveBeenCalledTimes(2); + expect(onKeyDown.mock.calls[1][0]).toHaveProperty('key', 'Backspace'); + }); + }); }); From 00f7be5484060750a70129b662c55f38985f7e12 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 14:01:35 +0900 Subject: [PATCH 05/15] test: remove redundant assertions for `onKeyDown` in ListBox tests --- packages/react-aria-components/test/ListBox.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js index ae754298691..cee1431c938 100644 --- a/packages/react-aria-components/test/ListBox.test.js +++ b/packages/react-aria-components/test/ListBox.test.js @@ -1870,11 +1870,9 @@ describe('ListBox', () => { fireEvent.keyDown(options[0], {key: 'Delete'}); expect(onKeyDown).toHaveBeenCalledTimes(1); - expect(onKeyDown.mock.calls[0][0]).toHaveProperty('key', 'Delete'); fireEvent.keyDown(options[1], {key: 'Backspace'}); expect(onKeyDown).toHaveBeenCalledTimes(2); - expect(onKeyDown.mock.calls[1][0]).toHaveProperty('key', 'Backspace'); }); }); }); From e7356f78088b3734ce5696072d853f102721d069 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Sat, 15 Nov 2025 15:59:55 +0900 Subject: [PATCH 06/15] test: update `onKeyDown` tests in ListBox to use user-event for better simulation --- packages/react-aria-components/test/ListBox.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js index cee1431c938..c1ffb040d50 100644 --- a/packages/react-aria-components/test/ListBox.test.js +++ b/packages/react-aria-components/test/ListBox.test.js @@ -1863,15 +1863,15 @@ describe('ListBox', () => { }); describe('onKeyDown', () => { - it('should call onKeyDown handler when key is pressed on item', () => { + it('should call onKeyDown handler when key is pressed on item', async () => { let onKeyDown = jest.fn(); - let {getAllByRole} = renderListbox({}, {onKeyDown}); - let options = getAllByRole('option'); + renderListbox({}, {onKeyDown}); - fireEvent.keyDown(options[0], {key: 'Delete'}); + await user.tab(); + await user.keyboard('{Delete}'); expect(onKeyDown).toHaveBeenCalledTimes(1); - fireEvent.keyDown(options[1], {key: 'Backspace'}); + await user.keyboard('{Backspace}'); expect(onKeyDown).toHaveBeenCalledTimes(2); }); }); From 2fbcf50ab995ca7ba9532d92ef70f95556fb84d0 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 01:59:48 +0900 Subject: [PATCH 07/15] Add `onKeyDown` prop to ListBoxItem for custom keyboard handling --- .../react-aria-components/docs/ListBox.mdx | 40 +++++++++++++++++ .../react-aria-components/src/ListBox.tsx | 8 +++- .../stories/ListBox.stories.tsx | 43 +++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/packages/react-aria-components/docs/ListBox.mdx b/packages/react-aria-components/docs/ListBox.mdx index dae009ba866..8dd6b940bfe 100644 --- a/packages/react-aria-components/docs/ListBox.mdx +++ b/packages/react-aria-components/docs/ListBox.mdx @@ -409,6 +409,46 @@ By default, link items in a ListBox are not selectable, and only perform navigat The `` 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 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 + +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 Backspace or Delete keys. + +```tsx example +import {useListData} from 'react-stately'; + +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: React.Key) => (e: React.KeyboardEvent) => { + if (e.key === 'Delete' || e.key === 'Backspace') { + e.preventDefault(); + list.remove(key); + } + }; + + return ( + + {item => ( + + {item.name} + + )} + + ); +} +``` + ## Sections ListBox supports sections in order to group options. Sections can be used by wrapping groups of items in a `ListBoxSection` element. A `
` element may also be included to label the section. diff --git a/packages/react-aria-components/src/ListBox.tsx b/packages/react-aria-components/src/ListBox.tsx index 636b0141c43..9e5424a7834 100644 --- a/packages/react-aria-components/src/ListBox.tsx +++ b/packages/react-aria-components/src/ListBox.tsx @@ -356,7 +356,9 @@ export interface ListBoxItemProps extends RenderProps void + onAction?: () => void, + /** Handler that is called when a key is pressed on the item. */ + onKeyDown?: (e: React.KeyboardEvent) => void } /** @@ -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); @@ -422,7 +426,7 @@ export const ListBoxItem = /*#__PURE__*/ createLeafComponent(ItemNode, function return ( { ); }; +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 ( +
+
+ Press Delete or Backspace to remove the focused item. +
+ + {item => ( + + {item.name} + + )} + +
+ ); +}; + From 139f601b2cdddf6ba29546747d425ebb52bac950 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 02:29:02 +0900 Subject: [PATCH 08/15] Update type annotation in ListBox example and fix style formatting in ListBox story --- packages/react-aria-components/docs/ListBox.mdx | 3 ++- packages/react-aria-components/stories/ListBox.stories.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-aria-components/docs/ListBox.mdx b/packages/react-aria-components/docs/ListBox.mdx index 8dd6b940bfe..011bded3f63 100644 --- a/packages/react-aria-components/docs/ListBox.mdx +++ b/packages/react-aria-components/docs/ListBox.mdx @@ -415,6 +415,7 @@ ListBox supports custom keyboard event handling on individual items via the `onK ```tsx example import {useListData} from 'react-stately'; +import type {Key} from 'react-aria-components'; function Example() { let list = useListData({ @@ -427,7 +428,7 @@ function Example() { ] }); - let handleKeyDown = (key: React.Key) => (e: React.KeyboardEvent) => { + let handleKeyDown = (key: Key) => (e: React.KeyboardEvent) => { if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); list.remove(key); diff --git a/packages/react-aria-components/stories/ListBox.stories.tsx b/packages/react-aria-components/stories/ListBox.stories.tsx index f7dfce2881e..33b90446f89 100644 --- a/packages/react-aria-components/stories/ListBox.stories.tsx +++ b/packages/react-aria-components/stories/ListBox.stories.tsx @@ -832,7 +832,7 @@ export const ListBoxWithKeyboardDelete: ListBoxStory = () => { }; return ( -
+
Press Delete or Backspace to remove the focused item.
From f1d2493e3bb958bccc42f1b717d8a5412c7bfd29 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 11:42:25 +0900 Subject: [PATCH 09/15] fix: remove custom keyboard handling example from ListBox docs and story --- .../react-aria-components/docs/ListBox.mdx | 41 ------------------ .../stories/ListBox.stories.tsx | 42 ------------------- 2 files changed, 83 deletions(-) diff --git a/packages/react-aria-components/docs/ListBox.mdx b/packages/react-aria-components/docs/ListBox.mdx index 011bded3f63..dae009ba866 100644 --- a/packages/react-aria-components/docs/ListBox.mdx +++ b/packages/react-aria-components/docs/ListBox.mdx @@ -409,47 +409,6 @@ By default, link items in a ListBox are not selectable, and only perform navigat The `` 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 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 - -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 Backspace or Delete 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 ( - - {item => ( - - {item.name} - - )} - - ); -} -``` - ## Sections ListBox supports sections in order to group options. Sections can be used by wrapping groups of items in a `ListBoxSection` element. A `
` element may also be included to label the section. diff --git a/packages/react-aria-components/stories/ListBox.stories.tsx b/packages/react-aria-components/stories/ListBox.stories.tsx index 33b90446f89..e9f33b3ba2a 100644 --- a/packages/react-aria-components/stories/ListBox.stories.tsx +++ b/packages/react-aria-components/stories/ListBox.stories.tsx @@ -12,8 +12,6 @@ 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'; @@ -810,44 +808,4 @@ 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 ( -
-
- Press Delete or Backspace to remove the focused item. -
- - {item => ( - - {item.name} - - )} - -
- ); -}; From 299d78c15a30ecb87d9062127470cc7b00ac8089 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 13:27:50 +0900 Subject: [PATCH 10/15] test: add unit tests for `onKeyDown` prop in ListBox component --- .../react-aria-components/test/ListBox.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js index a46b9fc71aa..ae754298691 100644 --- a/packages/react-aria-components/test/ListBox.test.js +++ b/packages/react-aria-components/test/ListBox.test.js @@ -1861,4 +1861,20 @@ describe('ListBox', () => { expect(onClick).toHaveBeenCalledTimes(1); }); }); + + describe('onKeyDown', () => { + it('should call onKeyDown handler when key is pressed on item', () => { + let onKeyDown = jest.fn(); + let {getAllByRole} = renderListbox({}, {onKeyDown}); + let options = getAllByRole('option'); + + fireEvent.keyDown(options[0], {key: 'Delete'}); + expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(onKeyDown.mock.calls[0][0]).toHaveProperty('key', 'Delete'); + + fireEvent.keyDown(options[1], {key: 'Backspace'}); + expect(onKeyDown).toHaveBeenCalledTimes(2); + expect(onKeyDown.mock.calls[1][0]).toHaveProperty('key', 'Backspace'); + }); + }); }); From 0c8979b494774da36f77da7fb8539f452c5d19d8 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 14:01:35 +0900 Subject: [PATCH 11/15] test: remove redundant assertions for `onKeyDown` in ListBox tests --- packages/react-aria-components/test/ListBox.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js index ae754298691..cee1431c938 100644 --- a/packages/react-aria-components/test/ListBox.test.js +++ b/packages/react-aria-components/test/ListBox.test.js @@ -1870,11 +1870,9 @@ describe('ListBox', () => { fireEvent.keyDown(options[0], {key: 'Delete'}); expect(onKeyDown).toHaveBeenCalledTimes(1); - expect(onKeyDown.mock.calls[0][0]).toHaveProperty('key', 'Delete'); fireEvent.keyDown(options[1], {key: 'Backspace'}); expect(onKeyDown).toHaveBeenCalledTimes(2); - expect(onKeyDown.mock.calls[1][0]).toHaveProperty('key', 'Backspace'); }); }); }); From 81ccc926ebd30db6f631a5dab6885b1b8340d9ad Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Sat, 15 Nov 2025 15:59:55 +0900 Subject: [PATCH 12/15] test: update `onKeyDown` tests in ListBox to use user-event for better simulation --- packages/react-aria-components/test/ListBox.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js index cee1431c938..c1ffb040d50 100644 --- a/packages/react-aria-components/test/ListBox.test.js +++ b/packages/react-aria-components/test/ListBox.test.js @@ -1863,15 +1863,15 @@ describe('ListBox', () => { }); describe('onKeyDown', () => { - it('should call onKeyDown handler when key is pressed on item', () => { + it('should call onKeyDown handler when key is pressed on item', async () => { let onKeyDown = jest.fn(); - let {getAllByRole} = renderListbox({}, {onKeyDown}); - let options = getAllByRole('option'); + renderListbox({}, {onKeyDown}); - fireEvent.keyDown(options[0], {key: 'Delete'}); + await user.tab(); + await user.keyboard('{Delete}'); expect(onKeyDown).toHaveBeenCalledTimes(1); - fireEvent.keyDown(options[1], {key: 'Backspace'}); + await user.keyboard('{Backspace}'); expect(onKeyDown).toHaveBeenCalledTimes(2); }); }); From cded5a0abfcddad17b9cda5f4ed2486a72f4f379 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Sat, 15 Nov 2025 16:23:47 +0900 Subject: [PATCH 13/15] chore: remove unnecessary blank lines in ListBox story --- packages/react-aria-components/stories/ListBox.stories.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-aria-components/stories/ListBox.stories.tsx b/packages/react-aria-components/stories/ListBox.stories.tsx index e9f33b3ba2a..47fb20b8e7b 100644 --- a/packages/react-aria-components/stories/ListBox.stories.tsx +++ b/packages/react-aria-components/stories/ListBox.stories.tsx @@ -807,5 +807,3 @@ export let VirtualizedListBoxDndOnAction: ListBoxStory = () => {
); }; - - From 175fc31ca7967dfb43eb201604b5104e82ec633a Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Sat, 15 Nov 2025 16:30:20 +0900 Subject: [PATCH 14/15] chore: remove trailing blank line in ListBox story --- packages/react-aria-components/stories/ListBox.stories.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-aria-components/stories/ListBox.stories.tsx b/packages/react-aria-components/stories/ListBox.stories.tsx index 47fb20b8e7b..0256660a9ee 100644 --- a/packages/react-aria-components/stories/ListBox.stories.tsx +++ b/packages/react-aria-components/stories/ListBox.stories.tsx @@ -807,3 +807,4 @@ export let VirtualizedListBoxDndOnAction: ListBoxStory = () => {
); }; + From d54a2d22114bfb737803293f4ccf706008abf579 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 18 Dec 2025 14:47:42 +1100 Subject: [PATCH 15/15] Add tests and move to useKeyboard --- .../react-aria-components/src/ListBox.tsx | 14 ++++++------- .../test/ListBox.test.js | 20 +++++++++++++------ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/react-aria-components/src/ListBox.tsx b/packages/react-aria-components/src/ListBox.tsx index 3e9e3ef1c64..db19ce65e7a 100644 --- a/packages/react-aria-components/src/ListBox.tsx +++ b/packages/react-aria-components/src/ListBox.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {AriaListBoxOptions, AriaListBoxProps, DraggableItemResult, DragPreviewRenderer, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocus, useFocusRing, useHover, useListBox, useListBoxSection, useLocale, useOption} from 'react-aria'; +import {AriaListBoxOptions, AriaListBoxProps, DraggableItemResult, DragPreviewRenderer, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocus, useFocusRing, useHover, useKeyboard, useListBox, useListBoxSection, useLocale, useOption} from 'react-aria'; import { ClassNameOrFunction, ContextValue, @@ -30,7 +30,7 @@ import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPers import {DragAndDropHooks} from './useDragAndDrop'; import {DraggableCollectionState, DroppableCollectionState, ListState, Node, Orientation, SelectionBehavior, UNSTABLE_useFilteredListState, useListState} from 'react-stately'; import {filterDOMProps, inertValue, LoadMoreSentinelProps, useLoadMoreSentinel, useObjectRef} from '@react-aria/utils'; -import {FocusEvents, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared'; +import {FocusEvents, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, KeyboardEvents, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared'; import {HeaderContext} from './Header'; import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react'; import {SelectableCollectionContext, SelectableCollectionContextValue} from './RSPContexts'; @@ -336,7 +336,7 @@ export const ListBoxSection = /*#__PURE__*/ createBranchComponent(SectionNode, L export interface ListBoxItemRenderProps extends ItemRenderProps {} -export interface ListBoxItemProps extends RenderProps, LinkDOMProps, HoverEvents, PressEvents, FocusEvents, Omit, 'onClick'> { +export interface ListBoxItemProps extends RenderProps, LinkDOMProps, HoverEvents, PressEvents, KeyboardEvents, FocusEvents, Omit, 'onClick'> { /** * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. * @default 'react-aria-ListBoxItem' @@ -356,9 +356,7 @@ export interface ListBoxItemProps extends RenderProps void, - /** Handler that is called when a key is pressed on the item. */ - onKeyDown?: (e: React.KeyboardEvent) => void + onAction?: () => void } /** @@ -381,7 +379,7 @@ export const ListBoxItem = /*#__PURE__*/ createLeafComponent(ItemNode, function onHoverEnd: item.props.onHoverEnd }); - let keyDownProps = props.onKeyDown ? {onKeyDown: props.onKeyDown} : undefined; + let {keyboardProps} = useKeyboard(props); let {focusProps} = useFocus(props); let draggableItem: DraggableItemResult | null = null; @@ -431,7 +429,7 @@ export const ListBoxItem = /*#__PURE__*/ createLeafComponent(ItemNode, function return ( { act(() => jest.runAllTimers()); expect(onReorder).toHaveBeenCalledTimes(1); - + // Verify we're no longer in drag mode options = getAllByRole('option'); expect(options.filter(opt => opt.classList.contains('react-aria-DropIndicator'))).toHaveLength(0); @@ -1863,16 +1863,24 @@ describe('ListBox', () => { }); describe('onKeyDown', () => { - it('should call onKeyDown handler when key is pressed on item', async () => { - let onKeyDown = jest.fn(); - renderListbox({}, {onKeyDown}); + it('should call key handler when key is pressed on item', async () => { + let onKeyDown = jest.fn((e) => e.continuePropagation()); + let onKeyUp = jest.fn(); + let onSelectionChange = jest.fn(); + renderListbox({selectionMode: 'multiple', onSelectionChange}, {onKeyDown, onKeyUp}); await user.tab(); - await user.keyboard('{Delete}'); + expect(onKeyUp).toHaveBeenCalledTimes(1); + onKeyUp.mockClear(); + await user.keyboard('{Enter}'); expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(onKeyUp).toHaveBeenCalledTimes(1); + expect(onSelectionChange).toHaveBeenCalledTimes(1); - await user.keyboard('{Backspace}'); + await user.keyboard('{Escape}'); expect(onKeyDown).toHaveBeenCalledTimes(2); + expect(onKeyUp).toHaveBeenCalledTimes(2); + expect(onSelectionChange).toHaveBeenCalledTimes(2); }); }); });