UNPKG

@atlaskit/popup

Version:

A popup displays brief content in an overlay.

185 lines (183 loc) 5.76 kB
import React, { createContext, useCallback, useContext, useState } from 'react'; import invariant from 'tiny-invariant'; import noop from '@atlaskit/ds-lib/noop'; import { useId } from '@atlaskit/ds-lib/use-id'; import { Layering } from '@atlaskit/layering'; import { useNotifyOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer'; import { Manager, Reference } from '@atlaskit/popper'; import Portal from '@atlaskit/portal'; import PopperWrapper from '../popper-wrapper'; import { usePopupAppearance } from '../use-appearance'; import { useGetMemoizedMergedTriggerRef } from '../use-get-memoized-merged-trigger-ref'; const IsOpenContext = /*#__PURE__*/createContext(false); const IdContext = /*#__PURE__*/createContext(undefined); const TriggerRefContext = /*#__PURE__*/createContext(null); const SetTriggerRefContext = /*#__PURE__*/createContext(noop); const EnsureIsInsidePopupContext = /*#__PURE__*/createContext(false); // Used to ensure popup sub-components are used within a Popup // and provide a useful error message if not. const useEnsureIsInsidePopup = () => { const context = useContext(EnsureIsInsidePopupContext); invariant(context, 'PopupTrigger and PopupContent components must be used within a Popup'); }; /** * __Popup__ * * Popup is a composable component that provides the context for the trigger and content components. * * Usage example: * ```jsx * <Popup> * <PopupTrigger> * {(props) => ( * <button type="button" {...props}>Click me</button> * )} * </PopupTrigger> * <PopupContent> * {(props) => <div>Hello world</div>} * </PopupContent> * </Popup> * ``` */ export const Popup = ({ children, id: providedId, isOpen = false }) => { const [triggerRef, setTriggerRef] = useState(null); const generatedId = useId(); const id = providedId || generatedId; return /*#__PURE__*/React.createElement(EnsureIsInsidePopupContext.Provider, { value: true }, /*#__PURE__*/React.createElement(IdContext.Provider, { value: id }, /*#__PURE__*/React.createElement(TriggerRefContext.Provider, { value: triggerRef }, /*#__PURE__*/React.createElement(SetTriggerRefContext.Provider, { value: setTriggerRef }, /*#__PURE__*/React.createElement(IsOpenContext.Provider, { value: isOpen }, /*#__PURE__*/React.createElement(Manager, null, children)))))); }; /** * __Popup trigger__ * * Popup trigger is the component that renders the trigger for the popup. * * It must be a child of the Popup component. */ export const PopupTrigger = ({ children }) => { useEnsureIsInsidePopup(); const isOpen = useContext(IsOpenContext); const id = useContext(IdContext); const setTriggerRef = useContext(SetTriggerRefContext); const getMergedTriggerRef = useGetMemoizedMergedTriggerRef(); return /*#__PURE__*/React.createElement(Reference, null, ({ ref }) => children({ ref: getMergedTriggerRef(ref, setTriggerRef, isOpen), 'aria-controls': id, 'aria-expanded': isOpen, 'aria-haspopup': true })); }; const defaultLayer = 400; /** * Disables popper.js GPU acceleration for this popup. * This means only positioning will be used, without any transforms. * * Performance will be degraded if the popup is expected to move. */ const shouldDisableGpuAccelerationModifiers = [{ name: 'computeStyles', options: { gpuAcceleration: false } }]; /** * __Popup content__ * * Popup content is the component that renders the content of the popup. * * It must be a child of the Popup component. */ export const PopupContent = ({ xcss, appearance: inAppearance = 'default', children, boundary, offset, strategy, onClose, testId, rootBoundary = 'viewport', shouldFlip = true, placement = 'auto', fallbackPlacements, popupComponent, autoFocus = true, zIndex = defaultLayer, shouldUseCaptureOnOutsideClick = false, shouldRenderToParent: inShouldRenderToParent, shouldDisableFocusLock = false, shouldFitContainer, shouldFitViewport, shouldDisableGpuAcceleration = false }) => { useEnsureIsInsidePopup(); const isOpen = useContext(IsOpenContext); const id = useContext(IdContext); const triggerRef = useContext(TriggerRefContext); const { appearance, shouldRenderToParent } = usePopupAppearance({ appearance: inAppearance, shouldRenderToParent: inShouldRenderToParent }); const handleOpenLayerObserverCloseSignal = useCallback(() => { onClose === null || onClose === void 0 ? void 0 : onClose(null); }, [onClose]); useNotifyOpenLayerObserver({ isOpen, onClose: handleOpenLayerObserverCloseSignal }); if (!isOpen) { return null; } const popperWrapper = /*#__PURE__*/React.createElement(Layering, { isDisabled: false }, /*#__PURE__*/React.createElement(PopperWrapper, { xcss: xcss, appearance: appearance, content: children, isOpen: isOpen, placement: placement, fallbackPlacements: fallbackPlacements, boundary: boundary, rootBoundary: rootBoundary, shouldFlip: shouldFlip, offset: offset, popupComponent: popupComponent, id: id, testId: testId, onClose: onClose, autoFocus: autoFocus, shouldFitContainer: shouldFitContainer, shouldUseCaptureOnOutsideClick: shouldUseCaptureOnOutsideClick, shouldRenderToParent: shouldRenderToParent, shouldDisableFocusLock: shouldDisableFocusLock, triggerRef: triggerRef, strategy: strategy, shouldFitViewport: shouldFitViewport, modifiers: shouldDisableGpuAcceleration ? shouldDisableGpuAccelerationModifiers : undefined })); if (shouldRenderToParent) { return popperWrapper; } return /*#__PURE__*/React.createElement(Portal, { zIndex: zIndex }, popperWrapper); };