UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

316 lines (312 loc) • 12.2 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 { useRefObjectAsForwardedRef } from '../hooks/useRefObjectAsForwardedRef.js'; import classes from './Overlay.module.css.js'; import { clsx } from 'clsx'; 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, 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(45); 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); let t6; if ($[18] !== ignoreClickRefs || $[19] !== initialFocusRef || $[20] !== onClickOutside || $[21] !== onEscape || $[22] !== preventFocusOnOpen || $[23] !== returnFocusRef) { t6 = { overlayRef, returnFocusRef, onEscape, ignoreClickRefs, onClickOutside, initialFocusRef, preventFocusOnOpen }; $[18] = ignoreClickRefs; $[19] = initialFocusRef; $[20] = onClickOutside; $[21] = onEscape; $[22] = preventFocusOnOpen; $[23] = returnFocusRef; $[24] = t6; } else { t6 = $[24]; } useOverlay(t6); let t7; let t8; if ($[25] !== 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]; $[25] = height; $[26] = t7; $[27] = t8; } else { t7 = $[26]; t8 = $[27]; } useEffect(t7, t8); let t10; let t9; if ($[28] !== anchorSide || $[29] !== 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]; $[28] = anchorSide; $[29] = visibility; $[30] = t10; $[31] = t9; } else { t10 = $[30]; t9 = $[31]; } useIsomorphicLayoutEffect(t9, t10); const leftPosition = left === undefined && right === undefined ? 0 : left; const overflowEnabled = useFeatureFlag("primer_react_overlay_overflow"); const t11 = overflowEnabled || !preventOverflow ? true : undefined; let t12; if ($[32] !== height || $[33] !== leftPosition || $[34] !== props || $[35] !== responsiveVariant || $[36] !== right || $[37] !== role || $[38] !== t11 || $[39] !== visibility || $[40] !== width) { t12 = /*#__PURE__*/jsx(BaseOverlay, { role: role, width: width, "data-reflow-container": t11, ref: overlayRef, left: leftPosition, right: right, height: height, visibility: visibility, "data-responsive": responsiveVariant, ...props }); $[32] = height; $[33] = leftPosition; $[34] = props; $[35] = responsiveVariant; $[36] = right; $[37] = role; $[38] = t11; $[39] = visibility; $[40] = width; $[41] = t12; } else { t12 = $[41]; } let t13; if ($[42] !== portalContainerName || $[43] !== t12) { t13 = /*#__PURE__*/jsx(Portal, { containerName: portalContainerName, children: t12 }); $[42] = portalContainerName; $[43] = t12; $[44] = t13; } else { t13 = $[44]; } return t13; }); export { BaseOverlay, Overlay as default, heightMap };