Skip to content

Commit

Permalink
feat(NotificationContext) add notification provider consumer and hook…
Browse files Browse the repository at this point in the history
… WD-4256
  • Loading branch information
edlerd committed Jul 7, 2023
1 parent a3b60d6 commit 56239ca
Show file tree
Hide file tree
Showing 11 changed files with 468 additions and 1 deletion.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@types/react-router-dom": "5.3.3",
"@typescript-eslint/eslint-plugin": "5.60.1",
"@typescript-eslint/parser": "5.60.1",
"babel-jest": "27.5.1",
Expand Down Expand Up @@ -91,6 +92,7 @@
"classnames": "2.3.2",
"nanoid": "3.3.6",
"prop-types": "15.8.1",
"react-router-dom": "6.6.1",
"react-table": "7.8.0",
"react-useportal": "1.0.18"
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/ContextualMenu/ContextualMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export type Props<L> = PropsWithSpread<
/**
* The toggle button's label.
*/
toggleLabel?: string | React.ReactNode | null;
toggleLabel?: React.ReactNode | null;
/**
* Whether the toggle lable or icon should appear first.
*/
Expand Down
7 changes: 7 additions & 0 deletions src/components/Notification/Notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ export const NotificationSeverity = {
POSITIVE: "positive",
} as const;

export const DefaultTitles = {
[NotificationSeverity.CAUTION]: "Warning",
[NotificationSeverity.INFORMATION]: "Info",
[NotificationSeverity.NEGATIVE]: "Error",
[NotificationSeverity.POSITIVE]: "Success",
};

type NotificationAction = {
label: string;
onClick: () => void;
Expand Down
79 changes: 79 additions & 0 deletions src/components/NotificationProvider/NotificationProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import {
NotificationConsumer,
NotificationProvider,
useNotify,
} from "./NotificationProvider";
import { BrowserRouter } from "react-router-dom";
import Button from "../Button";
import { act } from "react-dom/test-utils";

describe("NotificationProvider", () => {
it("stores and renders notifications", async () => {
const handleRetry = jest.fn();

const TriggerComponent = () => {
const notify = useNotify();
const handleSuccess = () => notify.success("My success!");
const handleClear = () => notify.clear();
const handleInfo = () => notify.info("Some more details", "Test info");
const handleError = () =>
notify.failure(
"Fail title",
new Error("error message"),
"Button caused a failure",
[
{
label: "Retry",
onClick: handleRetry,
},
]
);

return (
<div>
<Button onClick={handleSuccess} data-testid="success-btn" />
<Button onClick={handleClear} data-testid="clear-btn" />
<Button onClick={handleError} data-testid="error-btn" />
<Button onClick={handleInfo} data-testid="info-btn" />
</div>
);
};

render(
<div>
<NotificationProvider>
<TriggerComponent />
<div data-testid="notification-consumer">
<NotificationConsumer />
</div>
</NotificationProvider>
</div>,
{ wrapper: BrowserRouter }
);

const clickBtn = async (testId: string) =>
await act(async () => await userEvent.click(screen.getByTestId(testId)));

expect(screen.getByTestId("notification-consumer")).toBeEmptyDOMElement();

await clickBtn("success-btn");
expect(screen.getByTestId("notification-consumer")).toMatchSnapshot();

await clickBtn("clear-btn");
expect(screen.getByTestId("notification-consumer")).toBeEmptyDOMElement();

await clickBtn("error-btn");
expect(screen.getByTestId("notification-consumer")).toMatchSnapshot();

expect(handleRetry).not.toHaveBeenCalled();
await clickBtn("notification-action");
expect(handleRetry).toHaveBeenCalled();

await clickBtn("info-btn");
expect(screen.getByTestId("notification-consumer")).toMatchSnapshot();
});
});
103 changes: 103 additions & 0 deletions src/components/NotificationProvider/NotificationProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, {
createContext,
FC,
useContext,
useEffect,
useRef,
useState,
} from "react";
import {
NotificationType,
NotificationHelper,
QueuedNotification,
NotifyProviderProps,
} from "./types";
import { useLocation } from "react-router-dom";
import isEqual from "lodash/isEqual";
import { info, failure, success, queue } from "./messageBuilder";
import Notification, { DefaultTitles } from "../Notification/Notification";

const NotifyContext = createContext<NotificationHelper>({
notification: null,
clear: () => undefined,
failure: () => undefined,
success: () => undefined,
info: () => undefined,
queue: () => undefined,
});

export const NotificationProvider: FC<NotifyProviderProps> = ({ children }) => {
const [notification, setNotification] = useState<NotificationType | null>(
null
);
const { state, pathname } = useLocation() as QueuedNotification;

const clear = () => notification !== null && setNotification(null);

const setDeduplicated = (value: NotificationType) => {
if (!isEqual(value, notification)) {
setNotification(value);
}
return value;
};

useEffect(() => {
if (state?.queuedNotification) {
setDeduplicated(state.queuedNotification);
window.history.replaceState({}, "");
} else {
clear();
}
}, [state, pathname]);

const helper: NotificationHelper = {
notification,
clear,
queue,
failure: (title, error, message, actions) =>
setDeduplicated(failure(title, error, message, actions)),
info: (message, title) => setDeduplicated(info(message, title)),
success: (message) => setDeduplicated(success(message)),
};

return (
<NotifyContext.Provider value={helper}>{children}</NotifyContext.Provider>
);
};

export function useNotify() {
return useContext(NotifyContext);
}

export const NotificationConsumer: FC = () => {
const notify = useNotify();
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
if (ref.current?.hasAttribute("scrollIntoView")) {
ref.current.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "start",
});
}
}, [notify.notification]);

if (!notify.notification) {
return null;
}
const { actions, title, type, message } = notify.notification;

return (
<div ref={ref}>
<Notification
title={title ?? DefaultTitles[type]}
actions={actions}
severity={type}
onDismiss={notify.clear}
>
{message}
</Notification>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`NotificationProvider stores and renders notifications 1`] = `
<div
data-testid="notification-consumer"
>
<div>
<div
class="p-notification--positive"
>
<div
class="p-notification__content"
>
<h5
class="p-notification__title"
data-testid="notification-title"
>
Success
</h5>
<p
class="p-notification__message"
>
My success!
</p>
<button
class="p-notification__close"
data-testid="notification-close-button"
>
Close notification
</button>
</div>
</div>
</div>
</div>
`;

exports[`NotificationProvider stores and renders notifications 2`] = `
<div
data-testid="notification-consumer"
>
<div>
<div
class="p-notification--negative"
>
<div
class="p-notification__content"
>
<h5
class="p-notification__title"
data-testid="notification-title"
>
Fail title
</h5>
<p
class="p-notification__message"
>
Button caused a failure
error message
</p>
<button
class="p-notification__close"
data-testid="notification-close-button"
>
Close notification
</button>
</div>
<div
class="p-notification__meta"
data-testid="notification-meta"
>
<div
class="p-notification__actions"
>
<button
class="p-button--link p-notification__action"
data-testid="notification-action"
>
Retry
</button>
</div>
</div>
</div>
</div>
</div>
`;

exports[`NotificationProvider stores and renders notifications 3`] = `
<div
data-testid="notification-consumer"
>
<div>
<div
class="p-notification--information"
>
<div
class="p-notification__content"
>
<h5
class="p-notification__title"
data-testid="notification-title"
>
Test info
</h5>
<p
class="p-notification__message"
>
Some more details
</p>
<button
class="p-notification__close"
data-testid="notification-close-button"
>
Close notification
</button>
</div>
</div>
</div>
</div>
`;
12 changes: 12 additions & 0 deletions src/components/NotificationProvider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export {
NotificationConsumer,
NotificationProvider,
useNotify,
} from "./NotificationProvider";
export { info, success, failure, queue } from "./messageBuilder";
export type {
NotificationAction,
NotificationType,
QueuedNotification,
NotificationHelper,
} from "./types";
Loading

0 comments on commit 56239ca

Please sign in to comment.