Toast refactored to not use modals context

This commit is contained in:
nklhrstv 2020-02-01 10:46:00 +02:00
parent 0b7aa5068a
commit 56ebd9dafc
16 changed files with 119 additions and 118 deletions

View file

@ -2,7 +2,7 @@ require('spatial-navigation-polyfill');
const React = require('react');
const { Router } = require('stremio-router');
const { Core, KeyboardNavigation, ServicesProvider, Shell } = require('stremio/services');
const { Toasts } = require('stremio/common');
const { ToastProvider } = require('stremio/common');
const routerViewsConfig = require('./routerViewsConfig');
const styles = require('./styles');
@ -51,14 +51,14 @@ const App = () => {
<ServicesProvider services={services}>
{
shellInitialized && coreInitialized ?
<Toasts.ContainerProvider className={styles['toasts-container']}>
<ToastProvider className={styles['toasts-container']}>
<Router
className={styles['router']}
homePath={'/'}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</Toasts.ContainerProvider>
</ToastProvider>
:
<div className={styles['app-loader']} />
}

View file

@ -0,0 +1,10 @@
const React = require('react');
const ToastContext = React.createContext({
show: () => { },
clear: () => { }
});
ToastContext.displayName = 'ToastContext';
module.exports = ToastContext;

View file

@ -5,7 +5,7 @@ const Icon = require('stremio-icons/dom');
const Button = require('stremio/common/Button');
const styles = require('./styles');
const Toast = ({ type, title, message, icon, dataset, onSelect, onClose }) => {
const ToastItem = ({ type, title, message, icon, dataset, onSelect, onClose }) => {
const toastOnClick = React.useCallback((event) => {
if (!event.nativeEvent.selectPrevented && typeof onSelect === 'function') {
onSelect({
@ -28,7 +28,7 @@ const Toast = ({ type, title, message, icon, dataset, onSelect, onClose }) => {
}
}, [dataset, onClose]);
return (
<Button className={classnames(styles['toast-container'], styles['alert'], styles[type])} tabIndex={-1} onClick={toastOnClick}>
<Button className={classnames(styles['toast-item-container'], styles['success'], styles[type])} tabIndex={-1} onClick={toastOnClick}>
{
typeof icon === 'string' && icon.length > 0 ?
<div className={styles['icon-container']}>
@ -44,7 +44,12 @@ const Toast = ({ type, title, message, icon, dataset, onSelect, onClose }) => {
:
null
}
<div className={styles['message-container']}>{message}</div>
{
typeof message === 'string' && message.length > 0 ?
<div className={styles['message-container']}>{message}</div>
:
null
}
</div>
<Button className={styles['close-button-container']} title={'Close'} tabIndex={-1} onClick={closeButtonOnClick}>
<Icon className={styles['icon']} icon={'ic_x'} />
@ -53,14 +58,14 @@ const Toast = ({ type, title, message, icon, dataset, onSelect, onClose }) => {
);
};
Toast.propTypes = {
ToastItem.propTypes = {
type: PropTypes.oneOf(['success', 'alert', 'error']),
title: PropTypes.string,
message: PropTypes.string,
icon: PropTypes.string,
dataset: PropTypes.objectOf(PropTypes.string),
dataset: PropTypes.object,
onSelect: PropTypes.func,
onClose: PropTypes.func
};
module.exports = Toast;
module.exports = ToastItem;

View file

@ -0,0 +1,3 @@
const ToastItem = require('./ToastItem');
module.exports = ToastItem;

View file

@ -1,8 +1,8 @@
.toast-container {
.toast-item-container {
display: flex;
flex-direction: row;
width: 25rem;
min-height: 6rem;
max-width: 25rem;
margin-bottom: 1rem;
border: thin solid;
background-color: var(--color-surfacelighter);

View file

@ -0,0 +1,72 @@
const React = require('react');
const PropTypes = require('prop-types');
const ToastItem = require('./ToastItem');
const ToastContext = require('./ToastContext');
const DEFAULT_TIMEOUT = 3000;
const ToastProvider = ({ className, children }) => {
const [container, setContainer] = React.useState(null);
const [items, dispatch] = React.useReducer(
(items, action) => {
switch (action.type) {
case 'add':
return items.concat(action.item);
case 'remove':
return items.filter((item) => item.id !== action.id);
case 'clear':
return [];
default:
return items;
}
},
[]
);
const itemOnClose = React.useCallback((event) => {
clearTimeout(event.dataset.id);
dispatch({ type: 'remove', id: event.dataset.id });
}, []);
const toast = React.useMemo(() => ({
show: (item) => {
const timeout = typeof item.timeout === 'number' && !isNaN(item.timeout) ?
item.timeout
:
DEFAULT_TIMEOUT;
const id = setTimeout(() => {
dispatch({ type: 'remove', id });
}, timeout);
dispatch({
type: 'add',
item: {
...item,
id,
dataset: {
...item.dataset,
id
},
onClose: itemOnClose
}
});
},
clear: () => {
dispatch({ type: 'clear' });
}
}), []);
return (
<ToastContext.Provider value={toast}>
{container instanceof HTMLElement ? children : null}
<div ref={setContainer} className={className}>
{items.map((item, index) => (
<ToastItem key={index} {...item} />
))}
</div>
</ToastContext.Provider>
);
};
ToastProvider.propTypes = {
className: PropTypes.string,
children: PropTypes.node
};
module.exports = ToastProvider;

View file

@ -0,0 +1,7 @@
const ToastProvider = require('./ToastProvider');
const useToast = require('./useToast');
module.exports = {
ToastProvider,
useToast
};

View file

@ -0,0 +1,8 @@
const React = require('react');
const ToastContext = require('./ToastContext');
const useToast = () => {
return React.useContext(ToastContext);
};
module.exports = useToast;

View file

@ -1,3 +0,0 @@
const Toast = require('./Toast');
module.exports = Toast;

View file

@ -1,57 +0,0 @@
const React = require('react');
const { Modal } = require('stremio-router');
const { ModalsContainerContext } = require('stremio-router');
const { useToastsContainer, ToastsContainerProvider } = require('./ToastsContainerContext');
const Toast = require('./Toast');
const DEFAULT_TIMEOUT = 2000;
const Toasts = React.forwardRef(({ className }, ref) => {
const toastsContainer = useToastsContainer();
const [toasts, dispatch] = React.useReducer(
(state, action) => {
switch (action.type) {
case 'add':
return state.concat([action.item]);
case 'remove':
return state.filter(item => item !== action.item);
case 'removeAll':
state.forEach(item => clearTimeout(item.timerId));
return [];
default:
return state;
}
}, []
);
const hideAll = React.useCallback(() => {
dispatch({ type: 'removeAll' });
}, []);
const show = React.useCallback(({ type, icon, title, text, closeButton, timeout, onClick }) => {
timeout = timeout !== null && !isNaN(timeout) ? timeout : DEFAULT_TIMEOUT;
const close = () => {
clearTimeout(newItem.timerId);
dispatch({ type: 'remove', item: newItem });
};
const newItem = { type, icon, title, text, closeButton, timeout, onClick, onClose: close };
if (timeout !== 0) {
newItem.timerId = setTimeout(close, timeout);
}
dispatch({ type: 'add', item: newItem });
return close;
}, []);
React.useImperativeHandle(ref, () => ({ show, hideAll }));
return toasts.length === 0 ? null : (
<ModalsContainerContext.Provider value={toastsContainer}>
<Modal className={className} disabled={true}>
{toasts.map((toast, index) => (<Toast {...toast} key={index} />))}
</Modal>
</ModalsContainerContext.Provider>
);
});
Toasts.ContainerProvider = ToastsContainerProvider;
module.exports = Toasts;

View file

@ -1,7 +0,0 @@
const React = require('react');
const ToastsContainerContext = React.createContext(null);
ToastsContainerContext.displayName = 'ToastsContainerContext';
module.exports = ToastsContainerContext;

View file

@ -1,20 +0,0 @@
const React = require('react');
const PropTypes = require('prop-types');
const ToastsContainerContext = require('./ToastsContainerContext');
const ToastsContainerProvider = ({ className, children }) => {
const [container, setContainer] = React.useState(null);
return (
<ToastsContainerContext.Provider value={container}>
{container instanceof HTMLElement ? children : null}
<div ref={setContainer} className={className} />
</ToastsContainerContext.Provider>
);
};
ToastsContainerProvider.propTypes = {
className: PropTypes.string,
children: PropTypes.node
};
module.exports = ToastsContainerProvider;

View file

@ -1,7 +0,0 @@
const ToastsContainerProvider = require('./ToastsContainerProvider');
const useToastsContainer = require('./useToastsContainer');
module.exports = {
ToastsContainerProvider,
useToastsContainer
};

View file

@ -1,8 +0,0 @@
const React = require('react');
const ToastsContainerContext = require('./ToastsContainerContext');
const useToastsContainer = () => {
return React.useContext(ToastsContainerContext);
};
module.exports = useToastsContainer;

View file

@ -1,3 +0,0 @@
const Toasts = require('./Toasts');
module.exports = Toasts;

View file

@ -16,7 +16,7 @@ const Popup = require('./Popup');
const SharePrompt = require('./SharePrompt');
const Slider = require('./Slider');
const TextInput = require('./TextInput');
const Toasts = require('./Toasts');
const { ToastProvider, useToast } = require('./Toast');
const routesRegexp = require('./routesRegexp');
const useAnimationFrame = require('./useAnimationFrame');
const useBinaryState = require('./useBinaryState');
@ -47,7 +47,8 @@ module.exports = {
SharePrompt,
Slider,
TextInput,
Toasts,
ToastProvider,
useToast,
routesRegexp,
useAnimationFrame,
useBinaryState,