@carbon/ibm-products
Version:
Carbon for IBM Products
564 lines (555 loc) • 23 kB
JavaScript
/**
* Copyright IBM Corp. 2020, 2025
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
;
var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js');
var react = require('@carbon/react');
var utilities = require('@carbon/utilities');
var icons = require('@carbon/react/icons');
var React = require('react');
require('../EmptyStates/EmptyState.js');
require('../EmptyStates/EmptyStateV2.js');
require('../EmptyStates/ErrorEmptyState/ErrorEmptyState.js');
require('../EmptyStates/NoDataEmptyState/NoDataEmptyState.js');
require('../EmptyStates/NoTagsEmptyState/NoTagsEmptyState.js');
require('../EmptyStates/NotFoundEmptyState/NotFoundEmptyState.js');
var NotificationsEmptyState = require('../EmptyStates/NotificationsEmptyState/NotificationsEmptyState.js');
require('../EmptyStates/UnauthorizedEmptyState/UnauthorizedEmptyState.js');
var index = require('../../_virtual/index.js');
var cx = require('classnames');
var usePrefersReducedMotion = require('../../global/js/hooks/usePrefersReducedMotion.js');
var devtools = require('../../global/js/utils/devtools.js');
var propsHelper = require('../../global/js/utils/props-helper.js');
var getSupportedLocale = require('../../global/js/utils/getSupportedLocale.js');
var useId = require('../../global/js/utils/useId.js');
var settings = require('../../settings.js');
var utils = require('./utils.js');
var usePresence = require('../../global/js/hooks/usePresence.js');
var useClickOutside = require('../../global/js/hooks/useClickOutside.js');
var _Close;
// The block part of our conventional BEM class names (blockClass__E--M).
const componentName = 'NotificationsPanel';
const blockClass = `${settings.pkg.prefix}--notifications-panel`;
const DefaultLocale = 'en-US';
// Default values for props
const defaults = {
dateTimeStyle: 'long',
daysAgoText: value => `${value} days ago`,
dismissAllLabel: 'Dismiss all',
dismissSingleNotificationIconDescription: 'Dismiss',
doNotDisturbLabel: 'Do not disturb',
emptyStateLabel: 'You do not have any notifications',
hourAgoText: value => `${value} hour ago`,
hoursAgoText: value => `${value} hours ago`,
illustrationTheme: 'light',
minuteAgoText: value => `${value} minute ago`,
minutesAgoText: value => `${value} minutes ago`,
monthAgoText: value => `${value} month ago`,
monthsAgoText: value => `${value} months ago`,
nowText: 'Now',
onDismissAllNotifications: () => {},
onDismissSingleNotification: () => {},
previousLabel: 'Previous',
readLessLabel: 'Read less',
readMoreLabel: 'Read more',
secondsAgoText: value => `${value} seconds ago`,
settingsIconDescription: 'Settings',
title: 'Notifications',
todayLabel: 'Today',
viewAllLabel: value => `View all (${value})`,
yearAgoText: value => `${value} year ago`,
yearsAgoText: value => `${value} years ago`,
yesterdayAtText: value => `Yesterday at ${value}`,
yesterdayLabel: 'Yesterday'
};
/**
* The `NotificationsPanel` sets expectations on the behavior for notifications,
* allowing the user to view and interact with them all in one place.
*
* **To adopt the new localization:**
*
* **Step 1:** Provide a locale to the `dateTimeLocale` prop, such as "de" or "fr-CA".
*
* **Step 2:** Remove the now _deprecated_ props: `daysAgoText`,
* `hourAgoText`, `hoursAgoText`, `minuteAgoText`, `minutesAgoText`,
* `monthAgoText`, `monthsAgoText`, `nowText`, `secondsAgoText`,
* `yearAgoText`, `yearsAgoText`, `yesterdayAtText`.
*
* If you do not provide a locale, the deprecated props will be applied instead.
*/
exports.NotificationsPanel = /*#__PURE__*/React.forwardRef((_ref, ref) => {
let {
// The component props, in alphabetical order (for consistency).
className,
data,
dateTimeLocale,
dateTimeStyle = defaults.dateTimeStyle,
daysAgoText = defaults.daysAgoText,
dismissAllLabel = defaults.dismissAllLabel,
dismissSingleNotificationIconDescription = defaults.dismissSingleNotificationIconDescription,
doNotDisturbDefaultToggled,
doNotDisturbLabel = defaults.doNotDisturbLabel,
emptyStateLabel = defaults.emptyStateLabel,
hourAgoText = defaults.hourAgoText,
hoursAgoText = defaults.hoursAgoText,
illustrationTheme = defaults.illustrationTheme,
minuteAgoText = defaults.minuteAgoText,
minutesAgoText = defaults.minutesAgoText,
monthAgoText = defaults.monthAgoText,
monthsAgoText = defaults.monthsAgoText,
nowText = defaults.nowText,
onClickOutside,
onDismissAllNotifications = defaults.onDismissAllNotifications,
onDismissSingleNotification = defaults.onDismissSingleNotification,
onDoNotDisturbChange,
onSettingsClick,
onViewAllClick,
open,
previousLabel = defaults.previousLabel,
readLessLabel = defaults.readLessLabel,
readMoreLabel = defaults.readMoreLabel,
secondsAgoText = defaults.secondsAgoText,
settingsIconDescription = defaults.settingsIconDescription,
title = defaults.title,
todayLabel = defaults.todayLabel,
viewAllLabel = defaults.viewAllLabel,
yearAgoText = defaults.yearAgoText,
yearsAgoText = defaults.yearsAgoText,
yesterdayAtText = defaults.yesterdayAtText,
yesterdayLabel = defaults.yesterdayLabel,
triggerButtonRef,
// Collect any other property values passed in.
...rest
} = _ref;
const notificationPanelRef = React.useRef(null);
const notificationPanelInnerRef = React.useRef(null);
React.useRef(null);
React.useRef(null);
const [allNotifications, setAllNotifications] = React.useState([]);
const supportedLocale = getSupportedLocale.getSupportedLocale(dateTimeLocale, DefaultLocale);
react.usePrefix();
const headingId = useId.useId();
const reducedMotion = usePrefersReducedMotion.usePrefersReducedMotion();
const exitAnimationName = reducedMotion ? 'notifications-panel-exit-reduced' : 'notifications-panel-fade-out';
const {
shouldRender
} = usePresence.usePresence(open, notificationPanelRef, exitAnimationName);
React.useEffect(() => {
// Set the notifications passed to the state within this component
setAllNotifications(data);
}, [data]);
useClickOutside.useClickOutside(ref || notificationPanelRef, () => {
onClickOutside?.();
});
const handleKeydown = event => {
event.stopPropagation();
if (event.key === 'Escape') {
onClickOutside?.();
setTimeout(() => {
triggerButtonRef?.current?.focus();
}, 100);
}
};
React.useEffect(() => {
// initialize the notification panel to open
if (open) {
const observer = new MutationObserver(() => {
if (notificationPanelRef.current) {
const parentElement = notificationPanelRef.current;
parentElement?.querySelector(`.${blockClass}__dismiss-button`)?.focus();
observer.disconnect();
}
});
if (notificationPanelRef.current) {
const parentElement = notificationPanelRef.current;
const button = parentElement?.querySelector(`.${blockClass}__dismiss-button`);
button?.focus();
} else {
observer.observe(document.body, {
childList: true,
subtree: true
});
}
return () => observer.disconnect();
}
}, [open]);
const sortChronologically = arr => {
if (!arr || arr && !arr.length) {
return;
}
return arr.sort((a, b) => b.timestamp - a.timestamp);
};
// Notifications should be grouped by "Today", "Yesterday", and "Previous", the variables
// below filter the notifications based on those conditions and then render them in those groups
let yesterdayDate = new Date();
yesterdayDate = new Date(yesterdayDate.setDate(yesterdayDate.getDate() - 1));
let dayBeforeYesterdayDate = new Date();
dayBeforeYesterdayDate = new Date(dayBeforeYesterdayDate.setDate(dayBeforeYesterdayDate.getDate() - 2));
let withinLastDayNotifications = allNotifications && allNotifications.length && allNotifications.filter(item => item.timestamp.getTime() >= yesterdayDate.getTime());
withinLastDayNotifications = sortChronologically(withinLastDayNotifications);
let previousDayNotifications = allNotifications && allNotifications.length && allNotifications.filter(item => item.timestamp.getTime() < yesterdayDate.getTime() && item.timestamp.getTime() >= dayBeforeYesterdayDate.getTime());
previousDayNotifications = sortChronologically(previousDayNotifications);
let previousNotifications = allNotifications && allNotifications.length && allNotifications.filter(item => item.timestamp.getTime() < dayBeforeYesterdayDate.getTime());
previousNotifications = sortChronologically(previousNotifications);
const renderDescription = id => {
const notification = allNotifications && allNotifications.length && allNotifications.filter(item => item.id === id)[0];
const trimLength = 88;
const description = notification?.['description'];
const descriptionClassName = cx([`${blockClass}__notification-description`, {
[`${blockClass}__notification-long-description`]: notification?.['showAll'],
[`${blockClass}__notification-short-description`]: !notification?.['showAll']
}]);
const showMoreButtonClassName = cx([{
[`${blockClass}__notification-read-less-button`]: notification?.['showAll'],
[`${blockClass}__notification-read-more-button`]: !notification?.['showAll']
}]);
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("p", {
className: descriptionClassName
}, description), description.length > trimLength && /*#__PURE__*/React.createElement(react.Button, {
kind: "ghost",
size: "sm",
renderIcon: props => /*#__PURE__*/React.createElement(icons.ChevronDown, _rollupPluginBabelHelpers.extends({
size: 16
}, props)),
iconDescription: notification?.['showAll'] ? readLessLabel : readMoreLabel,
onClick: event => {
event.preventDefault();
event.stopPropagation();
const newData = allNotifications.map(item => {
if (item.id === notification?.['id']) {
return Object.assign({}, item, {
showAll: !item?.['showAll']
});
}
return item;
});
setAllNotifications(newData);
},
className: showMoreButtonClassName
}, notification?.['showAll'] ? readLessLabel : readMoreLabel));
};
const renderNotification = (group, notification, index) => {
const notificationClassName = cx([`${blockClass}__notification`, `${blockClass}__notification-${group}`]);
const notificationHeaderClassName = cx([`${blockClass}__notification-title`, {
[`${blockClass}__notification-title-unread`]: notification.unread
}]);
return /*#__PURE__*/React.createElement(react.Section, {
key: `${notification.timestamp}-${notification.title}-${index}`,
className: notificationClassName,
as: "div",
role: "button",
tabIndex: 0,
onClick: () => notification.onNotificationClick(notification),
onKeyDown: event => {
if (event.target instanceof HTMLElement && event.target.classList.contains(`${blockClass}__dismiss-single-button`)) {
return;
}
if (event.which === 13) {
notification.onNotificationClick(notification);
}
}
}, notification.type === 'error' && /*#__PURE__*/React.createElement(icons.ErrorFilled, {
size: 16,
className: cx([`${blockClass}__notification-status-icon`, `${blockClass}__notification-status-icon-error`])
}), notification.type === 'success' && /*#__PURE__*/React.createElement(icons.CheckmarkFilled, {
size: 16,
className: cx([`${blockClass}__notification-status-icon`, `${blockClass}__notification-status-icon-success`])
}), notification.type === 'warning' && /*#__PURE__*/React.createElement(icons.WarningAltFilled, {
size: 16,
className: cx([`${blockClass}__notification-status-icon`, `${blockClass}__notification-status-icon-warning`])
}), notification.type === 'informational' && /*#__PURE__*/React.createElement(icons.InformationSquareFilled, {
size: 16,
className: cx([`${blockClass}__notification-status-icon`, `${blockClass}__notification-status-icon-informational`])
}), /*#__PURE__*/React.createElement("div", {
className: `${blockClass}__notification-content`
}, /*#__PURE__*/React.createElement("p", {
className: `${blockClass}__notification-time-label`
}, dateTimeLocale ? utilities.dateTimeFormat.relative.format(notification.timestamp, {
locale: supportedLocale,
style: dateTimeStyle
}) : utils.timeAgo({
previousTime: notification.timestamp,
secondsAgoText,
minuteAgoText,
minutesAgoText,
hoursAgoText,
hourAgoText,
daysAgoText,
yesterdayAtText,
monthsAgoText,
monthAgoText,
yearsAgoText,
yearAgoText,
nowText
})), /*#__PURE__*/React.createElement(react.Heading, {
className: notificationHeaderClassName
}, notification.title), notification.description && notification.description.length && renderDescription(notification.id), notification.link && notification.link.text && notification.link.url && /*#__PURE__*/React.createElement(react.Link, _rollupPluginBabelHelpers.extends({
href: notification.link.url,
className: `${blockClass}__notifications-link`
}, propsHelper.prepareProps({}, notification.link, ['text', 'url'])), notification.link.text)), /*#__PURE__*/React.createElement(react.IconButton, {
align: "left",
kind: "ghost",
size: "sm",
label: dismissSingleNotificationIconDescription,
className: `${blockClass}__dismiss-single-button`,
onClick: event => dismissSingleNotification(event, notification)
}, _Close || (_Close = /*#__PURE__*/React.createElement(icons.Close, {
size: 16
}))));
};
const dismissSingleNotification = (event, notification) => {
event.preventDefault();
event.stopPropagation();
onDismissSingleNotification(notification);
};
const mainSectionClassName = cx([`${blockClass}__main-section`, {
[`${blockClass}__main-section-empty`]: allNotifications && !allNotifications.length
}]);
return shouldRender ? /*#__PURE__*/React.createElement(react.Section, _rollupPluginBabelHelpers.extends({
as: "div",
role: "dialog",
"aria-labelledby": headingId,
onKeyDown: handleKeydown,
tabIndex: 0
}, rest, {
id: blockClass,
className: cx(blockClass, className, `${blockClass}__container`, {
[`${blockClass}__entrance`]: open,
[`${blockClass}__exit`]: !open
}),
ref: ref || notificationPanelRef
}, devtools.getDevtoolsProps(componentName)), /*#__PURE__*/React.createElement("div", {
ref: notificationPanelInnerRef
}, /*#__PURE__*/React.createElement("div", {
className: `${blockClass}__header-container`
}, /*#__PURE__*/React.createElement("div", {
className: `${blockClass}__header-flex`
}, /*#__PURE__*/React.createElement(react.Heading, {
id: headingId,
className: `${blockClass}__header`
}, title), /*#__PURE__*/React.createElement(react.Button, {
size: "sm",
kind: "ghost",
className: `${blockClass}__dismiss-button`,
onClick: onDismissAllNotifications
}, dismissAllLabel)), onDoNotDisturbChange && /*#__PURE__*/React.createElement(react.Toggle, {
size: "sm",
className: `${blockClass}__do-not-disturb-toggle`,
id: `${blockClass}__do-not-disturb-toggle-component`,
labelA: doNotDisturbLabel,
labelB: doNotDisturbLabel,
onToggle: event => onDoNotDisturbChange(event),
defaultToggled: doNotDisturbDefaultToggled,
"aria-label": doNotDisturbLabel,
labelText: doNotDisturbLabel
})), /*#__PURE__*/React.createElement(react.Section, {
className: mainSectionClassName
}, withinLastDayNotifications && withinLastDayNotifications.length ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(react.Heading, {
className: `${blockClass}__time-section-label`
}, todayLabel), withinLastDayNotifications.map((notification, index) => renderNotification('today', notification, index))) : null, previousDayNotifications && previousDayNotifications.length ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(react.Heading, {
className: `${blockClass}__time-section-label`
}, yesterdayLabel), previousDayNotifications.map((notification, index) => renderNotification('yesterday', notification, index))) : null, previousNotifications && previousNotifications.length ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(react.Heading, {
className: `${blockClass}__time-section-label`
}, previousLabel), previousNotifications.map((notification, index) => renderNotification('previous', notification, index))) : null, !allNotifications.length && /*#__PURE__*/React.createElement(NotificationsEmptyState.NotificationsEmptyState, {
illustrationTheme: illustrationTheme,
title: "",
subtitle: emptyStateLabel
})), onViewAllClick && onSettingsClick && allNotifications && allNotifications.length > 0 && /*#__PURE__*/React.createElement("div", {
className: `${blockClass}__bottom-actions`
}, /*#__PURE__*/React.createElement(react.Button, {
kind: "ghost",
className: `${blockClass}__view-all-button`,
onClick: onViewAllClick
}, viewAllLabel(allNotifications.length)), /*#__PURE__*/React.createElement(react.Button, {
kind: "ghost",
size: "sm",
className: `${blockClass}__settings-button`,
renderIcon: props => /*#__PURE__*/React.createElement(icons.Settings, _rollupPluginBabelHelpers.extends({
size: 16
}, props)),
iconDescription: settingsIconDescription,
onClick: onSettingsClick,
hasIconOnly: true,
tooltipPosition: "left"
})))) : null;
});
// Return a placeholder if not released and not enabled by feature flag
exports.NotificationsPanel = settings.pkg.checkComponentEnabled(exports.NotificationsPanel, componentName);
// The display name of the component, used by React. Note that displayName
// is used in preference to relying on function.name.
exports.NotificationsPanel.displayName = componentName;
// The types and DocGen commentary for the component props,
// in alphabetical order (for consistency).
// See https://www.npmjs.com/package/prop-types#usage.
exports.NotificationsPanel.propTypes = {
/**
* Provide an optional class to be applied to the containing node.
*/
className: index.default.string,
/**
* Array of data for Notifications component to render
*/
/**@ts-ignore*/
data: index.default.arrayOf(index.default.shape({
id: index.default.oneOfType([index.default.string, index.default.number]),
type: index.default.oneOf(['error', 'warning', 'success', 'informational']),
timestamp: index.default.instanceOf(Date),
title: index.default.string,
description: index.default.string,
link: index.default.shape({
url: index.default.string,
text: index.default.string
}),
unread: index.default.bool,
onNotificationClick: index.default.func
})).isRequired,
/**
* The language for each notification's time stamp.
*/
dateTimeLocale: index.default.string,
/**
* The date/time format for each notification's time stamp.
*
* E.g. `long` as "6 minutes ago", `short` as "6m ago".
*/
dateTimeStyle: index.default.string,
/**
* Sets the `days ago` label text
*/
daysAgoText: index.default.func,
/**
* Label for Dismiss all button
*/
dismissAllLabel: index.default.string,
/**
* Label for Dismiss single notification icon button
*/
dismissSingleNotificationIconDescription: index.default.string,
/**
* Optional: Determines if the `Do not disturb` toggle is on or off when the component is rendered
*/
doNotDisturbDefaultToggled: index.default.bool,
/**
* Optional: Label for Do not disturb toggle
*/
doNotDisturbLabel: index.default.string,
/**
* Sets the empty state label text when there are no notifications
*/
emptyStateLabel: index.default.string,
/**
* Sets the `hour ago` label text
*/
hourAgoText: index.default.func,
/**
* Sets the `hours ago` label text
*/
hoursAgoText: index.default.func,
/**
* Determines the theme of the empty state's illustration.
*/
illustrationTheme: index.default.oneOf(['light', 'dark']),
/**
* Sets the `minute ago` label text
*/
minuteAgoText: index.default.func,
/**
* Sets the `minutes ago` label text
*/
minutesAgoText: index.default.func,
/**
* Sets the `month ago` label text
*/
monthAgoText: index.default.func,
/**
* Sets the `months ago` label text
*/
monthsAgoText: index.default.func,
/**
* Sets the `now` label text
*/
nowText: index.default.string,
/**
* Optional function called after clicking outside of the panel.
*/
onClickOutside: index.default.func,
/**
* Optional function called after clicking the "Dismiss all" button.
*/
onDismissAllNotifications: index.default.func,
/**
* Optional function called after clicking a notification's "X" button.
*/
onDismissSingleNotification: index.default.func,
/**
* Optional function called after toggling "Do not disturb".
*/
onDoNotDisturbChange: index.default.func,
/**
* Optional function called after clicking settings / gear icon button.
*/
onSettingsClick: index.default.func,
/**
* Optional function called after clicking the "View all" button.
*/
onViewAllClick: index.default.func,
/**
* Determines whether the notifications panel should render or not
*/
open: index.default.bool.isRequired,
/**
* Sets the previous label text
*/
previousLabel: index.default.string,
/**
* Sets the `read less` label text
*/
readLessLabel: index.default.string,
/**
* Sets the `read more` label text
*/
readMoreLabel: index.default.string,
/**
* Sets the `seconds ago` label text
*/
secondsAgoText: index.default.func,
/**
* Sets the settings icon description text
*/
settingsIconDescription: index.default.string,
/**
* Sets the title for the Notifications panel
*/
title: index.default.string,
/**
* Sets the today label text
*/
todayLabel: index.default.string,
/**
* Sets the today label text
*/
triggerButtonRef: index.default.any,
/**
* Sets the View all button text
*/
viewAllLabel: index.default.func,
/**
* Sets the `year ago` label text
*/
yearAgoText: index.default.func,
/**
* Sets the `years ago` label text
*/
yearsAgoText: index.default.func,
/**
* Sets the `Yesterday at` label text
*/
yesterdayAtText: index.default.func,
/**
* Sets the yesterday label text
*/
yesterdayLabel: index.default.string
};