Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: Implement a priority queue",
"packageName": "@fluentui/react-utilities",
"email": "[email protected]",
"dependentChangeType": "patch"
}
9 changes: 6 additions & 3 deletions packages/react-components/react-toast/src/state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ export interface ToastOptions {
pauseOnWindowBlur: boolean;
pauseOnHover: boolean;
toasterId: ToasterId | undefined;
priority: number;
dispatchedAt: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be under Toast

}

export interface ToasterOptions
extends Pick<ToastOptions, 'position' | 'timeout' | 'pauseOnWindowBlur' | 'pauseOnHover'> {
extends Pick<ToastOptions, 'position' | 'timeout' | 'pauseOnWindowBlur' | 'pauseOnHover' | 'priority'> {
offset?: number[];
toasterId?: ToasterId;
limit?: number;
}

export interface Toast extends ToastOptions {
Expand All @@ -31,11 +34,11 @@ export interface CommonToastDetail {
toasterId?: ToasterId;
}

export interface ShowToastEventDetail extends Partial<ToastOptions>, CommonToastDetail {
export interface ShowToastEventDetail extends Partial<Omit<ToastOptions, 'dispatchedAt'>>, CommonToastDetail {
toastId: ToastId;
}

export interface UpdateToastEventDetail extends Partial<ToastOptions>, CommonToastDetail {
export interface UpdateToastEventDetail extends Partial<Omit<ToastOptions, 'dispatchedAt'>>, CommonToastDetail {
toastId: ToastId;
}

Expand Down
17 changes: 9 additions & 8 deletions packages/react-components/react-toast/src/state/useToaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,19 @@ import { Toast, ToastPosition, ToasterOptions } from './types';
import { ToasterProps } from '../components/Toaster';

export function useToaster<TElement extends HTMLElement>(options: ToasterProps = {}) {
const { toasterId, ...rest } = options;
const forceRender = useForceUpdate();
const defaultToastOptions = useToastOptions(rest);
const toasterOptions = useToasterOptions(options);
const toasterRef = React.useRef<TElement>(null);
const [toaster] = React.useState(() => new Toaster(toasterId));
const [toaster] = React.useState(() => new Toaster());

React.useEffect(() => {
if (toasterRef.current) {
toaster.connectToDOM(toasterRef.current, defaultToastOptions);
toaster.connectToDOM(toasterRef.current, toasterOptions);
toaster.onUpdate = forceRender;
}

return () => toaster.disconnect();
}, [toaster, defaultToastOptions, forceRender]);
}, [toaster, forceRender, toasterOptions]);

const getToastsToRender = React.useCallback(
<T>(cb: (position: ToastPosition, toasts: Toast[]) => T) => {
Expand Down Expand Up @@ -53,16 +52,18 @@ export function useToaster<TElement extends HTMLElement>(options: ToasterProps =
};
}

function useToastOptions(options: ToasterProps): Partial<ToasterOptions> {
const { pauseOnHover, pauseOnWindowBlur, position, timeout } = options;
function useToasterOptions(options: ToasterProps): Partial<ToasterOptions> {
const { pauseOnHover, pauseOnWindowBlur, position, timeout, limit, toasterId } = options;

return React.useMemo<Partial<ToasterOptions>>(
() => ({
pauseOnHover,
pauseOnWindowBlur,
position,
timeout,
limit,
toasterId,
}),
[pauseOnHover, pauseOnWindowBlur, position, timeout],
[pauseOnHover, pauseOnWindowBlur, position, timeout, limit, toasterId],
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { EVENTS } from '../constants';

let counter = 0;

export function dispatchToast(content: unknown, options: Partial<ToastOptions> = {}, targetDocument: Document) {
export function dispatchToast(
content: unknown,
options: Partial<Omit<ToastOptions, 'dispatchedAt'>> = {},
targetDocument: Document,
) {
const detail: ShowToastEventDetail = {
...options,
content,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// The underlying implementation of priority queue is vanilla js
import { createPriorityQueue, PriorityQueue } from '@fluentui/react-utilities';
import {
Toast,
ToasterOptions,
ToastId,
ToastOptions,
ToastListenerMap,
UpdateToastEventDetail,
ToasterId,
ShowToastEventDetail,
CommonToastDetail,
ToasterId,
} from '../types';
import { EVENTS } from '../constants';

Expand All @@ -27,26 +29,37 @@ export class Toaster {
public toasts: Map<ToastId, Toast>;
public onUpdate: () => void;
private toasterElement?: HTMLElement;
private toasterOptions: ToasterOptions;
private toasterId?: ToastId;
private toastOptions: Pick<ToastOptions, 'priority' | 'pauseOnHover' | 'pauseOnWindowBlur' | 'position' | 'timeout'>;
private toasterId?: ToasterId;
private queue: PriorityQueue<Toast>;
private limit: number;

private listeners = new Map<keyof ToastListenerMap, ToastListenerMap[keyof ToastListenerMap]>();

constructor(toasterId?: ToasterId) {
this.toasterId = toasterId;
constructor() {
this.toasts = new Map<ToastId, Toast>();
this.visibleToasts = new Set<ToastId>();
this.onUpdate = () => null;
this.toasterOptions = {
this.toastOptions = {
priority: 0,
pauseOnHover: false,
pauseOnWindowBlur: false,
position: 'bottom-right',
timeout: 3000,
};
this.queue = createPriorityQueue<Toast>((a, b) => {
if (a.priority === b.priority) {
return a.dispatchedAt - b.dispatchedAt;
}

return a.priority - b.priority;
});
this.limit = Number.POSITIVE_INFINITY;
}

public disconnect() {
this.toasts.clear();
this.queue.clear();

for (const [event, callback] of this.listeners.entries()) {
this._removeEventListener(event, callback);
Expand All @@ -57,8 +70,11 @@ export class Toaster {
}

public connectToDOM(toasterElement: HTMLElement, options: Partial<ToasterOptions>) {
const { limit = Number.POSITIVE_INFINITY, toasterId, ...rest } = options;
this.toasterElement = toasterElement;
assignDefined(this.toasterOptions, options);
this.limit = limit;
this.toasterId = toasterId;
assignDefined(this.toastOptions, rest);

const buildToast: ToastListenerMap[typeof EVENTS.show] = e => this._buildToast(e.detail);
const updateToast: ToastListenerMap[typeof EVENTS.update] = e => this._updateToast(e.detail);
Expand Down Expand Up @@ -124,6 +140,7 @@ export class Toaster {

private _dismissAllToasts() {
this.visibleToasts.clear();
this.queue.clear();
this.onUpdate();
}

Expand All @@ -141,23 +158,33 @@ export class Toaster {

const remove = () => {
this.toasts.delete(toastId);
if (this.queue.peek()) {
const nextToast = this.queue.dequeue();
this.toasts.set(nextToast.toastId, nextToast);
this.visibleToasts.add(nextToast.toastId);
}
this.onUpdate();
};

const toast: Toast = {
...this.toasterOptions,
...this.toastOptions,
close,
remove,
toastId,
content,
updateId: 0,
toasterId,
dispatchedAt: Date.now(),
};

assignDefined<Toast>(toast, toastOptions);

this.visibleToasts.add(toastId);
this.toasts.set(toastId, toast);
this.onUpdate();
if (this.visibleToasts.size >= this.limit) {
this.queue.enqueue(toast);
} else {
this.toasts.set(toastId, toast);
this.visibleToasts.add(toastId);
this.onUpdate();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import { Toaster, useToastController } from '@fluentui/react-toast';
import { useId } from '@fluentui/react-components';

export const ToasterLimit = () => {
const toasterId = useId('toaster');
const { dispatchToast } = useToastController();
const notify = () => dispatchToast('This is a toast', { toasterId });

return (
<>
<Toaster toasterId={toasterId} limit={3} />
<button onClick={notify}>Make toast</button>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { PauseOnWindowBlur } from './PauseOnWindowBlur.stories';
export { PauseOnHover } from './PauseOnHover.stories';
export { UpdateToast } from './UpdateToast.stories';
export { MultipeToasters } from './MultipleToasters.stories';
export { ToasterLimit } from './ToasterLimit.stories';

export default {
title: 'Preview Components/Toast',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export type ComponentState<Slots extends SlotPropsRecord> = {
[Key in keyof Slots]: ReplaceNullWithUndefined<Exclude<Slots[Key], SlotShorthandValue | (Key extends 'root' ? null : never)>>;
};

// @internal (undocumented)
export function createPriorityQueue<T>(compare: PriorityQueueCompareFn<T>): PriorityQueue<T>;

// @public
export type ExtractSlotProps<S> = Exclude<S, SlotShorthandValue | null | undefined>;

Expand Down Expand Up @@ -113,6 +116,26 @@ export type NativeTouchOrMouseEvent = MouseEvent | TouchEvent;
// @public
export function omit<TObj extends Record<string, any>, Exclusions extends (keyof TObj)[]>(obj: TObj, exclusions: Exclusions): Omit<TObj, Exclusions[number]>;

// @internal (undocumented)
export interface PriorityQueue<T> {
// (undocumented)
all: () => T[];
// (undocumented)
clear: () => void;
// (undocumented)
contains: (item: T) => boolean;
// (undocumented)
dequeue: () => T;
// (undocumented)
enqueue: (item: T) => void;
// (undocumented)
peek: () => T | null;
// (undocumented)
remove: (item: T) => void;
// (undocumented)
size: () => number;
}

// @public (undocumented)
export type ReactTouchOrMouseEvent = React_2.MouseEvent | React_2.TouchEvent;

Expand Down
3 changes: 3 additions & 0 deletions packages/react-components/react-utilities/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ export {
isHTMLElement,
isInteractiveHTMLElement,
omit,
createPriorityQueue,
} from './utils/index';

export type { PriorityQueue } from './utils/priorityQueue';

export { applyTriggerPropsToChildren, getTriggerChild, isFluentTrigger } from './trigger/index';

export type { FluentTriggerComponent, TriggerProps } from './trigger/index';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './omit';
export * from './properties';
export * from './isHTMLElement';
export * from './isInteractiveHTMLElement';
export * from './priorityQueue';
Loading