UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

566 lines (562 loc) • 14.6 kB
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 };