0

这是我的useEffect电话:

    const ref = useRef(null);
    useEffect(() => {

        const clickListener = (e: MouseEvent) => {
            if (ref.current.contains(e.target as Node)) return;
            closePopout();
        }

        document.addEventListener('click', clickListener);
        
        return () => {
            document.removeEventListener('click', clickListener);
            closePopout();
        }

    }, [ref, closePopout]);

我正在使用它来控制弹出菜单。当您单击菜单图标以调出菜单时,它将打开它。当您单击不是弹出窗口的任何位置时,它会关闭弹出窗口。或者当组件被清理时,它也会关闭弹出窗口。

@testing-library/react-hooks用来渲染钩子:

https://github.com/testing-library/react-hooks-testing-library

我们也在使用 TypeScript,所以如果有任何 TS 特定的东西也会很有帮助。

希望这是足够的信息。如果不让我知道。

编辑:

我正在使用两个伴侣钩子。我在其中做了很多工作,我希望能简化问题,但这里是钩子的完整代码。渲染 PopoutMenu 组件时调用顶部挂钩 (useWithPopoutMenu)。底部的在 PopoutMenu 组件的主体内调用。

// for use when importing the component
export const useWithPopoutMenu = () => {

    const [isOpen, setIsOpenTo] = useState(false);
    const [h, setHorizontal] = useState(0);
    const [v, setVertical] = useState(0);
    const close = useCallback(() => setIsOpenTo(false), []);

    return {
        isOpen,
        menuEvent: {h, v, isOpen, close} as PopoutMenuEvent,
        open: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
            setIsOpenTo(true);
            setHorizontal(e.clientX);
            setVertical(e.clientY);
        },
        close
    };
}

type UsePopoutMenuArgs = {
    menuEvent: PopoutMenuEvent
    padding: number
    tickPosition: number
    horizontalFix: number | null
    verticalFix: number | null
    hPosition: number
    vPosition: number
    borderColor: string
}

// for use inside the component its self 
export const usePopoutMenu = ({
    menuEvent,
    padding,
    tickPosition,
    horizontalFix,
    verticalFix,
    hPosition,
    vPosition,
    borderColor
}: UsePopoutMenuArgs) => {
    const ref = useRef() as MutableRefObject<HTMLDivElement>;

    useEffect(() => {
        const handleClick = (e: MouseEvent) => {
            if (ref.current.contains(e.target as Node)) return;
            menuEvent.close();
        }
        document.addEventListener('click', handleClick);
        return () => {
            document.removeEventListener('click', handleClick);
            menuEvent.close();
        }
    }, [menuEvent.close, ref]);
    
    
    const menuContainerStyle = useMemo(() => {
        const left = horizontalFix || menuEvent.h;
        const top = verticalFix || menuEvent.v;
        return {
            padding,
            left,
            top, 
            marginLeft: hPosition,
            marginTop: vPosition,
            border: `1px solid ${borderColor}`
        }

    }, [
        padding, 
        horizontalFix, 
        verticalFix, 
        menuEvent,
        hPosition,
        vPosition,
        borderColor
    ]);

    const backgroundArrowStyle = useMemo(() => {
        return {
            marginLeft: `-${padding + 6}px`,
            marginTop: 4 - padding + tickPosition,
        }
    },[padding, tickPosition]);

    const foregroundArrowStyle = useMemo(() => {
        return {
            marginLeft: `-${padding + 5}px`,
            marginTop: 4 - padding + tickPosition,
        }
    },[padding, tickPosition]);

    return {
        ref,
        menuContainerStyle,
        backgroundArrowStyle,
        foregroundArrowStyle
    }
}

这是组件:


type PopoutMenuProps = {
    children: React.ReactChild | React.ReactChild[]  // normal props.children
    menuEvent: PopoutMenuEvent
    padding?: number                                 // padding that goes around the 
    tickPosition?: number                            // how far down the tick is from the top
    borderColor?: string                             // border color
    bgColor?: string                                 // background color 
    horizontalFix?: number | null
    verticalFix?: number | null
    vPosition?: number
    hPosition?: number
}

const Container = styled.div`
        position: fixed;
        display: block;
        padding: 0;
        border-radius: 4px;
        background-color: white;
        z-index: 10;
`;

const Arrow = styled.div`
    position: absolute;
`;

const PopoutMenu = ({
    children,
    menuEvent,
    padding = 16,
    tickPosition = 10,
    borderColor = Style.color.gray.medium,
    bgColor = Style.color.white,
    vPosition = -20,
    hPosition = 10,
    horizontalFix = null,
    verticalFix = null
}: PopoutMenuProps) => {
    
    const binding = usePopoutMenu({
        menuEvent, 
        padding, 
        tickPosition, 
        vPosition, 
        hPosition, 
        horizontalFix, 
        verticalFix,
        borderColor
    });

    return (
        <Container ref={binding.ref} style={binding.menuContainerStyle}>
            <Arrow style={binding.backgroundArrowStyle}>
                <Left color={borderColor} />
            </Arrow>
            <Arrow style={binding.foregroundArrowStyle}>
                <Left color={bgColor} />
            </Arrow>
            {children}
        </Container>
    );
}

export default PopoutMenu;

用法是这样的:

const Parent () => {
   const popoutMenu = useWithPopoutMenu();
   return (
      ...
      <ComponentThatOpensThePopout onClick={popoutMenu.open}>...
      ...
      {popoutMenu.isOpen && <PopoutMenu menuEvent={menuEvent}>PopoutMenu Content</PopoutMenu>}
   );
}
4

1 回答 1

0

你需要单独测试钩子吗?

测试使用钩子的组件会容易得多,而且它也将是一个更现实的测试,伪代码如下:

render(<PopoverConsumer />);

userEvent.click(screen.getByRole('button', { name: 'Menu' });
expect(screen.getByRole('dialog')).toBeInTheDocument();

userEvent.click(screen.getByText('somewhere outside');
expect(screen.getByRole('dialog')).not.toBeInTheDocument();

于 2020-11-14T12:58:07.427 回答