@carbon/react
Version:
React components for the Carbon Design System
559 lines (557 loc) • 22.5 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_keys = require("../../internal/keyboard/keys.js");
const require_match = require("../../internal/keyboard/match.js");
const require_useIsomorphicEffect = require("../../internal/useIsomorphicEffect.js");
const require_useId = require("../../internal/useId.js");
const require_noopFn = require("../../internal/noopFn.js");
const require_warning = require("../../internal/warning.js");
const require_deprecate = require("../../prop-types/deprecate.js");
const require_deprecateValuesWithin = require("../../prop-types/deprecateValuesWithin.js");
const require_index = require("../FeatureFlags/index.js");
const require_useNoInteractiveChildren = require("../../internal/useNoInteractiveChildren.js");
const require_index$1 = require("../Button/index.js");
const require_wrapFocus = require("../../internal/wrapFocus.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/Notification/Notification.tsx
/**
* 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.
*/
/**
* Conditionally call a callback when the escape key is pressed
* @param {node} ref - ref of the container element to scope the functionality to
* @param {func} callback - function to be called
* @param {bool} override - escape hatch to conditionally call the callback
*/
function useEscapeToClose(ref, callback, override = true) {
const handleKeyDown = (event) => {
const elementContainsFocus = ref.current && document.activeElement === ref.current || ref.current?.contains(document.activeElement);
if (require_match.matches(event, [require_keys.Escape]) && override && elementContainsFocus) callback(event);
};
require_useIsomorphicEffect.default(() => {
if (ref.current !== null) document.addEventListener("keydown", handleKeyDown, false);
return () => document.removeEventListener("keydown", handleKeyDown, false);
});
}
function NotificationActionButton({ children, className: customClassName, onClick, inline, ...rest }) {
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index$1.default, {
className: (0, classnames.default)(customClassName, { [`${require_usePrefix.usePrefix()}--actionable-notification__action-button`]: true }),
kind: inline ? "ghost" : "tertiary",
onClick,
size: "sm",
...rest,
children
});
}
NotificationActionButton.propTypes = {
children: prop_types.default.node,
className: prop_types.default.string,
inline: prop_types.default.bool,
onClick: prop_types.default.func
};
function NotificationButton({ "aria-label": ariaLabel = "close notification", ariaLabel: deprecatedAriaLabel, className, type = "button", renderIcon: IconTag = _carbon_icons_react.Close, name, notificationType = "toast", ...rest }) {
const prefix = require_usePrefix.usePrefix();
const buttonClassName = (0, classnames.default)(className, { [`${prefix}--${notificationType}-notification__close-button`]: notificationType });
const iconClassName = (0, classnames.default)({ [`${prefix}--${notificationType}-notification__close-icon`]: notificationType });
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
...rest,
type,
"aria-label": deprecatedAriaLabel || ariaLabel,
title: deprecatedAriaLabel || ariaLabel,
className: buttonClassName,
children: IconTag && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTag, {
className: iconClassName,
name
})
});
}
NotificationButton.propTypes = {
["aria-label"]: prop_types.default.string,
ariaLabel: require_deprecate.deprecate(prop_types.default.string, "This prop syntax has been deprecated. Please use the new `aria-label`."),
className: prop_types.default.string,
name: prop_types.default.string,
notificationType: prop_types.default.oneOf([
"toast",
"inline",
"actionable"
]),
renderIcon: prop_types.default.oneOfType([prop_types.default.func, prop_types.default.object]),
type: prop_types.default.string
};
/**
* NotificationIcon
* ================
*/
const iconTypes = {
error: _carbon_icons_react.ErrorFilled,
success: _carbon_icons_react.CheckmarkFilled,
warning: _carbon_icons_react.WarningFilled,
["warning-alt"]: _carbon_icons_react.WarningAltFilled,
info: _carbon_icons_react.InformationFilled,
["info-square"]: _carbon_icons_react.InformationSquareFilled
};
function NotificationIcon({ iconDescription, kind, notificationType }) {
const prefix = require_usePrefix.usePrefix();
const IconForKind = iconTypes[kind];
if (!IconForKind) return null;
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconForKind, {
className: `${prefix}--${notificationType}-notification__icon`,
size: 20,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("title", { children: iconDescription })
});
}
NotificationIcon.propTypes = {
iconDescription: prop_types.default.string.isRequired,
kind: prop_types.default.oneOf([
"error",
"success",
"warning",
"warning-alt",
"info",
"info-square"
]).isRequired,
notificationType: prop_types.default.oneOf(["inline", "toast"]).isRequired
};
function ToastNotification({ ["aria-label"]: ariaLabel, ariaLabel: deprecatedAriaLabel, role = "status", onClose, onCloseButtonClick = require_noopFn.noopFn, statusIconDescription, className, children, kind = "error", lowContrast, hideCloseButton = false, timeout = 0, title, caption, subtitle, ...rest }) {
const [isOpen, setIsOpen] = (0, react.useState)(true);
const prefix = require_usePrefix.usePrefix();
const containerClassName = (0, classnames.default)(className, {
[`${prefix}--toast-notification`]: true,
[`${prefix}--toast-notification--low-contrast`]: lowContrast,
[`${prefix}--toast-notification--${kind}`]: kind
});
const contentRef = (0, react.useRef)(null);
require_useNoInteractiveChildren.useNoInteractiveChildren(contentRef);
const handleClose = (evt) => {
if (!onClose || onClose(evt) !== false) setIsOpen(false);
};
const ref = (0, react.useRef)(null);
function handleCloseButtonClick(event) {
onCloseButtonClick(event);
handleClose(event);
}
const savedOnClose = (0, react.useRef)(onClose);
(0, react.useEffect)(() => {
savedOnClose.current = onClose;
});
(0, react.useEffect)(() => {
if (!timeout) return;
const timeoutId = window.setTimeout((event) => {
setIsOpen(false);
if (savedOnClose.current) savedOnClose.current(event);
}, timeout);
return () => {
window.clearTimeout(timeoutId);
};
}, [timeout]);
if (!isOpen) return null;
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
ref,
...rest,
role,
className: containerClassName,
children: [
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationIcon, {
notificationType: "toast",
kind,
iconDescription: statusIconDescription || `${kind} icon`
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
ref: contentRef,
className: `${prefix}--toast-notification__details`,
children: [
title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "div",
className: `${prefix}--toast-notification__title`,
children: title
}),
subtitle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "div",
className: `${prefix}--toast-notification__subtitle`,
children: subtitle
}),
caption && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "div",
className: `${prefix}--toast-notification__caption`,
children: caption
}),
children
]
}),
!hideCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationButton, {
notificationType: "toast",
onClick: handleCloseButtonClick,
"aria-label": deprecatedAriaLabel || ariaLabel
})
]
});
}
ToastNotification.propTypes = {
["aria-label"]: prop_types.default.string,
ariaLabel: require_deprecate.deprecate(prop_types.default.string, "This prop syntax has been deprecated. Please use the new `aria-label`."),
caption: prop_types.default.string,
children: prop_types.default.node,
className: prop_types.default.string,
hideCloseButton: prop_types.default.bool,
kind: prop_types.default.oneOf([
"error",
"info",
"info-square",
"success",
"warning",
"warning-alt"
]),
lowContrast: prop_types.default.bool,
onClose: prop_types.default.func,
onCloseButtonClick: prop_types.default.func,
role: prop_types.default.oneOf([
"alert",
"log",
"status"
]),
statusIconDescription: prop_types.default.string,
subtitle: prop_types.default.string,
timeout: prop_types.default.number,
title: prop_types.default.string
};
function InlineNotification({ ["aria-label"]: ariaLabel, children, title, subtitle, role = "status", onClose, onCloseButtonClick = require_noopFn.noopFn, statusIconDescription, className, kind = "error", lowContrast, hideCloseButton = false, ...rest }) {
const [isOpen, setIsOpen] = (0, react.useState)(true);
const prefix = require_usePrefix.usePrefix();
const containerClassName = (0, classnames.default)(className, {
[`${prefix}--inline-notification`]: true,
[`${prefix}--inline-notification--low-contrast`]: lowContrast,
[`${prefix}--inline-notification--${kind}`]: kind,
[`${prefix}--inline-notification--hide-close-button`]: hideCloseButton
});
const contentRef = (0, react.useRef)(null);
require_useNoInteractiveChildren.useNoInteractiveChildren(contentRef);
const handleClose = (evt) => {
if (!onClose || onClose(evt) !== false) setIsOpen(false);
};
const ref = (0, react.useRef)(null);
function handleCloseButtonClick(event) {
onCloseButtonClick(event);
handleClose(event);
}
if (!isOpen) return null;
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
ref,
...rest,
role,
className: containerClassName,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
className: `${prefix}--inline-notification__details`,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationIcon, {
notificationType: "inline",
kind,
iconDescription: statusIconDescription || `${kind} icon`
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
ref: contentRef,
className: `${prefix}--inline-notification__text-wrapper`,
children: [
title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "div",
className: `${prefix}--inline-notification__title`,
children: title
}),
subtitle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "div",
className: `${prefix}--inline-notification__subtitle`,
children: subtitle
}),
children
]
})]
}), !hideCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationButton, {
notificationType: "inline",
onClick: handleCloseButtonClick,
"aria-label": ariaLabel
})]
});
}
InlineNotification.propTypes = {
["aria-label"]: prop_types.default.string,
children: prop_types.default.node,
className: prop_types.default.string,
hideCloseButton: prop_types.default.bool,
kind: prop_types.default.oneOf([
"error",
"info",
"info-square",
"success",
"warning",
"warning-alt"
]),
lowContrast: prop_types.default.bool,
onClose: prop_types.default.func,
onCloseButtonClick: prop_types.default.func,
role: prop_types.default.oneOf([
"alert",
"log",
"status"
]),
statusIconDescription: prop_types.default.string,
subtitle: prop_types.default.string,
title: prop_types.default.string
};
function ActionableNotification({ actionButtonLabel, ["aria-label"]: ariaLabel, ariaLabel: deprecatedAriaLabel, caption, children, role = "alertdialog", onActionButtonClick, onClose, onCloseButtonClick = require_noopFn.noopFn, statusIconDescription, className, inline = false, kind = "error", lowContrast, hideCloseButton = false, hasFocus = true, closeOnEscape = true, title, subtitle, ...rest }) {
const [isOpen, setIsOpen] = (0, react.useState)(true);
const prefix = require_usePrefix.usePrefix();
const id = require_useId.useId("actionable-notification");
const subtitleId = require_useId.useId("actionable-notification-subtitle");
const containerClassName = (0, classnames.default)(className, {
[`${prefix}--actionable-notification`]: true,
[`${prefix}--actionable-notification--toast`]: !inline,
[`${prefix}--actionable-notification--low-contrast`]: lowContrast,
[`${prefix}--actionable-notification--${kind}`]: kind,
[`${prefix}--actionable-notification--hide-close-button`]: hideCloseButton
});
const innerModal = (0, react.useRef)(null);
const startTrap = (0, react.useRef)(null);
const endTrap = (0, react.useRef)(null);
const ref = (0, react.useRef)(null);
const deprecatedFlag = require_index.useFeatureFlag("enable-experimental-focus-wrap-without-sentinels");
const focusTrapWithoutSentinels = require_index.useFeatureFlag("enable-focus-wrap-without-sentinels") || deprecatedFlag;
require_useIsomorphicEffect.default(() => {
if (hasFocus && role === "alertdialog") document.querySelector(`button.${prefix}--actionable-notification__action-button`)?.focus();
});
function handleBlur({ target: oldActiveNode, relatedTarget: currentActiveNode }) {
if (isOpen && currentActiveNode && oldActiveNode && role === "alertdialog") {
const { current: bodyNode } = innerModal;
const { current: startTrapNode } = startTrap;
const { current: endTrapNode } = endTrap;
require_wrapFocus.wrapFocus({
bodyNode,
startTrapNode,
endTrapNode,
currentActiveNode,
oldActiveNode,
prefix
});
}
}
function handleKeyDown(event) {
if (isOpen && require_match.match(event, require_keys.Tab) && ref.current && role === "alertdialog") require_wrapFocus.wrapFocusWithoutSentinels({
containerNode: ref.current,
currentActiveNode: event.target,
event
});
}
const handleClose = (evt) => {
if (!onClose || onClose(evt) !== false) setIsOpen(false);
};
useEscapeToClose(ref, handleCloseButtonClick, closeOnEscape);
function handleCloseButtonClick(event) {
onCloseButtonClick(event);
handleClose(event);
}
if (!isOpen) return null;
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
...rest,
ref,
role,
className: containerClassName,
"aria-labelledby": title ? id : subtitleId,
onBlur: !focusTrapWithoutSentinels ? handleBlur : () => {},
onKeyDown: focusTrapWithoutSentinels ? handleKeyDown : () => {},
children: [
!focusTrapWithoutSentinels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
ref: startTrap,
tabIndex: 0,
role: "link",
className: `${prefix}--visually-hidden`,
children: "Focus sentinel"
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
ref: innerModal,
className: `${prefix}--actionable-notification__focus-wrapper`,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
className: `${prefix}--actionable-notification__details`,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationIcon, {
notificationType: inline ? "inline" : "toast",
kind,
iconDescription: statusIconDescription || `${kind} icon`
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: `${prefix}--actionable-notification__text-wrapper`,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
className: `${prefix}--actionable-notification__content`,
children: [
title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "div",
className: `${prefix}--actionable-notification__title`,
id,
children: title
}),
subtitle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "div",
className: `${prefix}--actionable-notification__subtitle`,
id: subtitleId,
children: subtitle
}),
caption && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "div",
className: `${prefix}--actionable-notification__caption`,
children: caption
}),
children
]
})
})]
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
className: `${prefix}--actionable-notification__button-wrapper`,
children: [actionButtonLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationActionButton, {
onClick: onActionButtonClick,
inline,
children: actionButtonLabel
}), !hideCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationButton, {
"aria-label": deprecatedAriaLabel || ariaLabel,
notificationType: "actionable",
onClick: handleCloseButtonClick
})]
})]
}),
!focusTrapWithoutSentinels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
ref: endTrap,
tabIndex: 0,
role: "link",
className: `${prefix}--visually-hidden`,
children: "Focus sentinel"
})
]
});
}
ActionableNotification.propTypes = {
actionButtonLabel: prop_types.default.string,
["aria-label"]: prop_types.default.string,
ariaLabel: require_deprecate.deprecate(prop_types.default.string, "This prop syntax has been deprecated. Please use the new `aria-label`."),
caption: prop_types.default.string,
children: prop_types.default.node,
className: prop_types.default.string,
closeOnEscape: prop_types.default.bool,
hasFocus: require_deprecate.deprecate(prop_types.default.bool, "hasFocus is deprecated. To conform to accessibility requirements hasFocus should always be `true` for ActionableNotification. If you were setting this prop to `false`, consider using the Callout component instead."),
hideCloseButton: prop_types.default.bool,
inline: prop_types.default.bool,
kind: prop_types.default.oneOf([
"error",
"info",
"info-square",
"success",
"warning",
"warning-alt"
]),
lowContrast: prop_types.default.bool,
onActionButtonClick: prop_types.default.func,
onClose: prop_types.default.func,
onCloseButtonClick: prop_types.default.func,
role: prop_types.default.string,
statusIconDescription: prop_types.default.string,
subtitle: prop_types.default.node,
title: prop_types.default.string
};
const mapping = {
error: "warning",
success: "info"
};
const propMappingFunction = (deprecatedValue) => {
return mapping[deprecatedValue];
};
function Callout({ actionButtonLabel, children, onActionButtonClick, title, titleId, subtitle, statusIconDescription, className, kind = "info", lowContrast, ...rest }) {
const prefix = require_usePrefix.usePrefix();
const containerClassName = (0, classnames.default)(className, {
[`${prefix}--actionable-notification`]: true,
[`${prefix}--actionable-notification--low-contrast`]: lowContrast,
[`${prefix}--actionable-notification--${kind}`]: kind,
[`${prefix}--actionable-notification--hide-close-button`]: true
});
const childrenContainer = (0, react.useRef)(null);
require_useNoInteractiveChildren.useInteractiveChildrenNeedDescription(childrenContainer, `interactive child node(s) should have an \`aria-describedby\` property with a value matching the value of \`titleId\``);
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
...rest,
className: containerClassName,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
className: `${prefix}--actionable-notification__details`,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationIcon, {
notificationType: "inline",
kind,
iconDescription: statusIconDescription || `${kind} icon`
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
ref: childrenContainer,
className: `${prefix}--actionable-notification__text-wrapper`,
children: [
title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "div",
id: titleId,
className: `${prefix}--actionable-notification__title`,
children: title
}),
subtitle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "div",
className: `${prefix}--actionable-notification__subtitle`,
children: subtitle
}),
children
]
})]
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: `${prefix}--actionable-notification__button-wrapper`,
children: actionButtonLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NotificationActionButton, {
onClick: onActionButtonClick,
"aria-describedby": titleId,
inline: true,
children: actionButtonLabel
})
})]
});
}
Callout.propTypes = {
actionButtonLabel: prop_types.default.string,
children: prop_types.default.node,
className: prop_types.default.string,
kind: require_deprecateValuesWithin.deprecateValuesWithin(prop_types.default.oneOf([
"error",
"info",
"info-square",
"success",
"warning",
"warning-alt"
]), ["warning", "info"], propMappingFunction),
lowContrast: prop_types.default.bool,
onActionButtonClick: prop_types.default.func,
statusIconDescription: prop_types.default.string,
subtitle: prop_types.default.node,
title: prop_types.default.string,
titleId: prop_types.default.string
};
let didWarnAboutDeprecation = false;
const StaticNotification = (props) => {
if (process.env.NODE_ENV !== "production") {
require_warning.warning(didWarnAboutDeprecation, "`StaticNotification` has been renamed to `Callout`.Run the following codemod to automatically update usages in yourproject: `npx @carbon/upgrade migrate refactor-to-callout --write`");
didWarnAboutDeprecation = true;
}
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Callout, { ...props });
};
//#endregion
exports.ActionableNotification = ActionableNotification;
exports.Callout = Callout;
exports.InlineNotification = InlineNotification;
exports.NotificationActionButton = NotificationActionButton;
exports.NotificationButton = NotificationButton;
exports.StaticNotification = StaticNotification;
exports.ToastNotification = ToastNotification;