@primer/react
Version:
An implementation of GitHub's Primer Design System using React
566 lines (562 loc) • 14.6 kB
JavaScript
import { c } from 'react-compiler-runtime';
import React, { useRef, useState, useCallback, useEffect } from 'react';
import { IconButton } from '../Button/IconButton.js';
import { ButtonComponent } from '../Button/Button.js';
import { useFocusTrap } from '../hooks/useFocusTrap.js';
import { XIcon } from '@primer/octicons-react';
import { useFocusZone } from '../hooks/useFocusZone.js';
import { FocusKeys } from '@primer/behaviors';
import { Portal } from '../Portal/Portal.js';
import { useRefObjectAsForwardedRef } from '../hooks/useRefObjectAsForwardedRef.js';
import { useId } from '../hooks/useId.js';
import classes from './Dialog.module.css.js';
import { clsx } from 'clsx';
import { BoxWithFallback } from '../internal/components/BoxWithFallback.js';
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
import { useOnEscapePress } from '../hooks/useOnEscapePress.js';
import { ScrollableRegion } from '../ScrollableRegion/ScrollableRegion.js';
import { useProvidedRefOrCreate } from '../hooks/useProvidedRefOrCreate.js';
import Box from '../Box/Box.js';
const DefaultHeader = t0 => {
const $ = c(16);
const {
dialogLabelId,
title,
subtitle,
dialogDescriptionId,
onClose
} = t0;
let t1;
if ($[0] !== onClose) {
t1 = () => {
onClose("close-button");
};
$[0] = onClose;
$[1] = t1;
} else {
t1 = $[1];
}
const onCloseClick = t1;
const t2 = title !== null && title !== void 0 ? title : "Dialog";
let t3;
if ($[2] !== dialogLabelId || $[3] !== t2) {
t3 = /*#__PURE__*/jsx(Dialog.Title, {
id: dialogLabelId,
children: t2
});
$[2] = dialogLabelId;
$[3] = t2;
$[4] = t3;
} else {
t3 = $[4];
}
let t4;
if ($[5] !== dialogDescriptionId || $[6] !== subtitle) {
t4 = subtitle && /*#__PURE__*/jsx(Dialog.Subtitle, {
id: dialogDescriptionId,
children: subtitle
});
$[5] = dialogDescriptionId;
$[6] = subtitle;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] !== t3 || $[9] !== t4) {
t5 = /*#__PURE__*/jsxs(Box, {
display: "flex",
px: 2,
py: "6px",
flexDirection: "column",
flexGrow: 1,
children: [t3, t4]
});
$[8] = t3;
$[9] = t4;
$[10] = t5;
} else {
t5 = $[10];
}
let t6;
if ($[11] !== onCloseClick) {
t6 = /*#__PURE__*/jsx(Dialog.CloseButton, {
onClose: onCloseClick
});
$[11] = onCloseClick;
$[12] = t6;
} else {
t6 = $[12];
}
let t7;
if ($[13] !== t5 || $[14] !== t6) {
t7 = /*#__PURE__*/jsx(Dialog.Header, {
children: /*#__PURE__*/jsxs(Box, {
display: "flex",
children: [t5, t6]
})
});
$[13] = t5;
$[14] = t6;
$[15] = t7;
} else {
t7 = $[15];
}
return t7;
};
const DefaultBody = t0 => {
const $ = c(2);
const {
children
} = t0;
let t1;
if ($[0] !== children) {
t1 = /*#__PURE__*/jsx(Dialog.Body, {
children: children
});
$[0] = children;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
};
const DefaultFooter = t0 => {
const $ = c(4);
const {
footerButtons
} = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = {
bindKeys: FocusKeys.ArrowHorizontal | FocusKeys.Tab,
focusInStrategy: "closest"
};
$[0] = t1;
} else {
t1 = $[0];
}
const {
containerRef: footerRef
} = useFocusZone(t1);
let t2;
if ($[1] !== footerButtons || $[2] !== footerRef) {
t2 = footerButtons ? /*#__PURE__*/jsx(Dialog.Footer, {
ref: footerRef,
children: /*#__PURE__*/jsx(Dialog.Buttons, {
buttons: footerButtons
})
}) : null;
$[1] = footerButtons;
$[2] = footerRef;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
};
const defaultPosition = {
narrow: 'center',
regular: 'center'
};
const defaultFooterButtons = [];
const _Dialog = /*#__PURE__*/React.forwardRef((props, forwardedRef) => {
const {
title = 'Dialog',
subtitle = '',
renderHeader,
renderBody,
renderFooter,
onClose,
role = 'dialog',
width = 'xlarge',
height = 'auto',
footerButtons = defaultFooterButtons,
position = defaultPosition,
returnFocusRef,
initialFocusRef,
sx,
className
} = props;
const dialogLabelId = useId();
const dialogDescriptionId = useId();
const autoFocusedFooterButtonRef = useRef(null);
for (const footerButton of footerButtons) {
if (footerButton.autoFocus) {
// eslint-disable-next-line react-compiler/react-compiler
footerButton.ref = autoFocusedFooterButtonRef;
}
}
const [lastMouseDownIsBackdrop, setLastMouseDownIsBackdrop] = useState(false);
const defaultedProps = {
...props,
title,
subtitle,
role,
dialogLabelId,
dialogDescriptionId
};
const onBackdropClick = useCallback(e => {
if (e.target === e.currentTarget && lastMouseDownIsBackdrop) {
onClose('escape');
}
}, [onClose, lastMouseDownIsBackdrop]);
const dialogRef = useRef(null);
useRefObjectAsForwardedRef(forwardedRef, dialogRef);
const backdropRef = useRef(null);
useFocusTrap({
containerRef: dialogRef,
initialFocusRef: initialFocusRef !== null && initialFocusRef !== void 0 ? initialFocusRef : autoFocusedFooterButtonRef,
restoreFocusOnCleanUp: returnFocusRef !== null && returnFocusRef !== void 0 && returnFocusRef.current ? false : true,
returnFocusRef
});
useOnEscapePress(event => {
onClose('escape');
event.preventDefault();
}, [onClose]);
React.useEffect(() => {
var _dialogRef$current;
const scrollbarWidth = window.innerWidth - document.body.clientWidth;
// If the dialog is rendered, we add a class to the dialog element to disable
(_dialogRef$current = dialogRef.current) === null || _dialogRef$current === void 0 ? void 0 : _dialogRef$current.classList.add(classes.DisableScroll);
// and set a CSS variable to the scrollbar width so that the dialog can
// account for the scrollbar width when calculating its width.
document.body.style.setProperty('--prc-dialog-scrollgutter', `${scrollbarWidth}px`);
}, []);
const header = (renderHeader !== null && renderHeader !== void 0 ? renderHeader : DefaultHeader)(defaultedProps);
const body = (renderBody !== null && renderBody !== void 0 ? renderBody : DefaultBody)(defaultedProps);
const footer = (renderFooter !== null && renderFooter !== void 0 ? renderFooter : DefaultFooter)(defaultedProps);
const positionDataAttributes = typeof position === 'string' ? {
'data-position-regular': position
} : Object.fromEntries(Object.entries(position).map(([key, value]) => {
return [`data-position-${key}`, value];
}));
return /*#__PURE__*/jsx(Fragment, {
children: /*#__PURE__*/jsx(Portal, {
children: /*#__PURE__*/jsx(BoxWithFallback, {
as: "div",
ref: backdropRef,
className: classes.Backdrop,
...positionDataAttributes,
onClick: onBackdropClick,
onMouseDown: e_0 => {
setLastMouseDownIsBackdrop(e_0.target === e_0.currentTarget);
},
children: /*#__PURE__*/jsxs(BoxWithFallback, {
as: "div",
ref: dialogRef,
role: role,
"aria-labelledby": dialogLabelId,
"aria-describedby": dialogDescriptionId,
"aria-modal": true,
...positionDataAttributes,
"data-width": width,
"data-height": height,
sx: sx,
className: clsx(className, classes.Dialog),
children: [header, /*#__PURE__*/jsx(ScrollableRegion, {
"aria-labelledby": dialogLabelId,
className: classes.DialogOverflowWrapper,
children: body
}), footer]
})
})
})
});
});
_Dialog.displayName = 'Dialog';
const Header = /*#__PURE__*/React.forwardRef(function Header(t0, forwardRef) {
const $ = c(9);
let className;
let rest;
if ($[0] !== t0) {
({
className,
...rest
} = t0);
$[0] = t0;
$[1] = className;
$[2] = rest;
} else {
className = $[1];
rest = $[2];
}
let t1;
if ($[3] !== className) {
t1 = clsx(className, classes.Header);
$[3] = className;
$[4] = t1;
} else {
t1 = $[4];
}
let t2;
if ($[5] !== forwardRef || $[6] !== rest || $[7] !== t1) {
t2 = /*#__PURE__*/jsx(BoxWithFallback, {
as: "div",
ref: forwardRef,
className: t1,
...rest
});
$[5] = forwardRef;
$[6] = rest;
$[7] = t1;
$[8] = t2;
} else {
t2 = $[8];
}
return t2;
});
Header.displayName = 'Dialog.Header';
const Title = /*#__PURE__*/React.forwardRef(function Title(t0, forwardRef) {
const $ = c(9);
let className;
let rest;
if ($[0] !== t0) {
({
className,
...rest
} = t0);
$[0] = t0;
$[1] = className;
$[2] = rest;
} else {
className = $[1];
rest = $[2];
}
let t1;
if ($[3] !== className) {
t1 = clsx(className, classes.Title);
$[3] = className;
$[4] = t1;
} else {
t1 = $[4];
}
let t2;
if ($[5] !== forwardRef || $[6] !== rest || $[7] !== t1) {
t2 = /*#__PURE__*/jsx(BoxWithFallback, {
as: "h1",
ref: forwardRef,
className: t1,
...rest
});
$[5] = forwardRef;
$[6] = rest;
$[7] = t1;
$[8] = t2;
} else {
t2 = $[8];
}
return t2;
});
Title.displayName = 'Dialog.Title';
const Subtitle = /*#__PURE__*/React.forwardRef(function Subtitle(t0, forwardRef) {
const $ = c(9);
let className;
let rest;
if ($[0] !== t0) {
({
className,
...rest
} = t0);
$[0] = t0;
$[1] = className;
$[2] = rest;
} else {
className = $[1];
rest = $[2];
}
let t1;
if ($[3] !== className) {
t1 = clsx(className, classes.Subtitle);
$[3] = className;
$[4] = t1;
} else {
t1 = $[4];
}
let t2;
if ($[5] !== forwardRef || $[6] !== rest || $[7] !== t1) {
t2 = /*#__PURE__*/jsx(BoxWithFallback, {
as: "h2",
ref: forwardRef,
className: t1,
...rest
});
$[5] = forwardRef;
$[6] = rest;
$[7] = t1;
$[8] = t2;
} else {
t2 = $[8];
}
return t2;
});
Subtitle.displayName = 'Dialog.Subtitle';
const Body = /*#__PURE__*/React.forwardRef(function Body(t0, forwardRef) {
const $ = c(9);
let className;
let rest;
if ($[0] !== t0) {
({
className,
...rest
} = t0);
$[0] = t0;
$[1] = className;
$[2] = rest;
} else {
className = $[1];
rest = $[2];
}
let t1;
if ($[3] !== className) {
t1 = clsx(className, classes.Body);
$[3] = className;
$[4] = t1;
} else {
t1 = $[4];
}
let t2;
if ($[5] !== forwardRef || $[6] !== rest || $[7] !== t1) {
t2 = /*#__PURE__*/jsx(BoxWithFallback, {
as: "div",
ref: forwardRef,
className: t1,
...rest
});
$[5] = forwardRef;
$[6] = rest;
$[7] = t1;
$[8] = t2;
} else {
t2 = $[8];
}
return t2;
});
Body.displayName = 'Dialog.Body';
const Footer = /*#__PURE__*/React.forwardRef(function Footer(t0, forwardRef) {
const $ = c(9);
let className;
let rest;
if ($[0] !== t0) {
({
className,
...rest
} = t0);
$[0] = t0;
$[1] = className;
$[2] = rest;
} else {
className = $[1];
rest = $[2];
}
let t1;
if ($[3] !== className) {
t1 = clsx(className, classes.Footer);
$[3] = className;
$[4] = t1;
} else {
t1 = $[4];
}
let t2;
if ($[5] !== forwardRef || $[6] !== rest || $[7] !== t1) {
t2 = /*#__PURE__*/jsx(BoxWithFallback, {
as: "div",
ref: forwardRef,
className: t1,
...rest
});
$[5] = forwardRef;
$[6] = rest;
$[7] = t1;
$[8] = t2;
} else {
t2 = $[8];
}
return t2;
});
Footer.displayName = 'Dialog.Footer';
const Buttons = ({
buttons
}) => {
var _buttons$find;
const autoFocusRef = useProvidedRefOrCreate((_buttons$find = buttons.find(button => button.autoFocus)) === null || _buttons$find === void 0 ? void 0 : _buttons$find.ref);
let autoFocusCount = 0;
const [hasRendered, setHasRendered] = useState(0);
useEffect(() => {
// hack to work around dialogs originating from other focus traps.
if (hasRendered === 1) {
var _autoFocusRef$current;
(_autoFocusRef$current = autoFocusRef.current) === null || _autoFocusRef$current === void 0 ? void 0 : _autoFocusRef$current.focus();
} else {
setHasRendered(hasRendered + 1);
}
}, [autoFocusRef, hasRendered]);
return /*#__PURE__*/jsx(Fragment, {
children: buttons.map((dialogButtonProps, index) => {
const {
content,
buttonType = 'default',
autoFocus = false,
...buttonProps
} = dialogButtonProps;
return /*#__PURE__*/jsx(ButtonComponent, {
...buttonProps,
// 'normal' value is equivalent to 'default', this is used for backwards compatibility
variant: buttonType === 'normal' ? 'default' : buttonType,
ref: autoFocus && autoFocusCount === 0 ? (autoFocusCount++, autoFocusRef) : null,
children: content
}, index);
})
});
};
const CloseButton = t0 => {
const $ = c(2);
const {
onClose
} = t0;
let t1;
if ($[0] !== onClose) {
t1 = /*#__PURE__*/jsx(IconButton, {
icon: XIcon,
"aria-label": "Close",
onClick: onClose,
variant: "invisible"
});
$[0] = onClose;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
};
/**
* A dialog is a type of overlay that can be used for confirming actions, asking
* for disambiguation, and presenting small forms. They generally allow the user
* to focus on a quick task without having to navigate to a different page.
*
* Dialogs appear in the page after a direct user interaction. Don't show dialogs
* on page load or as system alerts.
*
* Dialogs appear centered in the page, with a visible backdrop that dims the rest
* of the window for focus.
*
* All dialogs have a title and a close button.
*
* Dialogs are modal. Dialogs can be dismissed by clicking on the close button,
* pressing the escape key, or by interacting with another button in the dialog.
* To avoid losing information and missing important messages, clicking outside
* of the dialog will not close it.
*
* The sub components provided (e.g. Header, Title, etc.) are available for custom
* renderers only. They are not intended to be used otherwise.
*/
const Dialog = Object.assign(_Dialog, {
Header,
Title,
Subtitle,
Body,
Footer,
Buttons,
CloseButton
});
export { Dialog };