diff --git a/src/components/Notifications/ToastNotification/ToastNotification.stories.tsx b/src/components/Notifications/ToastNotification/ToastNotification.stories.tsx
index f6b37eea1..f868fd12e 100644
--- a/src/components/Notifications/ToastNotification/ToastNotification.stories.tsx
+++ b/src/components/Notifications/ToastNotification/ToastNotification.stories.tsx
@@ -71,6 +71,13 @@ const PreloadedList = () => {
"Please try again.",
);
toastNotify.caution("You have unsaved changes.");
+ toastNotify.caution(
+ "This cannot be dismissed until the issue is resolved.",
+ undefined,
+ "Action required",
+ undefined,
+ false,
+ );
toastNotify.toggleListView();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -120,6 +127,19 @@ const PreloadedList = () => {
+
);
diff --git a/src/components/Notifications/ToastNotification/ToastNotification.test.tsx b/src/components/Notifications/ToastNotification/ToastNotification.test.tsx
index 6c7d6408e..759a9b2ed 100644
--- a/src/components/Notifications/ToastNotification/ToastNotification.test.tsx
+++ b/src/components/Notifications/ToastNotification/ToastNotification.test.tsx
@@ -1,6 +1,6 @@
-import React from "react";
-import { render, screen } from "@testing-library/react";
+import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
+import React from "react";
import ToastNotification from "./ToastNotification";
import ToastNotificationList from "./ToastNotificationList";
import { ToastNotificationType } from "./ToastNotificationProvider";
@@ -106,6 +106,19 @@ describe("ToastNotification", () => {
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});
+ it("does not render a dismiss button when the notification is not dismissable", () => {
+ render(
+ ,
+ );
+ expect(
+ screen.queryByRole("button", { name: /close/i }),
+ ).not.toBeInTheDocument();
+ });
+
const notifications: ToastNotificationType[] = [
{
id: "1",
@@ -188,6 +201,133 @@ describe("ToastNotification", () => {
expect(onDismiss).toHaveBeenCalledWith(notifications);
});
+ it("does not render a dismiss button for a non-dismissable notification in the list", () => {
+ const mixedNotifications: ToastNotificationType[] = [
+ ...notifications,
+ {
+ id: "3",
+ type: "caution",
+ message: "Cannot be dismissed.",
+ title: "Warning",
+ dismissable: false,
+ },
+ ];
+
+ render(
+ ,
+ );
+
+ const warningItem = screen.getByText("Warning").closest("li");
+ expect(warningItem).not.toBeNull();
+ expect(
+ within(warningItem as HTMLElement).queryByTestId(
+ "notification-close-button",
+ ),
+ ).not.toBeInTheDocument();
+ });
+
+ it("excludes non-dismissable notifications from 'Dismiss all'", async () => {
+ const onDismiss = jest.fn();
+ const mixedNotifications: ToastNotificationType[] = [
+ ...notifications,
+ {
+ id: "3",
+ type: "caution",
+ message: "Cannot be dismissed.",
+ title: "Warning",
+ dismissable: false,
+ },
+ ];
+
+ render(
+ ,
+ );
+
+ await userEvent.click(screen.getByRole("button", { name: "Dismiss all" }));
+ expect(onDismiss).toHaveBeenCalledWith(notifications);
+ expect(screen.getByText("Cannot be dismissed.")).toBeInTheDocument();
+ });
+
+ it("does not render 'Dismiss all' when no notifications are dismissable", () => {
+ const nonDismissableNotifications: ToastNotificationType[] = [
+ {
+ id: "1",
+ type: "positive",
+ message: "Cannot be dismissed.",
+ title: "Success",
+ dismissable: false,
+ },
+ {
+ id: "2",
+ type: "negative",
+ message: "Also cannot be dismissed.",
+ title: "Failure",
+ dismissable: false,
+ },
+ ];
+
+ render(
+ ,
+ );
+
+ expect(
+ screen.queryByRole("button", { name: "Dismiss all" }),
+ ).not.toBeInTheDocument();
+ });
+
+ it("shows a dismiss count that excludes non-dismissable notifications within a filtered severity", async () => {
+ const onDismiss = jest.fn();
+ const mixedCautionNotifications: ToastNotificationType[] = [
+ {
+ id: "1",
+ type: "caution",
+ message: "Can be dismissed.",
+ title: "Warning",
+ },
+ {
+ id: "2",
+ type: "caution",
+ message: "Cannot be dismissed.",
+ title: "Warning",
+ dismissable: false,
+ },
+ ];
+
+ render(
+ ,
+ );
+
+ await userEvent.click(
+ screen.getByLabelText("Filter caution notifications"),
+ );
+ expect(
+ screen.getByRole("button", { name: "Dismiss 1" }),
+ ).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole("button", { name: "Dismiss 1" }));
+ expect(onDismiss).toHaveBeenCalledWith([mixedCautionNotifications[0]]);
+ });
+
it("calls onHoverStart on mouse enter and onHoverEnd on mouse leave", async () => {
const onHoverStart = jest.fn();
const onHoverEnd = jest.fn();
diff --git a/src/components/Notifications/ToastNotification/ToastNotification.tsx b/src/components/Notifications/ToastNotification/ToastNotification.tsx
index d95607dcd..d43a0d3a1 100644
--- a/src/components/Notifications/ToastNotification/ToastNotification.tsx
+++ b/src/components/Notifications/ToastNotification/ToastNotification.tsx
@@ -53,9 +53,13 @@ const ToastNotification: FC = ({
title={notification.title ?? DefaultTitles[notification.type]}
actions={notification.actions}
severity={notification.type}
- onDismiss={() => {
- onDismiss([notification]);
- }}
+ onDismiss={
+ notification.dismissable === false
+ ? undefined
+ : () => {
+ onDismiss([notification]);
+ }
+ }
className="u-no-margin--bottom"
timestamp={notification.timestamp}
titleElement="div"
diff --git a/src/components/Notifications/ToastNotification/ToastNotificationList.tsx b/src/components/Notifications/ToastNotification/ToastNotificationList.tsx
index 05745e253..0f591d81b 100644
--- a/src/components/Notifications/ToastNotification/ToastNotificationList.tsx
+++ b/src/components/Notifications/ToastNotification/ToastNotificationList.tsx
@@ -96,17 +96,25 @@ const ToastNotificationList: FC = ({
});
};
+ const dismissableNotifications = notifications.filter(
+ (notification) => notification.dismissable !== false,
+ );
+
+ // The notifications that would actually be cleared if the dismiss button
+ // were clicked right now, accounting for any active severity filters.
+ const dismissableNotificationsInView = hasFilters
+ ? dismissableNotifications.filter((notification) =>
+ filters.has(notification.type),
+ )
+ : dismissableNotifications;
+
const handleGroupedDismiss = () => {
if (hasFilters) {
- const notificationsToClear = notifications.filter((notification) =>
- filters.has(notification.type),
- );
- onDismiss(notificationsToClear);
+ onDismiss(dismissableNotificationsInView);
setFilters(new Set());
return;
}
-
- onDismiss(notifications);
+ onDismiss(dismissableNotifications);
};
const getSeverityFilters = () => {
@@ -149,18 +157,11 @@ const ToastNotificationList: FC = ({
const getDismissText = () => {
if (hasFilters) {
- const validFilters = Object.keys(groupedCount) as FilterTypes[];
- let totalCount = 0;
- for (const filter of validFilters) {
- if (filters.has(filter)) {
- totalCount += groupedCount[filter] || 0;
- }
- }
-
- const dismissText = (
- Dismiss {totalCount}
+ return (
+
+ Dismiss {dismissableNotificationsInView.length}
+
);
- return dismissText;
}
return Dismiss all;
@@ -221,9 +222,13 @@ const ToastNotificationList: FC = ({
title={notification.title ?? DefaultTitles[notification.type]}
actions={notification.actions}
severity={notification.type}
- onDismiss={() => {
- handleDismissNotification(notification);
- }}
+ onDismiss={
+ notification.dismissable === false
+ ? undefined
+ : () => {
+ handleDismissNotification(notification);
+ }
+ }
className={`u-no-margin--bottom individual-notification`}
timestamp={notification.timestamp}
titleElement="div"
@@ -256,14 +261,16 @@ const ToastNotificationList: FC = ({
{notificationEls}
{getSeverityFilters()}
-
+ {dismissableNotificationsInView.length > 0 && (
+
+ )}
,
diff --git a/src/components/Notifications/ToastNotification/ToastNotificationProvider.tsx b/src/components/Notifications/ToastNotification/ToastNotificationProvider.tsx
index 21b48ed73..22860da40 100644
--- a/src/components/Notifications/ToastNotification/ToastNotificationProvider.tsx
+++ b/src/components/Notifications/ToastNotification/ToastNotificationProvider.tsx
@@ -21,6 +21,7 @@ const HIDE_NOTIFICATION_DELAY = 5_000;
export type ToastNotificationType = NotificationType & {
timestamp?: ReactNode;
id: string;
+ dismissable?: boolean;
};
interface Props {
@@ -35,12 +36,14 @@ interface ToastNotificationHelper {
actions?: NotificationAction[],
title?: string,
id?: ToastNotificationType["id"],
+ dismissable?: boolean,
) => ToastNotificationType;
info: (
message: ReactNode,
title?: string,
actions?: NotificationAction[],
id?: ToastNotificationType["id"],
+ dismissable?: boolean,
) => ToastNotificationType;
failure: (
title: string,
@@ -48,12 +51,14 @@ interface ToastNotificationHelper {
message?: ReactNode,
actions?: NotificationAction[],
id?: ToastNotificationType["id"],
+ dismissable?: boolean,
) => ToastNotificationType;
caution: (
message: ReactNode,
actions?: NotificationAction[],
title?: string,
id?: ToastNotificationType["id"],
+ dismissable?: boolean,
) => ToastNotificationType;
clear: (notification?: ToastNotificationType[]) => void;
toggleListView: () => void;
@@ -234,6 +239,7 @@ const ToastNotificationProvider: FC> = ({
const addNotification = (
notification: NotificationType & { error?: unknown } & {
id?: ToastNotificationType["id"];
+ dismissable?: boolean;
},
) => {
const notificationToAdd = {
@@ -242,6 +248,7 @@ const ToastNotificationProvider: FC> = ({
id:
notification.id ??
Date.now().toString() + (Math.random() + 1).toString(36).substring(7),
+ dismissable: notification.dismissable ?? true,
};
setNotifications((prev) => {
@@ -299,14 +306,26 @@ const ToastNotificationProvider: FC> = ({
const helper: ToastNotificationHelper = {
notifications,
- failure: (title, error, message, actions, id) =>
- addNotification({ ...failure(title, error, message, actions), id }),
- info: (message, title, actions, id) =>
- addNotification({ ...info(message, title, actions), id }),
- success: (message, actions, title, id) =>
- addNotification({ ...success(message, title, actions), id }),
- caution: (message, actions, title, id) =>
- addNotification({ ...caution(message, title, actions), id }),
+ failure: (title, error, message, actions, id, dismissable) =>
+ addNotification({
+ ...failure(title, error, message, actions),
+ id,
+ dismissable,
+ }),
+ info: (message, title, actions, id, dismissable) =>
+ addNotification({ ...info(message, title, actions), id, dismissable }),
+ success: (message, actions, title, id, dismissable) =>
+ addNotification({
+ ...success(message, title, actions),
+ id,
+ dismissable,
+ }),
+ caution: (message, actions, title, id, dismissable) =>
+ addNotification({
+ ...caution(message, title, actions),
+ id,
+ dismissable,
+ }),
clear,
toggleListView,
isListView: showList,