UNPKG

@carbon/react

Version:

React components for the Carbon Design System

359 lines (357 loc) 14.7 kB
/** * Copyright IBM Corp. 2016, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ const require_runtime = require("../../_virtual/_rolldown/runtime.js"); const require_usePrefix = require("../../internal/usePrefix.js"); const require_Text = require("../Text/Text.js"); const require_useId = require("../../internal/useId.js"); const require_noopFn = require("../../internal/noopFn.js"); const require_deprecate = require("../../prop-types/deprecate.js"); const require_index = require("../IconButton/index.js"); const require_index$1 = require("../Button/index.js"); const require_index$2 = require("../ButtonSet/index.js"); const require_useResizeObserver = require("../../internal/useResizeObserver.js"); const require_index$3 = require("../Layer/index.js"); const require_InlineLoading = require("../InlineLoading/InlineLoading.js"); let classnames = require("classnames"); classnames = require_runtime.__toESM(classnames); let react = require("react"); react = require_runtime.__toESM(react); let prop_types = require("prop-types"); prop_types = require_runtime.__toESM(prop_types); let react_jsx_runtime = require("react/jsx-runtime"); let _carbon_icons_react = require("@carbon/icons-react"); //#region src/components/Dialog/Dialog.tsx /** * Copyright IBM Corp. 2025, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ const DialogContext = (0, react.createContext)({}); const Dialog = react.default.forwardRef(({ children, className, focusAfterCloseRef, modal, onCancel = require_noopFn.noopFn, onClick = require_noopFn.noopFn, onClose = require_noopFn.noopFn, onRequestClose = require_noopFn.noopFn, open = false, role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ariaLabel: deprecatedAriaLabel, ariaLabelledBy: deprecatedAriaLabelledBy, ariaDescribedBy: deprecatedAriaDescribedBy, ...rest }, forwardRef) => { const prefix = require_usePrefix.usePrefix(); const dialogId = require_useId.useId(); const [titleId, setTitleId] = (0, react.useState)(void 0); const [subtitleId, setSubtitleId] = (0, react.useState)(void 0); const backupRef = (0, react.useRef)(null); const ref = forwardRef ?? backupRef; function handleModalBackdropClick(e) { if (open && modal && e.target === ref.current) onRequestClose(e); } function handleClick(e) { handleModalBackdropClick(e); onClick(e); } (0, react.useEffect)(() => { if (ref.current) if (open) if (modal) ref.current.showModal(); else ref.current.show(); else ref.current.close(); }, [modal, open]); (0, react.useEffect)(() => { if (!open && focusAfterCloseRef) { const moveFocus = setTimeout(() => { focusAfterCloseRef.current?.focus(); }); return () => { clearTimeout(moveFocus); }; } }, [open, focusAfterCloseRef]); const containerClasses = (0, classnames.default)(`${prefix}--dialog-container`); const contextValue = { dialogId, titleId, subtitleId, isOpen: open, setTitleId, setSubtitleId }; (0, react.useEffect)(() => { const effectiveAriaLabel = ariaLabel || deprecatedAriaLabel; const effectiveAriaLabelledBy = ariaLabelledBy || deprecatedAriaLabelledBy; if (ref.current && open && !effectiveAriaLabel && !effectiveAriaLabelledBy) { const title = ref.current.querySelector(`.${prefix}--dialog-header__heading`); if (title && title.id) ref.current.setAttribute("aria-labelledby", title.id); } }, [ open, ariaLabel, deprecatedAriaLabel, ariaLabelledBy, deprecatedAriaLabelledBy, prefix ]); return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dialog", { ...rest, className: (0, classnames.default)(`${prefix}--dialog`, { [`${prefix}--dialog--modal`]: modal }, className), ref, onCancel, onClick: handleClick, onClose, role, "aria-label": ariaLabel || deprecatedAriaLabel, "aria-labelledby": !(ariaLabel || deprecatedAriaLabel) ? ariaLabelledBy || deprecatedAriaLabelledBy || titleId : void 0, "aria-describedby": ariaDescribedBy || deprecatedAriaDescribedBy, children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: containerClasses, children }) }) }); }); Dialog.displayName = "Dialog"; Dialog.propTypes = { children: prop_types.default.node, className: prop_types.default.string, focusAfterCloseRef: prop_types.default.oneOfType([prop_types.default.func, prop_types.default.shape({ current: prop_types.default.any })]), modal: prop_types.default.bool, onRequestClose: prop_types.default.func, open: prop_types.default.bool, role: prop_types.default.oneOf(["dialog", "alertdialog"]), "aria-label": prop_types.default.string, "aria-labelledby": prop_types.default.string, "aria-describedby": prop_types.default.string, ariaLabel: require_deprecate.deprecate(prop_types.default.string, "This prop syntax has been deprecated. Please use the new `aria-label`"), ariaLabelledBy: require_deprecate.deprecate(prop_types.default.string, "This prop syntax has been deprecated. Please use the new `aria-labelledby`"), ariaDescribedBy: require_deprecate.deprecate(prop_types.default.string, "This prop syntax has been deprecated. Please use the new `aria-describedby`") }; const DialogHeader = react.default.forwardRef(({ children, ...rest }, ref) => { return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `${require_usePrefix.usePrefix()}--dialog__header`, ref, ...rest, children }); }); DialogHeader.displayName = "DialogHeader"; DialogHeader.propTypes = { children: prop_types.default.node }; const DialogControls = react.default.forwardRef(({ children, ...rest }, ref) => { return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `${require_usePrefix.usePrefix()}--dialog__header-controls`, ref, ...rest, children }); }); DialogControls.displayName = "DialogControls"; DialogControls.propTypes = { children: prop_types.default.node }; const DialogCloseButton = react.default.forwardRef(({ onClick, ...rest }, ref) => { const prefix = require_usePrefix.usePrefix(); return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index.IconButton, { kind: "ghost", className: `${prefix}--dialog__close`, label: "Close", title: "Close", "aria-label": "Close", align: "left", onClick, ref, ...rest, children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_carbon_icons_react.Close, { size: 20, "aria-hidden": "true", tabIndex: -1, className: `${prefix}--icon__close` }) }); }); DialogCloseButton.displayName = "DialogCloseButton"; DialogCloseButton.propTypes = { onClick: prop_types.default.func }; const DialogTitle = react.default.forwardRef(({ children, className, id, ...rest }, ref) => { const prefix = require_usePrefix.usePrefix(); const { dialogId, setTitleId } = (0, react.useContext)(DialogContext); const defaultTitleId = `${prefix}--dialog-header__heading--${dialogId}`; const headingId = id ?? defaultTitleId; (0, react.useEffect)(() => { setTitleId?.(headingId); return () => setTitleId?.(void 0); }, [headingId, setTitleId]); return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, { as: "h2", id: headingId, className: (0, classnames.default)(`${prefix}--dialog-header__heading`, className), ref, ...rest, children }); }); DialogTitle.displayName = "DialogTitle"; DialogTitle.propTypes = { children: prop_types.default.node, className: prop_types.default.string, id: prop_types.default.string }; const DialogSubtitle = react.default.forwardRef(({ children, className, id, ...rest }, ref) => { const prefix = require_usePrefix.usePrefix(); const { dialogId, setSubtitleId } = (0, react.useContext)(DialogContext); const defaultSubtitleId = `${prefix}--dialog-header__label--${dialogId}`; const labelId = id ?? defaultSubtitleId; (0, react.useEffect)(() => { setSubtitleId?.(labelId); return () => setSubtitleId?.(void 0); }, [labelId, setSubtitleId]); return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, { as: "h2", id: labelId, className: (0, classnames.default)(`${prefix}--dialog-header__label`, className), ref, ...rest, children }); }); DialogSubtitle.displayName = "DialogSubtitle"; DialogSubtitle.propTypes = { children: prop_types.default.node, className: prop_types.default.string, id: prop_types.default.string }; const DialogBody = react.default.forwardRef(({ children, className, hasScrollingContent, ...rest }, ref) => { const prefix = require_usePrefix.usePrefix(); const contentRef = (0, react.useRef)(null); const dialogBodyId = `${prefix}--dialog-body--${require_useId.useId()}`; const { titleId, subtitleId } = (0, react.useContext)(DialogContext); const { height } = require_useResizeObserver.useResizeObserver({ ref: contentRef }); /** * isScrollable is implicitly dependent on height, when height gets updated * via `useResizeObserver`, clientHeight and scrollHeight get updated too */ const isScrollable = !!contentRef.current && contentRef?.current?.scrollHeight > contentRef?.current?.clientHeight; const contentClasses = (0, classnames.default)(`${prefix}--dialog-content`, { [`${prefix}--dialog-scroll-content`]: hasScrollingContent || isScrollable, [`${prefix}--dialog-scroll-content--no-fade`]: height <= 300 }, className); const hasScrollingContentProps = hasScrollingContent || isScrollable ? { tabIndex: 0, role: "region", "aria-labelledby": subtitleId ?? titleId } : {}; const combinedRef = (el) => { if (typeof ref === "function") ref(el); else if (ref) ref.current = el; contentRef.current = el; }; return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index$3.Layer, { ref: combinedRef, id: dialogBodyId, className: contentClasses, ...hasScrollingContentProps, ...rest, children }); }); DialogBody.displayName = "DialogBody"; DialogBody.propTypes = { children: prop_types.default.node, className: prop_types.default.string, hasScrollingContent: prop_types.default.bool }; const DialogFooter = react.default.forwardRef(({ children, className, onRequestClose = require_noopFn.noopFn, onSecondarySubmit, onRequestSubmit = require_noopFn.noopFn, primaryButtonText = "Save", primaryButtonDisabled = false, secondaryButtonText = "Cancel", secondaryButtons, loadingStatus = "inactive", loadingDescription, loadingIconDescription, onLoadingSuccess = require_noopFn.noopFn, danger = false, ...rest }, ref) => { const prefix = require_usePrefix.usePrefix(); const button = (0, react.useRef)(null); const { isOpen } = (0, react.useContext)(DialogContext); const [secondaryButtonRef, setSecondaryButtonRef] = (0, react.useState)(null); (0, react.useEffect)(() => { if (danger && secondaryButtonRef) { const focusFrame = requestAnimationFrame(() => { secondaryButtonRef.focus(); }); return () => cancelAnimationFrame(focusFrame); } }, [ danger, secondaryButtonRef, isOpen ]); const classes = (0, classnames.default)(`${prefix}--dialog-footer`, className, { [`${prefix}--dialog-footer--three-button`]: Array.isArray(secondaryButtons) && secondaryButtons.length === 2 }); const loadingActive = loadingStatus !== "inactive"; const primaryButtonClass = (0, classnames.default)({ [`${prefix}--btn--loading`]: loadingStatus !== "inactive" }); const onSecondaryButtonClick = onSecondarySubmit ? onSecondarySubmit : onRequestClose; if (children) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index$2.default, { className: classes, ref, ...rest, children }); return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_index$2.default, { className: classes, "aria-busy": loadingActive, ref, ...rest, children: [Array.isArray(secondaryButtons) && secondaryButtons.length <= 2 ? secondaryButtons.map(({ buttonText, onClick: onButtonClick }, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index$1.default, { autoFocus: danger, kind: "secondary", ref: i === 0 && danger ? setSecondaryButtonRef : void 0, onClick: onButtonClick, children: buttonText }, `${buttonText}-${i}`)) : secondaryButtonText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index$1.default, { ref: danger ? setSecondaryButtonRef : void 0, disabled: loadingActive, kind: "secondary", autoFocus: danger, onClick: onSecondaryButtonClick, children: secondaryButtonText }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index$1.default, { className: primaryButtonClass, kind: danger ? "danger" : "primary", disabled: loadingActive || primaryButtonDisabled, onClick: onRequestSubmit, ref: button, children: loadingStatus === "inactive" ? primaryButtonText : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_InlineLoading.default, { status: loadingStatus, description: loadingDescription, iconDescription: loadingIconDescription, className: `${prefix}--inline-loading--btn`, onSuccess: onLoadingSuccess }) })] }); }); DialogFooter.displayName = "DialogFooter"; DialogFooter.propTypes = { children: prop_types.default.node, className: prop_types.default.string, onRequestClose: prop_types.default.func, onSecondarySubmit: prop_types.default.func, onRequestSubmit: prop_types.default.func, primaryButtonText: prop_types.default.node, primaryButtonDisabled: prop_types.default.bool, secondaryButtonText: prop_types.default.node, secondaryButtons: (props, propName, componentName) => { if (props.secondaryButtons) { if (!Array.isArray(props.secondaryButtons) || props.secondaryButtons.length !== 2) return /* @__PURE__ */ new Error(`${propName} needs to be an array of two button config objects`); const shape = { buttonText: prop_types.default.node, onClick: prop_types.default.func }; props[propName].forEach((secondaryButton) => { prop_types.default.checkPropTypes(shape, secondaryButton, propName, componentName); }); } return null; }, danger: prop_types.default.bool, loadingStatus: prop_types.default.oneOf([ "inactive", "active", "finished", "error" ]), loadingDescription: prop_types.default.string, loadingIconDescription: prop_types.default.string, onLoadingSuccess: prop_types.default.func }; //#endregion exports.Dialog = Dialog; exports.DialogBody = DialogBody; exports.DialogCloseButton = DialogCloseButton; exports.DialogControls = DialogControls; exports.DialogFooter = DialogFooter; exports.DialogHeader = DialogHeader; exports.DialogSubtitle = DialogSubtitle; exports.DialogTitle = DialogTitle;