@@ -19,16 +19,25 @@ export type NotificationListVerticalAlignment = 'bottom' | 'top';
1919 * different notification triggers a swap (immediate or after `minDisplayMs`, see the
2020 * scheduling effect); returning `null` triggers the empty-slot exit.
2121 *
22- * Default: `defaultPickNext ` — applies persistent-wins, same-type-refresh and FIFO rules
23- * from the design spec. Pass your own selector to opt out of any of those policies.
24- * The library also exports `pickOldest` (FIFO) and `pickNewest` (LIFO) as low-level
25- * building blocks you can compose .
22+ * Default: `createDefaultPickNext(pickOldest) ` — applies persistent-wins,
23+ * same-type-refresh and FIFO queue rules from the design spec. Pass
24+ * `pickNext={createDefaultPickNext(pickNewest)}` to swap FIFO for LIFO without
25+ * reimplementing those rules, or your own `pickNext` to opt out entirely .
2626 */
2727export type PickNextNotification = (
2828 notifications : Notification [ ] ,
2929 displayed : Notification | null ,
3030) => Notification | null ;
3131
32+ /**
33+ * Picks the next queued notification when no higher-priority rule applies (persistent
34+ * wins, same-type refresh). Used by `createDefaultPickNext`.
35+ */
36+ export type PickFromQueue = (
37+ notifications : Notification [ ] ,
38+ displayed : Notification | null ,
39+ ) => Notification | null ;
40+
3241export type NotificationListProps = {
3342 /** Optional class name for the list container */
3443 className ?: string ;
@@ -53,8 +62,7 @@ export type NotificationListProps = {
5362 /**
5463 * Override the candidate-selection policy. The function decides which notification
5564 * should be displayed given the store and the currently displayed one. Defaults to
56- * `defaultPickNext`, which applies the persistent-wins / same-type-refresh / FIFO rules
57- * documented on `PickNextNotification`.
65+ * `createDefaultPickNext(pickOldest)`. Pass `createDefaultPickNext(pickNewest)` for LIFO.
5866 */
5967 pickNext ?: PickNextNotification ;
6068 /** Panel target consumed by this list. */
@@ -101,8 +109,8 @@ const isPersistent = (notification: Notification) => !notification.duration;
101109const haveSameType = ( a : Notification | null , b : Notification | null ) =>
102110 ! ! a ?. type && ! ! b ?. type && a . type === b . type ;
103111
104- /** FIFO selector — oldest `createdAt` other than `displayed` wins. */
105- export const pickOldest : PickNextNotification = ( notifications , displayed ) => {
112+ /** FIFO queue selector — oldest `createdAt` other than `displayed` wins. */
113+ export const pickOldest : PickFromQueue = ( notifications , displayed ) => {
106114 const excludeId = displayed ?. id ?? null ;
107115 let oldest : Notification | null = null ;
108116 for ( const notification of notifications ) {
@@ -114,8 +122,8 @@ export const pickOldest: PickNextNotification = (notifications, displayed) => {
114122 return oldest ;
115123} ;
116124
117- /** LIFO selector — newest `createdAt` other than `displayed` wins. */
118- export const pickNewest : PickNextNotification = ( notifications , displayed ) => {
125+ /** LIFO queue selector — newest `createdAt` other than `displayed` wins. */
126+ export const pickNewest : PickFromQueue = ( notifications , displayed ) => {
119127 const excludeId = displayed ?. id ?? null ;
120128 let newest : Notification | null = null ;
121129 for ( const notification of notifications ) {
@@ -160,9 +168,9 @@ const pickNewestOfType = (
160168} ;
161169
162170/**
163- * Default `PickNextNotification` selector. Encodes the snackbar concurrency rules from
164- * the design spec — the scheduling effect only decides *when* to swap, this function
165- * decides *what* to swap to.
171+ * Builds the default `PickNextNotification` selector with a configurable queue fallback.
172+ * Encodes the snackbar concurrency rules from the design spec — the scheduling effect
173+ * only decides *when* to swap, the returned function decides *what* to swap to.
166174 *
167175 * Rules, in order of precedence:
168176 * 1. **Persistent wins.** Persistent variants (no `duration`) jump ahead of any
@@ -173,52 +181,56 @@ const pickNewestOfType = (
173181 * same `type` collapses to its latest occurrence. (The scheduling effect detects
174182 * this via `haveSameType` and bypasses the dwell window so the refresh feels
175183 * instant; this function just makes sure the latest same-type is returned.)
176- * 4. **FIFO fallback.** Otherwise, the oldest queued notification (other than the
177- * displayed one) is shown next, so every notification is guaranteed at least
178- * `minDisplayMs` of visibility — nothing is silently dropped.
184+ * 4. **Queue fallback.** Otherwise, `pickFromQueue` selects the next notification
185+ * (default: `pickOldest` / FIFO).
179186 *
180187 * Exported so callers that want to layer behavior on top of the design rules can wrap
181- * this function instead of rewriting it.
188+ * the result instead of rewriting it.
182189 */
183- export const defaultPickNext : PickNextNotification = ( notifications , displayed ) => {
184- if ( notifications . length === 0 ) return null ;
190+ export const createDefaultPickNext =
191+ ( pickFromQueue : PickFromQueue = pickOldest ) : PickNextNotification =>
192+ ( notifications , displayed ) => {
193+ if ( notifications . length === 0 ) return null ;
185194
186- const newestPersistent = pickNewestPersistent ( notifications , null ) ;
195+ const newestPersistent = pickNewestPersistent ( notifications , null ) ;
187196
188- if ( ! displayed ) {
189- return newestPersistent ?? pickOldest ( notifications , null ) ;
190- }
197+ if ( ! displayed ) {
198+ return newestPersistent ?? pickFromQueue ( notifications , null ) ;
199+ }
191200
192- const displayedInStore = notifications . some ( ( { id } ) => id === displayed . id ) ;
193- if ( ! displayedInStore ) {
194- return newestPersistent ?? pickOldest ( notifications , null ) ;
195- }
201+ const displayedInStore = notifications . some ( ( { id } ) => id === displayed . id ) ;
202+ if ( ! displayedInStore ) {
203+ return newestPersistent ?? pickFromQueue ( notifications , null ) ;
204+ }
196205
197- if ( isPersistent ( displayed ) ) {
198- // Currently showing a persistent: only a newer persistent can replace it. Reuse the
199- // earlier lookup when it already points at a different persistent.
200- const newerPersistent =
201- newestPersistent && newestPersistent . id !== displayed . id
202- ? newestPersistent
203- : pickNewestPersistent ( notifications , displayed . id ) ;
204- if ( newerPersistent && newerPersistent . createdAt > displayed . createdAt ) {
205- return newerPersistent ;
206+ if ( isPersistent ( displayed ) ) {
207+ // Currently showing a persistent: only a newer persistent can replace it. Reuse the
208+ // earlier lookup when it already points at a different persistent.
209+ const newerPersistent =
210+ newestPersistent && newestPersistent . id !== displayed . id
211+ ? newestPersistent
212+ : pickNewestPersistent ( notifications , displayed . id ) ;
213+ if ( newerPersistent && newerPersistent . createdAt > displayed . createdAt ) {
214+ return newerPersistent ;
215+ }
216+ return displayed ;
206217 }
207- return displayed ;
208- }
209218
210- // Currently showing a transient.
211- // 1. Same-type "refresh of current" wins immediately — a repeated trigger of the
212- // displayed snackbar collapses to the latest occurrence of that type.
213- const sameTypeNewest = pickNewestOfType ( notifications , displayed . type , displayed . id ) ;
214- if ( sameTypeNewest ) return sameTypeNewest ;
219+ // Currently showing a transient.
220+ // 1. Same-type "refresh of current" wins immediately — a repeated trigger of the
221+ // displayed snackbar collapses to the latest occurrence of that type.
222+ const sameTypeNewest = pickNewestOfType ( notifications , displayed . type , displayed . id ) ;
223+ if ( sameTypeNewest ) return sameTypeNewest ;
215224
216- // 2. Any queued persistent jumps ahead of the queue.
217- if ( newestPersistent ) return newestPersistent ;
225+ // 2. Any queued persistent jumps ahead of the queue.
226+ if ( newestPersistent ) return newestPersistent ;
218227
219- // 3. FIFO fallback.
220- return pickOldest ( notifications , displayed ) ?? displayed ;
221- } ;
228+ // 3. Queue fallback.
229+ return pickFromQueue ( notifications , displayed ) ?? displayed ;
230+ } ;
231+
232+ /** Default selector — design-spec rules with FIFO queue fallback (`pickOldest`). */
233+ const defaultPickNext = createDefaultPickNext ( pickOldest ) ;
222234
223235export const NotificationList = ( {
224236 className,
@@ -316,7 +328,7 @@ export const NotificationList = ({
316328 } , [ displayedNotification , notifications , pickNext ] ) ;
317329
318330 // Main scheduling effect — owns *when* swaps happen. The *what* is delegated entirely
319- // to `pickNext` (default: `defaultPickNext `).
331+ // to `pickNext` (default: `createDefaultPickNext(pickOldest) `).
320332 //
321333 // Runs on every change to `displayedNotification`, `notifications`, `pickNext`, or
322334 // `transitionState`, and decides one of four outcomes:
0 commit comments