Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { type KeyboardEvent } from "react";
import { renderHook } from "jest-matrix-react";

import { useListKeyDown } from "./useListKeyDown";
import { useListKeyboardNavigation } from "./useListKeyboardNavigation";

describe("useListKeyDown", () => {
let mockList: HTMLUListElement;
Expand Down Expand Up @@ -51,9 +51,10 @@ describe("useListKeyDown", () => {
current: {
listRef: React.RefObject<HTMLUListElement | null>;
onKeyDown: React.KeyboardEventHandler<HTMLUListElement>;
onFocus: React.FocusEventHandler<HTMLUListElement>;
};
} {
const { result } = renderHook(() => useListKeyDown());
const { result } = renderHook(() => useListKeyboardNavigation());
result.current.listRef.current = mockList;
return result;
}
Expand Down Expand Up @@ -137,4 +138,18 @@ describe("useListKeyDown", () => {

expect(mockEvent.preventDefault).not.toHaveBeenCalled();
});

it("should focus the first item if list itself is focused", () => {
const result = render();
result.current.onFocus({ target: mockList } as React.FocusEvent<HTMLUListElement>);
expect(mockItems[0].focus).toHaveBeenCalledTimes(1);
});

it("should focus the selected item if list itself is focused", () => {
mockItems[1].setAttribute("aria-selected", "true");
const result = render();

result.current.onFocus({ target: mockList } as React.FocusEvent<HTMLUListElement>);
expect(mockItems[1].focus).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,45 @@
* Please see LICENSE files in the repository root for full details.
*/

import { useCallback, useRef, type RefObject, type KeyboardEvent, type KeyboardEventHandler } from "react";
import {
useCallback,
useRef,
type RefObject,
type KeyboardEvent,
type KeyboardEventHandler,
type FocusEventHandler,
type FocusEvent,
} from "react";

/**
* A hook that provides keyboard navigation for a list of options.
*/
export function useListKeyDown(): {
export function useListKeyboardNavigation(): {
listRef: RefObject<HTMLUListElement | null>;
onKeyDown: KeyboardEventHandler<HTMLUListElement>;
onFocus: FocusEventHandler<HTMLUListElement>;
} {
const listRef = useRef<HTMLUListElement>(null);

const onFocus = useCallback((evt: FocusEvent<HTMLUListElement>) => {
if (!listRef.current) return;

if (evt.target === listRef.current) {
// By default, focus the selected item
let selectedChild = listRef.current?.firstElementChild;

// If there is a selected item, focus that instead
for (const child of listRef.current.children) {
if (child.getAttribute("aria-selected") === "true") {
selectedChild = child;
break;
}
}

(selectedChild as HTMLElement)?.focus();
}
}, []);

const onKeyDown = useCallback((evt: KeyboardEvent<HTMLUListElement>) => {
const { key } = evt;

Expand Down Expand Up @@ -60,5 +88,5 @@ export function useListKeyDown(): {
evt.preventDefault();
}
}, []);
return { listRef, onKeyDown };
return { listRef, onKeyDown, onFocus };
}
2 changes: 1 addition & 1 deletion src/shared-components/rich-list/RichItem/RichItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const RichItem = memo(function RichItem({
<li
className={styles.richItem}
role="option"
tabIndex={0}
tabIndex={-1}
aria-selected={selected}
aria-label={title}
{...props}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exports[`RichItem renders the item in default state 1`] = `
aria-label="Rich Item Title"
class="richItem"
role="option"
tabindex="0"
tabindex="-1"
>
<div
class="flex avatar"
Expand Down Expand Up @@ -52,7 +52,7 @@ exports[`RichItem renders the item in selected state 1`] = `
aria-selected="true"
class="richItem"
role="option"
tabindex="0"
tabindex="-1"
>
<div
aria-hidden="true"
Expand Down Expand Up @@ -103,7 +103,7 @@ exports[`RichItem renders the item without timestamp 1`] = `
aria-label="Rich Item Title"
class="richItem"
role="option"
tabindex="0"
tabindex="-1"
>
<div
class="flex avatar"
Expand Down
5 changes: 3 additions & 2 deletions src/shared-components/rich-list/RichList/RichList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import classNames from "classnames";

import styles from "./RichList.module.css";
import { Flex } from "../../utils/Flex";
import { useListKeyDown } from "../../hooks/useListKeyDown";
import { useListKeyboardNavigation } from "../../hooks/useListKeyboardNavigation";

export interface RichListProps extends HTMLProps<HTMLDivElement> {
/**
Expand Down Expand Up @@ -53,7 +53,7 @@ export function RichList({
...props
}: PropsWithChildren<RichListProps>): JSX.Element {
const id = useId();
const { listRef, onKeyDown } = useListKeyDown();
const { listRef, onKeyDown, onFocus } = useListKeyboardNavigation();

return (
<Flex className={classNames(styles.richList, className)} direction="column" {...props}>
Expand All @@ -70,6 +70,7 @@ export function RichList({
aria-labelledby={id}
tabIndex={0}
onKeyDown={onKeyDown}
onFocus={onFocus}
>
{children}
</ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ exports[`RichItem renders the list 1`] = `
aria-label="First Item"
class="richItem"
role="option"
tabindex="0"
tabindex="-1"
>
<div
class="flex avatar"
Expand All @@ -51,7 +51,7 @@ exports[`RichItem renders the list 1`] = `
aria-selected="true"
class="richItem"
role="option"
tabindex="0"
tabindex="-1"
>
<div
aria-hidden="true"
Expand Down Expand Up @@ -86,7 +86,7 @@ exports[`RichItem renders the list 1`] = `
aria-label="Third Item"
class="richItem"
role="option"
tabindex="0"
tabindex="-1"
>
<div
class="flex avatar"
Expand All @@ -111,7 +111,7 @@ exports[`RichItem renders the list 1`] = `
aria-label="Fourth Item"
class="richItem"
role="option"
tabindex="0"
tabindex="-1"
>
<div
class="flex avatar"
Expand All @@ -136,7 +136,7 @@ exports[`RichItem renders the list 1`] = `
aria-label="Fifth Item"
class="richItem"
role="option"
tabindex="0"
tabindex="-1"
>
<div
class="flex avatar"
Expand Down
Loading