import {act, cleanup, render, screen} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import {useRef, useState} from 'react'; import {vi} from 'vitest'; import {useClick, useFloating, useInteractions, useTypeahead} from '../../src'; import type {UseTypeaheadProps} from '../../src/hooks/useTypeahead'; import {Main} from '../visual/components/Menu'; vi.useFakeTimers({shouldAdvanceTime: true}); const useImpl = ({ addUseClick = false, ...props }: Pick & { list?: Array; open?: boolean; onOpenChange?: (open: boolean) => void; addUseClick?: boolean; }) => { const [open, setOpen] = useState(true); const [activeIndex, setActiveIndex] = useState(null); const {refs, context} = useFloating({ open: props.open ?? open, onOpenChange: props.onOpenChange ?? setOpen, }); const listRef = useRef(props.list ?? ['one', 'two', 'three']); const typeahead = useTypeahead(context, { listRef, activeIndex, onMatch(index) { setActiveIndex(index); props.onMatch?.(index); }, onTypingChange: props.onTypingChange, }); const click = useClick(context, { enabled: addUseClick, }); const {getReferenceProps, getFloatingProps} = useInteractions([ typeahead, click, ]); return { activeIndex, open, getReferenceProps: (userProps?: React.HTMLProps) => getReferenceProps({ role: 'combobox', ...userProps, ref: refs.setReference, }), getFloatingProps: () => getFloatingProps({ role: 'listbox', ref: refs.setFloating, }), }; }; function Combobox( props: Pick & { list?: Array; }, ) { const {getReferenceProps, getFloatingProps} = useImpl(props); return ( <>
); } function Select( props: Pick & { list?: Array; }, ) { const [isOpen, setIsOpen] = useState(false); const {getReferenceProps, getFloatingProps} = useImpl({ ...props, open: isOpen, onOpenChange: setIsOpen, addUseClick: true, }); return ( <>
{isOpen &&
} ); } test('rapidly focuses list items when they start with the same letter', async () => { const spy = vi.fn(); render(); await userEvent.click(screen.getByRole('combobox')); await userEvent.keyboard('t'); expect(spy).toHaveBeenCalledWith(1); await userEvent.keyboard('t'); expect(spy).toHaveBeenCalledWith(2); await userEvent.keyboard('t'); expect(spy).toHaveBeenCalledWith(1); cleanup(); }); test('bails out of rapid focus of first letter if the list contains a string that starts with two of the same letter', async () => { const spy = vi.fn(); render(); await userEvent.click(screen.getByRole('combobox')); await userEvent.keyboard('a'); expect(spy).toHaveBeenCalledWith(0); await userEvent.keyboard('a'); expect(spy).toHaveBeenCalledWith(0); cleanup(); }); test('starts from the current activeIndex and correctly loops', async () => { const spy = vi.fn(); render( , ); await userEvent.click(screen.getByRole('combobox')); await userEvent.keyboard('t'); await userEvent.keyboard('o'); await userEvent.keyboard('y'); expect(spy).toHaveBeenCalledWith(0); spy.mockReset(); await userEvent.keyboard('t'); await userEvent.keyboard('o'); await userEvent.keyboard('y'); expect(spy).not.toHaveBeenCalled(); vi.advanceTimersByTime(750); await userEvent.keyboard('t'); await userEvent.keyboard('o'); await userEvent.keyboard('y'); expect(spy).toHaveBeenCalledWith(1); vi.advanceTimersByTime(750); await userEvent.keyboard('t'); await userEvent.keyboard('o'); await userEvent.keyboard('y'); expect(spy).toHaveBeenCalledWith(2); vi.advanceTimersByTime(750); await userEvent.keyboard('t'); await userEvent.keyboard('o'); await userEvent.keyboard('y'); expect(spy).toHaveBeenCalledWith(0); cleanup(); }); test('capslock characters continue to match', async () => { const spy = vi.fn(); render(); userEvent.click(screen.getByRole('combobox')); await userEvent.keyboard('{CapsLock}t'); expect(spy).toHaveBeenCalledWith(1); cleanup(); }); function App1( props: Pick & {list: Array}, ) { const {getReferenceProps, getFloatingProps, activeIndex, open} = useImpl(props); const inputRef = useRef(null); return ( <>
inputRef.current?.focus(), })} >
{open && (
{props.list.map((value, i) => (
{value}
))}
)} ); } test('matches when focus is withing reference', async () => { const spy = vi.fn(); render(); await userEvent.click(screen.getByRole('combobox')); await userEvent.keyboard('t'); expect(spy).toHaveBeenCalledWith(1); cleanup(); }); test('matches when focus is withing floating', async () => { const spy = vi.fn(); render(); await userEvent.click(screen.getByRole('combobox')); await userEvent.keyboard('t'); const option = await screen.findByRole('option', {selected: true}); expect(option.textContent).toBe('two'); option.focus(); expect(option).toHaveFocus(); await userEvent.keyboard('h'); expect( (await screen.findByRole('option', {selected: true})).textContent, ).toBe('three'); cleanup(); }); test('onTypingChange is called when typing starts or stops', async () => { const spy = vi.fn(); render(); act(() => screen.getByRole('combobox').focus()); await userEvent.keyboard('t'); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith(true); vi.advanceTimersByTime(750); expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledWith(false); cleanup(); }); test('Menu - skips disabled items and opens submenu on space if no match', async () => { vi.useRealTimers(); render(
); await userEvent.click(screen.getByText('Edit')); await act(async () => {}); expect(screen.getByRole('menu')).toBeInTheDocument(); await userEvent.keyboard('c'); expect(screen.getByText('Copy as')).toHaveFocus(); await userEvent.keyboard('opy as '); expect(screen.getByText('Copy as').getAttribute('aria-expanded')).toBe( 'false', ); await userEvent.keyboard(' '); expect(screen.getByText('Copy as').getAttribute('aria-expanded')).toBe( 'true', ); }); test('Menu - resets once a match is no longer found', async () => { vi.useRealTimers(); render(
); await userEvent.click(screen.getByText('Edit')); await act(async () => {}); expect(screen.getByRole('menu')).toBeInTheDocument(); await userEvent.keyboard('undr'); expect(screen.getByText('Undo')).toHaveFocus(); await userEvent.keyboard('r'); expect(screen.getByText('Redo')).toHaveFocus(); }); test('typing spaces on
references does not open the menu', async () => { const spy = vi.fn(); render(