@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.
- package/dist/LICENSE.txt +59 -0
- package/dist/notification.cjs.js +26 -0
- package/dist/notification.cjs.js.map +1 -0
- package/dist/notification.d.ts +47 -0
- package/dist/notification.d.ts.map +1 -0
- package/dist/notification.es.js +657 -0
- package/dist/notification.es.js.map +1 -0
- package/dist/notification.system.js +26 -0
- package/dist/notification.system.js.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +64 -0
- package/readme.mdx +56 -0
- package/src/global.d.ts +4 -0
- package/src/notification.module.scss +70 -0
- package/src/notification.stories.tsx +115 -0
- package/src/notification.test.tsx +150 -0
- package/src/notification.tsx +173 -0
package/dist/styles.css
ADDED
|
@@ -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
|
+
```
|
package/src/global.d.ts
ADDED
|
@@ -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";
|