UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

333 lines (329 loc) • 12.6 kB
import { c } from 'react-compiler-runtime'; import React, { useRef, useEffect } from 'react'; import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect.js'; import { Portal } from '../Portal/Portal.js'; import classes from './Overlay.module.css.js'; import { clsx } from 'clsx'; import { jsx } from 'react/jsx-runtime'; import { useMergedRefs } from '../hooks/useMergedRefs.js'; import { useFeatureFlag } from '../FeatureFlags/useFeatureFlag.js'; import { useOverlay } from '../hooks/useOverlay.js'; const heightMap = { xsmall: '192px', small: '256px', medium: '320px', large: '432px', xlarge: '600px', auto: 'auto', initial: 'auto', // Passing 'initial' initially applies 'auto' 'fit-content': 'fit-content' }; const widthMap = { small: '256px', medium: '320px', large: '480px', xlarge: '640px', xxlarge: '960px', auto: 'auto' }; const animationDuration = 200; function getSlideAnimationStartingVector(anchorSide) { if (anchorSide !== null && anchorSide !== void 0 && anchorSide.endsWith('bottom')) { return { x: 0, y: -1 }; } else if (anchorSide !== null && anchorSide !== void 0 && anchorSide.endsWith('top')) { return { x: 0, y: 1 }; } else if (anchorSide !== null && anchorSide !== void 0 && anchorSide.endsWith('right')) { return { x: -1, y: 0 }; } else if (anchorSide !== null && anchorSide !== void 0 && anchorSide.endsWith('left')) { return { x: 1, y: 0 }; } return { x: 0, y: 0 }; } /** * An `Overlay` is a flexible floating surface, used to display transient content such as menus, * selection options, dialogs, and more. Overlays use shadows to express elevation. The `Overlay` * component handles all behaviors needed by overlay UIs as well as the common styles that all overlays * should have. * @param height Sets the height of the `Overlay`, pick from our set list of heights, or pass `auto` to automatically set the height based on the content of the `Overlay`, or pass `initial` to set the height based on the initial content of the `Overlay` (i.e. ignoring content changes). `xsmall` corresponds to `192px`, `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `432px`, `xlarge` corresponds to `600px`. * @param width Sets the width of the `Overlay`, pick from our set list of widths, or pass `auto` to automatically set the width based on the content of the `Overlay`. `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `480px`, `xlarge` corresponds to `640px`, `xxlarge` corresponds to `960px`. * @param maxHeight Sets the maximum height of the `Overlay`, pick from our set list of heights. `xsmall` corresponds to `192px`, `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `432px`, `xlarge` corresponds to `600px`. * @param top Optional. Vertical top position of the overlay, relative to its closest positioned ancestor (often its `Portal`). * @param left Optional. Horizontal left position of the overlay, relative to its closest positioned ancestor (often its `Portal`). * @param right Optional. Horizontal right position of the overlay, relative to its closest positioned ancestor (often its `Portal`). * @param bottom Optional. Vertical bottom position of the overlay, relative to its closest positioned ancestor (often its `Portal`). * @param position Optional. Sets how an element is positioned in a document. Defaults to `absolute` positioning. */ const BaseOverlay = /*#__PURE__*/React.forwardRef(({ visibility, height, width, top, left, right, bottom, position, style: styleFromProps, className, maxHeight, maxWidth, as: Component = 'div', ...rest }, forwardedRef // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => { return /*#__PURE__*/jsx(Component, { ...rest, ref: forwardedRef, style: { '--top': typeof top === 'number' ? `${top}px` : top, '--left': typeof left === 'number' ? `${left}px` : left, '--right': typeof right === 'number' ? `${right}px` : right, '--bottom': typeof bottom === 'number' ? `${bottom}px` : bottom, position, ...styleFromProps }, [`data-width-${width}`]: '', [`data-max-width-${maxWidth}`]: maxWidth ? '' : undefined, [`data-height-${height}`]: '', [`data-max-height-${maxHeight}`]: maxHeight ? '' : undefined, [`data-visibility-${visibility}`]: '', [`data-overflow-${rest.overflow}`]: rest.overflow ? '' : undefined, className: clsx(className, classes.Overlay) }); }); /** * @param anchorSide If provided, the Overlay will slide into position from the side of the anchor with a brief animation * @param height Sets the height of the `Overlay`, pick from our set list of heights, or pass `auto` to automatically set the height based on the content of the `Overlay`, or pass `initial` to set the height based on the initial content of the `Overlay` (i.e. ignoring content changes). `xsmall` corresponds to `192px`, `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `432px`, `xlarge` corresponds to `600px`. * @param ignoreClickRefs Optional. An array of ref objects to ignore clicks on in the `onOutsideClick` behavior. This is often used to ignore clicking on the element that toggles the open/closed state for the `Overlay` to prevent the `Overlay` from being toggled twice. * @param initialFocusRef Optional. Ref for the element to focus when the `Overlay` is opened. If nothing is provided, the first focusable element in the `Overlay` body is focused. * @param left Optional. Horizontal left position of the overlay, relative to its closest positioned ancestor (often its `Portal`). * @param onClickOutside Required. Function to call when clicking outside of the `Overlay`. Typically this function removes the Overlay. * @param onEscape Required. Function to call when user presses `Escape`. Typically this function removes the Overlay. * @param portalContainerName Optional. The name of the portal container to render the Overlay into. * @param preventOverflow Optional. The Overlay width will be adjusted responsively if there is not enough space to display the Overlay. If `preventOverflow` is `true`, the width of the `Overlay` will not be adjusted. * @param preventFocusOnOpen Optional. If 'true', focus will not be applied when the component is first mounted, even if initialFocusRef prop is given. * @param returnFocusRef Required. Ref for the element to focus when the `Overlay` is closed. * @param right Optional. Horizontal right position of the overlay, relative to its closest positioned ancestor (often its `Portal`). * @param width Sets the width of the `Overlay`, pick from our set list of widths, or pass `auto` to automatically set the width based on the content of the `Overlay`. `small` corresponds to `256px`, `medium` corresponds to `320px`, `large` corresponds to `480px`, `xlarge` corresponds to `640px`, `xxlarge` corresponds to `960px`. */ const Overlay = /*#__PURE__*/React.forwardRef((t0, forwardedRef) => { const $ = c(47); let _PrivateDisablePortal; let anchorSide; let ignoreClickRefs; let initialFocusRef; let left; let onClickOutside; let onEscape; let portalContainerName; let preventFocusOnOpen; let props; let responsiveVariant; let returnFocusRef; let right; let t1; let t2; let t3; let t4; let t5; if ($[0] !== t0) { ({ anchorSide, _PrivateDisablePortal, height: t1, ignoreClickRefs, initialFocusRef, left, onClickOutside, onEscape, portalContainerName, preventOverflow: t2, preventFocusOnOpen, returnFocusRef, right, role: t3, visibility: t4, width: t5, responsiveVariant, ...props } = t0); $[0] = t0; $[1] = _PrivateDisablePortal; $[2] = anchorSide; $[3] = ignoreClickRefs; $[4] = initialFocusRef; $[5] = left; $[6] = onClickOutside; $[7] = onEscape; $[8] = portalContainerName; $[9] = preventFocusOnOpen; $[10] = props; $[11] = responsiveVariant; $[12] = returnFocusRef; $[13] = right; $[14] = t1; $[15] = t2; $[16] = t3; $[17] = t4; $[18] = t5; } else { _PrivateDisablePortal = $[1]; anchorSide = $[2]; ignoreClickRefs = $[3]; initialFocusRef = $[4]; left = $[5]; onClickOutside = $[6]; onEscape = $[7]; portalContainerName = $[8]; preventFocusOnOpen = $[9]; props = $[10]; responsiveVariant = $[11]; returnFocusRef = $[12]; right = $[13]; t1 = $[14]; t2 = $[15]; t3 = $[16]; t4 = $[17]; t5 = $[18]; } const height = t1 === undefined ? "auto" : t1; const preventOverflow = t2 === undefined ? true : t2; const role = t3 === undefined ? "none" : t3; const visibility = t4 === undefined ? "visible" : t4; const width = t5 === undefined ? "auto" : t5; const overlayRef = useRef(null); const mergedOverlayRef = useMergedRefs(forwardedRef, overlayRef); const cssAnchorPositioning = useFeatureFlag("primer_react_css_anchor_positioning"); let t6; if ($[19] !== ignoreClickRefs || $[20] !== initialFocusRef || $[21] !== onClickOutside || $[22] !== onEscape || $[23] !== preventFocusOnOpen || $[24] !== returnFocusRef) { t6 = { overlayRef, returnFocusRef, onEscape, ignoreClickRefs, onClickOutside, initialFocusRef, preventFocusOnOpen }; $[19] = ignoreClickRefs; $[20] = initialFocusRef; $[21] = onClickOutside; $[22] = onEscape; $[23] = preventFocusOnOpen; $[24] = returnFocusRef; $[25] = t6; } else { t6 = $[25]; } useOverlay(t6); let t7; let t8; if ($[26] !== height) { t7 = () => { var _overlayRef$current; if (height === "initial" && (_overlayRef$current = overlayRef.current) !== null && _overlayRef$current !== void 0 && _overlayRef$current.clientHeight) { overlayRef.current.style.height = `${overlayRef.current.clientHeight}px`; } }; t8 = [height]; $[26] = height; $[27] = t7; $[28] = t8; } else { t7 = $[27]; t8 = $[28]; } useEffect(t7, t8); let t10; let t9; if ($[29] !== anchorSide || $[30] !== visibility) { t9 = () => { var _overlayRef$current2; const { x, y } = getSlideAnimationStartingVector(anchorSide); if (!x && !y || !((_overlayRef$current2 = overlayRef.current) !== null && _overlayRef$current2 !== void 0 && _overlayRef$current2.animate) || visibility === "hidden") { return; } overlayRef.current.animate({ transform: [`translate(${8 * x}px, ${8 * y}px)`, "translate(0, 0)"] }, { duration: animationDuration, easing: "cubic-bezier(0.33, 1, 0.68, 1)" }); }; t10 = [anchorSide, 8, "cubic-bezier(0.33, 1, 0.68, 1)", visibility]; $[29] = anchorSide; $[30] = visibility; $[31] = t10; $[32] = t9; } else { t10 = $[31]; t9 = $[32]; } useIsomorphicLayoutEffect(t9, t10); const leftPosition = left === undefined && right === undefined ? 0 : left; const t11 = !preventOverflow ? true : undefined; let t12; if ($[33] !== height || $[34] !== leftPosition || $[35] !== mergedOverlayRef || $[36] !== props || $[37] !== responsiveVariant || $[38] !== right || $[39] !== role || $[40] !== t11 || $[41] !== visibility || $[42] !== width) { t12 = /*#__PURE__*/jsx(BaseOverlay, { role: role, width: width, "data-reflow-container": t11, ref: mergedOverlayRef, left: leftPosition, right: right, height: height, visibility: visibility, "data-responsive": responsiveVariant, ...props }); $[33] = height; $[34] = leftPosition; $[35] = mergedOverlayRef; $[36] = props; $[37] = responsiveVariant; $[38] = right; $[39] = role; $[40] = t11; $[41] = visibility; $[42] = width; $[43] = t12; } else { t12 = $[43]; } const overlayContent = t12; if (_PrivateDisablePortal && cssAnchorPositioning) { return overlayContent; } let t13; if ($[44] !== overlayContent || $[45] !== portalContainerName) { t13 = /*#__PURE__*/jsx(Portal, { containerName: portalContainerName, children: overlayContent }); $[44] = overlayContent; $[45] = portalContainerName; $[46] = t13; } else { t13 = $[46]; } return t13; }); export { BaseOverlay, Overlay as default, heightMap, widthMap };