UNPKG

@carbon/react

Version:

React components for the Carbon Design System

559 lines (557 loc) 22.5 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_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;