@canonical/react-components 2.2.4 → 2.3.1

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.
Files changed (67) hide show
  1. package/dist/components/NotificationProvider/NotificationProvider.js +3 -2
  2. package/dist/components/NotificationProvider/messageBuilder.js +4 -4
  3. package/dist/components/NotificationProvider/types.d.ts +1 -1
  4. package/dist/components/{Notification → Notifications/Notification}/Notification.d.ts +1 -1
  5. package/dist/components/{Notification → Notifications/Notification}/Notification.js +2 -2
  6. package/dist/components/Notifications/ToastNotification/Animate.d.ts +11 -0
  7. package/dist/components/Notifications/ToastNotification/Animate.js +59 -0
  8. package/dist/components/Notifications/ToastNotification/Toast.scss +107 -0
  9. package/dist/components/Notifications/ToastNotification/ToastNotification.d.ts +10 -0
  10. package/dist/components/Notifications/ToastNotification/ToastNotification.js +63 -0
  11. package/dist/components/Notifications/ToastNotification/ToastNotification.stories.d.ts +6 -0
  12. package/dist/components/Notifications/ToastNotification/ToastNotification.stories.js +64 -0
  13. package/dist/components/Notifications/ToastNotification/ToastNotificationList.d.ts +21 -0
  14. package/dist/components/Notifications/ToastNotification/ToastNotificationList.js +215 -0
  15. package/dist/components/Notifications/ToastNotification/ToastNotificationProvider.d.ts +74 -0
  16. package/dist/components/Notifications/ToastNotification/ToastNotificationProvider.js +207 -0
  17. package/dist/components/Notifications/ToastNotification/index.d.ts +5 -0
  18. package/dist/components/Notifications/ToastNotification/index.js +35 -0
  19. package/dist/components/Notifications/index.d.ts +4 -0
  20. package/dist/components/Notifications/index.js +45 -0
  21. package/dist/esm/components/NotificationProvider/NotificationProvider.js +2 -1
  22. package/dist/esm/components/NotificationProvider/messageBuilder.js +1 -1
  23. package/dist/esm/components/NotificationProvider/types.d.ts +1 -1
  24. package/dist/esm/components/{Notification → Notifications/Notification}/Notification.d.ts +1 -1
  25. package/dist/esm/components/{Notification → Notifications/Notification}/Notification.js +2 -2
  26. package/dist/esm/components/Notifications/Notification/Notification.test.d.ts +1 -0
  27. package/dist/esm/components/Notifications/ToastNotification/Animate.d.ts +11 -0
  28. package/dist/esm/components/Notifications/ToastNotification/Animate.js +52 -0
  29. package/dist/esm/components/Notifications/ToastNotification/Toast.scss +107 -0
  30. package/dist/esm/components/Notifications/ToastNotification/ToastNotification.d.ts +10 -0
  31. package/dist/esm/components/Notifications/ToastNotification/ToastNotification.js +56 -0
  32. package/dist/esm/components/Notifications/ToastNotification/ToastNotification.stories.d.ts +6 -0
  33. package/dist/esm/components/Notifications/ToastNotification/ToastNotification.stories.js +55 -0
  34. package/dist/esm/components/Notifications/ToastNotification/ToastNotification.test.d.ts +1 -0
  35. package/dist/esm/components/Notifications/ToastNotification/ToastNotificationList.d.ts +21 -0
  36. package/dist/esm/components/Notifications/ToastNotification/ToastNotificationList.js +208 -0
  37. package/dist/esm/components/Notifications/ToastNotification/ToastNotificationProvider.d.ts +74 -0
  38. package/dist/esm/components/Notifications/ToastNotification/ToastNotificationProvider.js +202 -0
  39. package/dist/esm/components/Notifications/ToastNotification/index.d.ts +5 -0
  40. package/dist/esm/components/Notifications/ToastNotification/index.js +4 -0
  41. package/dist/esm/components/Notifications/index.d.ts +4 -0
  42. package/dist/esm/components/Notifications/index.js +2 -0
  43. package/dist/esm/hooks/index.d.ts +1 -0
  44. package/dist/esm/hooks/index.js +1 -0
  45. package/dist/esm/hooks/usePrefersReducedMotion.d.ts +6 -0
  46. package/dist/esm/hooks/usePrefersReducedMotion.js +23 -0
  47. package/dist/esm/hooks/usePrefersReducedMotion.test.d.ts +1 -0
  48. package/dist/esm/index.d.ts +5 -3
  49. package/dist/esm/index.js +3 -2
  50. package/dist/hooks/index.d.ts +1 -0
  51. package/dist/hooks/index.js +7 -0
  52. package/dist/hooks/usePrefersReducedMotion.d.ts +6 -0
  53. package/dist/hooks/usePrefersReducedMotion.js +30 -0
  54. package/dist/hooks/usePrefersReducedMotion.test.d.ts +1 -0
  55. package/dist/index.d.ts +5 -3
  56. package/dist/index.js +38 -3
  57. package/package.json +1 -1
  58. /package/dist/components/{Notification → Notifications/Notification}/Notification.stories.d.ts +0 -0
  59. /package/dist/components/{Notification → Notifications/Notification}/Notification.stories.js +0 -0
  60. /package/dist/components/{Notification → Notifications/Notification}/Notification.test.d.ts +0 -0
  61. /package/dist/components/{Notification → Notifications/Notification}/index.d.ts +0 -0
  62. /package/dist/components/{Notification → Notifications/Notification}/index.js +0 -0
  63. /package/dist/{esm/components/Notification/Notification.test.d.ts → components/Notifications/ToastNotification/ToastNotification.test.d.ts} +0 -0
  64. /package/dist/esm/components/{Notification → Notifications/Notification}/Notification.stories.d.ts +0 -0
  65. /package/dist/esm/components/{Notification → Notifications/Notification}/Notification.stories.js +0 -0
  66. /package/dist/esm/components/{Notification → Notifications/Notification}/index.d.ts +0 -0
  67. /package/dist/esm/components/{Notification → Notifications/Notification}/index.js +0 -0
@@ -0,0 +1,6 @@
1
+ import { Meta, StoryObj } from "@storybook/react";
2
+ import { ToastNotificationProvider } from "./index";
3
+ declare const meta: Meta<typeof ToastNotificationProvider>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof ToastNotificationProvider>;
6
+ export declare const Default: Story;
@@ -0,0 +1,55 @@
1
+ import React, { useEffect } from "react";
2
+ import { ToastNotificationProvider, useToastNotification } from "./index";
3
+ import Button from "../../Button";
4
+ var meta = {
5
+ component: ToastNotificationProvider,
6
+ tags: ["autodocs"]
7
+ };
8
+ export default meta;
9
+ export var Default = {
10
+ name: "Default",
11
+ render: () => /*#__PURE__*/React.createElement(ToastNotificationStoryWrapper, null)
12
+ };
13
+ var ToastNotificationStoryWrapper = () => {
14
+ useEffect(() => {
15
+ var root = document.getElementById("storybook-root");
16
+ if (root) {
17
+ root.style.height = "90vh";
18
+ }
19
+ }, []);
20
+ return /*#__PURE__*/React.createElement(ToastNotificationProvider, null, /*#__PURE__*/React.createElement(PreloadedList, null));
21
+ };
22
+ var PreloadedList = () => {
23
+ var toastNotify = useToastNotification();
24
+ useEffect(() => {
25
+ toastNotify.success("Settings saved successfully");
26
+ toastNotify.info("Your changes are syncing in the background");
27
+ toastNotify.failure("Save failed", new Error("500 Internal Server Error"), "Please try again.");
28
+ toastNotify.toggleListView();
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps
30
+ }, []);
31
+ return /*#__PURE__*/React.createElement("div", {
32
+ style: {
33
+ margin: "2rem auto",
34
+ display: "flex",
35
+ flexDirection: "column",
36
+ flexWrap: "wrap",
37
+ alignContent: "flex-start",
38
+ gap: "0.75rem"
39
+ }
40
+ }, /*#__PURE__*/React.createElement(Button, {
41
+ onClick: () => toastNotify.success("Your changes have been saved.", [{
42
+ label: "Undo",
43
+ onClick: () => console.log("Undo clicked")
44
+ }])
45
+ }, "Add Success"), /*#__PURE__*/React.createElement(Button, {
46
+ onClick: () => toastNotify.info("Your changes are syncing in the background.", "Syncing")
47
+ }, "Add Info"), /*#__PURE__*/React.createElement(Button, {
48
+ onClick: () => toastNotify.failure("Save failed", new Error("500 Internal Server Error"), "Please try again.", [{
49
+ label: "Retry",
50
+ onClick: () => console.log("Retry clicked")
51
+ }])
52
+ }, "Add Error"), /*#__PURE__*/React.createElement(Button, {
53
+ onClick: toastNotify.toggleListView
54
+ }, "Toggle List View"));
55
+ };
@@ -0,0 +1,21 @@
1
+ import type { ValueOf } from "../../../types";
2
+ import { GroupedNotificationCount, ToastNotificationType } from "./ToastNotificationProvider";
3
+ import type { FC } from "react";
4
+ import type { NotificationSeverity } from "../../Notifications/Notification";
5
+ import "./Toast.scss";
6
+ export type FilterTypes = ValueOf<typeof NotificationSeverity>;
7
+ export declare const severityOrder: readonly ["positive", "caution", "negative", "information"];
8
+ export declare const iconLookup: {
9
+ readonly positive: "success";
10
+ readonly information: "information";
11
+ readonly caution: "warning";
12
+ readonly negative: "error";
13
+ };
14
+ interface Props {
15
+ notifications: ToastNotificationType[];
16
+ onDismiss: (notification?: ToastNotificationType[]) => void;
17
+ groupedCount: GroupedNotificationCount;
18
+ show: boolean;
19
+ }
20
+ declare const ToastNotificationList: FC<Props>;
21
+ export default ToastNotificationList;
@@ -0,0 +1,208 @@
1
+ import _pt from "prop-types";
2
+ import Button from "../../Button";
3
+ import Icon from "../../Icon";
4
+ import Notification from "../Notification";
5
+ import { DefaultTitles } from "../Notification/Notification";
6
+ import { useLayoutEffect, useRef, useState } from "react";
7
+ import { createPortal } from "react-dom";
8
+ import Animate from "./Animate";
9
+ import { usePrefersReducedMotion } from "../../../hooks";
10
+ import React from "react";
11
+ import { ICONS } from "../../Icon";
12
+ import "./Toast.scss";
13
+ export var severityOrder = ["positive", "caution", "negative", "information"];
14
+ export var iconLookup = {
15
+ positive: ICONS.success,
16
+ information: ICONS.information,
17
+ caution: ICONS.warning,
18
+ negative: ICONS.error
19
+ };
20
+ var ToastNotificationList = _ref => {
21
+ var {
22
+ notifications,
23
+ onDismiss,
24
+ groupedCount = {},
25
+ show
26
+ } = _ref;
27
+ var [filters, setFilters] = useState(new Set());
28
+ var prevNotificationsSize = useRef(notifications.length);
29
+ var containerRef = useRef(null);
30
+ var hasFilters = !!filters.size;
31
+ var preferReducedMotion = usePrefersReducedMotion();
32
+ useLayoutEffect(() => {
33
+ adjustScrollPosition();
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps
35
+ }, [notifications]);
36
+
37
+ // this layout effect is used to maintain scroll position of the
38
+ // notification list when new notifications are added to the list
39
+ // for only when the scroll is at the top
40
+ var adjustScrollPosition = () => {
41
+ var notificationsRemoved = notifications.length < prevNotificationsSize.current;
42
+ prevNotificationsSize.current = notifications.length;
43
+ if (!notifications.length || notificationsRemoved) {
44
+ return;
45
+ }
46
+ var container = containerRef.current;
47
+ var lastNotification = notifications[notifications.length - 1];
48
+ var notificationEl = document.getElementById(lastNotification.id);
49
+ if (container && notificationEl) {
50
+ var currentScrollY = container.scrollTop;
51
+ var offsetHeight = notificationEl.getBoundingClientRect().height + parseFloat(window.getComputedStyle(notificationEl).marginTop) + parseFloat(window.getComputedStyle(notificationEl).marginBottom);
52
+ // only adjust the scroll height if the scroll is at the top
53
+ if (currentScrollY === 0) {
54
+ container.scrollTop = currentScrollY + offsetHeight;
55
+ }
56
+ }
57
+ };
58
+ var handleFilterSelect = filter => {
59
+ setFilters(prevFilters => {
60
+ var newFilters = new Set(prevFilters);
61
+ if (!newFilters.has(filter)) {
62
+ newFilters.add(filter);
63
+ } else {
64
+ newFilters.delete(filter);
65
+ }
66
+ return newFilters;
67
+ });
68
+ };
69
+ var handleGroupedDismiss = () => {
70
+ if (hasFilters) {
71
+ var notificationsToClear = notifications.filter(notification => filters.has(notification.type));
72
+ onDismiss(notificationsToClear);
73
+ setFilters(new Set());
74
+ return;
75
+ }
76
+ onDismiss();
77
+ };
78
+ var getSeverityFilters = () => {
79
+ var filterButtons = severityOrder.map(severity => {
80
+ if (groupedCount[severity]) {
81
+ return /*#__PURE__*/React.createElement("button", {
82
+ "aria-label": "Filter ".concat(severity, " notifications"),
83
+ "aria-pressed": filters.has(severity),
84
+ key: severity,
85
+ className: "u-no-margin u-no-border filter-button",
86
+ onClick: () => {
87
+ handleFilterSelect(severity);
88
+ }
89
+ }, /*#__PURE__*/React.createElement(Icon, {
90
+ name: iconLookup[severity]
91
+ }), /*#__PURE__*/React.createElement("span", null, groupedCount[severity]));
92
+ }
93
+ return null;
94
+ });
95
+ return /*#__PURE__*/React.createElement("div", {
96
+ className: "filters"
97
+ }, filterButtons, hasFilters && /*#__PURE__*/React.createElement("button", {
98
+ className: "u-no-margin--bottom u-no-border",
99
+ onClick: () => {
100
+ setFilters(new Set());
101
+ }
102
+ }, "Clear filters"));
103
+ };
104
+ var getDismissText = () => {
105
+ if (hasFilters) {
106
+ var validFilters = Object.keys(groupedCount);
107
+ var totalCount = 0;
108
+ for (var filter of validFilters) {
109
+ if (filters.has(filter)) {
110
+ totalCount += groupedCount[filter] || 0;
111
+ }
112
+ }
113
+ var dismissText = /*#__PURE__*/React.createElement("span", {
114
+ className: "dismiss-text"
115
+ }, "Dismiss ", totalCount);
116
+ return dismissText;
117
+ }
118
+ return /*#__PURE__*/React.createElement("span", null, "Dismiss all");
119
+ };
120
+ var handleDismissNotification = notification => {
121
+ if (preferReducedMotion) {
122
+ onDismiss([notification]);
123
+ return;
124
+ }
125
+
126
+ // animate the notification dismissal before updating states to delay unmounting
127
+ var element = document.getElementById("li-".concat(notification.id));
128
+ if (element) {
129
+ element.style.transformOrigin = "center";
130
+ element.style.overflow = "hidden";
131
+ var animation = element.animate([{
132
+ height: "".concat(element.scrollHeight, "px"),
133
+ opacity: 1
134
+ }, {
135
+ height: "0px",
136
+ opacity: 0
137
+ }], {
138
+ duration: 200,
139
+ easing: "linear",
140
+ fill: "forwards"
141
+ });
142
+ animation.onfinish = () => {
143
+ element.style.display = "none";
144
+ onDismiss([notification]);
145
+ };
146
+ }
147
+ };
148
+
149
+ // Only filter input data if there are filters present
150
+ var filteredNotifications = hasFilters ? notifications.filter(notification => filters.has(notification.type)) : notifications;
151
+
152
+ // Don't assign alert role for notifications when expanded since we don't want
153
+ // screen readers to announce every existing notification
154
+ var notificationEls = filteredNotifications.map((_, index, array) => {
155
+ var _notification$title;
156
+ var lastNotificationIndex = array.length - 1;
157
+ // This will map notifications in reverse order
158
+ var notification = array[lastNotificationIndex - index];
159
+ return /*#__PURE__*/React.createElement("li", {
160
+ key: notification.id,
161
+ id: "li-".concat(notification.id)
162
+ }, /*#__PURE__*/React.createElement(Notification, {
163
+ id: notification.id,
164
+ title: (_notification$title = notification.title) !== null && _notification$title !== void 0 ? _notification$title : DefaultTitles[notification.type],
165
+ actions: notification.actions,
166
+ severity: notification.type,
167
+ onDismiss: () => {
168
+ handleDismissNotification(notification);
169
+ },
170
+ className: "u-no-margin--bottom individual-notification",
171
+ timestamp: notification.timestamp,
172
+ titleElement: "div"
173
+ }, notification.message));
174
+ });
175
+ return /*#__PURE__*/createPortal( /*#__PURE__*/React.createElement(Animate, {
176
+ show: show,
177
+ from: {
178
+ opacity: 0,
179
+ transform: "translateY(5vh)"
180
+ },
181
+ to: {
182
+ opacity: 1,
183
+ transform: "translateY(0)"
184
+ },
185
+ options: {
186
+ duration: 100
187
+ },
188
+ className: "toast-animate"
189
+ }, /*#__PURE__*/React.createElement("ul", {
190
+ className: "toast-notification-list",
191
+ "aria-label": "Notifications list",
192
+ ref: containerRef
193
+ }, notificationEls, /*#__PURE__*/React.createElement("li", {
194
+ className: "dismiss"
195
+ }, getSeverityFilters(), /*#__PURE__*/React.createElement(Button, {
196
+ className: "u-no-margin--bottom dismiss-button",
197
+ onClick: handleGroupedDismiss,
198
+ hasIcon: true
199
+ }, /*#__PURE__*/React.createElement(Icon, {
200
+ name: "tidy"
201
+ }), getDismissText())))), document.body);
202
+ };
203
+ ToastNotificationList.propTypes = {
204
+ notifications: _pt.array.isRequired,
205
+ onDismiss: _pt.func.isRequired,
206
+ show: _pt.bool.isRequired
207
+ };
208
+ export default ToastNotificationList;
@@ -0,0 +1,74 @@
1
+ import type { NotificationAction, NotificationType } from "../../NotificationProvider";
2
+ import type { ValueOf } from "../../../types";
3
+ import { NotificationSeverity } from "../../Notifications";
4
+ import type { FC, PropsWithChildren, ReactNode } from "react";
5
+ export type ToastNotificationType = NotificationType & {
6
+ timestamp?: ReactNode;
7
+ id: string;
8
+ };
9
+ interface ToastNotificationHelper {
10
+ notifications: ToastNotificationType[];
11
+ success: (message: ReactNode, actions?: NotificationAction[]) => ToastNotificationType;
12
+ info: (message: ReactNode, title?: string) => ToastNotificationType;
13
+ failure: (title: string, error: unknown, message?: ReactNode, actions?: NotificationAction[]) => ToastNotificationType;
14
+ clear: (notification?: ToastNotificationType[]) => void;
15
+ toggleListView: () => void;
16
+ isListView: boolean;
17
+ countBySeverity: GroupedNotificationCount;
18
+ }
19
+ export type GroupedNotificationCount = {
20
+ [key in ValueOf<typeof NotificationSeverity>]?: number;
21
+ };
22
+ /**
23
+ * The `ToastNotificationProvider` can be used to manage toast notifications.
24
+
25
+ Wrap your application with this provider, and in any child component you can get the helper with `const toastNotify = useToastNotification()` to trigger notifications.
26
+ Notifications automatically dismiss after a delay unless manually dismissed or expanded.
27
+
28
+ | **Values** | **Description** |
29
+ |----------------------------------|---------------------------------------------------------------------------------|
30
+ | `toastNotify.success()` | Displays a success toast. Optionally accepts actions. |
31
+ | `toastNotify.info()` | Displays an info toast. Optionally accepts a custom title. |
32
+ | `toastNotify.failure()` | Displays a failure toast with an error and optional message or actions. |
33
+ | `toastNotify.clear()` | Clears specific toasts, or all toasts if none are specified. |
34
+ | `toastNotify.toggleListView()` | Toggles the notification list view open or closed. |
35
+ | `toastNotify.countBySeverity` | Returns the count of notifications grouped by severity (e.g., success, info). |
36
+
37
+ Some example usages:
38
+
39
+ 1. **Show a success toast:**
40
+ ```
41
+ toastNotify.success("Your changes have been saved.");
42
+ toastNotify.success("Your changes have been saved.", [{label: "Undo", onClick: () => console.log("Undo clicked")}]);
43
+ ```
44
+ 2. **Show an info toast:**
45
+ ```
46
+ toastNotify.info("Your changes are syncing in the background.");
47
+ toastNotify.info("Your changes are syncing in the background.", "Syncing");
48
+ ```
49
+
50
+ 3. **Show a failure toast:**
51
+ ```
52
+ toastNotify.failure("Save failed", new Error("500 Internal Server Error"), "Please try again.");
53
+ toastNotify.failure("Save failed", new Error("500 Internal Server Error"), "Please try again.", [{label: "Retry", onClick: () => console.log("Retry clicked")}]);
54
+ ```
55
+ 4. **Clear notifications:**
56
+ ```
57
+ toastNotify.clear(); // clears all toast notifications
58
+ toastNotify.clear([notificationId]); // clears specific toast notifications
59
+ ```
60
+ 5. **Toggle the notification list view:**
61
+ ```
62
+ toastNotify.toggleListView();
63
+ ```
64
+ 6. **Get the count of notifications by severity:**
65
+ ```
66
+ const count = toastNotify.countBySeverity;
67
+ console.log(count.positive);
68
+ ```
69
+
70
+ Alternatively, you can use the `ToastNotification` and `ToastNotificationList` components directly, without using the provider.
71
+ */
72
+ declare const ToastNotificationProvider: FC<PropsWithChildren>;
73
+ export default ToastNotificationProvider;
74
+ export declare const useToastNotification: () => ToastNotificationHelper;
@@ -0,0 +1,202 @@
1
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
2
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
3
+ function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
4
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); }
5
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
6
+ import { failure as _failure, info as _info } from "../../NotificationProvider";
7
+ import { NotificationSeverity } from "./..";
8
+ import ToastNotification from "./ToastNotification";
9
+ import ToastNotificationList from "./ToastNotificationList";
10
+ import { createContext, useContext, useEffect, useState } from "react";
11
+ import React from "react";
12
+ var HIDE_NOTIFICATION_DELAY = 5000;
13
+ var initialNotification = {
14
+ id: "",
15
+ message: "",
16
+ type: "positive"
17
+ };
18
+ var ToastNotificationContext = /*#__PURE__*/createContext({
19
+ /** List of all active toast notifications */
20
+ notifications: [],
21
+ /** Show a success toast. Optionally pass actions. */
22
+ success: () => initialNotification,
23
+ /** Show an info toast. Optionally pass a custom title. */
24
+ info: () => initialNotification,
25
+ /** Show a failure toast with an error and optional message/actions. */
26
+ failure: () => initialNotification,
27
+ /** Clear one or more specific toasts, or all if none provided. */
28
+ clear: () => null,
29
+ /** Toggle between single toast view and list view. */
30
+ toggleListView: () => null,
31
+ /** Whether the notification list view is currently open. */
32
+ isListView: false,
33
+ /** Grouped count of notifications by severity (positive, info, etc.). */
34
+ countBySeverity: {}
35
+ });
36
+
37
+ /**
38
+ * The `ToastNotificationProvider` can be used to manage toast notifications.
39
+
40
+ Wrap your application with this provider, and in any child component you can get the helper with `const toastNotify = useToastNotification()` to trigger notifications.
41
+ Notifications automatically dismiss after a delay unless manually dismissed or expanded.
42
+
43
+ | **Values** | **Description** |
44
+ |----------------------------------|---------------------------------------------------------------------------------|
45
+ | `toastNotify.success()` | Displays a success toast. Optionally accepts actions. |
46
+ | `toastNotify.info()` | Displays an info toast. Optionally accepts a custom title. |
47
+ | `toastNotify.failure()` | Displays a failure toast with an error and optional message or actions. |
48
+ | `toastNotify.clear()` | Clears specific toasts, or all toasts if none are specified. |
49
+ | `toastNotify.toggleListView()` | Toggles the notification list view open or closed. |
50
+ | `toastNotify.countBySeverity` | Returns the count of notifications grouped by severity (e.g., success, info). |
51
+
52
+ Some example usages:
53
+
54
+ 1. **Show a success toast:**
55
+ ```
56
+ toastNotify.success("Your changes have been saved.");
57
+ toastNotify.success("Your changes have been saved.", [{label: "Undo", onClick: () => console.log("Undo clicked")}]);
58
+ ```
59
+ 2. **Show an info toast:**
60
+ ```
61
+ toastNotify.info("Your changes are syncing in the background.");
62
+ toastNotify.info("Your changes are syncing in the background.", "Syncing");
63
+ ```
64
+
65
+ 3. **Show a failure toast:**
66
+ ```
67
+ toastNotify.failure("Save failed", new Error("500 Internal Server Error"), "Please try again.");
68
+ toastNotify.failure("Save failed", new Error("500 Internal Server Error"), "Please try again.", [{label: "Retry", onClick: () => console.log("Retry clicked")}]);
69
+ ```
70
+ 4. **Clear notifications:**
71
+ ```
72
+ toastNotify.clear(); // clears all toast notifications
73
+ toastNotify.clear([notificationId]); // clears specific toast notifications
74
+ ```
75
+ 5. **Toggle the notification list view:**
76
+ ```
77
+ toastNotify.toggleListView();
78
+ ```
79
+ 6. **Get the count of notifications by severity:**
80
+ ```
81
+ const count = toastNotify.countBySeverity;
82
+ console.log(count.positive);
83
+ ```
84
+
85
+ Alternatively, you can use the `ToastNotification` and `ToastNotificationList` components directly, without using the provider.
86
+ */
87
+
88
+ var ToastNotificationProvider = _ref => {
89
+ var {
90
+ children
91
+ } = _ref;
92
+ var [notifications, setNotifications] = useState([]);
93
+ var [showList, setShowList] = useState(false);
94
+ var [notificationTimer, setNotificationTimer] = useState(null);
95
+
96
+ // cleanup on timer if unmounted
97
+ useEffect(() => {
98
+ return () => {
99
+ if (notificationTimer) {
100
+ clearTimeout(notificationTimer);
101
+ }
102
+ };
103
+ // eslint-disable-next-line react-hooks/exhaustive-deps
104
+ }, []);
105
+ var showNotificationWithDelay = () => {
106
+ setNotificationTimer(prevTimer => {
107
+ if (prevTimer) {
108
+ clearTimeout(prevTimer);
109
+ }
110
+ if (!showList) {
111
+ return setTimeout(() => {
112
+ setNotificationTimer(null);
113
+ }, HIDE_NOTIFICATION_DELAY);
114
+ }
115
+ return null;
116
+ });
117
+ };
118
+ var clearNotificationTimer = () => {
119
+ setNotificationTimer(prevTimer => {
120
+ if (prevTimer) {
121
+ clearTimeout(prevTimer);
122
+ }
123
+ return null;
124
+ });
125
+ };
126
+ var addNotification = notification => {
127
+ var notificationToAdd = _objectSpread(_objectSpread({}, notification), {}, {
128
+ timestamp: new Date().toLocaleString(),
129
+ id: Date.now().toString() + (Math.random() + 1).toString(36).substring(7)
130
+ });
131
+ setNotifications(prev => {
132
+ return [...prev, notificationToAdd];
133
+ });
134
+ showNotificationWithDelay();
135
+ return notificationToAdd;
136
+ };
137
+ var clear = notifications => {
138
+ if (!notifications) {
139
+ setNotifications([]);
140
+ setShowList(false);
141
+ clearNotificationTimer();
142
+ return;
143
+ }
144
+ setNotifications(prev => {
145
+ var removeIdLookup = new Set(notifications);
146
+ var newNotifications = prev.filter(item => !removeIdLookup.has(item));
147
+
148
+ // if we are clearing the last notification from an expanded list,
149
+ // then we want to collapse the list as well if all notifications has been cleared
150
+ if (!newNotifications.length) {
151
+ setShowList(false);
152
+ }
153
+ return newNotifications;
154
+ });
155
+ clearNotificationTimer();
156
+ };
157
+ var toggleListView = () => {
158
+ clearNotificationTimer();
159
+ setShowList(prev => !prev);
160
+ };
161
+ var countBySeverity = {
162
+ positive: 0,
163
+ negative: 0,
164
+ caution: 0,
165
+ information: 0
166
+ };
167
+ notifications.forEach(notification => {
168
+ countBySeverity[notification.type] += 1;
169
+ });
170
+ var helper = {
171
+ notifications,
172
+ failure: (title, error, message, actions) => addNotification(_failure(title, error, message, actions)),
173
+ info: (message, title) => addNotification(_info(message, title)),
174
+ success: (message, actions) => addNotification({
175
+ message,
176
+ actions,
177
+ type: NotificationSeverity.POSITIVE
178
+ }),
179
+ clear,
180
+ toggleListView,
181
+ isListView: showList,
182
+ countBySeverity
183
+ };
184
+ var latestNotification = notifications[notifications.length - 1];
185
+ var hasNotifications = !!notifications.length;
186
+ var showNotification = hasNotifications && !showList && notificationTimer;
187
+ var showNotificationList = hasNotifications && showList;
188
+ return /*#__PURE__*/React.createElement(ToastNotificationContext.Provider, {
189
+ value: helper
190
+ }, children, /*#__PURE__*/React.createElement(ToastNotification, {
191
+ notification: latestNotification,
192
+ onDismiss: clear,
193
+ show: !!showNotification
194
+ }), /*#__PURE__*/React.createElement(ToastNotificationList, {
195
+ notifications: notifications,
196
+ groupedCount: countBySeverity,
197
+ show: showNotificationList,
198
+ onDismiss: clear
199
+ }));
200
+ };
201
+ export default ToastNotificationProvider;
202
+ export var useToastNotification = () => useContext(ToastNotificationContext);
@@ -0,0 +1,5 @@
1
+ export { default as ToastNotificationProvider } from "./ToastNotificationProvider";
2
+ export { useToastNotification } from "./ToastNotificationProvider";
3
+ export type { ToastNotificationType } from "./ToastNotificationProvider";
4
+ export { default as ToastNotification } from "./ToastNotification";
5
+ export { default as ToastNotificationList } from "./ToastNotificationList";
@@ -0,0 +1,4 @@
1
+ export { default as ToastNotificationProvider } from "./ToastNotificationProvider";
2
+ export { useToastNotification } from "./ToastNotificationProvider";
3
+ export { default as ToastNotification } from "./ToastNotification";
4
+ export { default as ToastNotificationList } from "./ToastNotificationList";
@@ -0,0 +1,4 @@
1
+ export { default, NotificationSeverity } from "./Notification";
2
+ export type { NotificationProps } from "./Notification";
3
+ export type { ToastNotificationType } from "./ToastNotification";
4
+ export { ToastNotification, ToastNotificationList, useToastNotification, ToastNotificationProvider, } from "./ToastNotification";
@@ -0,0 +1,2 @@
1
+ export { default, NotificationSeverity } from "./Notification";
2
+ export { ToastNotification, ToastNotificationList, useToastNotification, ToastNotificationProvider } from "./ToastNotification";
@@ -5,5 +5,6 @@ export { useOnEscapePressed } from "./useOnEscapePressed";
5
5
  export { usePrevious } from "./usePrevious";
6
6
  export { useThrottle } from "./useThrottle";
7
7
  export { usePagination } from "./usePagination";
8
+ export { usePrefersReducedMotion } from "./usePrefersReducedMotion";
8
9
  export { useWindowFitment } from "./useWindowFitment";
9
10
  export type { WindowFitment } from "./useWindowFitment";
@@ -5,4 +5,5 @@ export { useOnEscapePressed } from "./useOnEscapePressed";
5
5
  export { usePrevious } from "./usePrevious";
6
6
  export { useThrottle } from "./useThrottle";
7
7
  export { usePagination } from "./usePagination";
8
+ export { usePrefersReducedMotion } from "./usePrefersReducedMotion";
8
9
  export { useWindowFitment } from "./useWindowFitment";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Returns true if the user prefers reduced motion.
3
+ * It's useful for conditionally disabling animations for users who prefer reduced motion.
4
+ * @returns {boolean} - true if the user prefers reduced motion, false otherwise.
5
+ */
6
+ export declare const usePrefersReducedMotion: () => boolean;
@@ -0,0 +1,23 @@
1
+ import { useEffect, useState } from "react";
2
+ var QUERY = "(prefers-reduced-motion: reduce)";
3
+ var getInitialState = () => window.matchMedia(QUERY).matches;
4
+
5
+ /**
6
+ * Returns true if the user prefers reduced motion.
7
+ * It's useful for conditionally disabling animations for users who prefer reduced motion.
8
+ * @returns {boolean} - true if the user prefers reduced motion, false otherwise.
9
+ */
10
+ export var usePrefersReducedMotion = () => {
11
+ var [prefersReducedMotion, setPrefersReducedMotion] = useState(getInitialState);
12
+ useEffect(() => {
13
+ var mediaQuery = window.matchMedia(QUERY);
14
+ var listener = event => {
15
+ setPrefersReducedMotion(event.matches);
16
+ };
17
+ mediaQuery.addEventListener("change", listener);
18
+ return () => {
19
+ mediaQuery.removeEventListener("change", listener);
20
+ };
21
+ }, []);
22
+ return prefersReducedMotion;
23
+ };
@@ -0,0 +1 @@
1
+ export {};