Skip to content

Commit

Permalink
feat: 新增Toast 轻提示组件
Browse files Browse the repository at this point in the history
  • Loading branch information
79E committed Dec 8, 2022
1 parent 94d9470 commit 26bf99c
Show file tree
Hide file tree
Showing 17 changed files with 682 additions and 0 deletions.
1 change: 1 addition & 0 deletions .umirc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export default defineConfig({
'components/overlay',
'components/back-top',
'components/notify',
'components/toast',
],
},
{
Expand Down
87 changes: 87 additions & 0 deletions src/components/toast/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Toast 轻提示

<code hidden="hidden" src="./demos/demo.tsx"></code>

## 介绍
对操作结果的轻量级反馈,适用于页面内容的变化不能直接反应操作结果时使用。

## 使用

```tsx
import { Toast } from 'aunt';
```

### 基本用法
通过调用`Toast` 方法进行展示提示。
<code src="./demos/demo-base.tsx"></code>

### 自定义图标
可通过`icon` 传入相关内容在自定义图标,可传入图片。
<code src="./demos/demo-icon.tsx"></code>

### 横向排布
通过`direction`属性设置展示的布局,可选`horizontal`进行横向排布。
<code src="./demos/demo-direction.tsx"></code>

### 自定义位置
通过`position`属性设置展示的位置。
<code src="./demos/demo-position.tsx"></code>

## 参数
| 参数 | 说明 | 类型 | 默认值 |
| --------- | -------------- | ---------- | --------- |
| type | 提示类型 | `'loading' \| 'success' \| 'fail' \| 'info'` | `info` |
| message | 文本内容,支持通过\n换行 | `number \| string` | `-` |
| duration | 展示时长(ms),值为 0 时,toast 不会消失 | `number` | `3000` |
| icon | 自定义图标 | `React.ReactNode` | `-` |
| iconSize | 展示时长(ms),值为 0 时,toast 不会消失 | `number \| string \| ((direction: ToastDirection) => number \| string)` | `-` |
| loadingType | 展示时长(ms),值为 0 时,toast 不会消失 | `LoadingType` | `gap` |
| direction | 图标和文字的排列方式 | `'vertical' \| 'horizontal'` | `vertical` |
| forbidClick | 是否禁止背景点击 | `boolean` | `false` |
| position | 位置,可选值为 top bottom | `'top' \| 'center' \| 'bottom'` | `center` |
| teleport | 轻提示弹出时的的父容器 | `HTMLElement \| (() => HTMLElement)` | `body` |
| onClose | 关闭时的回调函数 | `() => void` | `-` |
| onOpened | 完全展示后的回调函数 | `() => void` | `-` |

```tsx
const iconSize = (direction: ToastDirection)=>{
if(direction === 'horizontal') return 20;
return 34;
}
```

## 方法
| 方法名 | 说明 | 参数 | 返回值 |
| --- | --- | --- | --- |
| Toast | 展示提示 | `options \| message` | toast 实例 |
| Toast.info | 展示文字提示 | `options \| message` | toast 实例 |
| Toast.loading | 展示加载提示 | `options \| message` | toast 实例 |
| Toast.success | 展示成功提示 | `options \| message` | toast 实例 |
| Toast.fail | 展示失败提示 | `options \| message` | toast 实例 |
| Toast.clear | 关闭提示 | `-` | `void` |
| Toast.allowMultiple | 允许同时存在多个 Toast | `-` | `void` |
| Toast.setDefaultOptions | 修改默认配置,对所有 Toast 生效。<br>传入 type 可以修改指定类型的默认配置 | `options` | `void` |
| Toast.resetDefaultOptions | 重置默认配置,对所有 Toast 生效。<br>传入 type 可以重置指定类型的默认配置 | `-` | `void` |

```tsx
// toast 实例
export type ToastReturnType = {
/** 动态更新方法 */
config: React.Dispatch<React.SetStateAction<ToastProps>>
/** 清除单例toast */
clear: () => void
}
```
## 样式变量
| 属性名 | 说明 | 默认值 |
| ---------------- | -------- | --------- |
| --aunt-toast-z-index | 显示层级 | `var(--aunt-z-index-full-screen);` |
| --aunt-toast-content-background-color | 背景颜色| `rgba(0,0,0,.8);` |
| --aunt-toast-content-padding | 默认内边距| `var(--aunt-padding-base) var(--aunt-padding-s);` |
| --aunt-toast-content-border-radius | 默认圆角| `var(--aunt-border-radius-md);` |
| --aunt-toast-content-color | 默认字体颜色| `var(--aunt-white-color);` |
| --aunt-toast-content-top | 默认Top高度| `20%;` |
| --aunt-toast-content-bottom | 默认Bottom高度| `20%;` |
| --aunt-toast-content-text-size | 文字大小| `var(--aunt-font-size-sm);` |
| --aunt-toast-content-text-margin-left | 横行排布的文字左外边距 | `var(--aunt-padding-base);` |
178 changes: 178 additions & 0 deletions src/components/toast/controller.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React, { useEffect, useState, useCallback } from 'react';
import type { ToastType, ToastOptions, ToastProps, ToastInstance, ToastReturnType } from './types';
import Transition from '../transition';
import { Toast as BaseToast } from './toast';
import { isObject } from '../../utils';
import { render, unmount } from '../../utils/dom/render';
import { resolveContainer } from '../../utils/dom/getContainer';

const currentOptions: { [key: string]: ToastOptions } = {};

function parseOptions(message: string | ToastOptions): ToastOptions {
return isObject(message) ? message : { message };
}

const toastArray: (() => void)[] = [];

// 同步的销毁
function syncClear() {
let fn = toastArray.pop();
while (fn) {
fn();
fn = toastArray.pop();
}
}

// 针对 toast 还没弹出来就立刻销毁的情况,将销毁放到下一个 event loop 中,避免销毁失败。
function nextTickClear() {
setTimeout(syncClear);
}

const show = (p: ToastProps | string) => {
const props = parseOptions(p);
const update: ToastReturnType = {
config: () => {},
clear: () => null,
};
// 创建父亲节点
const userContainer = resolveContainer(props.teleport);
const container = document.createElement('div');
userContainer.appendChild(container);

// 定时器
let timer = 0;

const TempNotify = () => {
const options = {
...props,
};
const [state, setState] = useState<ToastProps>({ ...options });
const [visible, setVisible] = useState(false);

const destroy = useCallback(() => {
setVisible(false);
if (props.onClose) props.onClose();
}, []);

const internalOnClosed = useCallback(() => {
const unmountResult = unmount(container);
if (unmountResult && container.parentNode) {
container.parentNode.removeChild(container);
}
}, [container]);

update.clear = internalOnClosed;

update.config = useCallback(
nextState => {
setState(prev =>
typeof nextState === 'function'
? { ...prev, ...nextState(prev) }
: { ...prev, ...nextState }
);
},
[setState]
);

useEffect(() => {
setVisible(true);
syncClear();
toastArray.push(internalOnClosed);

if (state.duration && +state.duration > 0) {
timer = window.setTimeout(destroy, state.duration);
}

return () => {
if (timer !== 0) {
window.clearTimeout(timer);
}
};
}, []);

return (
<Transition in={visible} onEntered={props.onOpened} onExited={props.onClose}>
<BaseToast {...state} />
</Transition>
);
};

render(<TempNotify />, container);

return update;
};

const defaultOptions: ToastOptions = {
message: '',
duration: 3000,
direction: 'vertical',
loadingType: 'gap',
position: 'center',
};

['info', 'loading', 'success', 'fail'].forEach(method => {
currentOptions[method] = defaultOptions;
});

const setDefaultOptions = (type: ToastType, options: ToastOptions) => {
currentOptions[type] = Object.assign(currentOptions[type], options);
};

// 重置配置
const resetDefaultOptions = (type: ToastType) => {
currentOptions[type] = { ...defaultOptions };
};

const clear = nextTickClear;

const ToastDefault = (options: ToastProps | string) => {
let type: ToastType = 'info';
if (typeof options !== 'string') {
type = options.type || 'info';
}
return show({
type: type,
...currentOptions[type],
...parseOptions(options),
});
};

const info = (options: ToastOptions | string) =>
show({
...currentOptions['info'],
...parseOptions(options),
type: 'info',
});

const fail = (options: ToastOptions | string) =>
show({
...currentOptions['fail'],
...parseOptions(options),
type: 'fail',
});

const success = (options: ToastOptions | string) =>
show({
...currentOptions['success'],
...parseOptions(options),
type: 'success',
});

const loading = (options: ToastOptions | string) =>
show({
...currentOptions['loading'],
...parseOptions(options),
type: 'loading',
});

const ToastController: ToastInstance = Object.assign(ToastDefault, {
setDefaultOptions,
resetDefaultOptions,
clear,
info,
fail,
loading,
success,
});

export default ToastController;
21 changes: 21 additions & 0 deletions src/components/toast/demos/demo-base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { Space, Button, Toast } from 'aunt';

export default () => {
return (
<Space wrap direction='vertical' style={{ width: '100%' }}>
<Button type='primary' block onClick={() => Toast('文字提示')}>
文字提示
</Button>
<Button type='success' block onClick={() => Toast.success('成功提示')}>
成功提示
</Button>
<Button type='danger' block onClick={() => Toast.fail('失败提示')}>
失败提示
</Button>
<Button type='warning' block onClick={() => Toast.loading('加载提示')}>
加载提示
</Button>
</Space>
);
};
45 changes: 45 additions & 0 deletions src/components/toast/demos/demo-direction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { Space, Button, Toast } from 'aunt';

export default () => {
return (
<Space wrap direction='vertical' style={{ width: '100%' }}>
<Button
type='success'
block
onClick={() =>
Toast.success({
message: '成功提示',
direction: 'horizontal',
})
}
>
成功提示
</Button>
<Button
type='danger'
block
onClick={() =>
Toast.fail({
message: '失败提示',
direction: 'horizontal',
})
}
>
失败提示
</Button>
<Button
type='warning'
block
onClick={() =>
Toast.loading({
message: '加载提示',
direction: 'horizontal',
})
}
>
加载提示
</Button>
</Space>
);
};
48 changes: 48 additions & 0 deletions src/components/toast/demos/demo-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import { Space, Button, Toast, AuntIconCheckCircle, AuntIconXCircle } from 'aunt';

export default () => {
return (
<Space wrap direction='vertical' style={{ width: '100%' }}>
<Button
type='success'
block
onClick={() =>
Toast.success({
message: '成功提示',
icon: <AuntIconCheckCircle />,
})
}
>
成功提示
</Button>
<Button
type='danger'
block
onClick={() =>
Toast.fail({
message: '失败提示',
icon: <AuntIconXCircle />,
})
}
>
失败提示
</Button>
<Button
type='warning'
block
onClick={() =>
Toast.loading({
message: '加载提示',
loadingType: 'bars',
})
}
>
加载提示
</Button>
<Button type='primary' block onClick={() => Toast.loading({})}>
单图标模式
</Button>
</Space>
);
};
Loading

0 comments on commit 26bf99c

Please sign in to comment.