Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -7,7 +7,7 @@
import * as React_2 from 'react';

// @public (undocumented)
export const Toaster: React_2.FC;
export const Toaster: React_2.FC<ToasterProps>;

// @public (undocumented)
export type ToastId = string;
Expand All @@ -17,7 +17,7 @@ export type ToastPosition = 'top-right' | 'top-center' | 'top-left' | 'bottom-ri

// @public (undocumented)
export function useToastController(): {
dispatchToast: (content: React_2.ReactNode, options?: ToastOptions | undefined) => void;
dispatchToast: (content: React_2.ReactNode, options?: Partial<ToastOptions> | undefined) => void;
dismissToast: (toastId?: string | undefined) => void;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ const useStyles = makeStyles({
},
});

export const Toast: React.FC<Omit<ToastProps, 'content'> & { visible: boolean }> = props => {
export const Toast: React.FC<ToastProps & { visible: boolean }> = props => {
const styles = useStyles();
const { visible, children, close, remove, ...toastOptions } = props;
const { timeout } = toastOptions;
const { play, running, toastRef } = useToast<HTMLDivElement>(toastOptions);
const { play, running, toastRef } = useToast<HTMLDivElement>({ ...toastOptions, content: children });

// start the toast once it's fully in
useIsomorphicLayoutEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import * as React from 'react';
import { Portal } from '@fluentui/react-portal';
import { useToaster, getPositionStyles } from '../state';
import { useToaster, getPositionStyles, ToasterOptions } from '../state';
import { Toast } from './Toast';
import { makeStyles, mergeClasses } from '@griffel/react';

Expand All @@ -14,21 +14,23 @@ const useStyles = makeStyles({
},
});

export const Toaster: React.FC = () => {
const { getToastsToRender, isToastVisible } = useToaster();
export type ToasterProps = Partial<ToasterOptions>;

export const Toaster: React.FC<ToasterProps> = props => {
const { getToastsToRender, isToastVisible, toasterRef } = useToaster<HTMLDivElement>(props);

const styles = useStyles();

return (
<Portal>
<div>
<div ref={toasterRef}>
{getToastsToRender((position, toasts) => {
return (
<div key={position} style={getPositionStyles(position)} className={mergeClasses(styles.container)}>
{toasts.map(({ content, ...toastProps }) => {
{toasts.map(toastProps => {
return (
<Toast {...toastProps} key={toastProps.toastId} visible={isToastVisible(toastProps.toastId)}>
{content as React.ReactNode}
{toastProps.content as React.ReactNode}
</Toast>
);
})}
Expand Down
38 changes: 21 additions & 17 deletions packages/react-components/react-toast/src/state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,36 @@ export type ToastId = string;
export type ToastPosition = 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left';

export interface ToastOptions {
toastId?: ToastId;
position?: ToastPosition;
content?: unknown;
timeout?: number;
pauseOnWindowBlur?: boolean;
pauseOnHover?: boolean;
toastId: ToastId;
position: ToastPosition;
content: unknown;
timeout: number;
pauseOnWindowBlur: boolean;
pauseOnHover: boolean;
}

export interface DefaultToastOptions
extends Pick<ToastOptions, 'position' | 'timeout' | 'pauseOnWindowBlur' | 'pauseOnHover'> {}

export interface ValidatedToastOptions extends Required<DefaultToastOptions> {}
export interface ToasterOptions
extends Pick<ToastOptions, 'position' | 'timeout' | 'pauseOnWindowBlur' | 'pauseOnHover'> {
offset?: number[];
toasterId?: string;
}

export interface Toast extends Required<ToastOptions> {
export interface Toast extends ToastOptions {
close: () => void;
remove: () => void;
}

export interface ShowToastEventDetail extends Partial<ToastOptions> {
toastId: ToastId;
}

export interface DismissToastEventDetail {
toastId: ToastId | undefined;
}

export interface ToastEventMap {
[EVENTS.show]: CustomEvent<ToastOptions>;
[EVENTS.dismiss]: CustomEvent<DismissToastEventDetail>;
}
type EventListener<TDetail> = (e: CustomEvent<TDetail>) => void;

export type ToastEventListenerGeneric<K extends keyof ToastEventMap> = (e: ToastEventMap[K]) => void;
export type ToastEventListener = <K extends keyof ToastEventMap>(e: ToastEventMap[K]) => void;
export type ToastListenerMap = {
[EVENTS.show]: EventListener<ShowToastEventDetail>;
[EVENTS.dismiss]: EventListener<DismissToastEventDetail>;
};
29 changes: 10 additions & 19 deletions packages/react-components/react-toast/src/state/useToast.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import * as React from 'react';
import { useForceUpdate } from '@fluentui/react-utilities';
import { Toast } from './vanilla/toast';
import { ValidatedToastOptions } from './types';
import { ToastOptions } from './types';

const noop = () => null;

export function useToast<TElement extends HTMLElement>(options: ValidatedToastOptions) {
const toastOptions = useToastOptions(options);
export function useToast<TElement extends HTMLElement>(options: ToastOptions) {
const { pauseOnHover, pauseOnWindowBlur } = options;

const forceRender = useForceUpdate();
const [toast] = React.useState(() => new Toast());

Expand All @@ -15,10 +16,14 @@ export function useToast<TElement extends HTMLElement>(options: ValidatedToastOp
React.useEffect(() => {
if (toast && toastRef.current) {
toast.onUpdate = forceRender;
toast.connectToDOM(toastRef.current, toastOptions);
toast.connectToDOM(toastRef.current, {
pauseOnHover,
pauseOnWindowBlur,
});

return () => toast.disconnect();
}
}, [toast, toastOptions, forceRender]);
}, [toast, pauseOnWindowBlur, pauseOnHover, forceRender]);

if (!toast) {
return {
Expand All @@ -36,17 +41,3 @@ export function useToast<TElement extends HTMLElement>(options: ValidatedToastOp
running: toast.running,
};
}

function useToastOptions(options: ValidatedToastOptions) {
const { pauseOnHover, pauseOnWindowBlur, position, timeout } = options;

return React.useMemo<ValidatedToastOptions>(
() => ({
pauseOnHover,
pauseOnWindowBlur,
position,
timeout,
}),
[pauseOnHover, pauseOnWindowBlur, position, timeout],
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export function useToastController() {
const { targetDocument } = useFluent();

const dispatchToast = React.useCallback(
(content: React.ReactNode, options?: ToastOptions) => {
(content: React.ReactNode, options?: Partial<ToastOptions>) => {
if (targetDocument) {
dispatchToastVanilla(content, options, targetDocument);
}
Expand Down
47 changes: 30 additions & 17 deletions packages/react-components/react-toast/src/state/useToaster.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import * as React from 'react';
import { Toaster } from './vanilla/toaster';
import { useForceUpdate } from '@fluentui/react-utilities';
import { Toast, ToastId, ToastPosition } from './types';
import { Toast, ToastPosition, ToasterOptions } from './types';
import { ToasterProps } from '../components/Toaster';

export function useToaster() {
const { targetDocument } = useFluent();
export function useToaster<TElement extends HTMLElement>(options: ToasterProps = {}) {
const forceRender = useForceUpdate();
const [toaster] = React.useState(() => {
if (targetDocument) {
const newToaster = new Toaster(targetDocument);
newToaster.onUpdate = forceRender;
return newToaster;
const defaultToastOptions = useToastOptions(options);
const toasterRef = React.useRef<TElement>(null);
const [toaster] = React.useState(() => new Toaster());

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

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

const getToastsToRender = React.useCallback(
<T>(cb: (position: ToastPosition, toasts: Toast[]) => T) => {
Expand Down Expand Up @@ -42,13 +46,22 @@ export function useToaster() {
);

return {
isToastVisible: (toastId: ToastId) => {
if (toaster) {
return toaster.isToastVisible(toastId);
}

return false;
},
toasterRef,
isToastVisible: toaster.isToastVisible,
getToastsToRender,
};
}

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

return React.useMemo<Partial<ToasterOptions>>(
() => ({
pauseOnHover,
pauseOnWindowBlur,
position,
timeout,
}),
[pauseOnHover, pauseOnWindowBlur, position, timeout],
);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { ToastOptions } from '../types';
import { ShowToastEventDetail, ToastOptions } from '../types';
import { EVENTS } from '../constants';

let counter = 0;

export function dispatchToast(content: unknown, options: ToastOptions = {}, targetDocument: Document) {
options.toastId ??= (counter++).toString();
const event = new CustomEvent<ToastOptions>(EVENTS.show, {
export function dispatchToast(content: unknown, options: Partial<ToastOptions> = {}, targetDocument: Document) {
const detail: ShowToastEventDetail = {
...options,
content,
toastId: options.toastId ?? (counter++).toString(),
};
const event = new CustomEvent<ShowToastEventDetail>(EVENTS.show, {
bubbles: false,
cancelable: false,
detail: { ...options, content },
detail,
});
targetDocument.dispatchEvent(event);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { ValidatedToastOptions } from '../types';
import { ToastOptions } from '../types';

// TODO convert to closure
export class Toast {
public running: boolean;
public onUpdate: () => void;
private toastElement?: HTMLElement;
private pauseOnWindowBlur: ValidatedToastOptions['pauseOnWindowBlur'];
private pauseOnHover: ValidatedToastOptions['pauseOnHover'];
private pauseOnWindowBlur: ToastOptions['pauseOnWindowBlur'];
private pauseOnHover: ToastOptions['pauseOnHover'];

constructor() {
this.running = false;
Expand Down Expand Up @@ -36,8 +36,9 @@ export class Toast {
this.toastElement = undefined;
}

public connectToDOM(element: HTMLElement, options: ValidatedToastOptions) {
public connectToDOM(element: HTMLElement, options: Pick<ToastOptions, 'pauseOnWindowBlur' | 'pauseOnHover'>) {
const { pauseOnHover, pauseOnWindowBlur } = options;

this.pauseOnHover = pauseOnHover;
this.pauseOnWindowBlur = pauseOnWindowBlur;

Expand Down
Loading