@atlaskit/popup
Version:
A popup displays brief content in an overlay.
185 lines (183 loc) • 5.76 kB
JavaScript
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);
};