import {act, cleanup, fireEvent, render, screen} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import {cloneElement, useRef, useState} from 'react'; import {Context as ResponsiveContext} from 'react-responsive'; import { FloatingFocusManager, FloatingNode, FloatingPortal, FloatingTree, useClick, useDismiss, useFloating, useFloatingNodeId, useFloatingParentNodeId, useInteractions, useRole, } from '../../src'; import type {FloatingFocusManagerProps} from '../../src/components/FloatingFocusManager'; import {Main as Drawer} from '../visual/components/Drawer'; import {Main as Navigation} from '../visual/components/Navigation'; function App( props: Partial< Omit & { initialFocus?: 'two' | number; } >, ) { const ref = useRef(null); const [open, setOpen] = useState(false); const {refs, context} = useFloating({ open, onOpenChange: setOpen, }); return ( <> {props.children} )}
x
); } describe('initialFocus', () => { test('number', async () => { const {rerender} = render(); fireEvent.click(screen.getByTestId('reference')); await act(async () => {}); expect(screen.getByTestId('one')).toHaveFocus(); rerender(); expect(screen.getByTestId('two')).not.toHaveFocus(); rerender(); expect(screen.getByTestId('three')).not.toHaveFocus(); }); test('ref', async () => { render(); fireEvent.click(screen.getByTestId('reference')); await act(async () => {}); expect(screen.getByTestId('two')).toHaveFocus(); }); test('respects autoFocus', async () => { render( {/* biome-ignore lint/a11y/noAutofocus: */} , ); fireEvent.click(screen.getByTestId('reference')); await act(async () => {}); expect(screen.getByTestId('input')).toHaveFocus(); }); }); describe('returnFocus', () => { test('true', async () => { const {rerender} = render(); screen.getByTestId('reference').focus(); fireEvent.click(screen.getByTestId('reference')); await act(async () => {}); expect(screen.getByTestId('one')).toHaveFocus(); act(() => screen.getByTestId('two').focus()); rerender(); expect(screen.getByTestId('two')).toHaveFocus(); fireEvent.click(screen.getByTestId('three')); expect(screen.getByTestId('reference')).not.toHaveFocus(); }); test('false', async () => { render(); screen.getByTestId('reference').focus(); fireEvent.click(screen.getByTestId('reference')); await act(async () => {}); expect(screen.getByTestId('one')).toHaveFocus(); fireEvent.click(screen.getByTestId('three')); expect(screen.getByTestId('reference')).not.toHaveFocus(); }); test('always returns to the reference for nested elements', async () => { interface Props { open?: boolean; render: (props: {close: () => void}) => React.ReactNode; children: JSX.Element; } const Dialog = ({render, open: passedOpen = false, children}: Props) => { const [open, setOpen] = useState(passedOpen); const nodeId = useFloatingNodeId(); const {refs, context} = useFloating({ open, onOpenChange: setOpen, nodeId, }); const {getReferenceProps, getFloatingProps} = useInteractions([ useClick(context), useDismiss(context, {bubbles: false}), ]); return ( {cloneElement( children, getReferenceProps({ref: refs.setReference, ...children.props}), )} {open && (
{render({ close: () => setOpen(false), })}
)}
); }; const NestedDialog: React.FC = (props) => { const parentId = useFloatingParentNodeId(); if (parentId == null) { return ( ); } return ; }; render( ( <> ( )} ); } render(); await userEvent.click(screen.getByTestId('input')); await act(async () => {}); expect(screen.getByTestId('input')).not.toHaveFocus(); expect(screen.getByRole('button', {name: 'one'})).toHaveFocus(); await userEvent.tab(); expect(screen.getByRole('button', {name: 'two'})).toHaveFocus(); await userEvent.tab(); expect(screen.getByRole('button', {name: 'one'})).toHaveFocus(); cleanup(); }); test('untrapped combobox creates non-modal focus management', async () => { function App() { const [isOpen, setIsOpen] = useState(false); const {refs, floatingStyles, context} = useFloating({ open: isOpen, onOpenChange: setIsOpen, }); const role = useRole(context); const dismiss = useDismiss(context); const click = useClick(context); const {getReferenceProps, getFloatingProps} = useInteractions([ role, dismiss, click, ]); return ( <> {isOpen && (
)} ); } render(); await userEvent.click(screen.getByTestId('input')); await act(async () => {}); expect(screen.getByTestId('input')).toHaveFocus(); await userEvent.tab(); expect(screen.getByRole('button', {name: 'one'})).toHaveFocus(); await userEvent.tab({shift: true}); expect(screen.getByTestId('input')).toHaveFocus(); cleanup(); }); test('returns focus to last connected element', async () => { function Drawer({ open, onOpenChange, }: { open: boolean; onOpenChange: (open: boolean) => void; }) { const {refs, context} = useFloating({open, onOpenChange}); const dismiss = useDismiss(context); const {getFloatingProps} = useInteractions([dismiss]); return (
); } function Parent() { const [isOpen, setIsOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const {refs, context} = useFloating({ open: isOpen, onOpenChange: setIsOpen, }); const dismiss = useDismiss(context); const click = useClick(context); const {getReferenceProps, getFloatingProps} = useInteractions([ click, dismiss, ]); return ( <>