Skip to content

Commit

Permalink
feat: ✨implement toast
Browse files Browse the repository at this point in the history
  • Loading branch information
huang-xiao-jian committed Jun 24, 2020
1 parent 903baee commit 28794f6
Show file tree
Hide file tree
Showing 5 changed files with 429 additions and 0 deletions.
58 changes: 58 additions & 0 deletions packages/Toast/Toast.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
.van-toast {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: content-box;
color: #fff;
color: var(--toast-text-color, #fff);
font-size: 14px;
font-size: var(--toast-font-size, 14px);
line-height: 20px;
line-height: var(--toast-line-height, 20px);
white-space: pre-wrap;
word-wrap: break-word;
background-color: rgba(50, 50, 51, 0.88);
background-color: var(--toast-background-color, rgba(50, 50, 51, 0.88));
border-radius: 4px;
border-radius: var(--toast-border-radius, 4px);
}
.van-toast__container {
position: fixed;
top: 50%;
left: 50%;
width: fit-content;
transform: translate(-50%, -50%);
max-width: 70%;
max-width: var(--toast-max-width, 70%);
}
.van-toast--text {
min-width: 96px;
min-width: var(--toast-text-min-width, 96px);
padding: 8px 12px;
padding: var(--toast-text-padding, 8px 12px);
}
.van-toast--icon {
width: 90px;
width: var(--toast-default-width, 90px);
min-height: 90px;
min-height: var(--toast-default-min-height, 90px);
padding: 16px;
padding: var(--toast-default-padding, 16px);
}
.van-toast--icon .van-toast__icon {
font-size: 48px;
font-size: var(--toast-icon-size, 48px);
}
.van-toast--icon .van-toast__text {
padding-top: 8px;
}
.van-toast__loading {
margin: 10px 0;
}
.van-toast--top {
transform: translate(0, -30vh);
}
.van-toast--bottom {
transform: translate(0, 30vh);
}
252 changes: 252 additions & 0 deletions packages/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/* eslint-disable import/prefer-default-export */
// packages
import React, {
FunctionComponent,
useState,
CSSProperties,
useEffect,
} from 'react';
import clsx from 'clsx';
import { View, Text } from 'remax/wechat';
// internal
import Overlay from '../Overlay';
import Loading from '../Loading';
import Transition from '../Transition';
import Icon from '../Icon';
import { Select, Switch, Case } from '../tools/Switch';
import pickStyle from '../tools/pick-style';
import './Toast.css';

export type ToastType = 'loading' | 'success' | 'fail' | 'text';
export interface ToastOptions {
type: ToastType;
position: 'top' | 'middle' | 'bottom';
message: string;
mask: boolean;
forbidClick: boolean;
loadingType: 'circular' | 'spinner';
zIndex: number;
duration: number;
visible: boolean;
// 容器类名,用以覆盖内部
className?: string;
// 事件回调
onClose?: () => void;
}

// scope
const ToastBox: FunctionComponent<ToastOptions> = (props) => {
const {
mask,
visible,
forbidClick,
type,
position,
loadingType,
zIndex,
message,
className,
children,
} = props;

// 样式派生
const stylesheets: Record<string, CSSProperties> = {
mask: pickStyle({
backgroundColor: mask ? '' : 'transparent',
}),
transition: {
zIndex,
},
};
const classnames = {
container: clsx(className, 'van-toast__container'),
toast: clsx(className, 'van-toast', `van-toast--${position}`, {
'van-toast--text': type === 'text',
'van-toast--icon': type !== 'text',
}),
};

return (
<>
<Select in={mask || forbidClick}>
<Overlay visible={visible} zIndex={zIndex} style={stylesheets.mask} />
</Select>
<Transition
visible={visible}
style={stylesheets.transition}
className={classnames.container}
>
<View className={classnames.toast}>
<Switch>
<Case in={type === 'text'}>
<Text>{message}</Text>
</Case>
<Case in={type === 'loading'}>
<Loading
color="#fff"
type={loadingType}
className="van-toast__loading"
/>
<Select in={!!message}>
<Text className="van-toast__text">{message}</Text>
</Select>
</Case>
<Case default>
<Icon name={type} className="van-toast__icon" />
<Select in={!!message}>
<Text className="van-toast__text">{message}</Text>
</Select>
</Case>
</Switch>
{children}
</View>
</Transition>
</>
);
};

type ToastParam = string | Partial<ToastOptions>;
type Subscriber = (options: ToastOptions) => void;

const parseOptions: (options: ToastParam) => Partial<ToastOptions> = (
options
) => {
return typeof options === 'string' ? { message: options } : options;
};

const DefautlToastOptions: ToastOptions = {
type: 'text',
position: 'middle',
message: '',
mask: false,
forbidClick: false,
loadingType: 'circular',
zIndex: 1000,
duration: 2000,
visible: false,
};

// 目前仅支持单个 toast
class ToastManager {
public options: ToastOptions;

private queue: Subscriber[];

private timer: number | null;

constructor(options: ToastOptions) {
this.options = options;
this.queue = [];
this.timer = null;
}

private pipe(options: Partial<ToastOptions>) {
if (this.queue.length === 0) {
throw new Error('ToastProvider required');
}

// 重置延迟关闭
if (typeof this.timer === 'number') {
clearInterval(this.timer);
}

this.options = { ...this.options, ...options };
this.queue.forEach((callback) => {
callback(this.options);
});

// duration === 0 为手动控制模式
if (this.options.duration > 0) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.timer = setTimeout(() => {
this.clear();
}, this.options.duration);
}
}

// 预设选项管理
setDefaultOptions(options: Partial<ToastOptions>) {
this.options = { ...this.options, ...options };
}

resetDefaultOptions() {
this.options = { ...DefautlToastOptions };
}

// 动态处理
info(options: ToastParam) {
this.pipe({ ...parseOptions(options), type: 'te', visible: true });
}

loading(options: ToastParam) {
this.pipe({ ...parseOptions(options), type: 'loading', visible: true });
}

success(options: ToastParam) {
this.pipe({ ...parseOptions(options), type: 'success', visible: true });
}

fail(options: ToastParam) {
this.pipe({ ...parseOptions(options), type: 'fail', visible: true });
}

clear() {
// 重置内部参数
this.options = { ...DefautlToastOptions };
this.queue.forEach((callback) => {
callback(this.options);
});
}

subscribe(callback: Subscriber) {
this.queue.push(callback);

// 释放订阅
return () => this.queue.splice(this.queue.indexOf(callback), 1);
}
}

export const Toast = new ToastManager(DefautlToastOptions);

// 目前仅支持单个 toast
export const ToastProvider: FunctionComponent = () => {
const [options, setOptions] = useState<ToastOptions>(Toast.options);
const {
type,
position,
message,
mask,
forbidClick,
loadingType,
zIndex,
duration,
visible,
onClose,
} = options;

useEffect(() => {
const unsubscribe = Toast.subscribe((_options) => {
setOptions(_options);
});

return () => {
unsubscribe();
};
}, []);

return (
<ToastBox
type={type}
position={position}
message={message}
mask={mask}
forbidClick={forbidClick}
loadingType={loadingType}
zIndex={zIndex}
duration={duration}
visible={visible}
onClose={onClose}
/>
);
};
32 changes: 32 additions & 0 deletions packages/Toast/ToastContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// packages
import { createContext } from 'react';

export interface ToastContextAbstract {
type: 'loading' | 'success' | 'fail' | 'html' | 'text';
position: 'top' | 'middle' | 'bottom';
message: string;
mask: boolean;
forbidClick: boolean;
loadingType: 'circular' | 'spinner';
zIndex: number;
duration: number;
selector: string;
visible: boolean;
}

export const DefautlToastContextPayload: ToastContextAbstract = {
type: 'fail',
position: 'middle',
message: '失败文案',
mask: false,
forbidClick: false,
loadingType: 'spinner',
zIndex: 1000,
duration: 2000,
selector: 'van-toast',
visible: true,
};

export const ToastContext = createContext<ToastContextAbstract>(
DefautlToastContextPayload
);
4 changes: 4 additions & 0 deletions packages/Toast/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* @description - nothing but export component
*/
export * from './Toast';
Loading

0 comments on commit 28794f6

Please sign in to comment.