/** * @file Popup Container Provider * @desc 指定子树中所有会渲染弹出框的组件(`Select`、`Dropdown` 等)的 popup container 逻辑 */ import React, { createContext, ReactNode, RefObject, useCallback, useContext } from 'react' /** 获取 popup container 的函数(可能返回 `null`) */ export type GetPopupContainer = (node: HTMLElement) => HTMLElement | null /** 获取 popup container 的函数(一定会返回一个 HTML 元素) */ export type GetRequiredPopupContainer = (node: HTMLElement) => HTMLElement const getDefaultPopupContainer = () => window.document.body const getPopupContainerCtx = createContext(null) /** 从上下文得到“获取 popup container 的函数” */ export function useGetPopupContainer(fnFromProps?: GetPopupContainer): GetRequiredPopupContainer { const getPopupContainer = useContext(getPopupContainerCtx) return useCallback((node: HTMLElement) => ( fnFromProps?.(node) ?? getPopupContainer?.(node) ?? getDefaultPopupContainer() ), [fnFromProps, getPopupContainer]) } export interface Props { /** popup container 对应的 React Ref 对象 */ containerRef?: RefObject /** 获取 popup container 的函数,为避免子组件不必要的 rerender,建议 memo 之 */ getContainer?: GetPopupContainer /** 内容 */ children?: ReactNode } /** 向子树提供 popup container */ export default function PopupContainerProvider({ containerRef, getContainer: getContainerFromProps, children }: Props) { const getContainerFromRef = useCallback(() => containerRef?.current ?? null, [containerRef]) const getContainer = getContainerFromProps ?? getContainerFromRef return ( {children} ) }