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
},