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