From d69c2c17e48a69f375786782b190d861b9507c12 Mon Sep 17 00:00:00 2001 From: hemengke <23536175@qq.com> Date: Tue, 27 Aug 2024 19:01:53 +0800 Subject: [PATCH] refactor: enhance maintainability and reusability --- example/src/main.tsx | 2 - package.json | 16 +-- src/component/toast-container.tsx | 61 +++++++++++ src/component/toast-message.tsx | 56 ++++++---- src/index.tsx | 168 ++++++++---------------------- src/type/common.ts | 26 +---- tsconfig.json | 1 - 7 files changed, 154 insertions(+), 176 deletions(-) create mode 100644 src/component/toast-container.tsx diff --git a/example/src/main.tsx b/example/src/main.tsx index 74128c4..b5e6bae 100644 --- a/example/src/main.tsx +++ b/example/src/main.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './app'; import './index.css'; @@ -9,7 +8,6 @@ import.meta.globEager('/node_modules/react-simple-toasts/dist/theme/*.css'); toastConfig({ theme: 'dark', - duration: null, }); ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); diff --git a/package.json b/package.json index 033b4ac..7768cc5 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,14 @@ "require": "./dist/index.js" }, "./style.css": "./dist/style.css", - "./style.css.map": "./dist/style.css.map", - "./dist/style.css": "./dist/style.css", - "./dist/style.css.map": "./dist/style.css.map" + "./*": "./*" + }, + "typesVersions": { + "*": { + "./*": [ + "./*" + ] + } }, "scripts": { "test": "jest", @@ -82,9 +87,6 @@ "files": [ "dist" ], - "engines": { - "node": ">=16.17.0 <=20.x" - }, "sideEffects": [ "*.css" ], @@ -99,4 +101,4 @@ "react-toastify" ], "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" -} \ No newline at end of file +} diff --git a/src/component/toast-container.tsx b/src/component/toast-container.tsx new file mode 100644 index 0000000..5476dda --- /dev/null +++ b/src/component/toast-container.tsx @@ -0,0 +1,61 @@ +import React, { cloneElement, Fragment } from 'react'; +import { reverse } from '../lib/utils'; +import { ToastComponent, ToastEnterEvent } from '../type/common'; + +export interface ToastContainerProps { + toastComponentList: ToastComponent[]; + onToastEnter: () => void +} + +function ToastContainer(props: ToastContainerProps) { + const { toastComponentList, onToastEnter } = props + + const handleToastEnter = (t: ToastComponent, e: ToastEnterEvent) => { + toastComponentList.forEach((toast) => { + if (toast.id !== t.id) return; + toast.startCloseTimer(); + toast.height = e.height; + }); + + onToastEnter(); + }; + + return ( + <> + {toastComponentList.map((t) => { + const toastComponents = t.position.includes('top') + ? reverse(toastComponentList) + : toastComponentList; + + const currentIndex = toastComponents.findIndex((toast) => toast.id === t.id); + const bottomToasts = toastComponents + .slice(currentIndex + 1) + .filter((toast) => toast.position === t.position && !toast.isExit); + + const bottomToastsHeight = bottomToasts.reduce((acc, toast) => { + return acc + (toast.height ?? 0) + t.gap; + }, 0); + + const deltaOffsetX = t.position.includes('left') || t.position.includes('right') ? '0%' : '-50%'; + const offsetYAlpha = t.position.includes('top') ? 1 : -1; + const baseOffsetY = bottomToastsHeight * offsetYAlpha; + const deltaOffsetY = + t.position === 'center' ? `calc(-50% - ${baseOffsetY * -1}px)` : `${baseOffsetY}px`; + + return ( + + {cloneElement(t.component, { + isExit: t.isExit, + deltaOffsetX, + deltaOffsetY, + _onEnter: (event: ToastEnterEvent) => handleToastEnter(t, event), + })} + + ); + })} + + ); +} + + +export default ToastContainer diff --git a/src/component/toast-message.tsx b/src/component/toast-message.tsx index 6e84376..be6b545 100644 --- a/src/component/toast-message.tsx +++ b/src/component/toast-message.tsx @@ -25,19 +25,30 @@ function Loading({ color, children }: LoadingProps) { } export interface ToastMessageProps - extends Required> { + extends Required< + Pick< + ToastOptions, + | 'className' + | 'clickable' + | 'position' + | 'render' + | 'theme' + | 'onClick' + | 'clickClosable' + | 'offsetX' + | 'offsetY' + | 'zIndex' + | 'loading' + | 'loadingText' + | 'onClose' + | 'onCloseStart' + > + > { id: number; message: ReactNode; isExit?: boolean; - offsetX?: string; - offsetY?: string; - baseOffsetX?: number; - baseOffsetY?: number; - zIndex?: number; - loading?: boolean | Promise; + deltaOffsetX?: string; + deltaOffsetY?: string; _onEnter?: (e: ToastEnterEvent) => void; } @@ -50,15 +61,16 @@ function ToastMessage({ id, message, className, - clickable, + clickable: clickableProp, + clickClosable, position, isExit, render, theme, offsetX, offsetY, - baseOffsetX, - baseOffsetY, + deltaOffsetX, + deltaOffsetY, zIndex, loading, loadingText, @@ -67,6 +79,8 @@ function ToastMessage({ onCloseStart, _onEnter, }: ToastMessageProps): ReactElement { + const clickable = clickableProp || clickClosable; + const messageDOM = useRef(null); const hasTopPosition = position?.includes('top'); const hasBottomPosition = position?.includes('bottom'); @@ -76,23 +90,23 @@ function ToastMessage({ const isCenterPosition = position === ToastPosition.CENTER; const [isEnter, setIsEnter] = useState(false); const [messageStyle, setMessageStyle] = useState({ - transform: `translate(${offsetX}, ${ + transform: `translate(${deltaOffsetX}, ${ isCenterPosition ? 'calc(50% - 20px)' - : `${parseInt(offsetY || '0') + 20 * (hasTopPosition ? -1 : 1)}px` + : `${parseInt(deltaOffsetY || '0') + 20 * (hasTopPosition ? -1 : 1)}px` })`, }); const [localLoading, setLocalLoading] = useState(!!loading); const [loadingColor, setLoadingColor] = useState(); - const top = isCenterPosition ? '50%' : hasTopPosition ? baseOffsetY : undefined; - const bottom = hasBottomPosition ? baseOffsetY : undefined; - const right = hasRightPosition ? baseOffsetX : undefined; + const top = isCenterPosition ? '50%' : hasTopPosition ? offsetY : undefined; + const bottom = hasBottomPosition ? offsetY : undefined; + const right = hasRightPosition ? offsetX : undefined; const left = - hasCenterPosition || isCenterPosition ? '50%' : hasLeftPosition ? baseOffsetX : undefined; + hasCenterPosition || isCenterPosition ? '50%' : hasLeftPosition ? offsetX : undefined; useIsomorphicLayoutEffect(() => { - const transform = `translate(${offsetX}, ${offsetY})`; + const transform = `translate(${deltaOffsetX}, ${deltaOffsetY})`; setMessageStyle({ top, @@ -103,7 +117,7 @@ function ToastMessage({ transform, WebkitTransform: transform, }); - }, [offsetX, offsetY, zIndex, top, right, bottom, left]); + }, [deltaOffsetX, deltaOffsetY, zIndex, top, right, bottom, left]); useIsomorphicLayoutEffect(() => { if (messageDOM.current?.clientHeight == null || isEnter) return; diff --git a/src/index.tsx b/src/index.tsx index 2136241..32ad40b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,21 +1,21 @@ -import React, { cloneElement, Fragment, ReactNode, SyntheticEvent } from 'react'; +import React, { ReactNode, SyntheticEvent } from 'react'; import { addRootElement, createElement } from './lib/generateElement'; import { render as reactRender } from './lib/react-render'; -import { createId, isBrowser, reverse } from './lib/utils'; +import { createId, isBrowser } from './lib/utils'; import { SET_TIMEOUT_MAX, Themes, ToastPosition as Position } from './lib/constants'; import { - ConfigArgs, Theme, Toast, ToastClickHandler, ToastComponent, - ToastEnterEvent, ToastOptions, ToastPosition, + ToastUpdateArgs, ToastUpdateOptions, } from './type/common'; import ToastMessage from './component/toast-message'; import { isToastUpdateOptions } from './lib/type-guard'; +import ToastContainer from './component/toast-container'; let toastComponentList: ToastComponent[] = []; @@ -32,7 +32,7 @@ const init = () => { // eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {}; -const defaultOptions: Required = { +const defaultOptions: Required = { duration: 3000, className: '', position: 'bottom-center', @@ -44,11 +44,13 @@ const defaultOptions: Required = { maxVisibleToasts: null, isReversedOrder: false, theme: null, - zIndex: null, loadingText: 'loading', + zIndex: 1000, + clickable: false, onClick: noop, - onCloseStart: noop, onClose: noop, + onCloseStart: noop, + loading: false, }; const isValidPosition = (position: ToastPosition): boolean => { @@ -62,82 +64,27 @@ const isValidPosition = (position: ToastPosition): boolean => { return true; }; -export const toastConfig = (options: ConfigArgs) => { - if (!isBrowser()) return; - - if (options.theme) defaultOptions.theme = options.theme; - if (options.duration) defaultOptions.duration = options.duration; - if (options.className) defaultOptions.className = options.className; - if (options.position && isValidPosition(options.position)) - defaultOptions.position = options.position; - if (options.clickClosable) defaultOptions.clickClosable = options.clickClosable; - if (options.render) defaultOptions.render = options.render; - if (options.maxVisibleToasts) defaultOptions.maxVisibleToasts = options.maxVisibleToasts; - if (options.isReversedOrder) defaultOptions.isReversedOrder = options.isReversedOrder; - if (options.zIndex != null) defaultOptions.zIndex = options.zIndex; - if (options.offsetX != null) defaultOptions.offsetX = options.offsetX; - if (options.offsetY != null) defaultOptions.offsetY = options.offsetY; - if (options.gap != null) defaultOptions.gap = options.gap; - if (options.loadingText) defaultOptions.loadingText = options.loadingText; - if (options.onClick) defaultOptions.onClick = options.onClick; - if (options.onCloseStart) defaultOptions.onCloseStart = options.onCloseStart; - if (options.onClose) defaultOptions.onClose = options.onClose; +const validateOptions = (options: ToastOptions) => { + options.position && isValidPosition(options.position); }; -function ToastContainer() { - const handleToastEnter = (t: ToastComponent, e: ToastEnterEvent) => { - toastComponentList.forEach((toast) => { - if (toast.id !== t.id) return; - toast.startCloseTimer(); - toast.height = e.height; - }); +export const toastConfig = (options: ToastOptions) => { + if (!isBrowser()) return; - renderDOM(); - }; + validateOptions(options); - return ( - <> - {toastComponentList.map((t) => { - const toastComponents = t.position.includes('top') - ? reverse(toastComponentList) - : toastComponentList; - - const currentIndex = toastComponents.findIndex((toast) => toast.id === t.id); - const bottomToasts = toastComponents - .slice(currentIndex + 1) - .filter((toast) => toast.position === t.position && !toast.isExit); - - const bottomToastsHeight = bottomToasts.reduce((acc, toast) => { - return acc + (toast.height ?? 0) + t.gap; - }, 0); - - const offsetX = t.position.includes('left') || t.position.includes('right') ? '0%' : '-50%'; - const offsetYAlpha = t.position.includes('top') ? 1 : -1; - const baseOffsetY = bottomToastsHeight * offsetYAlpha; - const offsetY = - t.position === 'center' ? `calc(-50% - ${baseOffsetY * -1}px)` : `${baseOffsetY}px`; - - return ( - - {cloneElement(t.component, { - isExit: t.isExit, - offsetX, - offsetY, - _onEnter: (event: ToastEnterEvent) => handleToastEnter(t, event), - })} - - ); - })} - - ); -} + Object.assign(defaultOptions, options); +}; const renderDOM = () => { if (!isBrowser()) return; const toastContainer = document.getElementById('#toast__container'); if (!toastContainer) return; - reactRender(, toastContainer); + reactRender( + , + toastContainer, + ); }; export const clearToasts = () => { @@ -153,15 +100,7 @@ function closeToast(id: number) { renderDOM(); } -function renderToast( - message: ReactNode, - options?: ToastOptions & { - toastInstanceId?: number; - offsetX?: number; - offsetY?: number; - gap?: number; - }, -): Toast { +function renderToast(message: ReactNode, _options?: ToastOptions): Toast { const dummyReturn = { close: () => null, updateDuration: () => null, @@ -171,26 +110,27 @@ function renderToast( let closeTimer: number; const id = createId(); + + const options = { + ...defaultOptions, + ..._options, + }; + const { - duration, - clickable = false, - clickClosable = defaultOptions.clickClosable, - className = defaultOptions.className, - position = defaultOptions.position, - offsetX = defaultOptions.offsetX, - offsetY = defaultOptions.offsetY, - gap = defaultOptions.gap, - maxVisibleToasts = defaultOptions.maxVisibleToasts, - isReversedOrder = defaultOptions.isReversedOrder, - render = defaultOptions.render, - theme = defaultOptions.theme, - zIndex = defaultOptions.zIndex, - onClick = defaultOptions.onClick, - onClose = defaultOptions.onClose, - onCloseStart = defaultOptions.onCloseStart, - loadingText = defaultOptions.loadingText, loading, + loadingText, + onClose, + onCloseStart, + clickClosable, + position, + onClick, + gap, + theme, + duration, + isReversedOrder, + maxVisibleToasts, } = options || {}; + const durationTime = duration === undefined ? defaultOptions.duration : duration; if (!isValidPosition(position)) { @@ -215,7 +155,7 @@ function renderToast( onClose?.(); }; - const startCloseTimer = (duration = durationTime) => { + const startCloseTimer = (duration = options.duration) => { if (duration === null || duration === 0 || duration > SET_TIMEOUT_MAX) return; if (closeTimer) { clearTimeout(closeTimer); @@ -233,18 +173,9 @@ function renderToast( gap, component: ( { - const toastInstanceId = createId(); - +export const createToast = (options: ToastOptions): typeof toast => { return (message, durationOrOptions) => { if (typeof durationOrOptions === 'number') { return renderToast(message, { - toastInstanceId, duration: durationOrOptions || options.duration, }); } if (durationOrOptions === undefined || typeof durationOrOptions === 'object') { const mergedOptions = { - toastInstanceId, ...options, ...durationOrOptions, }; @@ -362,7 +283,8 @@ export type { Theme, ToastClickHandler, ToastOptions, - ConfigArgs, Toast, ToastComponent, + ToastUpdateOptions, + ToastUpdateArgs, }; diff --git a/src/type/common.ts b/src/type/common.ts index 615fe3d..f1307d6 100644 --- a/src/type/common.ts +++ b/src/type/common.ts @@ -13,11 +13,14 @@ export interface ToastOptions { clickable?: boolean; clickClosable?: boolean; position?: ToastPosition; + offsetX?: number; + offsetY?: number; + gap?: number; maxVisibleToasts?: number | null; isReversedOrder?: boolean; render?: ((message: ReactNode) => ReactNode) | null; theme?: Theme | string | null; - zIndex?: number | null; + zIndex?: number; loading?: boolean | Promise; loadingText?: ReactNode; onClick?: ToastClickHandler; @@ -27,27 +30,6 @@ export interface ToastOptions { export type ToastEnterEvent = { target: HTMLDivElement; width: number; height: number }; -export type ConfigArgs = Pick< - ToastOptions, - | 'duration' - | 'className' - | 'clickClosable' - | 'position' - | 'maxVisibleToasts' - | 'render' - | 'theme' - | 'zIndex' - | 'isReversedOrder' - | 'onClick' - | 'onCloseStart' - | 'onClose' - | 'loadingText' -> & { - offsetX?: number; - offsetY?: number; - gap?: number; -}; - export type ToastUpdateOptions = { message?: ReactNode; duration?: number; diff --git a/tsconfig.json b/tsconfig.json index c06619e..eb9940e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,6 @@ "noImplicitThis": true, "noImplicitAny": true, "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true },