Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 18 additions & 3 deletions src/internal/components/listbox/__tests__/use-listbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ describe('useListbox', () => {
});

describe('Home and End', () => {
test('move the highlight and keep the native action', () => {
test('move the highlight and keep the native action in a filter input, to move the text cursor', () => {
const { hook, keyDown } = renderListbox();

const end = keyDown(END);
Expand All @@ -142,6 +142,21 @@ describe('useListbox', () => {
expect(hook.result.current.highlightedIndex).toBe(1);
expect(home.defaultPrevented).toBe(false);
});

test('move the highlight and prevent the native action on the list, so the page does not scroll', () => {
const { hook } = renderListbox();
const listKeyDown = (keyCode: number) => {
const event = createKeyEvent(keyCode);
act(() => hook.result.current.getListProps({ withKeyboard: true }).onKeyDown!(event));
return event;
};
const end = listKeyDown(END);
expect(hook.result.current.highlightedIndex).toBe(4);
expect(end.defaultPrevented).toBe(true);
const home = listKeyDown(HOME);
expect(hook.result.current.highlightedIndex).toBe(1);
expect(home.defaultPrevented).toBe(true);
});
});

describe('choosing', () => {
Expand Down Expand Up @@ -270,12 +285,12 @@ describe('useListbox', () => {
test('adds onKeyDown and aria-activedescendant with withKeyboard', () => {
const { hook } = renderListbox();
act(() => hook.result.current.mouseHandlers.onMouseMove(3));
const { listId, listRef, mouseHandlers, onKeyDown, getListProps } = hook.result.current;
const { listId, listRef, mouseHandlers, getListProps } = hook.result.current;
expect(getListProps({ withKeyboard: true })).toEqual({
id: listId,
ref: listRef,
...mouseHandlers,
onKeyDown,
onKeyDown: expect.any(Function),
nativeAttributes: { 'aria-activedescendant': getOptionId(listId, 3) },
});
});
Expand Down
20 changes: 17 additions & 3 deletions src/internal/components/listbox/use-listbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useUniqueId } from '@cloudscape-design/component-toolkit/internal';
import { SelectProps } from '../../../select/interfaces';
import { BaseKeyDetail, CancelableEventHandler } from '../../../types/events';
import { OptionDefinition, OptionGroup } from '../../../types/option';
import { KeyCode } from '../../keycode';
import { DropdownOption } from '../option/interfaces';
import { isGroupInteractive, isInteractive } from '../option/utils/filter-options';
import {
Expand Down Expand Up @@ -76,15 +77,20 @@ export interface UseListboxApi {
highlight: HighlightedOptionHandlers<DropdownOption>;
/** Chooses `option`, or the highlighted option when omitted. Ignores non-interactive options. */
selectOption(option?: DropdownOption): void;
/** Keydown handler for the element that has focus (list or filter input). */
/**
* Keydown handler for a filter input that controls the list. Home and End keep their native action there, to move
* the text cursor. The list element gets its own handler from `getListProps({ withKeyboard: true })`, which
* prevents the native Home and End action.
*/
onKeyDown: CancelableEventHandler<BaseKeyDetail>;
/** `onMouseUp` / `onMouseMove` for the list element. */
mouseHandlers: ReturnType<typeof getListMouseHandlers>;
/** Ref of the list element, also set by `getListProps`. */
listRef: RefObject<HTMLDivElement>;
/**
* Base props for the list element: id, ref and mouse handlers. With `withKeyboard`, also `onKeyDown` and
* `aria-activedescendant`, for a list that has focus itself (not through a filter input). With `embedded`, also
* `aria-activedescendant`, for a list that has focus itself (not through a filter input). This `onKeyDown`
* prevents the native Home and End action, so they don't scroll the page or a scroll container. With `embedded`, also
* `onFocus` and `onBlur`, for an embedded list: an always visible list that is a tab stop. Focus highlights the
* first selected option (in selection order), or the first option when nothing is selected, and blur clears the
* highlight. A hover highlight is kept on focus, so a click selects the hovered option.
Expand Down Expand Up @@ -133,6 +139,7 @@ export function useListbox({
}
};

// Shared by the list element and a filter input. In a filter input, Home and End also move the text cursor.
const onKeyDown = useMenuKeyboard({
...getListboxNavigation({
options,
Expand All @@ -150,6 +157,13 @@ export function useListbox({
// With 'keyboard-highlight', Space chooses only while an option has a keyboard highlight, and types otherwise.
preventNativeSpace: spaceSelects === 'always' || (!!highlightedOption && highlightType.type === 'keyboard'),
});
// On the list element, Home and End only move the highlight, so they don't scroll the page or a scroll container.
const onListKeyDown: CancelableEventHandler<BaseKeyDetail> = event => {
if (event.detail.keyCode === KeyCode.home || event.detail.keyCode === KeyCode.end) {
event.preventDefault();
}
onKeyDown(event);
};

const mouseHandlers = getListMouseHandlers({
options,
Expand Down Expand Up @@ -188,7 +202,7 @@ export function useListbox({
const getListProps = ({ withKeyboard, embedded }: { withKeyboard: boolean; embedded?: boolean }) => {
const listProps: BaseListProps = { id: listId, ref: listRef, ...mouseHandlers };
if (withKeyboard) {
listProps.onKeyDown = onKeyDown;
listProps.onKeyDown = onListKeyDown;
listProps.nativeAttributes = { 'aria-activedescendant': activeDescendantId };
}
if (embedded) {
Expand Down
Loading