From 649d0c53afcefa69ab22083cc7d4372e9d44e3be Mon Sep 17 00:00:00 2001 From: NeverEllipsis Date: Tue, 10 Sep 2024 21:41:59 +0800 Subject: [PATCH] feat: Taost 80% --- packages/bui-core/src/Picker/index.md | 2 +- .../bui-core/src/Toast/FunctionalToast.tsx | 74 ++-- packages/bui-core/src/Toast/Toast.less | 85 ++-- packages/bui-core/src/Toast/Toast.tsx | 51 +-- packages/bui-core/src/Toast/Toast.types.ts | 23 +- packages/bui-core/src/Toast/index.md | 397 +++++++++++++++++- .../src/Transition/Transition.miniapp.tsx | 9 +- packages/bui-utils/package.json | 3 +- .../src/getRootElement/index.miniapp.ts | 14 + .../bui-utils/src/getRootElement/index.ts | 8 + packages/bui-utils/src/index.ts | 2 + packages/bui-utils/src/render.ts | 89 ++++ websites/mini-program/package.json | 2 +- 13 files changed, 626 insertions(+), 133 deletions(-) create mode 100644 packages/bui-utils/src/getRootElement/index.miniapp.ts create mode 100644 packages/bui-utils/src/getRootElement/index.ts create mode 100644 packages/bui-utils/src/render.ts diff --git a/packages/bui-core/src/Picker/index.md b/packages/bui-core/src/Picker/index.md index 8ae9f98..f94d0a1 100644 --- a/packages/bui-core/src/Picker/index.md +++ b/packages/bui-core/src/Picker/index.md @@ -9,7 +9,7 @@ name: Picker 选择器 ## 代码演示 -### 基础弹窗 +### 基础选择器 使用`open`控制选择器的打开/关闭,点击遮罩层等关闭的事件会通过`onClose`回调返回 diff --git a/packages/bui-core/src/Toast/FunctionalToast.tsx b/packages/bui-core/src/Toast/FunctionalToast.tsx index 3a2cacd..a0db566 100644 --- a/packages/bui-core/src/Toast/FunctionalToast.tsx +++ b/packages/bui-core/src/Toast/FunctionalToast.tsx @@ -1,4 +1,4 @@ -import { createRoot } from 'react-dom/client'; +import { render, unmount, getRootElement } from '@bifrostui/utils'; import React, { useCallback, useEffect, useState } from 'react'; import ToastView from './Toast'; import { @@ -21,18 +21,6 @@ const formatProps = (props) => { return props; }; -/** - * Toast组件容器 - */ -const createContainer = ( - getContainer?: HTMLElement | (() => HTMLElement) | undefined, -): HTMLElement => { - const container = - typeof getContainer === 'function' ? getContainer() : getContainer; - - return container || document.body; -}; - /** * 销毁全部Toast */ @@ -49,28 +37,49 @@ const functionalToast = (props: ToastProps) => { duration: 2000, position: 'center', allowMultiple: false, - closeOnClickBackdrop: false, + disableClick: false, ...(formatProps(props) || {}), }; - const rootWrapper = document.createElement('div'); - const container = createContainer(); - container.appendChild(rootWrapper); - const instance: ToastReturnType = { close: () => null, }; + const rootWrapper = document.createElement('div'); + if (options.disableClick) { + const styles = { + position: 'fixed', + top: '0', + bottom: '0', + left: '0', + right: '0', + zIndex: 'var(--bui-z-index-toast)', + }; + Object.keys(styles).forEach((property) => { + rootWrapper.style[property] = styles[property]; + }); + } + + const rootElement = getRootElement(); + rootElement.appendChild(rootWrapper); const ToastComponent = () => { - const { duration, allowMultiple, ...others } = options; + const { duration, allowMultiple, onClose, ...others } = options; const [open, setOpen] = useState(false); let timer; + const fadeTimeout = { + enter: 350, + exit: 150, + }; const close = useCallback(() => { setOpen(false); - if (rootWrapper.parentNode) { - rootWrapper.parentNode.removeChild(rootWrapper); - } + setTimeout(() => { + const unmountRes = unmount(rootWrapper); + if (unmountRes && rootWrapper.parentNode) { + rootWrapper.parentNode.removeChild(rootWrapper); + } + }, fadeTimeout.exit); + onClose?.(); }, [rootWrapper]); useEffect(() => { @@ -92,21 +101,25 @@ const functionalToast = (props: ToastProps) => { return ( { - setOpen(false); - }} {...others} + open={open} + timeout={fadeTimeout} + onClose={close} /> ); }; - const root = createRoot(rootWrapper); - root.render(); + render(, rootWrapper); return instance; }; +/** + * Toast.warning(options: ToastOptions) + * Toast.loading(options: ToastOptions) + * Toast.success(options: ToastOptions) + * Toast.fail(options: ToastOptions) + */ ['warning', 'loading', 'success', 'fail'].forEach((methodName: ToastType) => { functionalToast[methodName] = (options: ToastOptions) => functionalToast({ @@ -114,6 +127,11 @@ const functionalToast = (props: ToastProps) => { ...(formatProps(options) || {}), }); }); + +/** + * 清除所有Toast + * Toast.clear() + */ functionalToast.clear = () => { // 处理toast还未弹出就立刻销毁的情况,将销毁放到下一个时间循环中,避免销毁失败 setTimeout(() => { diff --git a/packages/bui-core/src/Toast/Toast.less b/packages/bui-core/src/Toast/Toast.less index c7a4e54..ce16a2f 100644 --- a/packages/bui-core/src/Toast/Toast.less +++ b/packages/bui-core/src/Toast/Toast.less @@ -1,58 +1,63 @@ @import '~@bifrostui/styles/mixins/index.less'; .bui-toast { - position: fixed; - right: unset; - bottom: unset; - font-family: var(--bui-font-family); - + --min-width: 86px; + --max-width: 80%; --font-size: var(--bui-text-size-1); --color: var(--bui-color-white); --padding: var(--bui-spacing-xl); --word-break: break-all; - --top: 15%; - --bottom: 85%; + --position-top: 15%; + --position-bottom: 85%; --background-color: rgba(0, 0, 0, 0.8); --border-radius: var(--bui-shape-radius-default); + --text-align: center; - &-content { + &.bui-toast-allow-click { position: fixed; - left: 50%; - z-index: var(--bui-z-index-toast); - width: fit-content; - // 无效,最大宽度取决于绝对定位的left值 - max-width: 92%; - padding: var(--padding); - font-size: var(--font-size); - color: var(--color); - border-radius: var(--border-radius); - word-break: var(--word-break); - background-color: var(--background-color); - - &-center { - top: 50%; - transform: translate(-50%, -50%); - } + right: unset; + bottom: unset; + } - &-top { - top: var(--top); - transform: translate(-50%, calc(-1 * var(--top))); - } + position: fixed; + left: 50%; + z-index: var(--bui-z-index-toast); + width: fit-content; + min-width: var(--min-width); + max-width: var(--max-width); + padding: var(--padding); + font-size: var(--font-size); + color: var(--color); + border-radius: var(--border-radius); + word-break: var(--word-break); + white-space: pre-wrap; + background-color: var(--background-color); + text-align: var(--text-align); + font-family: var(--bui-font-family); - &-bottom { - top: var(--bottom); - transform: translate(-50%, calc(-1 * var(--bottom))); - } + &-center { + top: 50%; + transform: translate(-50%, -50%); + } + + &-top { + top: var(--position-top); + transform: translate(-50%, calc(-1 * var(--position-top))); + } + + &-bottom { + top: var(--position-bottom); + transform: translate(-50%, calc(-1 * var(--position-bottom))); + } - &-icon { - display: flex; - flex-direction: column; - align-items: center; + &-icon { + display: flex; + flex-direction: column; + align-items: center; - .bui-svg-icon { - margin-bottom: 8px; - font-size: 30px; - } + .bui-svg-icon { + margin-bottom: 8px; + font-size: 30px; } } } diff --git a/packages/bui-core/src/Toast/Toast.tsx b/packages/bui-core/src/Toast/Toast.tsx index 3e32bf7..29abb5d 100644 --- a/packages/bui-core/src/Toast/Toast.tsx +++ b/packages/bui-core/src/Toast/Toast.tsx @@ -7,7 +7,6 @@ import { SuccessCircleFilledBoldIcon, } from '@bifrostui/icons'; import { ToastProps } from './Toast.types'; -import Modal from '../Modal'; import Fade from '../Fade'; import './Toast.less'; @@ -18,13 +17,12 @@ const ToastComponent = React.forwardRef( const { className, style, + open, type, + icon, message, position = 'center', - icon, - FadeProps, - closeOnClickBackdrop = false, - onClose, + disableClick = false, ...others } = props; @@ -37,31 +35,24 @@ const ToastComponent = React.forwardRef( const iconDom = iconMap[type] || icon; return ( - - -
- {iconDom} - {message} -
-
-
+ +
+ {iconDom} + {message} +
+
); }, ); diff --git a/packages/bui-core/src/Toast/Toast.types.ts b/packages/bui-core/src/Toast/Toast.types.ts index 9fff42a..91e2e29 100644 --- a/packages/bui-core/src/Toast/Toast.types.ts +++ b/packages/bui-core/src/Toast/Toast.types.ts @@ -1,5 +1,4 @@ import React from 'react'; -import { ModalProps } from '../Modal/Modal.types'; import { FadeProps } from '../Fade/Fade.types'; /** @@ -7,13 +6,17 @@ import { FadeProps } from '../Fade/Fade.types'; */ export type ToastType = 'loading' | 'success' | 'fail' | 'warning'; -export interface ToastProps extends ModalProps { +export interface ToastProps extends FadeProps { + /** + * 是否展示 + */ + open?: boolean; /** * 提示类型 */ type?: ToastType; /** - * toast内容 + * toast内容,支持使用`\n`换行 */ message?: string; /** @@ -27,7 +30,7 @@ export interface ToastProps extends ModalProps { */ position?: 'top' | 'center' | 'bottom'; /** - * 是否允许存在多个Toast + * 是否允许同时存在多个Toast * @default false */ allowMultiple?: boolean; @@ -36,24 +39,20 @@ export interface ToastProps extends ModalProps { */ icon?: React.ReactNode; /** - * Fade组件的Props - */ - FadeProps?: Partial; - /** - * 是否在点击遮罩层后关闭 + * 展示Toast时,页面内容是否可以点击 * @default false */ - closeOnClickBackdrop?: boolean; + disableClick?: boolean; /** * 关闭时的回调函数 */ - onClose?: ModalProps['onClose']; + onClose?: () => void; } /** * 函数式调用配置参数 */ -export type ToastOptions = Omit | string; +export type ToastOptions = Omit | string; /** * 函数式调用返回值类型 diff --git a/packages/bui-core/src/Toast/index.md b/packages/bui-core/src/Toast/index.md index eba5944..beb43ae 100644 --- a/packages/bui-core/src/Toast/index.md +++ b/packages/bui-core/src/Toast/index.md @@ -9,30 +9,356 @@ name: Toast 轻提示 ## 代码演示 -### 基础标签 +### 基础提示 + +展示提示内容。 + +```tsx +import { Stack, Button, Toast } from '@bifrostui/react'; +import React from 'react'; + +export default () => { + return ( + + + + ); +}; +``` + +### 常用模式 + +Toast提供了 `warning`、`loading`、`success`、`fail`四种常用模式。 + +```tsx +import { Stack, Button, Toast } from '@bifrostui/react'; +import React from 'react'; + +export default () => { + return ( + + + + + + + ); +}; +``` + +### 提示文案换行 + +提示文案支持使用`\n`换行。 + +```tsx +import { Stack, Button, Toast } from '@bifrostui/react'; +import React from 'react'; + +export default () => { + return ( + + + + ); +}; +``` + +### 展示时长 + +使用`duration`控制提示展示时长,默认展示2秒,当`duration`为0时,Toast不会自动关闭,当然你可以接收返回值,并使用其`close`函数,手动关闭当前Toast。 ```tsx import { Stack, Button, Toast } from '@bifrostui/react'; import React from 'react'; export default () => { - const toastCenter = () => { - Toast.success({ - message: '提示内容', - position: 'center', + let toast; + const showToastA = () => { + toast = Toast({ + message: '我不会自动关闭', + duration: 0, }); }; - const toastTop = () => { - Toast.success({ - message: '提示内容', - position: 'bottom', - }); + + const closeToastA = () => { + toast?.close(); }; return ( - - + + + + + + + ); +}; +``` + +### 展示位置 + +Toast提供了`top`、`center`、`bottom`三种展示位置,默认为`center`。 + +```tsx +import { Stack, Button, Toast } from '@bifrostui/react'; +import React from 'react'; + +export default () => { + return ( + + + + + + ); +}; +``` + +### 同时存在多个Toast + +使用`allowMultiple`可允许页面中同时存在多个Taost提示,默认每次只展示一个Toast。 + +```tsx +import { Stack, Button, Toast } from '@bifrostui/react'; +import React from 'react'; + +export default () => { + return ( + + + + + + ); +}; +``` + +### 自定义图标 + +使用`icon`可定制图标。 + +```tsx +import { Stack, Button, Toast } from '@bifrostui/react'; +import { LocationFilledIcon } from '@bifrostui/icons'; +import React from 'react'; + +export default () => { + return ( + + + + ); +}; +``` + +### 禁止背景点击 + +使用`disableClick`可控制展示Toast提示时,页面其他内容是否可点击,默认可点击。 + +```tsx +import { Stack, Button, Toast } from '@bifrostui/react'; +import React from 'react'; + +export default () => { + return ( + + + + ); +}; +``` + +### 关闭回调 + +可通过`onClose`监听Toast关闭时的回调。 + +```tsx +import { Stack, Button, Toast } from '@bifrostui/react'; +import React from 'react'; + +export default () => { + return ( + + + + ); +}; +``` + +### 关闭所有Toast + +Toast提供了`clear`方法,用于关闭页面中所有存在的弹窗。 + +```tsx +import { Stack, Button, Toast } from '@bifrostui/react'; +import React from 'react'; + +export default () => { + return ( + + + + ); }; @@ -40,14 +366,47 @@ export default () => { ### API -##### ToastProps +##### ToastOptions + +| 属性 | 说明 | 类型 | 默认值 | +| ------------- | --------------------------------------- | ----------------------------- | -------- | +| message | toast内容,支持使用`\n`换行 | string | - | +| duration | 展示时长(ms),值为 0 时,toast 不会消失 | number | 2000 | +| position | 展示位置 | `top` \| `center` \| `bottom` | `center` | +| allowMultiple | 是否允许同时存在多个Toast | boolean | false | +| icon | 自定义图标 | React.ReactNode | - | +| disableClick | 展示Toast时,页面内容是否可以点击 | boolean | false | +| onClose | 关闭时的回调函数 | () => void | - | + +##### 方法 + +| 方法名 | 说明 | 参数 | 返回值 | +| ------------- | -------- | ---------------------- | --------------- | +| Taost | 展示提示 | ToastOptions \| string | ToastReturnType | +| Taost.warning | 警告提示 | ToastOptions \| string | ToastReturnType | +| Taost.loading | 加载提示 | ToastOptions \| string | ToastReturnType | +| Taost.success | 成功提示 | ToastOptions \| string | ToastReturnType | +| Taost.fail | 失败提示 | ToastOptions \| string | ToastReturnType | +| Taost.clear | 清空提示 | - | - | + +##### ToastReturnType -| 属性 | 说明 | 类型 | 默认值 | -| ----- | ---------- | ---- | ------ | -| color | 标签主题色 | - | - | +| 属性名 | 说明 | 类型 | 返回值 | +| ------ | ------------ | ---------- | ------ | +| close | 关闭当前提示 | () => void | - | ### 样式变量 -| 属性 | 说明 | 默认值 | 全局变量 | -| ---------- | ---- | --------------- | ------------------ | -| --bg-color | - | --bui-color-gay | --bui-tag-bg-color | +| 属性 | 说明 | 默认值 | 全局变量 | +| ------------------ | ------------------------ | -------------------------- | -------- | +| --min-width | 最小宽度 | 86px | - | +| --max-width | 最大宽度 | 80% | - | +| --font-size | 字体大小 | --bui-text-size-1 | - | +| --color | 字体颜色 | --bui-color-white | - | +| --padding | 内边距 | --bui-spacing-xl | - | +| --word-break | 换行规则 | break-all | - | +| --position-top | 顶部展示时,距离顶部距离 | 15% | - | +| --position-bottom | 底部展示时,距离顶部距离 | 85% | - | +| --background-color | 背景颜色 | rgba(0, 0, 0, 0.8) | - | +| --border-radius | 圆角 | --bui-shape-radius-default | - | +| --text-align | 文字位置 | center | - | diff --git a/packages/bui-core/src/Transition/Transition.miniapp.tsx b/packages/bui-core/src/Transition/Transition.miniapp.tsx index 0f6bd5f..3527681 100644 --- a/packages/bui-core/src/Transition/Transition.miniapp.tsx +++ b/packages/bui-core/src/Transition/Transition.miniapp.tsx @@ -5,7 +5,14 @@ import TransitionCore from './TransitionCore'; export const Transition = React.forwardRef( (props, ref) => { - return ; + return ( + setTimeout(() => Taro.nextTick(cb), 16)} + ref={ref} + /> + ); }, ); diff --git a/packages/bui-utils/package.json b/packages/bui-utils/package.json index bcf129a..39f414a 100644 --- a/packages/bui-utils/package.json +++ b/packages/bui-utils/package.json @@ -21,7 +21,8 @@ }, "peerDependencies": { "@tarojs/taro": "^3.2.0", - "react": "^17.0.0 || ^18.0.0" + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" }, "license": "MIT", "publishConfig": { diff --git a/packages/bui-utils/src/getRootElement/index.miniapp.ts b/packages/bui-utils/src/getRootElement/index.miniapp.ts new file mode 100644 index 0000000..d185c38 --- /dev/null +++ b/packages/bui-utils/src/getRootElement/index.miniapp.ts @@ -0,0 +1,14 @@ +import Taro from '@tarojs/taro'; +import type { TaroElement } from '@tarojs/runtime'; + +const getRootElement = (rootEle?: TaroElement | (() => TaroElement)) => { + const currentPages = Taro.getCurrentPages() || []; + const currentPage = currentPages[currentPages.length - 1]; + const pageElement = currentPage?.$taroPath; + const defaultRootElement = document.getElementById(pageElement); + + const rootElement = typeof rootEle === 'function' ? rootEle() : rootEle; + return rootElement || defaultRootElement; +}; + +export default getRootElement; diff --git a/packages/bui-utils/src/getRootElement/index.ts b/packages/bui-utils/src/getRootElement/index.ts new file mode 100644 index 0000000..b209c56 --- /dev/null +++ b/packages/bui-utils/src/getRootElement/index.ts @@ -0,0 +1,8 @@ +const getRootElement = (rootEle?: HTMLElement | (() => HTMLElement)) => { + const rootElement = typeof rootEle === 'function' ? rootEle() : rootEle; + const defaultRootElement = document.body; + + return rootElement || defaultRootElement; +}; + +export default getRootElement; diff --git a/packages/bui-utils/src/index.ts b/packages/bui-utils/src/index.ts index c3b4b50..24caa18 100644 --- a/packages/bui-utils/src/index.ts +++ b/packages/bui-utils/src/index.ts @@ -23,5 +23,7 @@ export { getTransitionProps, createTransitions, } from './transitions'; +export { default as getRootElement } from './getRootElement'; export { default as getBoundingClientRect } from './getBoundingClientRect'; export * from './isType'; +export * from './render'; diff --git a/packages/bui-utils/src/render.ts b/packages/bui-utils/src/render.ts new file mode 100644 index 0000000..538c8d4 --- /dev/null +++ b/packages/bui-utils/src/render.ts @@ -0,0 +1,89 @@ +// 参考rc-util: https://github.com/react-component/util/blob/master/src/React/render.ts +import { ReactElement } from 'react'; +import * as ReactDOM from 'react-dom'; +import type { Root } from 'react-dom/client'; + +const MARK = '__bifrostui_react_root__'; +type ContainerType = (Element | DocumentFragment) & { + [MARK]?: Root; +}; +type CreateRoot = (container: ContainerType) => Root; + +// Let compiler not to search module usage +const fullClone = { + ...ReactDOM, +} as typeof ReactDOM & { + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?: { + usingClientEntryPoint?: boolean; + }; + createRoot?: CreateRoot; +}; + +const { version, render: reactRender, unmountComponentAtNode } = fullClone; + +let createRoot: CreateRoot; +try { + if (Number((version || '').split('.')[0]) >= 18 && fullClone.createRoot) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + createRoot = fullClone.createRoot; + } +} catch (e) { + // Do nothing; +} + +function toggleWarning(skip: boolean) { + const { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } = fullClone; + + if ( + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED && + typeof __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED === 'object' + ) { + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = + skip; + } +} + +// ========================== Render ========================== +function modernRender(node: ReactElement, container: ContainerType) { + toggleWarning(true); + const root = container[MARK] || createRoot(container); + toggleWarning(false); + root.render(node); + // eslint-disable-next-line no-param-reassign + container[MARK] = root; +} + +function legacyRender(node: ReactElement, container: ContainerType) { + reactRender(node, container); +} + +export function render(node: ReactElement, container: ContainerType) { + if (createRoot) { + modernRender(node, container); + return; + } + legacyRender(node, container); +} + +// ========================== Unmount ========================= +async function modernUnmount(container: ContainerType) { + // Delay to unmount to avoid React 18 sync warning + return Promise.resolve().then(() => { + container[MARK]?.unmount(); + // eslint-disable-next-line no-param-reassign + delete container[MARK]; + }); +} + +function legacyUnmount(container: ContainerType) { + return unmountComponentAtNode(container); +} + +export function unmount(container: ContainerType) { + if (createRoot) { + // Delay to unmount to avoid React 18 sync warning + return modernUnmount(container); + } + + return legacyUnmount(container); +} diff --git a/websites/mini-program/package.json b/websites/mini-program/package.json index be501c5..edffcf9 100644 --- a/websites/mini-program/package.json +++ b/websites/mini-program/package.json @@ -64,7 +64,7 @@ "@tarojs/cli": "3.6.8", "@types/webpack-env": "^1.13.6", "@types/react": "^18.0.0", - "webpack": "5.78.0", + "webpack": "5.94.0", "@tarojs/taro-loader": "3.6.8", "@tarojs/webpack5-runner": "3.6.8", "babel-preset-taro": "3.6.8",