UNPKG

@workday/canvas-kit-react

Version:

The parent module that contains all Workday Canvas Kit React components

133 lines (132 loc) 6.26 kB
import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { createPopper, } from '@popperjs/core'; export const defaultFallbackPlacements = ['top', 'right', 'bottom', 'left']; import { usePopupStack } from './hooks'; import { useLocalRef } from '@workday/canvas-kit-react/common'; import { fallbackPlacementsModifier } from './fallbackPlacements'; /** * A thin wrapper component around the Popper.js positioning engine. For reference: * https://popper.js.org/. `Popper` also automatically works with the {@link PopupStack} system. * `Popper` has no UI and will render any children to the `body` element and position around a * provided `anchorElement`. * * Prefer using {@link PopupPopper Popup.Popper} instead. Use this to make Popups that don't utilize * a PopupModel or any associate popup [hooks](#hooks). * * > **Note:** `Popper` renders any children to a `div` element created by the `PopupStack`. This * > element is not controlled by React, so any extra element props will _not_ be forwarded. The * > `ref` will point to the `div` element created by the `PopupStack`, however. In v4, an extra * > `div` element was rendered and that's where extra props were spread to. In v5+, you can provide * > your own element if you wish. */ export const Popper = React.forwardRef(({ portal = true, open = true, ...elemProps }, ref) => { if (!open) { return null; } return React.createElement(OpenPopper, { ref: ref, portal: portal, ...elemProps }); }); const getElementFromRefOrElement = (input) => { if (input === null) { return undefined; } else if ('current' in input) { return input.current || undefined; } else { return input; } }; // prevent unnecessary renders if popperOptions are not passed const defaultPopperOptions = {}; // Popper bails early if `open` is false and React hooks cannot be called conditionally, // so we're breaking out the open version into another component. const OpenPopper = React.forwardRef(({ anchorElement, getAnchorClientRect, popperOptions = defaultPopperOptions, placement: popperPlacement = 'bottom', fallbackPlacements = defaultFallbackPlacements, onPlacementChange, children, portal, popperInstanceRef, }, ref) => { const firstRender = React.useRef(true); const { localRef, elementRef } = useLocalRef(popperInstanceRef); const [placement, setPlacement] = React.useState(popperPlacement); const stackRef = usePopupStack(ref, anchorElement); const placementRef = React.useRef(popperPlacement); placementRef.current = placement; const placementModifier = React.useMemo(() => { return { name: 'setPlacement', enabled: true, phase: 'afterWrite', fn({ state }) { setPlacement(state.placement); onPlacementChange === null || onPlacementChange === void 0 ? void 0 : onPlacementChange(state.placement); }, }; }, [setPlacement, onPlacementChange]); // useLayoutEffect prevents flashing of the popup before position is determined React.useLayoutEffect(() => { const anchorEl = getAnchorClientRect ? { getBoundingClientRect: getAnchorClientRect } : getElementFromRefOrElement(anchorElement !== null && anchorElement !== void 0 ? anchorElement : null); if (!anchorEl) { console.warn(`Popper: neither anchorElement or getAnchorClientRect was defined. A valid anchorElement or getAnchorClientRect callback must be provided to render a Popper`); return undefined; } if (stackRef.current) { const instance = createPopper(anchorEl, stackRef.current, { placement: popperPlacement, ...popperOptions, modifiers: [ placementModifier, { ...fallbackPlacementsModifier, options: { fallbackPlacements, }, }, ...(popperOptions.modifiers || []), ], }); elementRef(instance); // update the ref with the instance return () => { instance === null || instance === void 0 ? void 0 : instance.destroy(); }; } return undefined; // We will maintain our own list of dependencies. We need to separate "create" and "update" // prop dependencies. We do _not_ want to destroy the Popper instance if options or placement // change, only if anchor or target refs change // eslint-disable-next-line react-hooks/exhaustive-deps }, [anchorElement, getAnchorClientRect, stackRef]); React.useLayoutEffect(() => { var _a; // Only update options if this is _not_ the first render if (!firstRender.current) { (_a = localRef.current) === null || _a === void 0 ? void 0 : _a.setOptions({ placement: popperPlacement, ...popperOptions, modifiers: [ placementModifier, { ...fallbackPlacementsModifier, options: { fallbackPlacements, }, }, ...(popperOptions.modifiers || []), ], }); } firstRender.current = false; }, [popperOptions, popperPlacement, fallbackPlacements, placementModifier, localRef]); const contents = React.createElement(React.Fragment, null, isRenderProp(children) ? children({ placement }) : children); if (!portal) { return contents; } return ReactDOM.createPortal(contents, stackRef.current); }); // Typescript threw an error about non-callable signatures. Using typeof as a 'function' returns // a type of `Function` which isn't descriptive enough for Typescript. We don't do any detection // against the _type_ of function that gets passed, but we'll assume it is a render prop for now... function isRenderProp(children) { if (typeof children === 'function') { return true; } return false; }