UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

340 lines (336 loc) • 12.7 kB
import { c } from 'react-compiler-runtime'; import React, { useRef, useEffect } from 'react'; import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect.js'; import { get } from '../constants.js'; import { Portal } from '../Portal/Portal.js'; import { useRefObjectAsForwardedRef } from '../hooks/useRefObjectAsForwardedRef.js'; import { useTheme } from '../ThemeProvider.js'; import classes from './Overlay.module.css.js'; import { clsx } from 'clsx'; import { BoxWithFallback } from '../internal/components/BoxWithFallback.js'; import { jsx } from 'react/jsx-runtime'; import { useOverlay } from '../hooks/useOverlay.js'; import { useFeatureFlag } from '../FeatureFlags/useFeatureFlag.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 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, ...rest }, forwardedRef) => { return /*#__PURE__*/jsx(BoxWithFallback, { as: "div", ...rest, ref: forwardedRef, style: { left, right, top, 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(51); 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, 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] = 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; } else { anchorSide = $[1]; ignoreClickRefs = $[2]; initialFocusRef = $[3]; left = $[4]; onClickOutside = $[5]; onEscape = $[6]; portalContainerName = $[7]; preventFocusOnOpen = $[8]; props = $[9]; responsiveVariant = $[10]; returnFocusRef = $[11]; right = $[12]; t1 = $[13]; t2 = $[14]; t3 = $[15]; t4 = $[16]; t5 = $[17]; } 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); useRefObjectAsForwardedRef(forwardedRef, overlayRef); const { theme } = useTheme(); let t6; if ($[18] !== theme) { t6 = get("space.2")(theme).replace("px", ""); $[18] = theme; $[19] = t6; } else { t6 = $[19]; } const slideAnimationDistance = parseInt(t6); let t7; if ($[20] !== theme) { t7 = get("animation.easeOutCubic")(theme); $[20] = theme; $[21] = t7; } else { t7 = $[21]; } const slideAnimationEasing = t7; let t8; if ($[22] !== ignoreClickRefs || $[23] !== initialFocusRef || $[24] !== onClickOutside || $[25] !== onEscape || $[26] !== preventFocusOnOpen || $[27] !== returnFocusRef) { t8 = { overlayRef, returnFocusRef, onEscape, ignoreClickRefs, onClickOutside, initialFocusRef, preventFocusOnOpen }; $[22] = ignoreClickRefs; $[23] = initialFocusRef; $[24] = onClickOutside; $[25] = onEscape; $[26] = preventFocusOnOpen; $[27] = returnFocusRef; $[28] = t8; } else { t8 = $[28]; } useOverlay(t8); let t10; let t9; if ($[29] !== height) { t9 = () => { 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`; } }; t10 = [height]; $[29] = height; $[30] = t10; $[31] = t9; } else { t10 = $[30]; t9 = $[31]; } useEffect(t9, t10); let t11; let t12; if ($[32] !== anchorSide || $[33] !== slideAnimationDistance || $[34] !== slideAnimationEasing || $[35] !== visibility) { t11 = () => { 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(${slideAnimationDistance * x}px, ${slideAnimationDistance * y}px)`, "translate(0, 0)"] }, { duration: animationDuration, easing: slideAnimationEasing }); }; t12 = [anchorSide, slideAnimationDistance, slideAnimationEasing, visibility]; $[32] = anchorSide; $[33] = slideAnimationDistance; $[34] = slideAnimationEasing; $[35] = visibility; $[36] = t11; $[37] = t12; } else { t11 = $[36]; t12 = $[37]; } useIsomorphicLayoutEffect(t11, t12); const leftPosition = left === undefined && right === undefined ? 0 : left; const overflowEnabled = useFeatureFlag("primer_react_overlay_overflow"); const t13 = overflowEnabled || !preventOverflow ? true : undefined; let t14; if ($[38] !== height || $[39] !== leftPosition || $[40] !== props || $[41] !== responsiveVariant || $[42] !== right || $[43] !== role || $[44] !== t13 || $[45] !== visibility || $[46] !== width) { t14 = /*#__PURE__*/jsx(BaseOverlay, { role: role, width: width, "data-reflow-container": t13, ref: overlayRef, left: leftPosition, right: right, height: height, visibility: visibility, "data-responsive": responsiveVariant, ...props }); $[38] = height; $[39] = leftPosition; $[40] = props; $[41] = responsiveVariant; $[42] = right; $[43] = role; $[44] = t13; $[45] = visibility; $[46] = width; $[47] = t14; } else { t14 = $[47]; } let t15; if ($[48] !== portalContainerName || $[49] !== t14) { t15 = /*#__PURE__*/jsx(Portal, { containerName: portalContainerName, children: t14 }); $[48] = portalContainerName; $[49] = t14; $[50] = t15; } else { t15 = $[50]; } return t15; }); export { BaseOverlay, Overlay as default, heightMap };