@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
133 lines (132 loc) • 6.26 kB
JavaScript
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;
}