@carbon/react
Version:
React components for the Carbon Design System
359 lines (357 loc) • 14.7 kB
JavaScript
/**
* 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;