import {act, cleanup, fireEvent, render, screen, within} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import {cloneElement, useState} from 'react'; import {vi} from 'vitest'; import { FloatingFocusManager, useClick, useDismiss, useFloating, useFocus, useHover, useInteractions, } from '../../src'; import type {UseFocusProps} from '../../src/hooks/useFocus'; vi.useFakeTimers(); beforeAll(() => { customElements.define('render-root', class RenderRoot extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }) .appendChild(document.createElement('div')); } }); }); function App(props: UseFocusProps & {dismiss?: boolean; hover?: boolean}) { const [open, setOpen] = useState(false); const {refs, context} = useFloating({ open, onOpenChange: setOpen, }); const {getReferenceProps, getFloatingProps} = useInteractions([ useFocus(context, props), useDismiss(context, {enabled: !!props.dismiss, referencePress: true}), useHover(context, {enabled: !!props.hover}), ]); return ( <> {open && (
)} ); } test('opens on focus', () => { render(); const button = screen.getByRole('button'); fireEvent.focus(button); expect(screen.queryByRole('tooltip')).toBeInTheDocument(); cleanup(); }); test('closes on blur', () => { render(); const button = screen.getByRole('button'); act(() => button.focus()); act(() => button.blur()); act(() => { vi.runAllTimers(); }); expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); cleanup(); }); test('stays open when focus moves to tooltip rendered inside a shadow root', async () => { const container = document.body.appendChild(document.createElement('render-root')); const renderRoot = container.shadowRoot?.firstElementChild as HTMLElement; render(, { container: renderRoot }); const root = within(renderRoot) // Open the tooltip by focusing the reference const button = root.getByRole('button'); await fireEvent.focusIn(button); // Move focus to the tooltip const tooltip = root.getByRole('tooltip'); tooltip.focus(); // trigger the blur event caused by the focus move, note relatedTarget points to the shadow root here fireEvent.focusOut(button, { relatedTarget: container }) act(() => { vi.runAllTimers(); }); expect(root.getByRole('tooltip')).toBeInTheDocument(); cleanup(); }); test('stays open when focus moves to element inside reference that is rendered inside a shadow root', async () => { const container = document.body.appendChild(document.createElement('render-root')); const renderRoot = container.shadowRoot?.firstElementChild as HTMLElement; render(, { container: renderRoot }); const root = within(renderRoot) // Open the tooltip by focusing the reference const button = root.getByRole('button'); await fireEvent.focusIn(button); // Move focus to an element inside the reference const insideReference = root.getByTestId('inside-reference'); insideReference.focus(); // trigger the blur event caused by the focus move, note relatedTarget points to the shadow root here fireEvent.focusOut(button, { relatedTarget: container }) act(() => { vi.runAllTimers(); }); expect(root.getByRole('tooltip')).toBeInTheDocument(); cleanup(); }); test('does not open with a reference pointerDown dismissal', async () => { render(); const button = screen.getByRole('button'); fireEvent.pointerDown(button); fireEvent.focus(button); expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); cleanup(); }); test('does not open when window blurs then receives focus', async () => { // TODO — not sure how to test this in JSDOM }); test('blurs when hitting an "inside" focus guard', async () => { vi.useRealTimers(); function Tooltip({children}: {children: JSX.Element}) { const [open, setOpen] = useState(false); const {refs, context} = useFloating({ open, onOpenChange: setOpen, }); const {getReferenceProps, getFloatingProps} = useInteractions([ useFocus(context), ]); return ( <> {cloneElement(children, getReferenceProps({ref: refs.setReference}))} {open && (
Label
)} ); } function App() { const [open, setOpen] = useState(false); const {refs, context} = useFloating({ open, onOpenChange: setOpen, }); const {getReferenceProps, getFloatingProps} = useInteractions([ useClick(context), ]); return ( <>
)} ); } render(); await userEvent.click(screen.getByRole('button')); await userEvent.tab(); expect(screen.queryByRole('tooltip')).toBeInTheDocument(); await userEvent.tab(); // Wait for the timeout in `onBlur()`. await act(() => new Promise((resolve) => setTimeout(resolve))); expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); cleanup(); }); test('reason string', async () => { function App() { const [isOpen, setIsOpen] = useState(false); const {refs, context} = useFloating({ open: isOpen, onOpenChange(isOpen, _, reason) { setIsOpen(isOpen); expect(reason).toBe('focus'); }, }); const focus = useFocus(context); const {getReferenceProps, getFloatingProps} = useInteractions([focus]); return ( <>