UNPKG

@mskcc/carbon-react

Version:

Carbon react components for the MSKCC DSM

624 lines (610 loc) 19 kB
/** * MSKCC 2021, 2024 */ import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js'; import PropTypes from 'prop-types'; import React__default, { useState, useRef, useEffect } from 'react'; import deprecate from '../../prop-types/deprecate.js'; import cx from 'classnames'; import Button from '../Button/Button.js'; import '../Button/Button.Skeleton.js'; import useIsomorphicEffect from '../../internal/useIsomorphicEffect.js'; import { useNoInteractiveChildren } from '../../internal/useNoInteractiveChildren.js'; import { usePrefix } from '../../internal/usePrefix.js'; import { useId } from '../../internal/useId.js'; import { Icon } from '../Icon/MskIcon.js'; import { matches } from '../../internal/keyboard/match.js'; import { Escape } from '../../internal/keyboard/keys.js'; /** * 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) { let override = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; const handleKeyDown = event => { // The callback should only be called when focus is on or within the container const elementContainsFocus = ref.current && document.activeElement === ref.current || ref.current.contains(document.activeElement); if (matches(event, [Escape]) && override && elementContainsFocus) { callback(event); } }; useIsomorphicEffect(() => { document.addEventListener('keydown', handleKeyDown, false); return () => document.removeEventListener('keydown', handleKeyDown, false); }); } function NotificationActionButton(_ref) { let { children, className: customClassName, onClick, ...rest } = _ref; const prefix = usePrefix(); const className = cx(customClassName, { [`${prefix}--actionable-notification__action-button`]: true }); return /*#__PURE__*/React__default.createElement(Button, _extends({ className: className, kind: "tertiary", onClick: onClick }, rest), children); } NotificationActionButton.propTypes = { /** * Specify the content of the notification action button. */ children: PropTypes.node, /** * Specify an optional className to be applied to the notification action button */ className: PropTypes.string, /** * Specify if the visual treatment of the button should be for an inline notification */ inline: PropTypes.bool, /** * Optionally specify a click handler for the notification action button. */ onClick: PropTypes.func }; /** * NotificationButton * ================== */ function NotificationButton(_ref2) { let { 'aria-label': ariaLabel, // @ts-expect-error: deprecated prop ariaLabel: deprecatedAriaLabel, className, type, // renderIcon: IconTag, name = 'clear', notificationType, ...rest } = _ref2; const prefix = usePrefix(); const buttonClassName = cx(className, { [`${prefix}--${notificationType}-notification__close-button`]: notificationType }); const iconClassName = cx({ [`${prefix}--${notificationType}-notification__close-icon`]: notificationType }); return /*#__PURE__*/React__default.createElement("button", _extends({}, rest, { // eslint-disable-next-line react/button-has-type type: type, "aria-label": deprecatedAriaLabel || ariaLabel, title: deprecatedAriaLabel || ariaLabel, className: buttonClassName }), /*#__PURE__*/React__default.createElement(Icon, { className: iconClassName, icon: name })); } NotificationButton.propTypes = { /** * Specify a label to be read by screen readers on the container node */ ['aria-label']: PropTypes.string, /** * Deprecated, please use `aria-label` instead. * Specify a label to be read by screen readers on the container note. */ ariaLabel: deprecate(PropTypes.string, 'This prop syntax has been deprecated. Please use the new `aria-label`.'), /** * Specify an optional className to be applied to the notification button */ className: PropTypes.string, /** * Specify an optional icon for the Button through a string, * if something but regular "close" icon is desirable */ name: PropTypes.string, /** * Specify the notification type */ notificationType: PropTypes.oneOf(['toast', 'inline', 'actionable']), // /** // * Optional prop to allow overriding the icon rendering. // * Can be a React component class // */ // renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), /** * Optional prop to specify the type of the Button */ type: PropTypes.string }; /** * NotificationIcon * ================ */ function NotificationIcon(_ref3) { let { kind, notificationType } = _ref3; const prefix = usePrefix(); if (!kind) { return null; } function switchIcon() { switch (kind) { case 'error': return 'error'; case 'success': return 'check_circle'; case 'warning': return 'warning'; case 'info': default: return 'info'; } } return /*#__PURE__*/React__default.createElement(Icon, { icon: switchIcon(), className: `${prefix}--${notificationType}-notification__icon` }); } NotificationIcon.propTypes = { iconDescription: PropTypes.string.isRequired, kind: PropTypes.oneOf(['error', 'success', 'warning', 'info']).isRequired, notificationType: PropTypes.oneOf(['inline', 'toast']).isRequired }; /** * ToastNotification * ================= */ function ToastNotification(_ref4) { let { ['aria-label']: ariaLabel, // @ts-expect-error: deprecated prop ariaLabel: deprecatedAriaLabel, role = 'status', onClose, onCloseButtonClick, statusIconDescription, className, children, kind, lowContrast, hideCloseButton, timeout, title, caption, subtitle, ...rest } = _ref4; const [isOpen, setIsOpen] = useState(true); const prefix = usePrefix(); const containerClassName = cx(className, { [`${prefix}--toast-notification`]: true, [`${prefix}--toast-notification--low-contrast`]: lowContrast, [`${prefix}--toast-notification--${kind}`]: kind }); const contentRef = useRef(null); useNoInteractiveChildren(contentRef); const handleClose = evt => { if (!onClose || onClose(evt) !== false) { setIsOpen(false); } }; const ref = useRef(null); function handleCloseButtonClick(event) { onCloseButtonClick(event); handleClose(event); } const savedOnClose = useRef(onClose); useEffect(() => { savedOnClose.current = onClose; }); 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__*/React__default.createElement("div", _extends({ ref: ref }, rest, { role: role, className: containerClassName }), /*#__PURE__*/React__default.createElement(NotificationIcon, { notificationType: "toast", kind: kind, iconDescription: statusIconDescription || `${kind} icon` }), /*#__PURE__*/React__default.createElement("div", { ref: contentRef, className: `${prefix}--toast-notification__details` }, title && /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--toast-notification__title` }, title), subtitle && /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--toast-notification__subtitle` }, subtitle), caption && /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--toast-notification__caption` }, caption), children), !hideCloseButton && /*#__PURE__*/React__default.createElement(NotificationButton, { notificationType: "toast", onClick: handleCloseButtonClick, "aria-hidden": "true", "aria-label": deprecatedAriaLabel || ariaLabel, tabIndex: -1 })); } ToastNotification.propTypes = { /** * Provide a description for "close" icon button that can be read by screen readers */ ['aria-label']: PropTypes.string, /** * Deprecated, please use `aria-label` instead. * Provide a description for "close" icon button that can be read by screen readers */ ariaLabel: deprecate(PropTypes.string, 'This prop syntax has been deprecated. Please use the new `aria-label`.'), /** * Specify the caption */ caption: PropTypes.string, /** * Specify the content */ children: PropTypes.node, /** * Specify an optional className to be applied to the notification box */ className: PropTypes.string, /** * Specify the close button should be disabled, or not */ hideCloseButton: PropTypes.bool, /** * Specify what state the notification represents */ kind: PropTypes.oneOf(['error', 'info', 'success', 'warning']), /** * Specify whether you are using the low contrast variant of the ToastNotification. */ lowContrast: PropTypes.bool, /** * Provide a function that is called when menu is closed */ onClose: PropTypes.func, /** * Provide a function that is called when the close button is clicked */ onCloseButtonClick: PropTypes.func, /** * By default, this value is "status". You can also provide an alternate * role if it makes sense from the accessibility-side */ role: PropTypes.oneOf(['alert', 'log', 'status']), /** * Provide a description for "status" icon that can be read by screen readers */ statusIconDescription: PropTypes.string, /** * Specify the subtitle */ subtitle: PropTypes.string, /** * Specify an optional duration the notification should be closed in */ timeout: PropTypes.number, /** * Specify the title */ title: PropTypes.string }; /** * InlineNotification * ================== */ function InlineNotification(_ref5) { let { ['aria-label']: ariaLabel, children, title, subtitle, role = 'status', onClose = () => {}, onCloseButtonClick, statusIconDescription, className, kind = 'error', lowContrast, hideCloseButton = false, ...rest } = _ref5; const [isOpen, setIsOpen] = useState(true); const prefix = usePrefix(); const containerClassName = cx(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 = useRef(null); useNoInteractiveChildren(contentRef); const handleClose = evt => { if (!onClose || onClose(evt) !== false) { setIsOpen(false); } }; const ref = useRef(null); function handleCloseButtonClick(event) { onCloseButtonClick(event); handleClose(event); } if (!isOpen) { return null; } return /*#__PURE__*/React__default.createElement("div", _extends({ ref: ref }, rest, { role: role, className: containerClassName }), /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--inline-notification__details` }, /*#__PURE__*/React__default.createElement(NotificationIcon, { notificationType: "inline", kind: kind, iconDescription: statusIconDescription || `${kind} icon` }), /*#__PURE__*/React__default.createElement("div", { ref: contentRef, className: `${prefix}--inline-notification__text-wrapper` }, title && /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--inline-notification__title` }, title), subtitle && /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--inline-notification__subtitle` }, subtitle), children)), !hideCloseButton && /*#__PURE__*/React__default.createElement(NotificationButton, { notificationType: "inline", onClick: handleCloseButtonClick, "aria-hidden": "true", "aria-label": ariaLabel, tabIndex: -1 })); } InlineNotification.propTypes = { /** * Provide a description for "close" icon button that can be read by screen readers */ ['aria-label']: PropTypes.string, /** * Specify the content */ children: PropTypes.node, /** * Specify an optional className to be applied to the notification box */ className: PropTypes.string, /** * Specify the close button should be disabled, or not */ hideCloseButton: PropTypes.bool, /** * Specify what state the notification represents */ kind: PropTypes.oneOf(['error', 'info', 'success', 'warning']), /** * Specify whether you are using the low contrast variant of the InlineNotification. */ lowContrast: PropTypes.bool, /** * Provide a function that is called when menu is closed */ onClose: PropTypes.func, /** * Provide a function that is called when the close button is clicked */ onCloseButtonClick: PropTypes.func, /** * By default, this value is "status". You can also provide an alternate * role if it makes sense from the accessibility-side. */ role: PropTypes.oneOf(['alert', 'log', 'status']), /** * Provide a description for "status" icon that can be read by screen readers */ statusIconDescription: PropTypes.string, /** * Specify the subtitle */ subtitle: PropTypes.string, /** * Specify the title */ title: PropTypes.string }; /** * ActionableNotification * ====================== */ function ActionableNotification(_ref6) { let { actionButtonLabel, ['aria-label']: ariaLabel, // @ts-expect-error: deprecated prop ariaLabel: deprecatedAriaLabel, children, role = 'status', onActionButtonClick, onClose, onCloseButtonClick, statusIconDescription, className, inline, kind, lowContrast, hideCloseButton, hasFocus, closeOnEscape, title, subtitle, ...rest } = _ref6; const [isOpen, setIsOpen] = useState(true); const prefix = usePrefix(); const id = useId('actionable-notification'); const containerClassName = cx(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 ref = useRef(null); useIsomorphicEffect(() => { if (ref.current && hasFocus) { ref.current.focus(); } }); 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__*/React__default.createElement("div", _extends({}, rest, { ref: ref, role: role, className: containerClassName, "aria-labelledby": title ? id : undefined }), /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--actionable-notification__details` }, /*#__PURE__*/React__default.createElement(NotificationIcon, { notificationType: inline ? 'inline' : 'toast', kind: kind, iconDescription: statusIconDescription || `${kind} icon` }), /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--actionable-notification__text-wrapper` }, /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--actionable-notification__content` }, title && /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--actionable-notification__title`, id: id }, title), subtitle && /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--actionable-notification__subtitle` }, subtitle), children))), actionButtonLabel && /*#__PURE__*/React__default.createElement(NotificationActionButton, { onClick: onActionButtonClick }, actionButtonLabel), !hideCloseButton && /*#__PURE__*/React__default.createElement(NotificationButton, { "aria-label": deprecatedAriaLabel || ariaLabel, notificationType: "actionable", onClick: handleCloseButtonClick })); } ActionableNotification.propTypes = { /** * Pass in the action button label that will be rendered within the ActionableNotification. */ actionButtonLabel: PropTypes.string, /** * Provide a description for "close" icon button that can be read by screen readers */ ['aria-label']: PropTypes.string, /** * Deprecated, please use `aria-label` instead. * Provide a description for "close" icon button that can be read by screen readers */ ariaLabel: deprecate(PropTypes.string, 'This prop syntax has been deprecated. Please use the new `aria-label`.'), /** * Specify the content */ children: PropTypes.node, /** * Specify an optional className to be applied to the notification box */ className: PropTypes.string, /** * Specify if pressing the escape key should close notifications */ closeOnEscape: PropTypes.bool, /** * Specify if focus should be moved to the component when the notification contains actions */ hasFocus: PropTypes.bool, /** * Specify the close button should be disabled, or not */ hideCloseButton: PropTypes.bool, /* * Specify if the notification should have inline styling applied instead of toast */ inline: PropTypes.bool, /** * Specify what state the notification represents */ kind: PropTypes.oneOf(['error', 'info', 'success', 'warning']).isRequired, /** * Specify whether you are using the low contrast variant of the ActionableNotification. */ lowContrast: PropTypes.bool, /** * Provide a function that is called when the action is clicked */ onActionButtonClick: PropTypes.func, /** * Provide a function that is called when menu is closed */ onClose: PropTypes.func, /** * Provide a function that is called when the close button is clicked */ onCloseButtonClick: PropTypes.func, /** * By default, this value is "alertdialog". You can also provide an alternate * role if it makes sense from the accessibility-side. */ role: PropTypes.string, /** * Provide a description for "status" icon that can be read by screen readers */ statusIconDescription: PropTypes.string, /** * Specify the subtitle */ subtitle: PropTypes.node, /** * Specify the title */ title: PropTypes.string }; export { ActionableNotification, InlineNotification, NotificationActionButton, NotificationButton, ToastNotification };