diff --git a/apps/ligo-virgo/src/pages/tools/icons/Icons.tsx b/apps/ligo-virgo/src/pages/tools/icons/Icons.tsx index c27504e6a9..a496a58676 100644 --- a/apps/ligo-virgo/src/pages/tools/icons/Icons.tsx +++ b/apps/ligo-virgo/src/pages/tools/icons/Icons.tsx @@ -6,9 +6,7 @@ import { copy } from './copy'; const IconBooth: FC<{ name: string; Icon: FC }> = ({ name, Icon }) => { const on_click = () => { copy(`<${name} />`); - message.success({ - content: 'Copied.', - }); + message.success('Copied ~'); }; return ( diff --git a/libs/components/ui/package.json b/libs/components/ui/package.json index a1b2bc97d2..6df3065fa9 100644 --- a/libs/components/ui/package.json +++ b/libs/components/ui/package.json @@ -14,6 +14,7 @@ "@mui/x-date-pickers-pro": "^5.0.0-alpha.7", "@types/react-date-range": "^1.4.4", "clsx": "^1.2.0", + "notistack": "^2.0.5", "react-date-range": "^1.4.0" } } diff --git a/libs/components/ui/src/message/Message.tsx b/libs/components/ui/src/message/Message.tsx new file mode 100644 index 0000000000..ecc9a8ad1c --- /dev/null +++ b/libs/components/ui/src/message/Message.tsx @@ -0,0 +1,83 @@ +import { + NotificationInstance, + type NotificationController, + type NotificationInstanceProps, + type NotificationContent, + type NotificationOption, + type NotificationKey, +} from '../notification'; +import { + SuccessMessage, + ErrorMessage, + InfoMessage, + WarningMessage, +} from './MessageContent'; + +type MessageMethod = ( + message: NotificationContent, + option?: Omit +) => Promise; + +export class Message { + private _notificationController: NotificationController = null; + private _notificationProps: NotificationInstanceProps = {}; + + constructor(props: NotificationInstanceProps) { + this._notificationProps = props; + } + + private _ensureController() { + if (!this._notificationController) { + NotificationInstance( + this._notificationProps, + notificationController => { + this._notificationController = notificationController; + } + ); + } + } + + public success: MessageMethod = async (message, option) => { + await this._ensureController(); + + return this._notificationController.add(message, { + content: (key, message) => ( + + ), + ...option, + }); + }; + + public error: MessageMethod = async (message, option) => { + await this._ensureController(); + + return this._notificationController.add(message, { + content: (key, message) => ( + + ), + ...option, + }); + }; + + public warning: MessageMethod = async (message, option) => { + await this._ensureController(); + + return this._notificationController.add(message, { + content: (key, message) => ( + + ), + ...option, + }); + }; + + public info: MessageMethod = async (message, option) => { + await this._ensureController(); + + return this._notificationController.add(message, { + content: (key, message) => ( + + ), + ...option, + }); + }; +} diff --git a/libs/components/ui/src/message/MessageContent.tsx b/libs/components/ui/src/message/MessageContent.tsx new file mode 100644 index 0000000000..0eae2b0602 --- /dev/null +++ b/libs/components/ui/src/message/MessageContent.tsx @@ -0,0 +1,69 @@ +import { forwardRef } from 'react'; +import { NotificationContentProps } from '../notification'; +/* eslint-disable no-restricted-imports */ +import Alert from '@mui/material/Alert'; + +// TODO: Variants types of message content await designers +const commonStyle = { background: '#fff' }; +export const SuccessMessage = forwardRef< + HTMLDivElement, + NotificationContentProps +>(({ message, id }, ref) => { + return ( + + {message} + + ); +}); + +export const ErrorMessage = forwardRef< + HTMLDivElement, + NotificationContentProps +>(({ message, id }, ref) => { + return ( + + {message} + + ); +}); + +export const WarningMessage = forwardRef< + HTMLDivElement, + NotificationContentProps +>(({ message, id }, ref) => { + return ( + + {message} + + ); +}); + +export const InfoMessage = forwardRef( + ({ message, id }, ref) => { + return ( + + {message} + + ); + } +); diff --git a/libs/components/ui/src/message/base.tsx b/libs/components/ui/src/message/base.tsx deleted file mode 100644 index c65901e627..0000000000 --- a/libs/components/ui/src/message/base.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type { FC, ReactNode } from 'react'; -import { createRoot } from 'react-dom/client'; -import { styled } from '../styled'; - -import type { ContainerProps } from './types'; - -interface ShowProps { - Container: FC; - /** - * 自动关闭延时,单位毫秒。设为 0 时,不自动关闭。默认 2000 - */ - duration?: number; - content: ReactNode; -} - -export const show = ({ Container, duration = 2000, content }: ShowProps) => { - const root_element = document.createElement('div'); - document.body.appendChild(root_element); - - function close() { - document.body.removeChild(root_element); - } - - const react_root = createRoot(root_element); - - react_root.render( - - - - ); - - if (duration > 0) { - setTimeout(() => { - close(); - }, duration); - } - - return close; -}; - -const PortalContainer = styled('div')({ - position: 'fixed', - top: '100px', - left: '50%', - transform: 'translateX(-50%)', - backgroundColor: '#fff', - zIndex: 100, -}); diff --git a/libs/components/ui/src/message/index.tsx b/libs/components/ui/src/message/index.tsx index 1061f37d44..5cdb12d485 100644 --- a/libs/components/ui/src/message/index.tsx +++ b/libs/components/ui/src/message/index.tsx @@ -1,18 +1,9 @@ -import type { ReactNode } from 'react'; +import { Message } from './Message'; -import { show } from './base'; -import { Success } from './success'; - -interface SuccessProps { - /** - * 自动关闭延时,单位毫秒。默认2000 - */ - duration?: number; - content: ReactNode; -} - -export const message = { - success({ duration, content }: SuccessProps) { - return show({ Container: Success, duration, content }); +export const message = new Message({ + anchorOrigin: { + vertical: 'top', + horizontal: 'center', }, -}; + autoHideDuration: 2000, +}); diff --git a/libs/components/ui/src/message/success.tsx b/libs/components/ui/src/message/success.tsx deleted file mode 100644 index ee452b9b31..0000000000 --- a/libs/components/ui/src/message/success.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { FC } from 'react'; -import { styled } from '../styled'; -import type { ContainerProps } from './types'; - -export const Success: FC = ({ content }) => { - return {content}; -}; - -const Container = styled('div')({ - maxWidth: '200px', - backgroundColor: 'rgba(64, 223, 155)', - borderRadius: '4px', - padding: '8px', -}); diff --git a/libs/components/ui/src/message/types.ts b/libs/components/ui/src/message/types.ts deleted file mode 100644 index e0f2d510d8..0000000000 --- a/libs/components/ui/src/message/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { ReactNode } from 'react'; - -export interface ContainerProps { - content: ReactNode; - duration: number; - close: () => void; -} diff --git a/libs/components/ui/src/notification/Notification.tsx b/libs/components/ui/src/notification/Notification.tsx new file mode 100644 index 0000000000..7d4305c462 --- /dev/null +++ b/libs/components/ui/src/notification/Notification.tsx @@ -0,0 +1,62 @@ +import React, { forwardRef, useImperativeHandle } from 'react'; + +import { + SnackbarProvider, + useSnackbar, + type SnackbarProviderProps, + type OptionsObject, + type SnackbarMessage, + type SnackbarKey, +} from 'notistack'; +import { createRoot } from 'react-dom/client'; + +export type NotificationController = { + add: (message: SnackbarMessage, options?: OptionsObject) => SnackbarKey; + close: (key?: SnackbarKey) => void; +}; +export type NotificationInstanceProps = Omit; +export type NotificationContentProps = { + id: SnackbarKey; + message: SnackbarMessage; +}; +export type NotificationContent = SnackbarMessage; +export type NotificationOption = OptionsObject; +export type NotificationKey = SnackbarKey; + +const Notification = forwardRef((props, ref) => { + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + + useImperativeHandle(ref, () => ({ + add: (message, options) => { + return enqueueSnackbar(message, options); + }, + close: key => closeSnackbar(key), + })); + + return null; +}); + +export const NotificationInstance = ( + snackbarProviderProps: NotificationInstanceProps, + callback: (notificationController: NotificationController) => void +) => { + const rootElement = document.createElement('div'); + document.body.appendChild(rootElement); + const reactRoot = createRoot(rootElement); + // ReactDOM.unmountComponentAtNode will call the ref function again after execution, called to prevent repeated calls + let called = false; + // react mounted dom is async + const ref = async (notificationController: NotificationController) => { + if (called) { + return; + } + called = true; + callback(notificationController); + }; + + reactRoot.render( + + + + ); +}; diff --git a/libs/components/ui/src/notification/index.ts b/libs/components/ui/src/notification/index.ts new file mode 100644 index 0000000000..ed80171ae9 --- /dev/null +++ b/libs/components/ui/src/notification/index.ts @@ -0,0 +1 @@ +export * from './Notification';