Skip to content

Commit f3e3157

Browse files
feat: add onKeyDown prop to ListBoxItem for custom keyboard handling (adobe#9181)
* Add `onKeyDown` prop to ListBoxItem for custom keyboard handling * Update type annotation in ListBox example and fix style formatting in ListBox story * fix: remove custom keyboard handling example from ListBox docs and story * test: add unit tests for `onKeyDown` prop in ListBox component * test: remove redundant assertions for `onKeyDown` in ListBox tests * test: update `onKeyDown` tests in ListBox to use user-event for better simulation * Add `onKeyDown` prop to ListBoxItem for custom keyboard handling * Update type annotation in ListBox example and fix style formatting in ListBox story * fix: remove custom keyboard handling example from ListBox docs and story * test: add unit tests for `onKeyDown` prop in ListBox component * test: remove redundant assertions for `onKeyDown` in ListBox tests * test: update `onKeyDown` tests in ListBox to use user-event for better simulation * chore: remove unnecessary blank lines in ListBox story * chore: remove trailing blank line in ListBox story * Add tests and move to useKeyboard --------- Co-authored-by: Robert Snow <snowystinger@gmail.com>
1 parent acc3263 commit f3e3157

2 files changed

Lines changed: 28 additions & 5 deletions

File tree

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {AriaListBoxOptions, AriaListBoxProps, DraggableItemResult, DragPreviewRenderer, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocus, useFocusRing, useHover, useListBox, useListBoxSection, useLocale, useOption} from 'react-aria';
13+
import {AriaListBoxOptions, AriaListBoxProps, DraggableItemResult, DragPreviewRenderer, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocus, useFocusRing, useHover, useKeyboard, useListBox, useListBoxSection, useLocale, useOption} from 'react-aria';
1414
import {
1515
ClassNameOrFunction,
1616
ContextValue,
@@ -33,7 +33,7 @@ import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPers
3333
import {DragAndDropHooks} from './useDragAndDrop';
3434
import {DraggableCollectionState, DroppableCollectionState, ListState, Node, Orientation, SelectionBehavior, UNSTABLE_useFilteredListState, useListState} from 'react-stately';
3535
import {filterDOMProps, inertValue, LoadMoreSentinelProps, useLoadMoreSentinel, useObjectRef} from '@react-aria/utils';
36-
import {FocusEvents, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared';
36+
import {FocusEvents, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, KeyboardEvents, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared';
3737
import {HeaderContext} from './Header';
3838
import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
3939
import {SelectableCollectionContext, SelectableCollectionContextValue} from './RSPContexts';
@@ -340,7 +340,7 @@ export const ListBoxSection = /*#__PURE__*/ createBranchComponent(SectionNode, L
340340

341341
export interface ListBoxItemRenderProps extends ItemRenderProps {}
342342

343-
export interface ListBoxItemProps<T = object> extends Omit<RenderProps<ListBoxItemRenderProps>, 'render'>, PossibleLinkDOMRenderProps<'div', ListBoxItemRenderProps>, LinkDOMProps, HoverEvents, PressEvents, FocusEvents<HTMLDivElement>, Omit<GlobalDOMAttributes<HTMLDivElement>, 'onClick'> {
343+
export interface ListBoxItemProps<T = object> extends Omit<RenderProps<ListBoxItemRenderProps>, 'render'>, PossibleLinkDOMRenderProps<'div', ListBoxItemRenderProps>, LinkDOMProps, HoverEvents, PressEvents, KeyboardEvents, FocusEvents<HTMLDivElement>, Omit<GlobalDOMAttributes<HTMLDivElement>, 'onClick'> {
344344
/**
345345
* 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.
346346
* @default 'react-aria-ListBoxItem'
@@ -383,6 +383,7 @@ export const ListBoxItem = /*#__PURE__*/ createLeafComponent(ItemNode, function
383383
onHoverEnd: item.props.onHoverEnd
384384
});
385385

386+
let {keyboardProps} = useKeyboard(props);
386387
let {focusProps} = useFocus(props);
387388

388389
let draggableItem: DraggableItemResult | null = null;
@@ -431,7 +432,7 @@ export const ListBoxItem = /*#__PURE__*/ createLeafComponent(ItemNode, function
431432

432433
return (
433434
<ElementType
434-
{...mergeProps(DOMProps, renderProps, optionProps, hoverProps, focusProps, draggableItem?.dragProps, droppableItem?.dropProps)}
435+
{...mergeProps(DOMProps, renderProps, optionProps, hoverProps, keyboardProps, focusProps, draggableItem?.dragProps, droppableItem?.dropProps)}
435436
ref={ref}
436437
data-allows-dragging={!!dragState || undefined}
437438
data-selected={states.isSelected || undefined}

packages/react-aria-components/test/ListBox.test.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1406,7 +1406,7 @@ describe('ListBox', () => {
14061406
act(() => jest.runAllTimers());
14071407

14081408
expect(onReorder).toHaveBeenCalledTimes(1);
1409-
1409+
14101410
// Verify we're no longer in drag mode
14111411
options = getAllByRole('option');
14121412
expect(options.filter(opt => opt.classList.contains('react-aria-DropIndicator'))).toHaveLength(0);
@@ -1949,6 +1949,28 @@ describe('ListBox', () => {
19491949
});
19501950
});
19511951

1952+
describe('onKeyDown', () => {
1953+
it('should call key handler when key is pressed on item', async () => {
1954+
let onKeyDown = jest.fn((e) => e.continuePropagation());
1955+
let onKeyUp = jest.fn();
1956+
let onSelectionChange = jest.fn();
1957+
renderListbox({selectionMode: 'multiple', onSelectionChange}, {onKeyDown, onKeyUp});
1958+
1959+
await user.tab();
1960+
expect(onKeyUp).toHaveBeenCalledTimes(1);
1961+
onKeyUp.mockClear();
1962+
await user.keyboard('{Enter}');
1963+
expect(onKeyDown).toHaveBeenCalledTimes(1);
1964+
expect(onKeyUp).toHaveBeenCalledTimes(1);
1965+
expect(onSelectionChange).toHaveBeenCalledTimes(1);
1966+
1967+
await user.keyboard('{Escape}');
1968+
expect(onKeyDown).toHaveBeenCalledTimes(2);
1969+
expect(onKeyUp).toHaveBeenCalledTimes(2);
1970+
expect(onSelectionChange).toHaveBeenCalledTimes(2);
1971+
});
1972+
});
1973+
19521974
if (React.version.startsWith('19')) {
19531975
it('supports Activity', async () => {
19541976
function ActivityListBox() {

0 commit comments

Comments
 (0)