-
Notifications
You must be signed in to change notification settings - Fork 1
feat(ui): notification manager #1689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
vlad-schur-external-sap
merged 21 commits into
main
from
vlad-notification-manager-second-iteration
Jun 10, 2026
Merged
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
33cd9f7
feat(ui): notification manager first version
vlad-schur-external-sap a7ff95c
Merge branch 'main' into vlad-notification-manager-second-iteration
vlad-schur-external-sap a6e6203
fix(ui): coderabbit and copilot code-review comments
vlad-schur-external-sap d79fda6
feat(ui): add story prop interface to customise them
vlad-schur-external-sap 8472e6c
feat(ui): add infinity story
vlad-schur-external-sap 71b2883
fix(ui): apply default juno-font to notification-manager
vlad-schur-external-sap b3f4910
feat(ui): move logic from toast to notification-manager
vlad-schur-external-sap d736498
feat(ui): add stories for on-dismiss and on-close callbacks
vlad-schur-external-sap b16b25e
refactor(ui): common types and naming for notification-manager and to…
vlad-schur-external-sap 6d27ce5
Merge branch 'main' into vlad-notification-manager-second-iteration
vlad-schur-external-sap a841de6
fix(ui): tests for toast and not-manager
vlad-schur-external-sap eb97a02
feat(ui): make notification-manager dismissable
vlad-schur-external-sap 819fb13
Merge branch 'main' into vlad-notification-manager-second-iteration
vlad-schur-external-sap 1351366
fix(ui): copilot code review comments
vlad-schur-external-sap 0247531
fix(ui): ai pr comments
vlad-schur-external-sap 001ab45
fix(ui): copilot types and test comments
vlad-schur-external-sap 36fc34c
fix(ui): notification-manager registry concurrent rendering
vlad-schur-external-sap 95f48be
fix(ui): notification-manager type export
vlad-schur-external-sap c745d5d
Merge branch 'main' into vlad-notification-manager-second-iteration
vlad-schur-external-sap 600413d
fix(ui): franz comments for lifecycle management of notification-manager
vlad-schur-external-sap f59da03
fix(ui): sonner locked version
vlad-schur-external-sap File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
201 changes: 201 additions & 0 deletions
201
packages/ui-components/src/components/NotificationManager/NotificationManager.component.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import React from "react" | ||
| import { Toaster, toast as sonnerToast } from "sonner" | ||
| import { customToast, NotificationToast, ToastHandler, ToastPosition, ToastVariants } from "./NotificationManager.types" | ||
| import { Toast } from "../Toast" | ||
|
vlad-schur-external-sap marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * NotificationManager wraps the Sonner toast library and supports rendering | ||
| * multiple notifications simultaneously, each with independent durations and | ||
| * dismissibility settings. | ||
| * | ||
| * @example | ||
| * // Multiple notifications appear at the same time, each with its own timer | ||
| * toast("First notification", { duration: 2000 }) | ||
| * toast("Second notification", { duration: 5000 }) | ||
| * // First closes at 2s, second at 5s, independently | ||
| */ | ||
| export interface NotificationManagerProps { | ||
| /** | ||
| * Optional Sonner toaster id. Use this to scope notifications to a specific | ||
| * NotificationManager instance. | ||
| */ | ||
| id?: string | ||
|
|
||
| /** | ||
| * Controls whether notifications can be dismissed manually. | ||
| * | ||
| * Set to `false` to render non-dismissible notifications that disappear only | ||
| * after their configured duration (or when dismissed programmatically). | ||
| * Can be overridden for individual notifications by passing | ||
| * `closeButton` in `toast()` options. | ||
| * | ||
| * @example | ||
| * toast("Background sync started", { closeButton: false }) | ||
| * | ||
| * @default true | ||
| */ | ||
| dismissible?: boolean | ||
|
|
||
| /** | ||
| * Default display time for notifications in milliseconds. | ||
| * | ||
| * Use this to customize how long timed notifications stay visible before | ||
| * auto-dismiss. | ||
| * Can be overridden for individual notifications by passing | ||
| * `duration` in `toast()` options. | ||
| * | ||
| * @example | ||
| * toast("Changes saved", { duration: 10000 }) | ||
| * | ||
| * @default 4000 | ||
| */ | ||
| duration?: number | ||
|
|
||
| /** | ||
| * Maximum number of notifications visible on screen at once. | ||
| * | ||
| * Additional notifications queue internally and appear as others close. | ||
| * If more toasts exist than this limit allows, hidden toasts remain invisible | ||
| * (CSS `data-visible="false"`). Handling extreme overflow (e.g., >10 simultaneous | ||
| * toasts) via custom scrolling/pagination is not a common requirement and should | ||
| * be addressed per app design if needed. | ||
| * | ||
| * @default 3 | ||
| */ | ||
| visibleToasts?: number | ||
|
|
||
| /** | ||
| * Position of the notification stack on screen. | ||
| * | ||
| * @default "bottom-right" | ||
| */ | ||
| position?: ToastPosition | ||
| } | ||
|
|
||
| /** | ||
| * NotificationManager component that wraps Sonner's Toaster. | ||
| * | ||
| * All lifecycle logic (timers, auto-dismiss, dismissal handling) is delegated | ||
| * to the Sonner library, allowing the Toast component to be a fully logic-less | ||
| * presentational component. | ||
| * | ||
| * Existing notifications can be targeted by id in order to update or dismiss | ||
| * them programmatically. | ||
| * | ||
| * @example | ||
| * const notificationId = toast.error("Error occurred") | ||
| * toast("Error resolved", { id: notificationId }) | ||
| * toast.dismiss(notificationId) | ||
| * | ||
| * Events can also be fired per toast call: | ||
| * - shown: when `toast(...)` returns an id for the created notification | ||
| * - onclick/dismissed/disappeared: through `onClick`, `onDismiss` and `onAutoClose` in toast options | ||
| * | ||
| * @example | ||
| * toast.info("Upload started", { | ||
| * onClick: () => console.log("run the callback passed via onClick"), | ||
| * onDismiss: () => console.log("dismissed by user or programmatically"), | ||
| * onAutoClose: () => console.log("closed after duration"), | ||
| * }) | ||
| * | ||
| * **Visibility & Overflow Behavior:** | ||
| * - By default, `expand={true}` renders all visible notifications at full height | ||
| * with consistent spacing, rather than stacking diminished copies. | ||
| * - The `visibleToasts` prop (default: 3) limits how many notifications display | ||
| * simultaneously. Additional notifications queue invisibly and appear as others close. | ||
| * | ||
| * **Notification History:** | ||
| * - `toast.getToasts()` returns currently active (not dismissed) notifications. | ||
| * - `toast.getHistory()` returns all notifications created during the current runtime, | ||
| * including notifications that have already been dismissed/expired. | ||
| * | ||
| * If persistence across page reloads/navigation/app restarts is needed, it must be | ||
| * implemented with an explicit storage mechanism (e.g., application state, backend, | ||
| * or localStorage). Long-term retention is intentionally the consumer's responsibility. | ||
| * | ||
| * @see Toast - The presentational component (should remain logic-less) | ||
| * @see https://sonner.emilkowal.ski/ | ||
| */ | ||
| export const NotificationManager = ({ | ||
| dismissible = true, | ||
| duration = 4000, | ||
| visibleToasts = 3, | ||
| position = "bottom-right", | ||
| ...props | ||
| }: NotificationManagerProps) => ( | ||
| <Toaster | ||
| expand | ||
| className="juno-notification-manager" | ||
| closeButton={dismissible} | ||
| duration={duration} | ||
| visibleToasts={visibleToasts} | ||
| position={position} | ||
| toastOptions={{ | ||
| classNames: { toast: "juno-toast" }, | ||
| }} | ||
| {...props} | ||
| /> | ||
|
vlad-schur-external-sap marked this conversation as resolved.
Outdated
|
||
| ) | ||
|
|
||
| /** | ||
| * Builds a semantic toast handler that renders Juno's `Toast` component through | ||
| * Sonner's `custom` API. | ||
| * | ||
| * Why this exists: | ||
| * - Keeps Sonner in charge of queueing, timing, and dismissal lifecycle. | ||
| * - Allows Juno-specific markup and visual semantics per variant. | ||
| * - Preserves id-based dismiss/update capabilities from Sonner. | ||
| * | ||
| * The incoming message/description can be values or lazy functions; both are | ||
| * normalized before rendering into the custom toast body. | ||
| */ | ||
| const createSemanticToast = (variant: ToastVariants): ToastHandler => { | ||
| return (message, data) => { | ||
| const title = typeof message === "function" ? message() : message | ||
| const description = typeof data?.description === "function" ? data.description() : data?.description | ||
|
|
||
| // const { description: _description, ...customOptions } = data || {} | ||
|
vlad-schur-external-sap marked this conversation as resolved.
Outdated
|
||
| const { description: _description, ...options } = data ?? {} | ||
|
|
||
| // Use Sonner's custom renderer but keep dismissal bound to Sonner toast id. | ||
| return customToast( | ||
| ({ dismiss }) => ( | ||
| <Toast variant={variant} onDismiss={dismiss}> | ||
| <div className="jn:flex jn:flex-col jn:gap-1"> | ||
| <div>{title}</div> | ||
| {description ? <div className="jn:text-theme-medium">{description}</div> : null} | ||
| </div> | ||
| </Toast> | ||
| ), | ||
| options | ||
| ) | ||
|
vlad-schur-external-sap marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Sonner exposes a broader internal set of toast types | ||
| * (`normal | action | success | info | warning | error | loading | default`), | ||
| * but the semantic helper surface is intentionally aligned with Juno semantics. | ||
| * Juno notification API overrides the semantic variant helpers to: | ||
| * `info | success | warning | error | danger`. | ||
| * | ||
| * Calling `toast()` without a variant renders as the `info` semantic variant. | ||
| * | ||
| * All other Sonner methods, such as `dismiss`, `getHistory`, `getToasts`, | ||
| * `loading`, `promise`, and `custom`, remain available on the exported API. | ||
| */ | ||
| const semanticToasts = { | ||
| info: createSemanticToast("info"), | ||
| success: createSemanticToast("success"), | ||
| warning: createSemanticToast("warning"), | ||
| error: createSemanticToast("error"), | ||
| danger: createSemanticToast("danger"), | ||
| } satisfies Record<ToastVariants, ToastHandler> | ||
|
|
||
| const baseToast: ToastHandler = (message, data) => semanticToasts.info(message, data) | ||
| export const toast = Object.assign(baseToast, sonnerToast, semanticToasts) as NotificationToast | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.