ECHOES-1357 Aggregate repeated toasts with a counter badge#693
ECHOES-1357 Aggregate repeated toasts with a counter badge#693david-cho-lerat-sonarsource wants to merge 1 commit into
Conversation
✅ Deploy Preview for echoes-react ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
29fc534 to
00d336b
Compare
00d336b to
c1a873a
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds automatic aggregation for repeated “same text + same variety” toast notifications, showing a repetition counter badge instead of rendering duplicate toasts. It extends the toast utility to track repetition state and refresh the auto-close timer on repeats, while updating the Toast UI to display the new counter in an accessible way.
Changes:
- Implemented automatic same-text toast aggregation in
src/utils/toasts.tsx, including deterministic IDs, repetition tracking, and timer refresh behavior. - Updated
Toastto accept and render arepetitionCountbadge (capped at99+) with a screen-reader announcement, plus added a new i18n key. - Added Storybook coverage and unit tests validating aggregation, variety separation, non-text exclusion, counter capping, and timer reset behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| stories/Toast-stories.tsx | Adds an “AggregatedRepeatedToasts” story and aligns controls/defaults to the updated toast behavior. |
| src/utils/toasts.tsx | Core aggregation logic: computes aggregation IDs from plain text, tracks repetition state, refreshes duration, and clears state on dismiss/auto-close. |
| src/utils/tests/toasts-test.tsx | Adds unit tests covering aggregation behavior, counter capping, non-text exclusion, and timer refresh. |
| src/components/badges/BadgeCounter.tsx | Broadens badge props to support standard <span> attributes while keeping value as the rendered content. |
| src/common/components/Toast.tsx | Renders repetition counter badge + screen-reader label, and adjusts trailing layout to accommodate badge + dismiss button. |
| i18n/keys.json | Adds toast.repetition-count message used for the repetition announcement. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
c1a873a to
46535ce
Compare
46535ce to
48c1d64
Compare
48c1d64 to
2a8f2ea
Compare
2a8f2ea to
d2dd6fe
Compare
d2dd6fe to
a707905
Compare
a707905 to
36ab138
Compare
36ab138 to
7a983ca
Compare
5814d6e to
4f0a389
Compare
4f0a389 to
2e07316
Compare
2e07316 to
1fe96d6
Compare
1fe96d6 to
4624dae
Compare
4624dae to
781e6bb
Compare
781e6bb to
700ed13
Compare
jeremy-davis-sonarsource
left a comment
There was a problem hiding this comment.
Why don't we allow aggregation when onDismiss or onAutoClose is defined?
Looks good, apart from the stories not working properly
| variety: ToastVariety.Info, | ||
| description: TEST_MESSAGE, | ||
| }); | ||
| trackToastId( |
There was a problem hiding this comment.
Why track the toast id?
There was a problem hiding this comment.
The tests track every created toast id so afterEach can dismiss leftovers deterministically and wait for Sonner’s deferred removal. That keeps the toast tests isolated and avoids state leaking between timer-based cases.
I kept lifecycle callbacks as an explicit opt-out because aggregation collapses several toast calls into one visible toast lifecycle. With callbacks, the behavior becomes ambiguous: once per underlying call, once per visible aggregated toast, or only for the last update..? I preferred the explicit opt-out over inventing callback semantics in this PR. |
|
| function hasVisibleToastTitle(title: TextNodeOptional | undefined): boolean { | ||
| if (typeof title === 'string') { | ||
| return title.trim().length > 0; | ||
| } | ||
|
|
||
| if (Array.isArray(title)) { | ||
| return title.some((child) => hasVisibleToastTitle(child)); | ||
| } | ||
|
|
||
| return Boolean(title); | ||
| } |
There was a problem hiding this comment.
💡 Quality: Title-visibility logic duplicated across two modules
hasVisibleToastTitle in Toast.tsx (lines 310-320) and the title-detection branch of getRepeatedToastKey in repeated-toast-tracking.ts (lines 108-115) independently decide whether a title counts as 'visible plain text'. They use different mechanics: Toast.tsx returns Boolean(title) for non-string/non-array nodes and some() for arrays, while the aggregation path concatenates arrays via getToastPlainText and bails on any non-text node. Today this does not produce a user-visible bug (non-text/numeric titles are never aggregated, so repetitionCount stays undefined and no badge renders regardless of where the layout function would place it). However, the two definitions can drift independently as the title type evolves, and a future change to one (e.g. supporting numeric titles in aggregation) could place the counter badge inconsistently relative to where the title actually renders. Consider extracting a single shared helper for 'visible plain-text title' so layout and aggregation stay in agreement.
Was this helpful? React with 👍 / 👎
Code Review 👍 Approved with suggestions 18 resolved / 19 findingsImplements toast aggregation with counter badges for repeated identical messages, successfully resolving numerous edge cases regarding lifecycle management and state cleanup. Consolidate the duplicated 💡 Quality: Title-visibility logic duplicated across two modules📄 src/common/components/Toast.tsx:310-320 📄 src/utils/toast-internals/repeated-toast-tracking.ts:105-119
✅ 18 resolved✅ Edge Case: Repetition counter badge has no upper bound
✅ Quality: Screen-reader "Repeated {count} times" is off-by-one vs occurrences
✅ Quality: New i18n key 'toast.repetition-count' missing from i18n/keys.json
✅ Quality: hasActiveToast relies on undocumented sonner internals
✅ Quality: Timer refresh relies on undocumented Sonner duration behavior
...and 13 more resolved from earlier reviews 🤖 Prompt for agentsOptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |




Summary
description, the samevariety, and no explicitid.title, that title must match too. Description-only toasts aggregate too, and blank plain-text titles are treated like no title.99+while keeping the full count for screen readers.1.Notes
idstill follows the existing stable-id update behavior and bypasses automatic same-text aggregation.toast.dismiss(...).