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