UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

397 lines (386 loc) • 19.2 kB
'use strict'; var React = require('react'); var styled = require('styled-components'); var IconButton = require('../Button/IconButton.js'); require('../Button/ButtonBase.js'); require('../utils/defaultSxProp.js'); var Button = require('../Button/Button.js'); var Box = require('../Box/Box.js'); var constants = require('../constants.js'); var useProvidedRefOrCreate = require('../hooks/useProvidedRefOrCreate.js'); var useOnEscapePress = require('../hooks/useOnEscapePress.js'); require('@primer/behaviors/utils'); var behaviors = require('@primer/behaviors'); var useRefObjectAsForwardedRef = require('../hooks/useRefObjectAsForwardedRef.js'); var useId = require('../hooks/useId.js'); var useFocusTrap = require('../hooks/useFocusTrap.js'); var sx = require('../sx.js'); var octiconsReact = require('@primer/octicons-react'); var useFocusZone = require('../hooks/useFocusZone.js'); var Portal = require('../Portal/Portal.js'); var ScrollableRegion = require('../ScrollableRegion/ScrollableRegion.js'); var toggleStyledComponent = require('../internal/utils/toggleStyledComponent.js'); var Dialog_module = require('./Dialog.module.css.js'); require('../FeatureFlags/FeatureFlags.js'); var useFeatureFlag = require('../FeatureFlags/useFeatureFlag.js'); require('../FeatureFlags/DefaultFeatureFlags.js'); var clsx = require('clsx'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var React__default = /*#__PURE__*/_interopDefault(React); var styled__default = /*#__PURE__*/_interopDefault(styled); function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } /* Dialog Version 2 */ /** * Props that characterize a button to be rendered into the footer of * a Dialog. */ /** * Props to customize the rendering of the Dialog. */ /** * Props that are passed to a component that serves as a dialog header */ const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_ga'; const Backdrop = toggleStyledComponent.toggleStyledComponent(CSS_MODULES_FEATURE_FLAG, 'div', styled__default.default.div.withConfig({ displayName: "Dialog__Backdrop", componentId: "sc-uaxjsn-0" })(["position:fixed;top:0;left:0;bottom:0;right:0;display:flex;align-items:center;justify-content:center;background-color:", ";animation:dialog-backdrop-appear 200ms ", ";&[data-position-regular='center']{align-items:center;justify-content:center;}&[data-position-regular='left']{align-items:center;justify-content:flex-start;}&[data-position-regular='right']{align-items:center;justify-content:flex-end;}.DialogOverflowWrapper{flex-grow:1;}@media (max-width:767px){&[data-position-narrow='center']{align-items:center;justify-content:center;}&[data-position-narrow='bottom']{align-items:end;justify-content:center;}}@keyframes dialog-backdrop-appear{0%{opacity:0;}100%{opacity:1;}}"], constants.get('colors.primer.canvas.backdrop'), constants.get('animation.easeOutCubic'))); Backdrop.displayName = 'Backdrop'; const heightMap = { small: '480px', large: '640px', auto: 'auto' }; const widthMap = { small: '296px', medium: '320px', large: '480px', xlarge: '640px' }; const StyledDialog = toggleStyledComponent.toggleStyledComponent(CSS_MODULES_FEATURE_FLAG, 'div', styled__default.default.div.withConfig({ displayName: "Dialog__StyledDialog", componentId: "sc-uaxjsn-1" })(["display:flex;flex-direction:column;background-color:", ";box-shadow:", ";width:", ";height:", ";min-width:296px;max-width:calc(100dvw - 64px);max-height:calc(100dvh - 64px);border-radius:12px;opacity:1;@media screen and (prefers-reduced-motion:no-preference){animation:Overlay--motion-scaleFade 0.2s cubic-bezier(0.33,1,0.68,1) 0s 1 normal none running;}&[data-position-regular='center']{border-radius:var(--borderRadius-large,0.75rem);@media screen and (prefers-reduced-motion:no-preference){animation:Overlay--motion-scaleFade 0.2s cubic-bezier(0.33,1,0.68,1) 0s 1 normal none running;}}&[data-position-regular='left']{height:100dvh;max-height:unset;border-radius:var(--borderRadius-large,0.75rem);border-top-left-radius:0;border-bottom-left-radius:0;@media screen and (prefers-reduced-motion:no-preference){animation:Overlay--motion-slideInRight 0.25s cubic-bezier(0.33,1,0.68,1) 0s 1 normal none running;}}&[data-position-regular='right']{height:100dvh;max-height:unset;border-radius:var(--borderRadius-large,0.75rem);border-top-right-radius:0;border-bottom-right-radius:0;@media screen and (prefers-reduced-motion:no-preference){animation:Overlay--motion-slideInLeft 0.25s cubic-bezier(0.33,1,0.68,1) 0s 1 normal none running;}}@media (max-width:767px){&[data-position-narrow='center']{border-radius:var(--borderRadius-large,0.75rem);width:", ";height:", ";}&[data-position-narrow='bottom']{width:100dvw;height:auto;max-width:100dvw;max-height:calc(100dvh - 64px);border-radius:var(--borderRadius-large,0.75rem);border-bottom-right-radius:0;border-bottom-left-radius:0;@media screen and (prefers-reduced-motion:no-preference){animation:Overlay--motion-slideUp 0.25s cubic-bezier(0.33,1,0.68,1) 0s 1 normal none running;}}&[data-position-narrow='fullscreen']{width:100%;max-width:100dvw;height:100%;max-height:100dvh;border-radius:unset !important;flex-grow:1;@media screen and (prefers-reduced-motion:no-preference){animation:Overlay--motion-scaleFade 0.2s cubic-bezier(0.33,1,0.68,1) 0s 1 normal none running;}}}@keyframes Overlay--motion-scaleFade{0%{opacity:0;transform:scale(0.5);}100%{opacity:1;transform:scale(1);}}@keyframes Overlay--motion-slideUp{from{transform:translateY(100%);}}@keyframes Overlay--motion-slideInRight{from{transform:translateX(-100%);}}@keyframes Overlay--motion-slideInLeft{from{transform:translateX(100%);}}", ";"], constants.get('colors.canvas.overlay'), constants.get('shadows.overlay.shadow'), props => { var _props$width; return widthMap[(_props$width = props.width) !== null && _props$width !== undefined ? _props$width : 'xlarge']; }, props => { var _props$height; return heightMap[(_props$height = props.height) !== null && _props$height !== undefined ? _props$height : 'auto']; }, props => { var _props$width2; return widthMap[(_props$width2 = props.width) !== null && _props$width2 !== undefined ? _props$width2 : 'xlarge']; }, props => { var _props$height2; return heightMap[(_props$height2 = props.height) !== null && _props$height2 !== undefined ? _props$height2 : 'auto']; }, sx.default)); const DefaultHeader = ({ dialogLabelId, title, subtitle, dialogDescriptionId, onClose }) => { const onCloseClick = React.useCallback(() => { onClose('close-button'); }, [onClose]); return /*#__PURE__*/React__default.default.createElement(Dialog.Header, null, /*#__PURE__*/React__default.default.createElement(Box, { display: "flex" }, /*#__PURE__*/React__default.default.createElement(Box, { display: "flex", px: 2, py: "6px", flexDirection: "column", flexGrow: 1 }, /*#__PURE__*/React__default.default.createElement(Dialog.Title, { id: dialogLabelId }, title !== null && title !== undefined ? title : 'Dialog'), subtitle && /*#__PURE__*/React__default.default.createElement(Dialog.Subtitle, { id: dialogDescriptionId }, subtitle)), /*#__PURE__*/React__default.default.createElement(Dialog.CloseButton, { onClose: onCloseClick }))); }; DefaultHeader.displayName = "DefaultHeader"; const DefaultBody = ({ children }) => { return /*#__PURE__*/React__default.default.createElement(Dialog.Body, null, children); }; DefaultBody.displayName = "DefaultBody"; const DefaultFooter = ({ footerButtons }) => { const { containerRef: footerRef } = useFocusZone.useFocusZone({ bindKeys: behaviors.FocusKeys.ArrowHorizontal | behaviors.FocusKeys.Tab, focusInStrategy: 'closest' }); return footerButtons ? /*#__PURE__*/React__default.default.createElement(Dialog.Footer, { ref: footerRef }, /*#__PURE__*/React__default.default.createElement(Dialog.Buttons, { buttons: footerButtons })) : null; }; const defaultPosition = { narrow: 'center', regular: 'center' }; const _Dialog = /*#__PURE__*/React__default.default.forwardRef((props, forwardedRef) => { const { title = 'Dialog', subtitle = '', renderHeader, renderBody, renderFooter, onClose, role = 'dialog', width = 'xlarge', height = 'auto', footerButtons = [], position = defaultPosition, returnFocusRef, initialFocusRef, sx, className } = props; const enabled = useFeatureFlag.useFeatureFlag(CSS_MODULES_FEATURE_FLAG); const dialogLabelId = useId.useId(); const dialogDescriptionId = useId.useId(); const autoFocusedFooterButtonRef = React.useRef(null); for (const footerButton of footerButtons) { if (footerButton.autoFocus) { footerButton.ref = autoFocusedFooterButtonRef; } } const [lastMouseDownIsBackdrop, setLastMouseDownIsBackdrop] = React.useState(false); const defaultedProps = { ...props, title, subtitle, role, dialogLabelId, dialogDescriptionId }; const onBackdropClick = React.useCallback(e => { if (e.target === e.currentTarget && lastMouseDownIsBackdrop) { onClose('escape'); } }, [onClose, lastMouseDownIsBackdrop]); const dialogRef = React.useRef(null); useRefObjectAsForwardedRef.useRefObjectAsForwardedRef(forwardedRef, dialogRef); const backdropRef = React.useRef(null); useFocusTrap.useFocusTrap({ containerRef: dialogRef, initialFocusRef: initialFocusRef !== null && initialFocusRef !== undefined ? initialFocusRef : autoFocusedFooterButtonRef, restoreFocusOnCleanUp: returnFocusRef !== null && returnFocusRef !== undefined && returnFocusRef.current ? false : true, returnFocusRef }); useOnEscapePress.useOnEscapePress(event => { onClose('escape'); event.preventDefault(); }, [onClose]); React__default.default.useEffect(() => { const bodyOverflowStyle = document.body.style.overflow || ''; // If the body is already set to overflow: hidden, it likely means // that there is already a modal open. In that case, we should bail // so we don't re-enable scroll after the second dialog is closed. if (bodyOverflowStyle === 'hidden') { return; } document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = bodyOverflowStyle; }; }, []); const header = (renderHeader !== null && renderHeader !== undefined ? renderHeader : DefaultHeader)(defaultedProps); const body = (renderBody !== null && renderBody !== undefined ? renderBody : DefaultBody)(defaultedProps); const footer = (renderFooter !== null && renderFooter !== undefined ? 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]; })); const dimensionProps = enabled ? { 'data-width': width, 'data-height': height } : { width, height }; return /*#__PURE__*/React__default.default.createElement(React__default.default.Fragment, null, /*#__PURE__*/React__default.default.createElement(Portal.Portal, null, /*#__PURE__*/React__default.default.createElement(Backdrop, _extends({ ref: backdropRef, className: enabled ? Dialog_module.Backdrop : undefined }, positionDataAttributes, { onClick: onBackdropClick, onMouseDown: e => { setLastMouseDownIsBackdrop(e.target === e.currentTarget); } }), /*#__PURE__*/React__default.default.createElement(StyledDialog, _extends({ ref: dialogRef, role: role, "aria-labelledby": dialogLabelId, "aria-describedby": dialogDescriptionId, "aria-modal": true }, positionDataAttributes, dimensionProps, { sx: sx, className: clsx.clsx(className, enabled && Dialog_module.Dialog) }), header, /*#__PURE__*/React__default.default.createElement(ScrollableRegion.ScrollableRegion, { "aria-labelledby": dialogLabelId, className: "DialogOverflowWrapper" }, body), footer)))); }); _Dialog.displayName = 'Dialog'; const StyledHeader = toggleStyledComponent.toggleStyledComponent(CSS_MODULES_FEATURE_FLAG, 'div', styled__default.default.div.withConfig({ displayName: "Dialog__StyledHeader", componentId: "sc-uaxjsn-2" })(["box-shadow:0 1px 0 ", ";padding:", ";z-index:1;flex-shrink:0;", ";"], constants.get('colors.border.default'), constants.get('space.2'), sx.default)); const Header = /*#__PURE__*/React__default.default.forwardRef(function Header({ className, ...rest }, forwardRef) { const enabled = useFeatureFlag.useFeatureFlag(CSS_MODULES_FEATURE_FLAG); return /*#__PURE__*/React__default.default.createElement(StyledHeader, _extends({ ref: forwardRef, className: clsx.clsx(className, enabled && Dialog_module.Header) }, rest)); }); Header.displayName = 'Dialog.Header'; const StyledTitle = toggleStyledComponent.toggleStyledComponent(CSS_MODULES_FEATURE_FLAG, 'h1', styled__default.default.h1.withConfig({ displayName: "Dialog__StyledTitle", componentId: "sc-uaxjsn-3" })(["font-size:", ";font-weight:", ";margin:0;", ";"], constants.get('fontSizes.1'), constants.get('fontWeights.bold'), sx.default)); const Title = /*#__PURE__*/React__default.default.forwardRef(function Title({ className, ...rest }, forwardRef) { const enabled = useFeatureFlag.useFeatureFlag(CSS_MODULES_FEATURE_FLAG); return /*#__PURE__*/React__default.default.createElement(StyledTitle, _extends({ ref: forwardRef, className: clsx.clsx(className, enabled && Dialog_module.Title) }, rest)); }); Title.displayName = 'Dialog.Title'; const StyledSubtitle = toggleStyledComponent.toggleStyledComponent(CSS_MODULES_FEATURE_FLAG, 'h2', styled__default.default.h2.withConfig({ displayName: "Dialog__StyledSubtitle", componentId: "sc-uaxjsn-4" })(["font-size:", ";color:", ";margin:0;margin-top:", ";font-weight:normal;", ";"], constants.get('fontSizes.0'), constants.get('colors.fg.muted'), constants.get('space.1'), sx.default)); const Subtitle = /*#__PURE__*/React__default.default.forwardRef(function Subtitle({ className, ...rest }, forwardRef) { const enabled = useFeatureFlag.useFeatureFlag(CSS_MODULES_FEATURE_FLAG); return /*#__PURE__*/React__default.default.createElement(StyledSubtitle, _extends({ ref: forwardRef, className: clsx.clsx(className, enabled && Dialog_module.Subtitle) }, rest)); }); Subtitle.displayName = 'Dialog.Subtitle'; const StyledBody = toggleStyledComponent.toggleStyledComponent(CSS_MODULES_FEATURE_FLAG, 'div', styled__default.default.div.withConfig({ displayName: "Dialog__StyledBody", componentId: "sc-uaxjsn-5" })(["flex-grow:1;overflow:auto;padding:", ";", ";"], constants.get('space.3'), sx.default)); const Body = /*#__PURE__*/React__default.default.forwardRef(function Body({ className, ...rest }, forwardRef) { const enabled = useFeatureFlag.useFeatureFlag(CSS_MODULES_FEATURE_FLAG); return /*#__PURE__*/React__default.default.createElement(StyledBody, _extends({ ref: forwardRef, className: clsx.clsx(className, enabled && Dialog_module.Body) }, rest)); }); Body.displayName = 'Dialog.Body'; const StyledFooter = toggleStyledComponent.toggleStyledComponent(CSS_MODULES_FEATURE_FLAG, 'div', styled__default.default.div.withConfig({ displayName: "Dialog__StyledFooter", componentId: "sc-uaxjsn-6" })(["box-shadow:0 -1px 0 ", ";padding:", ";display:flex;flex-flow:wrap;justify-content:flex-end;gap:", ";z-index:1;flex-shrink:0;", ";"], constants.get('colors.border.default'), constants.get('space.3'), constants.get('space.2'), sx.default)); const Footer = /*#__PURE__*/React__default.default.forwardRef(function Footer({ className, ...rest }, forwardRef) { const enabled = useFeatureFlag.useFeatureFlag(CSS_MODULES_FEATURE_FLAG); return /*#__PURE__*/React__default.default.createElement(StyledFooter, _extends({ ref: forwardRef, className: clsx.clsx(className, enabled && Dialog_module.Footer) }, rest)); }); Footer.displayName = 'Dialog.Footer'; const Buttons = ({ buttons }) => { var _buttons$find; const autoFocusRef = useProvidedRefOrCreate.useProvidedRefOrCreate((_buttons$find = buttons.find(button => button.autoFocus)) === null || _buttons$find === undefined ? undefined : _buttons$find.ref); let autoFocusCount = 0; const [hasRendered, setHasRendered] = React.useState(0); React.useEffect(() => { // hack to work around dialogs originating from other focus traps. if (hasRendered === 1) { var _autoFocusRef$current; (_autoFocusRef$current = autoFocusRef.current) === null || _autoFocusRef$current === undefined ? undefined : _autoFocusRef$current.focus(); } else { setHasRendered(hasRendered + 1); } }, [autoFocusRef, hasRendered]); return /*#__PURE__*/React__default.default.createElement(React__default.default.Fragment, null, buttons.map((dialogButtonProps, index) => { const { content, buttonType = 'default', autoFocus = false, ...buttonProps } = dialogButtonProps; return /*#__PURE__*/React__default.default.createElement(Button.ButtonComponent, _extends({ key: index }, 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 }), content); })); }; const CloseButton = ({ onClose }) => { return ( /*#__PURE__*/ // eslint-disable-next-line primer-react/a11y-remove-disable-tooltip React__default.default.createElement(IconButton.IconButton, { unsafeDisableTooltip: true, icon: octiconsReact.XIcon, "aria-label": "Close", onClick: onClose, variant: "invisible" }) ); }; CloseButton.displayName = "CloseButton"; /** * 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 }); exports.Dialog = Dialog;