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 @@ -77,9 +77,9 @@ const useStyles = makeStyles({

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

// start the toast once it's fully in
useIsomorphicLayoutEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@ export const Toaster: React.FC = () => {
<div key={position} style={getPositionStyles(position)} className={mergeClasses(styles.container)}>
{toasts.map(({ content, ...toastProps }) => {
return (
<Toast
{...toastProps}
key={`toast-${toastProps.toastId}`}
visible={isToastVisible(toastProps.toastId)}
>
<Toast {...toastProps} key={toastProps.toastId} visible={isToastVisible(toastProps.toastId)}>
{content as React.ReactNode}
</Toast>
);
Expand Down
9 changes: 8 additions & 1 deletion packages/react-components/react-toast/src/state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ export interface ToastOptions {
position?: ToastPosition;
content?: unknown;
timeout?: number;
pauseOnWindowBlur?: boolean;
pauseOnHover?: boolean;
}

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

export interface ValidatedToastOptions extends Required<DefaultToastOptions> {}

export interface Toast extends Required<ToastOptions> {
close: () => void;
remove: () => void;
}
Expand Down
48 changes: 41 additions & 7 deletions packages/react-components/react-toast/src/state/useToast.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,52 @@
import * as React from 'react';
import { Toast } from './vanilla/toast';
import { useForceUpdate } from '@fluentui/react-utilities';
import { Toast } from './vanilla/toast';
import { ValidatedToastOptions } from './types';

const noop = () => null;

export function useToast() {
export function useToast<TElement extends HTMLElement>(options: ValidatedToastOptions) {
const toastOptions = useToastOptions(options);
const forceRender = useForceUpdate();
const [toast] = React.useState(() => {
const newToast = new Toast();
newToast.onUpdate = forceRender;
return newToast;
});
const [toast] = React.useState(() => new Toast());

const toastRef = React.useRef<TElement>(null);

React.useEffect(() => {
if (toast && toastRef.current) {
toast.onUpdate = forceRender;
toast.connectToDOM(toastRef.current, toastOptions);
return () => toast.disconnect();
}
}, [toast, toastOptions, forceRender]);

if (!toast) {
return {
toastRef,
play: noop,
pause: noop,
running: false,
};
}

return {
toastRef,
play: toast.play,
pause: toast.pause,
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
@@ -1,13 +1,66 @@
import { ValidatedToastOptions } from '../types';

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

public play() {
this.running = true;
constructor() {
this.running = false;
this.onUpdate = () => null;

this.pauseOnHover = false;
this.pauseOnWindowBlur = false;
}

public pause() {
this.running = false;
public disconnect() {
if (!this.toastElement) {
return;
}

const targetDocument = this.toastElement.ownerDocument;

if (this.pauseOnWindowBlur) {
targetDocument.defaultView?.removeEventListener('focus', this.play);
targetDocument.defaultView?.removeEventListener('blur', this.pause);
}

if (this.pauseOnHover) {
this.toastElement.addEventListener('mouseenter', this.pause);
this.toastElement.addEventListener('mouseleave', this.play);
}

this.toastElement = undefined;
}

public connectToDOM(element: HTMLElement, options: ValidatedToastOptions) {
const { pauseOnHover, pauseOnWindowBlur } = options;
this.pauseOnHover = pauseOnHover;
this.pauseOnWindowBlur = pauseOnWindowBlur;

this.toastElement = element;
const targetDocument = element.ownerDocument;
if (this.pauseOnWindowBlur) {
targetDocument.defaultView?.addEventListener('focus', this.play);
targetDocument.defaultView?.addEventListener('blur', this.pause);
}

if (this.pauseOnHover) {
this.toastElement.addEventListener('mouseenter', this.pause);
this.toastElement.addEventListener('mouseleave', this.play);
}
}

public play = () => {
this.running = true;
this.onUpdate();
};

public pause = () => {
this.running = false;
this.onUpdate();
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,14 @@ export class Toaster {
}

private _buildToast(toastOptions: ToastOptions) {
const { toastId = '', position = 'bottom-right', timeout = 3000, content = '' } = toastOptions;
const {
toastId = '',
position = 'bottom-right',
timeout = 3000,
content = '',
pauseOnHover = false,
pauseOnWindowBlur = false,
} = toastOptions;
if (this.toasts.has(toastId)) {
return;
}
Expand All @@ -90,6 +97,8 @@ export class Toaster {
};

const toast: Toast = {
pauseOnHover,
pauseOnWindowBlur,
position,
toastId,
timeout,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from 'react';
import { Toaster, useToastController } from '@fluentui/react-toast';

export const PauseOnHover = () => {
const { dispatchToast } = useToastController();
const notify = () => dispatchToast('Hover me!', { pauseOnHover: true });

return (
<>
<Toaster />
<button onClick={notify}>Make toast</button>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from 'react';
import { Toaster, useToastController } from '@fluentui/react-toast';

export const PauseOnWindowBlur = () => {
const { dispatchToast } = useToastController();
const notify = () => dispatchToast('Click on another window!', { pauseOnWindowBlur: true });

return (
<>
<Toaster />
<button onClick={notify}>Make toast</button>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export { CustomTimeout } from './CustomTimeout.stories';
export { ToastPositions } from './ToastPositions.stories';
export { DismissToast } from './DismissToast.stories';
export { DismissAll } from './DismissAll.stories';
export { PauseOnWindowBlur } from './PauseOnWindowBlur.stories';
export { PauseOnHover } from './PauseOnHover.stories';

export default {
title: 'Preview Components/Toast',
Expand Down