Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -4,7 +4,7 @@

import * as React from 'react';
import { Transition } from 'react-transition-group';
import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities';
import { useIsomorphicLayoutEffect, useMergedRefs } from '@fluentui/react-utilities';
import { makeStyles, mergeClasses, shorthands } from '@griffel/react';
import { useToast, Toast as ToastProps } from '../state';
import { Timer } from './Timer';
Expand Down Expand Up @@ -77,9 +77,10 @@ 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: toastRefBase } = useToast(toastOptions);
const toastRef = useMergedRefs<HTMLElement | null>(React.useRef<HTMLDivElement>(null), toastRefBase);

// 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
43 changes: 35 additions & 8 deletions packages/react-components/react-toast/src/state/useToast.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
import * as React from 'react';
import { Toast } from './vanilla/toast';
import { useForceUpdate } from '@fluentui/react-utilities';
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
import { Toast } from './vanilla/toast';
import { ValidatedToastOptions } from './types';

export function useToast() {
export function useToast(options: ValidatedToastOptions) {
const forceRender = useForceUpdate();
const { targetDocument } = useFluent();
const [toast] = React.useState(() => {
const newToast = new Toast();
newToast.onUpdate = forceRender;
return newToast;
if (targetDocument) {
const newToast = new Toast(targetDocument, options);
newToast.onUpdate = forceRender;
return newToast;
}
});

const toastRef = React.useCallback(
(el: HTMLElement | null) => {
if (el && toast) {
toast.setToastElement(el);
}
},
[toast],
);

const play = React.useCallback(() => {
if (toast) {
toast.play();
}
}, [toast]);

const pause = React.useCallback(() => {
if (toast) {
toast.pause();
}
}, [toast]);

return {
play: toast.play,
pause: toast.pause,
running: toast.running,
toastRef,
play,
pause,
running: toast?.running ?? false,
};
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,58 @@
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 targetDocument: Document;
private toastElement?: HTMLElement;
private pauseOnWindowBlur: ValidatedToastOptions['pauseOnWindowBlur'];
private pauseOnHover: ValidatedToastOptions['pauseOnHover'];

public play() {
this.running = true;
constructor(targetDocument: Document, options: ValidatedToastOptions) {
this.running = false;
this.onUpdate = () => null;
this.targetDocument = targetDocument;

const { pauseOnHover, pauseOnWindowBlur } = options;
this.pauseOnHover = pauseOnHover;
this.pauseOnWindowBlur = pauseOnWindowBlur;

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

public pause() {
this.running = false;
public dispose() {
if (this.pauseOnWindowBlur) {
this.targetDocument.defaultView?.removeEventListener('focus', this.play);
this.targetDocument.defaultView?.removeEventListener('blur', this.pause);
}

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

this.toastElement = undefined;
}

public setToastElement = (element: HTMLElement) => {
this.toastElement = element;
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