import {render} from '@testing-library/react'; import {useEffect, useRef, useState} from 'react'; import {vi} from 'vitest'; import { useClick, useDismiss, useFloating, useFocus, useHover, useInnerOffset, useInteractions, useListNavigation, useRole, useTypeahead, } from '../../src'; test('correctly merges functions', () => { const firstInteractionOnClick = vi.fn(); const secondInteractionOnClick = vi.fn(); const secondInteractionOnKeyDown = vi.fn(); const userOnClick = vi.fn(); function App() { const {getReferenceProps} = useInteractions([ {reference: {onClick: firstInteractionOnClick}}, { reference: { onClick: secondInteractionOnClick, onKeyDown: secondInteractionOnKeyDown, }, }, ]); const {onClick, onKeyDown} = getReferenceProps({onClick: userOnClick}); // @ts-expect-error onClick(); // @ts-expect-error onKeyDown(); return null; } render(); expect(firstInteractionOnClick).toHaveBeenCalledTimes(1); expect(secondInteractionOnClick).toHaveBeenCalledTimes(1); expect(userOnClick).toHaveBeenCalledTimes(1); expect(secondInteractionOnKeyDown).toHaveBeenCalledTimes(1); }); test('does not error with undefined user supplied functions', () => { function App() { const {getReferenceProps} = useInteractions([{reference: {onClick() {}}}]); expect(() => // @ts-expect-error getReferenceProps({onClick: undefined}).onClick(), ).not.toThrowError(); return null; } render(); }); test('does not break props that start with `on`', () => { function App() { const {getReferenceProps} = useInteractions([]); const props = getReferenceProps({ // @ts-expect-error onlyShowVotes: true, onyx: () => {}, }); expect(props.onlyShowVotes).toBe(true); expect(typeof props.onyx).toBe('function'); return null; } render(); }); test('does not break props that return values', () => { function App() { const {getReferenceProps} = useInteractions([]); const props = getReferenceProps({ // @ts-expect-error onyx: () => 'returned value', }); // @ts-expect-error expect(props.onyx()).toBe('returned value'); return null; } render(); }); test('prop getters are memoized', () => { function App() { const [open, setOpen] = useState(false); const [c, setCount] = useState(0); c; const handleClose = () => () => {}; handleClose.__options = {blockPointerEvents: true}; const listRef = useRef([]); const overflowRef = useRef({top: 0, left: 0, bottom: 0, right: 0}); const {context} = useFloating({open, onOpenChange: setOpen}); // NOTE: if `ref`-related props are not memoized, this will cause // an infinite loop as they must be memoized externally (as done by React). // Other non-primitives like functions and arrays get memoized by the hooks. const {getReferenceProps, getFloatingProps, getItemProps} = useInteractions( [ useHover(context, {handleClose}), useFocus(context), useClick(context), useRole(context), useDismiss(context), useListNavigation(context, { listRef, activeIndex: 0, onNavigate: () => {}, disabledIndices: [], }), useTypeahead(context, { listRef, activeIndex: 0, ignoreKeys: [], onMatch: () => {}, findMatch: () => '', }), useInnerOffset(context, { onChange: () => {}, overflowRef, }), ], ); // biome-ignore lint/correctness/useExhaustiveDependencies: testing useEffect(() => { // Should NOT cause an infinite loop as the prop getters are memoized. setCount((c) => c + 1); }, [getReferenceProps, getFloatingProps, getItemProps]); return null; } render(); });