@purpurds/notification 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ ._purpur-notification_10qwf_1{display:flex;flex-direction:column;padding:var(--purpur-spacing-200) var(--purpur-spacing-200) var(--purpur-spacing-200) var(--purpur-spacing-250);box-sizing:border-box;width:100%;background:var(--purpur-color-background-primary);border-radius:var(--purpur-border-radius-md);border:var(--purpur-border-width-sm) solid;border-left:var(--purpur-border-width-lg) solid;gap:var(--purpur-spacing-150);position:relative}._purpur-notification__top_10qwf_14{display:flex;align-items:flex-start;align-self:stretch;justify-content:space-between}._purpur-notification__header_10qwf_20{display:flex;align-items:center;gap:var(--purpur-spacing-100)}._purpur-notification__close-button_10qwf_25{position:absolute;right:calc(var(--purpur-spacing-50) + var(--purpur-spacing-25));top:calc(var(--purpur-spacing-50) + var(--purpur-spacing-25))}._purpur-notification__icon_10qwf_30{flex-shrink:0}._purpur-notification__body_10qwf_33{padding:var(--purpur-spacing-0) var(--purpur-spacing-150) var(--purpur-spacing-50) var(--purpur-spacing-0)}._purpur-notification--success_10qwf_36{border-color:var(--purpur-color-border-status-success)}._purpur-notification--success_10qwf_36 ._purpur-notification__heading_10qwf_39{color:var(--purpur-color-text-status-success-strong)}._purpur-notification--success_10qwf_36 ._purpur-notification__icon_10qwf_30{color:var(--purpur-color-text-status-success-medium)}._purpur-notification--warning_10qwf_45{border-color:var(--purpur-color-border-status-warning)}._purpur-notification--warning_10qwf_45 ._purpur-notification__heading_10qwf_39{color:var(--purpur-color-text-status-warning-strong)}._purpur-notification--warning_10qwf_45 ._purpur-notification__icon_10qwf_30{color:var(--purpur-color-text-status-warning-medium)}._purpur-notification--error_10qwf_54{border-color:var(--purpur-color-border-status-error)}._purpur-notification--error_10qwf_54 ._purpur-notification__heading_10qwf_39{color:var(--purpur-color-text-status-error-strong)}._purpur-notification--error_10qwf_54 ._purpur-notification__icon_10qwf_30{color:var(--purpur-color-text-status-error-medium)}._purpur-notification--info_10qwf_63{border-color:var(--purpur-color-border-status-info)}._purpur-notification--info_10qwf_63 ._purpur-notification__heading_10qwf_39{color:var(--purpur-color-text-status-info-strong)}._purpur-notification--info_10qwf_63 ._purpur-notification__icon_10qwf_30{color:var(--purpur-color-text-status-info-medium)}._purpur-notification--hidden_10qwf_72{display:none;visibility:hidden}._purpur-notification--has-close-button_10qwf_76 ._purpur-notification__top_10qwf_14{padding-right:var(--purpur-spacing-400)}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@purpurds/notification",
3
+ "version": "3.0.0",
4
+ "license": "AGPL-3.0-only",
5
+ "main": "./dist/notification.cjs.js",
6
+ "types": "./dist/notification.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./dist/notification.cjs.js",
10
+ "systemjs": "./dist/notification.system.js",
11
+ "types": "./dist/notification.d.ts",
12
+ "default": "./dist/notification.es.js"
13
+ },
14
+ "./styles": "./dist/styles.css"
15
+ },
16
+ "source": "src/notification.tsx",
17
+ "dependencies": {
18
+ "classnames": "~2.5.0",
19
+ "@purpurds/button": "3.0.0",
20
+ "@purpurds/heading": "3.0.0",
21
+ "@purpurds/paragraph": "3.0.0",
22
+ "@purpurds/icon": "3.0.0",
23
+ "@purpurds/tokens": "3.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@rushstack/eslint-patch": "~1.7.0",
27
+ "@storybook/blocks": "~7.6.0",
28
+ "@storybook/client-api": "~7.6.0",
29
+ "@storybook/react": "~7.6.0",
30
+ "@telia/base-rig": "~8.2.0",
31
+ "@telia/react-rig": "~3.2.0",
32
+ "@testing-library/dom": "~9.3.3",
33
+ "@testing-library/jest-dom": "~6.3.0",
34
+ "@testing-library/react": "~14.1.2",
35
+ "@types/node": "18",
36
+ "@types/react-dom": "~18.2.17",
37
+ "@types/react": "~18.2.42",
38
+ "eslint-plugin-testing-library": "~6.2.0",
39
+ "eslint": "~8.56.0",
40
+ "jsdom": "~22.1.0",
41
+ "lint-staged": "~10.5.3",
42
+ "prettier": "~2.8.8",
43
+ "react-dom": "~18.2.0",
44
+ "react": "~18.2.0",
45
+ "typescript": "~5.2.2",
46
+ "vite": "~5.0.6",
47
+ "vitest": "~1.2.0",
48
+ "@purpurds/component-rig": "1.0.0"
49
+ },
50
+ "scripts": {
51
+ "build:dev": "vite",
52
+ "build:watch": "vite build --watch",
53
+ "build": "rm -rf dist && vite build && vite build --mode systemjs",
54
+ "ci:build": "rushx build",
55
+ "coverage": "vitest run --coverage",
56
+ "lint:fix": "eslint . --fix",
57
+ "lint": "lint-staged --no-stash 2>&1",
58
+ "sbdev": "rush sbdev",
59
+ "test:unit": "vitest run --passWithNoTests",
60
+ "test:watch": "vitest --watch",
61
+ "test": "rushx test:unit",
62
+ "typecheck": "tsc -p ./tsconfig.json"
63
+ }
64
+ }
package/readme.mdx ADDED
@@ -0,0 +1,56 @@
1
+ import { Meta, Stories, ArgTypes, Primary, Subtitle } from "@storybook/blocks";
2
+
3
+ import * as NotificationStories from "./src/notification.stories";
4
+ import packageInfo from "./package.json";
5
+
6
+ <Meta name="Docs" title="Components/Notification" of={NotificationStories} />
7
+
8
+ # Notification
9
+
10
+ <Subtitle>Version {packageInfo.version}</Subtitle>
11
+
12
+ ### Showcase
13
+
14
+ <Primary />
15
+
16
+ ### Properties
17
+
18
+ <ArgTypes />
19
+
20
+ ### Installation
21
+
22
+ #### Via NPM
23
+
24
+ Add the dependency to your consumer app like `"@purpurds/notification": "x.y.z"`
25
+
26
+ #### From outside the monorepo (build-time)
27
+
28
+ To install this package, you need to setup access to the artifactory. [Click here to go to the guide on how to do that](https://github.com/telia-company/jfrog-documentation/blob/main/doc/JFrog/JFrog_Onboarding.md#getting-access-to-artifactory-and-other-jfrog-applications).
29
+
30
+ ---
31
+
32
+ In MyApp.tsx
33
+
34
+ ```tsx
35
+ import "@purpurds/tokens/index.css";
36
+ ```
37
+
38
+ and
39
+
40
+ ```tsx
41
+ import "@purpurds/notification/dist/styles.css";
42
+ ```
43
+
44
+ In MyComponent.tsx
45
+
46
+ ```tsx
47
+ import { Notification } from "@purpurds/notification";
48
+
49
+ export const MyComponent = () => {
50
+ return (
51
+ <div>
52
+ <Notification {...someProps}>Some content</Notification>
53
+ </div>
54
+ );
55
+ };
56
+ ```
@@ -0,0 +1,4 @@
1
+ declare module "*.scss" {
2
+ const styles: { [className: string]: string };
3
+ export default styles;
4
+ }
@@ -0,0 +1,70 @@
1
+ .purpur-notification {
2
+ $root: &;
3
+
4
+ display: flex;
5
+ flex-direction: column;
6
+ padding: var(--purpur-spacing-200) var(--purpur-spacing-200) var(--purpur-spacing-200)
7
+ var(--purpur-spacing-250);
8
+ box-sizing: border-box;
9
+ width: 100%;
10
+ background: var(--purpur-color-background-primary);
11
+ border-radius: var(--purpur-border-radius-md);
12
+ border: var(--purpur-border-width-sm) solid;
13
+ border-left: var(--purpur-border-width-lg) solid;
14
+ gap: var(--purpur-spacing-150);
15
+ position: relative;
16
+
17
+ &__top {
18
+ display: flex;
19
+ align-items: flex-start;
20
+ align-self: stretch;
21
+ justify-content: space-between;
22
+ }
23
+
24
+ &__header {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: var(--purpur-spacing-100);
28
+ }
29
+
30
+ &__close-button {
31
+ position: absolute;
32
+ right: calc(var(--purpur-spacing-50) + var(--purpur-spacing-25));
33
+ top: calc(var(--purpur-spacing-50) + var(--purpur-spacing-25));
34
+ }
35
+
36
+ &__icon {
37
+ flex-shrink: 0;
38
+ }
39
+
40
+ &__body {
41
+ padding: var(--purpur-spacing-0) var(--purpur-spacing-150) var(--purpur-spacing-50)
42
+ var(--purpur-spacing-0);
43
+ }
44
+
45
+ $statuses: "success", "warning", "error", "info";
46
+ @each $status in $statuses {
47
+ &--#{$status} {
48
+ border-color: var(--purpur-color-border-status-#{$status});
49
+
50
+ #{$root}__heading {
51
+ color: var(--purpur-color-text-status-#{$status}-strong);
52
+ }
53
+
54
+ #{$root}__icon {
55
+ color: var(--purpur-color-text-status-#{$status}-medium);
56
+ }
57
+ }
58
+ }
59
+
60
+ &--hidden {
61
+ display: none;
62
+ visibility: hidden;
63
+ }
64
+
65
+ &--has-close-button {
66
+ #{$root}__top {
67
+ padding-right: var(--purpur-spacing-400);
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,115 @@
1
+ import React from "react";
2
+ import { Button } from "@purpurds/button";
3
+ import { useArgs } from "@storybook/client-api";
4
+ import type { Meta, StoryObj } from "@storybook/react";
5
+
6
+ import "@purpurds/icon/styles";
7
+ import "@purpurds/button/styles";
8
+ import "@purpurds/heading/styles";
9
+ import "@purpurds/paragraph/styles";
10
+ import {
11
+ Notification,
12
+ notificationAriaLiveValues,
13
+ notificationRoles,
14
+ notificationStatuses,
15
+ } from "./notification";
16
+
17
+ const meta: Meta<typeof Notification> = {
18
+ title: "Components/Notification",
19
+ component: Notification,
20
+ };
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof Notification>;
24
+
25
+ export const NotificationWithoutCloseButton: Story = {
26
+ name: "Notification without close button",
27
+ parameters: {
28
+ design: [
29
+ {
30
+ name: "Notification",
31
+ type: "figma",
32
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?type=design&node-id=4489-1840",
33
+ },
34
+ ],
35
+ },
36
+ argTypes: {
37
+ status: {
38
+ options: [undefined, ...notificationStatuses],
39
+ control: "select",
40
+ },
41
+ ["aria-live"]: {
42
+ options: [undefined, ...notificationAriaLiveValues],
43
+ control: "select",
44
+ },
45
+ role: {
46
+ options: [undefined, ...notificationRoles],
47
+ control: "select",
48
+ },
49
+ },
50
+ args: {
51
+ status: "info",
52
+ heading: "Title",
53
+ children:
54
+ "The body of the notification. This can also be a ReactNode. If so, make sure to wrap the text content with a Purpur paragraph ✌🏻.",
55
+ hidden: false,
56
+ id: "story-notification",
57
+ onClose: undefined,
58
+ },
59
+ decorators: [
60
+ (Story) => (
61
+ <div style={{ maxWidth: "450px" }}>
62
+ <Story />
63
+ </div>
64
+ ),
65
+ ],
66
+ };
67
+
68
+ export const NotificationWithCloseButton: Story = {
69
+ ...NotificationWithoutCloseButton,
70
+ name: "Notification with close button",
71
+ argTypes: {
72
+ ...NotificationWithoutCloseButton.argTypes,
73
+ onClose: { action: "closed" },
74
+ },
75
+ args: {
76
+ ...NotificationWithoutCloseButton.args,
77
+ closeButtonAllyLabel: "Close notification",
78
+ },
79
+ decorators: [
80
+ (Story) => (
81
+ <div
82
+ style={{
83
+ maxWidth: "450px",
84
+ display: "flex",
85
+ flexDirection: "column",
86
+ gap: "var(--purpur-spacing-gutter-sm)",
87
+ alignItems: "flex-start",
88
+ }}
89
+ >
90
+ <Story />
91
+ </div>
92
+ ),
93
+ ],
94
+ render: ({ children, ...args }) => {
95
+ const [{ hidden }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
96
+ const toggleHidden = () => updateArgs({ hidden: !hidden });
97
+ return (
98
+ <>
99
+ <Notification
100
+ {...args}
101
+ onClose={() => {
102
+ toggleHidden();
103
+ args.onClose?.();
104
+ }}
105
+ closeButtonAllyLabel={args.closeButtonAllyLabel || ""}
106
+ >
107
+ {children}
108
+ </Notification>
109
+ <Button variant="primary" onClick={toggleHidden} disabled={!hidden}>
110
+ Reveal notification
111
+ </Button>
112
+ </>
113
+ );
114
+ },
115
+ };
@@ -0,0 +1,150 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { cleanup, render, screen } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import { Notification, notificationStatuses } from "./notification";
7
+ const rootClassName = "purpur-notification";
8
+
9
+ expect.extend(matchers);
10
+
11
+ describe("Notification", () => {
12
+ afterEach(cleanup);
13
+
14
+ notificationStatuses.forEach((status) => {
15
+ it(`should render with status ${status}`, () => {
16
+ render(
17
+ <Notification heading="Test" data-testid="notification-test" status={status}>
18
+ Some text content
19
+ </Notification>
20
+ );
21
+
22
+ expect(screen.getByTestId("notification-test")).toHaveAttribute("aria-hidden", "false");
23
+ expect(screen.getByTestId("notification-test")).toHaveClass(
24
+ rootClassName,
25
+ `${rootClassName}--${status}`
26
+ );
27
+ expect(screen.getByTestId("notification-test")).not.toHaveClass(
28
+ `${rootClassName}--has-close-button`
29
+ );
30
+ expect(screen.getByTestId("notification-test-icon")).toBeInTheDocument();
31
+ expect(screen.getByTestId("notification-test-heading")).toHaveTextContent("Test");
32
+ expect(screen.getByTestId("notification-test-body")).toHaveTextContent("Some text content");
33
+ expect(screen.getByTestId("notification-test-paragraph")).toHaveTextContent(
34
+ "Some text content"
35
+ );
36
+ expect(screen.queryByTestId("notification-test-close-button")).not.toBeInTheDocument();
37
+ });
38
+ });
39
+
40
+ it("should not render heading given no heading", () => {
41
+ render(<Notification data-testid="notification-test">Some text content</Notification>);
42
+
43
+ expect(screen.getByTestId("notification-test")).toHaveAttribute("aria-hidden", "false");
44
+ expect(screen.getByTestId("notification-test")).toHaveClass(`${rootClassName}--info`);
45
+ expect(screen.getByTestId("notification-test")).not.toHaveClass(
46
+ `${rootClassName}--has-close-button`
47
+ );
48
+ expect(screen.getByTestId("notification-test-icon")).toBeInTheDocument();
49
+ expect(screen.queryByTestId("notification-test-heading")).not.toBeInTheDocument();
50
+ expect(screen.getByTestId("notification-test-body")).toHaveTextContent("Some text content");
51
+ expect(screen.getByTestId("notification-test-paragraph")).toHaveTextContent(
52
+ "Some text content"
53
+ );
54
+ expect(screen.queryByTestId("notification-test-close-button")).not.toBeInTheDocument();
55
+ });
56
+
57
+ it("should not render body given no children", () => {
58
+ render(<Notification heading="Test" data-testid="notification-test" />);
59
+
60
+ expect(screen.getByTestId("notification-test")).toHaveAttribute("aria-hidden", "false");
61
+ expect(screen.getByTestId("notification-test")).toHaveClass(`${rootClassName}--info`);
62
+ expect(screen.getByTestId("notification-test")).not.toHaveClass(
63
+ `${rootClassName}--has-close-button`
64
+ );
65
+ expect(screen.getByTestId("notification-test-icon")).toBeInTheDocument();
66
+ expect(screen.getByTestId("notification-test-heading")).toHaveTextContent("Test");
67
+ expect(screen.queryByTestId("notification-test-body")).not.toBeInTheDocument();
68
+ expect(screen.queryByTestId("notification-test-paragraph")).not.toBeInTheDocument();
69
+ expect(screen.queryByTestId("notification-test-close-button")).not.toBeInTheDocument();
70
+ });
71
+
72
+ it("should not render paragraph given non-string children", () => {
73
+ render(
74
+ <Notification heading="Test" data-testid="notification-test">
75
+ <p>I am not a string ?!</p>
76
+ </Notification>
77
+ );
78
+
79
+ expect(screen.getByTestId("notification-test")).toHaveAttribute("aria-hidden", "false");
80
+ expect(screen.getByTestId("notification-test")).toHaveClass(`${rootClassName}--info`);
81
+ expect(screen.getByTestId("notification-test")).not.toHaveClass(
82
+ `${rootClassName}--has-close-button`
83
+ );
84
+ expect(screen.getByTestId("notification-test-icon")).toBeInTheDocument();
85
+ expect(screen.getByTestId("notification-test-heading")).toHaveTextContent("Test");
86
+ expect(screen.queryByTestId("notification-test-body")).toHaveTextContent(
87
+ "I am not a string ?!"
88
+ );
89
+ expect(screen.queryByTestId("notification-test-paragraph")).not.toBeInTheDocument();
90
+ expect(screen.queryByTestId("notification-test-close-button")).not.toBeInTheDocument();
91
+ });
92
+
93
+ it("should render close button given onClose callback", () => {
94
+ const onCloseMock = vi.fn();
95
+ render(
96
+ <Notification
97
+ heading="Test"
98
+ data-testid="notification-test"
99
+ onClose={onCloseMock}
100
+ closeButtonAllyLabel="Close notification"
101
+ >
102
+ Some text content
103
+ </Notification>
104
+ );
105
+
106
+ expect(screen.getByTestId("notification-test")).toHaveAttribute("aria-hidden", "false");
107
+ expect(screen.getByTestId("notification-test")).toHaveClass(`${rootClassName}--info`);
108
+ expect(screen.getByTestId("notification-test")).toHaveClass(
109
+ `${rootClassName}--has-close-button`
110
+ );
111
+ expect(screen.getByTestId("notification-test-icon")).toBeInTheDocument();
112
+ expect(screen.getByTestId("notification-test-heading")).toHaveTextContent("Test");
113
+ expect(screen.getByTestId("notification-test-body")).toHaveTextContent("Some text content");
114
+ expect(screen.getByTestId("notification-test-paragraph")).toHaveTextContent(
115
+ "Some text content"
116
+ );
117
+
118
+ const closeButton = screen.getByTestId("notification-test-close-button");
119
+ expect(closeButton).toHaveAttribute("aria-label", "Close notification");
120
+ closeButton.click();
121
+ expect(onCloseMock).toHaveBeenCalledOnce();
122
+ });
123
+
124
+ it("should be hidden when given hidden", () => {
125
+ render(
126
+ <Notification
127
+ heading="Test"
128
+ data-testid="notification-test"
129
+ onClose={vi.fn()}
130
+ closeButtonAllyLabel="Close notification"
131
+ hidden
132
+ >
133
+ Some text content
134
+ </Notification>
135
+ );
136
+
137
+ expect(screen.getByTestId("notification-test")).toHaveAttribute("aria-hidden", "true");
138
+ expect(screen.getByTestId("notification-test")).toHaveClass(`${rootClassName}--info`);
139
+ expect(screen.getByTestId("notification-test")).toHaveClass(
140
+ `${rootClassName}--has-close-button`
141
+ );
142
+ expect(screen.getByTestId("notification-test-icon")).toBeInTheDocument();
143
+ expect(screen.getByTestId("notification-test-heading")).toHaveTextContent("Test");
144
+ expect(screen.getByTestId("notification-test-body")).toHaveTextContent("Some text content");
145
+ expect(screen.getByTestId("notification-test-paragraph")).toHaveTextContent(
146
+ "Some text content"
147
+ );
148
+ expect(screen.getByTestId("notification-test-close-button")).toBeInTheDocument();
149
+ });
150
+ });
@@ -0,0 +1,173 @@
1
+ import React, { ForwardedRef, forwardRef, ReactNode } from "react";
2
+ import { Button } from "@purpurds/button";
3
+ import { Heading, HeadingTagType } from "@purpurds/heading";
4
+ import {
5
+ alertFilled,
6
+ checkCircleFilled,
7
+ close,
8
+ errorFilled,
9
+ Icon,
10
+ infoFilled,
11
+ } from "@purpurds/icon";
12
+ import { Paragraph } from "@purpurds/paragraph";
13
+ import c from "classnames";
14
+
15
+ import styles from "./notification.module.scss";
16
+
17
+ export const NOTIFICATION_STATUS = {
18
+ SUCCESS: "success",
19
+ WARNING: "warning",
20
+ ERROR: "error",
21
+ INFO: "info",
22
+ } as const;
23
+
24
+ export const notificationStatuses = Object.values(NOTIFICATION_STATUS);
25
+ export type NotificationStatus = (typeof NOTIFICATION_STATUS)[keyof typeof NOTIFICATION_STATUS];
26
+
27
+ export const NOTIFICATION_ARIA_LIVE = {
28
+ POLITE: "polite",
29
+ ASSERTIVE: "assertive",
30
+ } as const;
31
+
32
+ export const notificationAriaLiveValues = Object.values(NOTIFICATION_ARIA_LIVE);
33
+ export type NotificationAriaLive =
34
+ (typeof NOTIFICATION_ARIA_LIVE)[keyof typeof NOTIFICATION_ARIA_LIVE];
35
+
36
+ export const NOTIFICATION_ROLE = {
37
+ ALERT: "alert",
38
+ DIALOG: "dialog",
39
+ ALERTDIALOG: "alertdialog",
40
+ } as const;
41
+
42
+ export const notificationRoles = Object.values(NOTIFICATION_ROLE);
43
+ export type NotificationRole = (typeof NOTIFICATION_ROLE)[keyof typeof NOTIFICATION_ROLE];
44
+
45
+ const rootClassName = "purpur-notification";
46
+
47
+ const getStatusIcon = (status: NotificationStatus) => {
48
+ switch (status) {
49
+ case NOTIFICATION_STATUS.INFO:
50
+ return infoFilled;
51
+ case NOTIFICATION_STATUS.SUCCESS:
52
+ return checkCircleFilled;
53
+ case NOTIFICATION_STATUS.WARNING:
54
+ return alertFilled;
55
+ case NOTIFICATION_STATUS.ERROR:
56
+ return errorFilled;
57
+ }
58
+ };
59
+
60
+ type DefaultProps = {
61
+ ["data-testid"]?: string;
62
+ ["aria-live"]?: NotificationAriaLive;
63
+ children?: ReactNode;
64
+ className?: string;
65
+ headingTag?: HeadingTagType;
66
+ status?: NotificationStatus;
67
+ heading?: string;
68
+ role?: NotificationRole;
69
+ hidden?: boolean;
70
+ id?: string;
71
+ };
72
+
73
+ type CloseButtonProps = {
74
+ onClose: () => void;
75
+ closeButtonAllyLabel: string;
76
+ };
77
+
78
+ type NoCloseButtonProps = {
79
+ onClose?: never;
80
+ closeButtonAllyLabel?: never;
81
+ };
82
+
83
+ export type NotificationProps = DefaultProps & (CloseButtonProps | NoCloseButtonProps);
84
+
85
+ const NotificationComponent = <T extends HTMLDivElement>(
86
+ {
87
+ ["data-testid"]: dataTestId,
88
+ ["aria-live"]: ariaLive,
89
+ children,
90
+ className,
91
+ closeButtonAllyLabel,
92
+ headingTag = "h4",
93
+ role,
94
+ status = "info",
95
+ heading,
96
+ hidden,
97
+ id,
98
+ onClose,
99
+ ...props
100
+ }: NotificationProps,
101
+ ref: ForwardedRef<T>
102
+ ) => (
103
+ <div
104
+ id={id}
105
+ aria-labelledby={id ? `${id}-heading` : undefined}
106
+ aria-describedby={id ? `${id}-body` : undefined}
107
+ aria-live={ariaLive}
108
+ aria-hidden={!!hidden}
109
+ className={c(className, styles[rootClassName], styles[`${rootClassName}--${status}`], {
110
+ [styles[`${rootClassName}--hidden`]]: hidden,
111
+ [styles[`${rootClassName}--has-close-button`]]: !!onClose,
112
+ })}
113
+ data-testid={dataTestId}
114
+ ref={ref}
115
+ role={role}
116
+ {...props}
117
+ >
118
+ <div className={styles[`${rootClassName}__top`]}>
119
+ <div className={styles[`${rootClassName}__header`]}>
120
+ <Icon
121
+ data-testid={dataTestId ? `${dataTestId}-icon` : undefined}
122
+ className={styles[`${rootClassName}__icon`]}
123
+ svg={getStatusIcon(status)}
124
+ size="md"
125
+ />
126
+ {!!heading && (
127
+ <Heading
128
+ data-testid={dataTestId ? `${dataTestId}-heading` : undefined}
129
+ variant="subsection-100"
130
+ tag={headingTag}
131
+ className={styles[`${rootClassName}__heading`]}
132
+ >
133
+ <span id={id ? `${id}-heading` : undefined}> {heading}</span>
134
+ </Heading>
135
+ )}
136
+ </div>
137
+ {onClose && (
138
+ <Button
139
+ data-testid={dataTestId ? `${dataTestId}-close-button` : undefined}
140
+ variant="tertiary-purple"
141
+ size="sm"
142
+ iconOnly
143
+ aria-label={closeButtonAllyLabel}
144
+ onClick={onClose}
145
+ className={styles[`${rootClassName}__close-button`]}
146
+ >
147
+ <Icon svg={close} size="sm" />
148
+ </Button>
149
+ )}
150
+ </div>
151
+ {children && (
152
+ <div
153
+ data-testid={dataTestId ? `${dataTestId}-body` : undefined}
154
+ className={styles[`${rootClassName}__body`]}
155
+ id={id ? `${id}-body` : undefined}
156
+ >
157
+ {typeof children === "string" ? (
158
+ <Paragraph
159
+ data-testid={dataTestId ? `${dataTestId}-paragraph` : undefined}
160
+ variant="paragraph-100"
161
+ >
162
+ {children}
163
+ </Paragraph>
164
+ ) : (
165
+ children
166
+ )}
167
+ </div>
168
+ )}
169
+ </div>
170
+ );
171
+
172
+ export const Notification = forwardRef(NotificationComponent);
173
+ Notification.displayName = "Notification";