@primer/react
Version:
An implementation of GitHub's Primer Design System using React
316 lines (312 loc) • 12.2 kB
JavaScript
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 };