UNPKG

@carbon/react

Version:

React components for the Carbon Design System

611 lines (585 loc) 18.2 kB
/** * Copyright IBM Corp. 2016, 2023 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var PropTypes = require('prop-types'); var React = require('react'); var useIsomorphicEffect = require('../../internal/useIsomorphicEffect.js'); var usePrefix = require('../../internal/usePrefix.js'); var cx = require('classnames'); var iconsReact = require('@carbon/icons-react'); var index = require('../IconButton/index.js'); var noopFn = require('../../internal/noopFn.js'); var Text = require('../Text/Text.js'); require('../Text/TextDirection.js'); var index$1 = require('../Layer/index.js'); var ButtonSet = require('../ButtonSet/ButtonSet.js'); var Button = require('../Button/Button.js'); require('../Button/Button.Skeleton.js'); var useId = require('../../internal/useId.js'); var InlineLoading = require('../InlineLoading/InlineLoading.js'); var debounce = require('../../node_modules/es-toolkit/dist/compat/function/debounce.js'); const DialogContext = /*#__PURE__*/React.createContext({}); /** * ---------- * Dialog * ---------- */ const Dialog = /*#__PURE__*/React.forwardRef(({ children, className, focusAfterCloseRef, modal, onCancel = noopFn.noopFn, onClick = noopFn.noopFn, onClose = noopFn.noopFn, onRequestClose = noopFn.noopFn, open = false, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }, forwardRef) => { const prefix = usePrefix.usePrefix(); const dialogId = useId.useId(); const titleId = `${prefix}--dialog-header__heading--${dialogId}`; const subtitleId = `${prefix}--dialog-header__label--${dialogId}`; // This component needs access to a ref, placed on the dialog, to call the // various imperative dialog functions (show(), close(), etc.). // If the parent component has not passed a ref for forwardRef, forwardRef // will be null. A "backup" ref is needed to ensure the dialog's instance // methods can always be called within this component. const backupRef = React.useRef(null); const ref = forwardRef ?? backupRef; // Clicks on the backdrop of an open modal dialog should request the consuming component to close // the dialog. Clicks elsewhere, or on non-modal dialogs should not request // to close the dialog. function handleModalBackdropClick(e) { if (open && modal && e.target === ref.current) { onRequestClose(e); } } function handleClick(e) { handleModalBackdropClick(e); // onClick should always be called, no matter if the target is a modal // dialog, modal dialog backdrop, or non-modal dialog. onClick(e); } React.useEffect(() => { if (ref.current) { if (open) { if (modal) { // Display the dialog as a modal, over the top of any other dialogs // that might be present. Everything outside the dialog are inert // with interactions outside the dialog being blocked. ref.current.showModal(); } else { // Display the dialog modelessly, i.e. still allowing interaction // with content outside of the dialog. ref.current.show(); } } else { ref.current.close(); } } // eslint-disable-next-line react-hooks/exhaustive-deps -- https://github.com/carbon-design-system/carbon/issues/20452 }, [modal, open]); React.useEffect(() => { if (!open && focusAfterCloseRef) { // use setTimeout to ensure focus is set after all other default focus behavior const moveFocus = setTimeout(() => { focusAfterCloseRef.current?.focus(); }); //component did unmount equivalent return () => { clearTimeout(moveFocus); }; } }, [open, focusAfterCloseRef]); const containerClasses = cx(`${prefix}--dialog-container`); const contextValue = { dialogId, titleId, subtitleId, isOpen: open }; React.useEffect(() => { if (ref.current && open && !ariaLabel && !ariaLabelledBy) { const title = ref.current.querySelector(`.${prefix}--dialog-header__heading`); // Set aria-labelledby to the title's ID if it exists if (title && title.id) { ref.current.setAttribute('aria-labelledby', title.id); } } // eslint-disable-next-line react-hooks/exhaustive-deps -- https://github.com/carbon-design-system/carbon/issues/20452 }, [open, ariaLabel, ariaLabelledBy, prefix]); return /*#__PURE__*/React.createElement(DialogContext.Provider, { value: contextValue }, /*#__PURE__*/React.createElement("dialog", _rollupPluginBabelHelpers.extends({}, rest, { className: cx(`${prefix}--dialog`, { [`${prefix}--dialog--modal`]: modal }, className), ref: ref, onCancel: onCancel, onClick: handleClick, onClose: onClose, role: role, "aria-label": ariaLabel, "aria-labelledby": !ariaLabel ? ariaLabelledBy || titleId : undefined, "aria-describedby": ariaDescribedBy }), /*#__PURE__*/React.createElement("div", { className: containerClasses }, children))); }); Dialog.displayName = 'Dialog'; Dialog.propTypes = { /** * Provide children to be rendered inside of the Dialog */ children: PropTypes.node, /** * Specify an optional className to be applied to the modal root node */ className: PropTypes.string, /** * Provide a ref to return focus to once the dialog is closed. */ focusAfterCloseRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any })]), /** * Modal specifies whether the Dialog is modal or non-modal. This cannot be * changed while open=true */ modal: PropTypes.bool, /** * Specify a handler for closing Dialog. * The handler should care of closing Dialog, e.g. changing `open` prop. */ onRequestClose: PropTypes.func, /** * open initial state */ open: PropTypes.bool, /** * Specify the role of the dialog for accessibility */ role: PropTypes.oneOf(['dialog', 'alertdialog']), /** * Specify a label for screen readers */ 'aria-label': PropTypes.string, /** * Specify the ID of an element that labels this dialog */ 'aria-labelledby': PropTypes.string, /** * Specify the ID of an element that describes this dialog */ ariaDescribedBy: PropTypes.string }; /** * ------------- * DialogHeader * ------------- */ const DialogHeader = /*#__PURE__*/React.forwardRef(({ children, ...rest }, ref) => { const prefix = usePrefix.usePrefix(); return /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({ className: `${prefix}--dialog__header`, ref: ref }, rest), children); }); DialogHeader.displayName = 'DialogHeader'; DialogHeader.propTypes = { /** * Provide the contents to be rendered inside of this component */ children: PropTypes.node }; /** * --------------- * DialogControls * --------------- */ const DialogControls = /*#__PURE__*/React.forwardRef(({ children, ...rest }, ref) => { const prefix = usePrefix.usePrefix(); return ( /*#__PURE__*/ // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- https://github.com/carbon-design-system/carbon/issues/20452 // @ts-ignore React.createElement("div", _rollupPluginBabelHelpers.extends({ className: `${prefix}--dialog__header-controls`, ref: ref }, rest), children) ); }); DialogControls.displayName = 'DialogControls'; DialogControls.propTypes = { /** * Provide children to be rendered inside of this component */ children: PropTypes.node }; /** * ------------------- * DialogCloseButton * ------------------- */ const DialogCloseButton = /*#__PURE__*/React.forwardRef(({ onClick, ...rest }, ref) => { const prefix = usePrefix.usePrefix(); return ( /*#__PURE__*/ // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- https://github.com/carbon-design-system/carbon/issues/20452 // @ts-ignore React.createElement(index.IconButton, _rollupPluginBabelHelpers.extends({ kind: "ghost", className: `${prefix}--dialog__close`, label: "Close", title: "Close", "aria-label": "Close", align: "left", onClick: onClick, ref: ref }, rest), /*#__PURE__*/React.createElement(iconsReact.Close, { size: 20, "aria-hidden": "true", tabIndex: -1, className: `${prefix}--icon__close` })) ); }); DialogCloseButton.displayName = 'DialogCloseButton'; DialogCloseButton.propTypes = { /** * Specify a click handler applied to the IconButton */ onClick: PropTypes.func }; /** * ------------ * DialogTitle * ------------ */ const DialogTitle = /*#__PURE__*/React.forwardRef(({ children, className, id, ...rest }, ref) => { const prefix = usePrefix.usePrefix(); const { titleId } = React.useContext(DialogContext); const headingId = id || titleId; return /*#__PURE__*/React.createElement(Text.Text, _rollupPluginBabelHelpers.extends({ as: "h2", id: headingId, className: cx(`${prefix}--dialog-header__heading`, className), ref: ref }, rest), children); }); DialogTitle.displayName = 'DialogTitle'; DialogTitle.propTypes = { /** * Provide the contents to be rendered inside of this component */ children: PropTypes.node, /** * Specify an optional className to be applied to the title node */ className: PropTypes.string, /** * Specify an optional id for the title element */ id: PropTypes.string }; /** * --------------- * DialogSubtitle * --------------- */ const DialogSubtitle = /*#__PURE__*/React.forwardRef(({ children, className, id, ...rest }, ref) => { const prefix = usePrefix.usePrefix(); const { subtitleId } = React.useContext(DialogContext); const labelId = id || subtitleId; return /*#__PURE__*/React.createElement(Text.Text, _rollupPluginBabelHelpers.extends({ as: "h2", id: labelId, className: cx(`${prefix}--dialog-header__label`, className), ref: ref }, rest), children); }); DialogSubtitle.displayName = 'DialogSubtitle'; DialogSubtitle.propTypes = { /** * Provide the contents to be rendered inside of this component */ children: PropTypes.node, /** * Specify an optional className to be applied to the subtitle node */ className: PropTypes.string, /** * Specify an optional id for the subtitle element */ id: PropTypes.string }; /** * ----------- * DialogBody * ----------- */ const DialogBody = /*#__PURE__*/React.forwardRef(({ children, className, hasScrollingContent, ...rest }, ref) => { const prefix = usePrefix.usePrefix(); const contentRef = React.useRef(null); const [isScrollable, setIsScrollable] = React.useState(false); const dialogId = useId.useId(); const dialogBodyId = `${prefix}--dialog-body--${dialogId}`; useIsomorphicEffect.default(() => { if (contentRef.current) { setIsScrollable(contentRef.current.scrollHeight > contentRef.current.clientHeight); } function handler() { if (contentRef.current) { setIsScrollable(contentRef.current.scrollHeight > contentRef.current.clientHeight); } } const debouncedHandler = debounce.debounce(handler, 200); window.addEventListener('resize', debouncedHandler); return () => { debouncedHandler.cancel(); window.removeEventListener('resize', debouncedHandler); }; }, []); const contentClasses = cx(`${prefix}--dialog-content`, { [`${prefix}--dialog-scroll-content`]: hasScrollingContent || isScrollable }, className); const hasScrollingContentProps = hasScrollingContent || isScrollable ? { tabIndex: 0, role: 'region' } : {}; const combinedRef = el => { if (typeof ref === 'function') { ref(el); } else if (ref) { ref.current = el; } contentRef.current = el; }; return /*#__PURE__*/React.createElement(index$1.Layer, _rollupPluginBabelHelpers.extends({ ref: combinedRef, id: dialogBodyId, className: contentClasses }, hasScrollingContentProps, rest), children); }); DialogBody.displayName = 'DialogBody'; DialogBody.propTypes = { /** * Provide the contents to be rendered inside of this component */ children: PropTypes.node, /** * Specify an optional className to be applied to the body node */ className: PropTypes.string, /** * Specify whether the content has overflow that should be scrollable */ hasScrollingContent: PropTypes.bool }; /** * ------------- * DialogFooter * ------------- */ const DialogFooter = /*#__PURE__*/React.forwardRef(({ children, className, onRequestClose = noopFn.noopFn, onSecondarySubmit, onRequestSubmit = noopFn.noopFn, primaryButtonText = 'Save', primaryButtonDisabled = false, secondaryButtonText = 'Cancel', secondaryButtons, loadingStatus = 'inactive', loadingDescription, loadingIconDescription, onLoadingSuccess = noopFn.noopFn, danger = false, ...rest }, ref) => { const prefix = usePrefix.usePrefix(); const button = React.useRef(null); const { isOpen } = React.useContext(DialogContext); const [secondaryButtonRef, setSecondaryButtonRef] = React.useState(null); React.useEffect(() => { if (danger && secondaryButtonRef) { const focusFrame = requestAnimationFrame(() => { secondaryButtonRef.focus(); }); return () => cancelAnimationFrame(focusFrame); } }, [danger, secondaryButtonRef, isOpen]); const classes = cx(`${prefix}--dialog-footer`, className, { [`${prefix}--dialog-footer--three-button`]: Array.isArray(secondaryButtons) && secondaryButtons.length === 2 }); const loadingActive = loadingStatus !== 'inactive'; const primaryButtonClass = cx({ [`${prefix}--btn--loading`]: loadingStatus !== 'inactive' }); const onSecondaryButtonClick = onSecondarySubmit ? onSecondarySubmit : onRequestClose; if (children) { return /*#__PURE__*/React.createElement(ButtonSet.default, _rollupPluginBabelHelpers.extends({ className: classes, ref: ref }, rest), children); } return /*#__PURE__*/React.createElement(ButtonSet.default, _rollupPluginBabelHelpers.extends({ className: classes, "aria-busy": loadingActive, ref: ref }, rest), Array.isArray(secondaryButtons) && secondaryButtons.length <= 2 ? secondaryButtons.map(({ buttonText, onClick: onButtonClick }, i) => /*#__PURE__*/React.createElement(Button.default, { key: `${buttonText}-${i}` // eslint-disable-next-line jsx-a11y/no-autofocus -- https://github.com/carbon-design-system/carbon/issues/20452 , autoFocus: danger, kind: "secondary", ref: i === 0 && danger ? setSecondaryButtonRef : undefined, onClick: onButtonClick }, buttonText)) : secondaryButtonText && /*#__PURE__*/React.createElement(Button.default, { ref: danger ? setSecondaryButtonRef : undefined, disabled: loadingActive, kind: "secondary" // eslint-disable-next-line jsx-a11y/no-autofocus -- https://github.com/carbon-design-system/carbon/issues/20452 , autoFocus: danger, onClick: onSecondaryButtonClick }, secondaryButtonText), /*#__PURE__*/React.createElement(Button.default, { className: primaryButtonClass, kind: danger ? 'danger' : 'primary', disabled: loadingActive || primaryButtonDisabled, onClick: onRequestSubmit, ref: button }, loadingStatus === 'inactive' ? primaryButtonText : /*#__PURE__*/React.createElement(InlineLoading.default, { status: loadingStatus, description: loadingDescription, iconDescription: loadingIconDescription, className: `${prefix}--inline-loading--btn`, onSuccess: onLoadingSuccess }))); }); DialogFooter.displayName = 'DialogFooter'; DialogFooter.propTypes = { /** * Provide the contents to be rendered inside of this component */ children: PropTypes.node, /** * Specify an optional className to be applied to the footer node */ className: PropTypes.string, /** * Specify a handler for closing dialog. */ onRequestClose: PropTypes.func, /** * Specify a handler for the secondary button. */ onSecondarySubmit: PropTypes.func, /** * Specify a handler for submitting dialog. */ onRequestSubmit: PropTypes.func, /** * Specify the text for the primary button */ primaryButtonText: PropTypes.node, /** * Specify whether the Button should be disabled, or not */ primaryButtonDisabled: PropTypes.bool, /** * Specify the text for the secondary button */ secondaryButtonText: PropTypes.node, /** * Specify an array of config objects for secondary buttons */ secondaryButtons: (props, propName, componentName) => { if (props.secondaryButtons) { if (!Array.isArray(props.secondaryButtons) || props.secondaryButtons.length !== 2) { return new Error(`${propName} needs to be an array of two button config objects`); } const shape = { buttonText: PropTypes.node, onClick: PropTypes.func }; props[propName].forEach(secondaryButton => { PropTypes.checkPropTypes(shape, secondaryButton, propName, componentName); }); } return null; }, /** * Specify whether the Dialog is for dangerous actions */ danger: PropTypes.bool, /** * Specify loading status */ loadingStatus: PropTypes.oneOf(['inactive', 'active', 'finished', 'error']), /** * Specify the description for the loading text */ loadingDescription: PropTypes.string, /** * Specify the description for the loading icon */ loadingIconDescription: PropTypes.string, /** * Provide an optional handler to be invoked when loading is * successful */ onLoadingSuccess: PropTypes.func }; exports.Dialog = Dialog; exports.DialogBody = DialogBody; exports.DialogCloseButton = DialogCloseButton; exports.DialogControls = DialogControls; exports.DialogFooter = DialogFooter; exports.DialogHeader = DialogHeader; exports.DialogSubtitle = DialogSubtitle; exports.DialogTitle = DialogTitle;