Skip to content

Instantly share code, notes, and snippets.

@RStankov
Created February 8, 2020 09:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RStankov/826c4010bc5917e1df59b178c4f09983 to your computer and use it in GitHub Desktop.
Save RStankov/826c4010bc5917e1df59b178c4f09983 to your computer and use it in GitHub Desktop.
import Flex from '~/components/Flex';
import Font from '~/components/Font';
import IconClose from './IconClose.svg';
import React from 'react';
import classNames from 'classnames';
import styles from './styles.module.css';
type IToastType = 'notice' | 'success' | 'alert';
type ICloseFn = () => void;
interface IToastOptions {
content?: string | React.ReactNode | ((close: ICloseFn) => React.ReactNode);
title: string;
icon?: string | React.ReactNode;
type?: IToastType;
}
interface IToast extends IToastOptions {
id: number;
type: IToastType;
}
interface IToastContext {
toast: IToast | null;
open: (toast: IToastOptions) => void;
close: () => void;
}
const ToastContext = React.createContext<IToastContext>({
toast: null,
open() {},
close() {},
});
let gid = 0;
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toast, setToast] = React.useState<IToast | null>(null);
const contextValue = React.useMemo(
() => ({
toast,
open(value: IToastOptions) {
setToast({ ...value, type: value.type || 'notice', id: gid += 1 });
},
close() {
setToast(null);
},
}),
[toast, setToast],
);
return (
<ToastContext.Provider value={contextValue}>
{children}
</ToastContext.Provider>
);
}
// NOTE(rstankov): This isn't inside `ToasProvider`, because toasts need to access other contexts (like ModalProvider).
ToastProvider.Content = () => {
const context = React.useContext(ToastContext);
if (!context.toast) {
return null;
}
return (
<div className={styles.container}>
<Toast
key={context.toast.id}
toast={context.toast}
close={context.close}
/>
</div>
);
};
export function useToast() {
return React.useContext(ToastContext).open;
}
function Toast({ toast, close }: { toast: IToast; close: () => void }) {
const [active, setActive] = React.useState(false);
const activate = () => setActive(true);
const deactivate = () => setActive(false);
React.useEffect(() => {
if (active) {
return;
}
const timeoutId = setTimeout(close, 5000);
return () => clearTimeout(timeoutId);
}, [active, close]);
return (
<div
className={styles.content}
key={toast.id}
onFocus={activate}
onBlur={deactivate}
onKeyUp={activate}
onMouseOver={activate}
onClick={activate}
onMouseEnter={activate}
onMouseLeave={deactivate}>
<IconClose className={styles.close} onClick={close} />
<Flex.StaticRow margin={toast.content ? 'small' : null}>
{typeof toast.icon === 'string' ? (
<Font size="medium" bold={true}>
{toast.title}
</Font>
) : (
toast.icon
)}
<Font size="medium" bold={true} className={styles.title}>
{toast.title}
</Font>
</Flex.StaticRow>
{toast.content && (
<Font size="small">
{typeof toast.content === 'function'
? toast.content(close)
: toast.content}
</Font>
)}
<div className={classNames(styles.type, (styles as any)[toast.type])} />
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment