Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
}, []);
Expand Down Expand Up @@ -120,6 +127,19 @@ const PreloadedList = () => {
<Button onClick={() => toastNotify.caution("You have unsaved changes")}>
Add Warning
</Button>
<Button
onClick={() =>
toastNotify.caution(
"This cannot be dismissed until the issue is resolved.",
undefined,
"Action required",
undefined,
false,
)
}
>
Add Non-dismissable Warning
</Button>
Comment thread
Ninfa-Jeon marked this conversation as resolved.
Resolved
<Button onClick={toastNotify.toggleListView}>Toggle List View</Button>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(
<ToastNotification
notification={{ ...baseNotification, dismissable: false }}
show={true}
onDismiss={jest.fn()}
/>,
);
expect(
screen.queryByRole("button", { name: /close/i }),
).not.toBeInTheDocument();
});

const notifications: ToastNotificationType[] = [
{
id: "1",
Expand Down Expand Up @@ -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(
<ToastNotificationList
notifications={mixedNotifications}
onDismiss={jest.fn()}
groupedCount={{ ...groupedCount, caution: 1 }}
show={true}
/>,
);

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(
<ToastNotificationList
notifications={mixedNotifications}
onDismiss={onDismiss}
groupedCount={{ ...groupedCount, caution: 1 }}
show={true}
/>,
);

await userEvent.click(screen.getByRole("button", { name: "Dismiss all" }));
expect(onDismiss).toHaveBeenCalledWith(notifications);
Comment thread
Ninfa-Jeon marked this conversation as resolved.
Resolved
Comment thread
Ninfa-Jeon marked this conversation as resolved.
Resolved
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(
<ToastNotificationList
notifications={nonDismissableNotifications}
onDismiss={jest.fn()}
groupedCount={groupedCount}
show={true}
/>,
);

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(
<ToastNotificationList
notifications={mixedCautionNotifications}
onDismiss={onDismiss}
groupedCount={{ caution: 2 }}
show={true}
/>,
);

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]]);
Comment thread
Ninfa-Jeon marked this conversation as resolved.
Resolved
});

it("calls onHoverStart on mouse enter and onHoverEnd on mouse leave", async () => {
const onHoverStart = jest.fn();
const onHoverEnd = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,13 @@ const ToastNotification: FC<Props> = ({
title={notification.title ?? DefaultTitles[notification.type]}
actions={notification.actions}
severity={notification.type}
onDismiss={() => {
onDismiss([notification]);
}}
onDismiss={
notification.dismissable === false
? undefined
: () => {
onDismiss([notification]);
}
}
Comment thread
rumanowskak marked this conversation as resolved.
Resolved
className="u-no-margin--bottom"
timestamp={notification.timestamp}
titleElement="div"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,25 @@ const ToastNotificationList: FC<Props> = ({
});
};

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) {
Comment thread
Ninfa-Jeon marked this conversation as resolved.
Resolved
const notificationsToClear = notifications.filter((notification) =>
filters.has(notification.type),
);
onDismiss(notificationsToClear);
onDismiss(dismissableNotificationsInView);
setFilters(new Set());
return;
}

onDismiss(notifications);
onDismiss(dismissableNotifications);
};

const getSeverityFilters = () => {
Expand Down Expand Up @@ -149,18 +157,11 @@ const ToastNotificationList: FC<Props> = ({

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 = (
<span className="dismiss-text">Dismiss {totalCount}</span>
return (
<span className="dismiss-text">
Dismiss {dismissableNotificationsInView.length}
</span>
);
return dismissText;
}

return <span>Dismiss all</span>;
Expand Down Expand Up @@ -221,9 +222,13 @@ const ToastNotificationList: FC<Props> = ({
title={notification.title ?? DefaultTitles[notification.type]}
actions={notification.actions}
severity={notification.type}
onDismiss={() => {
handleDismissNotification(notification);
}}
onDismiss={
notification.dismissable === false
? undefined
: () => {
handleDismissNotification(notification);
}
}
Comment thread
rumanowskak marked this conversation as resolved.
Resolved
className={`u-no-margin--bottom individual-notification`}
timestamp={notification.timestamp}
titleElement="div"
Expand Down Expand Up @@ -256,14 +261,16 @@ const ToastNotificationList: FC<Props> = ({
{notificationEls}
<li className="dismiss">
{getSeverityFilters()}
<Button
className="u-no-margin--bottom dismiss-button"
onClick={handleGroupedDismiss}
hasIcon
>
<Icon name="tidy" />
{getDismissText()}
</Button>
{dismissableNotificationsInView.length > 0 && (
<Button
className="u-no-margin--bottom dismiss-button"
onClick={handleGroupedDismiss}
hasIcon
>
<Icon name="tidy" />
{getDismissText()}
</Button>
)}
</li>
</ul>
</Animate>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const HIDE_NOTIFICATION_DELAY = 5_000;
export type ToastNotificationType = NotificationType & {
timestamp?: ReactNode;
id: string;
dismissable?: boolean;
};

interface Props {
Expand All @@ -35,25 +36,29 @@ 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,
error: unknown,
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;
Expand Down Expand Up @@ -234,6 +239,7 @@ const ToastNotificationProvider: FC<PropsWithChildren<Props>> = ({
const addNotification = (
notification: NotificationType & { error?: unknown } & {
id?: ToastNotificationType["id"];
dismissable?: boolean;
},
) => {
const notificationToAdd = {
Expand All @@ -242,6 +248,7 @@ const ToastNotificationProvider: FC<PropsWithChildren<Props>> = ({
id:
notification.id ??
Date.now().toString() + (Math.random() + 1).toString(36).substring(7),
dismissable: notification.dismissable ?? true,
};

setNotifications((prev) => {
Expand Down Expand Up @@ -299,14 +306,26 @@ const ToastNotificationProvider: FC<PropsWithChildren<Props>> = ({

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,
Expand Down
Loading