From 34794229eac48fec52439d2030de18b5c7835cc7 Mon Sep 17 00:00:00 2001 From: CCherry07 <2405693142@qq.com> Date: Thu, 4 May 2023 17:13:20 +0800 Subject: [PATCH 1/8] feat(Message): add useMessage hook --- components/_util/util.ts | 19 +++ components/message/PurePanel.tsx | 79 +++++++++++ components/message/index.tsx | 6 +- components/message/interface.ts | 48 +++++++ components/message/useMessage.tsx | 228 ++++++++++++++++++++++++++++++ 5 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 components/message/PurePanel.tsx create mode 100644 components/message/interface.ts create mode 100644 components/message/useMessage.tsx diff --git a/components/_util/util.ts b/components/_util/util.ts index 7be837e532..aff3788321 100644 --- a/components/_util/util.ts +++ b/components/_util/util.ts @@ -78,5 +78,24 @@ export function renderHelper>( } return v ?? defaultV; } +export function wrapPromiseFn(openFn: (resolve: VoidFunction) => VoidFunction) { + let closeFn: VoidFunction; + + const closePromise = new Promise(resolve => { + closeFn = openFn(() => { + resolve(true); + }); + }); + + const result: any = () => { + closeFn?.(); + }; + + result.then = (filled: VoidFunction, rejected: VoidFunction) => + closePromise.then(filled, rejected); + result.promise = closePromise; + + return result; +} export { isOn, cacheStringFunction, camelize, hyphenate, capitalize, resolvePropValue }; diff --git a/components/message/PurePanel.tsx b/components/message/PurePanel.tsx new file mode 100644 index 0000000000..5ce5b45002 --- /dev/null +++ b/components/message/PurePanel.tsx @@ -0,0 +1,79 @@ +import Notice from '../vc-notification/Notice'; +import type { NoticeProps } from '../vc-notification/Notice'; +import useStyle from './style'; +import type { NoticeType } from './interface'; +import { + CheckCircleFilled, + CloseCircleFilled, + ExclamationCircleFilled, + InfoCircleFilled, + LoadingOutlined, +} from '@ant-design/icons-vue'; +import type { VueNode } from '../_util/type'; +import classNames from '../_util/classNames'; +import { useConfigContextInject } from '../config-provider/context'; +import { computed, defineComponent } from 'vue'; + +export const TypeIcon = { + info: , + success: , + error: , + warning: , + loading: , +}; + +export interface PureContentProps { + prefixCls: string; + type?: NoticeType; + icon?: VueNode; + children: VueNode; +} + +export const PureContent = defineComponent({ + name: 'PureContent', + inheritAttrs: false, + props: ['prefixCls', 'type', 'icon'] as any, + + setup(props, { slots }) { + return () => ( +
+ {props.icon || TypeIcon[props.type!]} + {slots.default?.()} +
+ ); + }, +}); + +export interface PurePanelProps + extends Omit, + Omit { + prefixCls?: string; +} + +/** @private Internal Component. Do not use in your production. */ + +export default defineComponent({ + name: 'PurePanel', + inheritAttrs: false, + props: ['prefixCls', 'class', 'type', 'icon', 'content'] as any, + setup(props, { slots, attrs }) { + const { getPrefixCls } = useConfigContextInject(); + const prefixCls = computed(() => props.staticPrefixCls || getPrefixCls('message')); + const [, hashId] = useStyle(prefixCls); + return ( + + + {slots.default?.()} + + + ); + }, +}); diff --git a/components/message/index.tsx b/components/message/index.tsx index 14ef956e38..e192f4f04d 100644 --- a/components/message/index.tsx +++ b/components/message/index.tsx @@ -9,7 +9,7 @@ import type { Key, VueNode } from '../_util/type'; import type { NotificationInstance } from '../vc-notification/Notification'; import classNames from '../_util/classNames'; import useStyle from './style'; - +import useMessage from './useMessage'; let defaultDuration = 3; let defaultTop: string; let messageInstance: NotificationInstance; @@ -70,6 +70,7 @@ function getMessageInstance(args: MessageArgsProps, callback: (i: NotificationIn callback(messageInstance); return; } + Notification.newInstance( { appContext: args.appContext, @@ -225,7 +226,7 @@ export function attachTypeApi(originalApi: MessageApi, type: NoticeType) { typeList.forEach(type => attachTypeApi(api, type)); api.warn = api.warning; - +api.useMessage = useMessage; export interface MessageInstance { info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; @@ -233,6 +234,7 @@ export interface MessageInstance { warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; open(args: MessageArgsProps): MessageType; + useMessage: typeof useMessage; } export interface MessageApi extends MessageInstance { diff --git a/components/message/interface.ts b/components/message/interface.ts new file mode 100644 index 0000000000..ed7e224f1c --- /dev/null +++ b/components/message/interface.ts @@ -0,0 +1,48 @@ +import type { CSSProperties } from 'vue'; +import type { Key, VueNode } from '../_util/type'; + +export type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading'; + +export interface ConfigOptions { + top?: number; + duration?: number; + prefixCls?: string; + getContainer?: () => HTMLElement; + transitionName?: string; + maxCount?: number; + rtl?: boolean; +} + +export interface ArgsProps { + content: VueNode; + duration?: number; + type?: NoticeType; + onClose?: () => void; + icon?: VueNode; + key?: string | number; + style?: CSSProperties; + className?: string; + onClick?: (e: Event) => void; +} + +export type JointContent = VueNode | ArgsProps; + +export interface MessageType extends PromiseLike { + (): void; +} + +export type TypeOpen = ( + content: JointContent, + duration?: number | VoidFunction, // Also can use onClose directly + onClose?: VoidFunction, +) => MessageType; + +export interface MessageInstance { + info: TypeOpen; + success: TypeOpen; + error: TypeOpen; + warning: TypeOpen; + loading: TypeOpen; + open(args: ArgsProps): MessageType; + destroy(key?: Key): void; +} diff --git a/components/message/useMessage.tsx b/components/message/useMessage.tsx new file mode 100644 index 0000000000..d6f75caed7 --- /dev/null +++ b/components/message/useMessage.tsx @@ -0,0 +1,228 @@ +import { shallowRef, computed, defineComponent } from 'vue'; +import { useNotification as useVcNotification } from '../vc-notification'; +import type { NotificationAPI } from '../vc-notification'; +import CloseOutlined from '@ant-design/icons-vue'; +import useStyle from './style'; +import type { + MessageInstance, + ArgsProps, + MessageType, + ConfigOptions, + NoticeType, + TypeOpen, +} from './interface'; + +import { PureContent } from './PurePanel'; +import { getMotion } from '../vc-trigger/utils/motionUtil'; +import type { Key } from '../_util/type'; +import { wrapPromiseFn } from '../_util/util'; +import type { VNode } from 'vue'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; +import classNames from '../_util/classNames'; + +const DEFAULT_OFFSET = 8; +const DEFAULT_DURATION = 3; + +// ============================================================================== +// == Holder == +// ============================================================================== +type HolderProps = ConfigOptions & { + onAllRemoved?: VoidFunction; +}; + +interface HolderRef extends NotificationAPI { + prefixCls: string; + hashId: string; +} + +const Holder = defineComponent({ + name: 'Holder', + inheritAttrs: false, + props: [ + 'top', + 'prefixCls', + 'getContainer', + 'maxCount', + 'duration', + 'rtl', + 'transitionName', + 'onAllRemoved', + ] as any, + setup(props, { expose }) { + const { getPrefixCls, getPopupContainer } = useConfigInject('message', props); + + const prefixCls = computed(() => getPrefixCls('message', props.prefixCls)); + + const [, hashId] = useStyle(prefixCls); + + // =============================== Style =============================== + const getStyles = () => ({ + left: '50%', + transform: 'translateX(-50%)', + top: top ?? DEFAULT_OFFSET, + }); + const getClassName = () => classNames(hashId.value, props.rtl ? `${prefixCls.value}-rtl` : ''); + + // ============================== Motion =============================== + const getNotificationMotion = () => + getMotion({ + prefixCls: prefixCls.value, + animation: props.animation ?? `move-up`, + transitionName: props.transitionName, + }); + + // ============================ Close Icon ============================= + const mergedCloseIcon = ( + + + + ); + + // ============================== Origin =============================== + const [api, holder] = useVcNotification({ + //@ts-ignore + getStyles, + prefixCls: prefixCls.value, + getClassName, + motion: getNotificationMotion, + closable: false, + closeIcon: mergedCloseIcon, + duration: props.duration ?? DEFAULT_DURATION, + getContainer: () => + props.staticGetContainer?.() || getPopupContainer.value?.() || document.body, + maxCount: props.maxCount, + onAllRemoved: props.onAllRemoved, + }); + + // ================================ Ref ================================ + expose({ + ...api, + prefixCls, + hashId, + }); + return holder; + }, +}); + +// ============================================================================== +// == Hook == +// ============================================================================== +let keyIndex = 0; + +export function useInternalMessage( + messageConfig?: HolderProps, +): readonly [MessageInstance, () => VNode] { + const holderRef = shallowRef(null); + // ================================ API ================================ + const wrapAPI = computed(() => { + // Wrap with notification content + // >>> close + const close = (key: Key) => { + holderRef.value?.close(key); + }; + + // >>> Open + const open = (config: ArgsProps): MessageType => { + if (!holderRef.value) { + const fakeResult: any = () => {}; + fakeResult.then = () => {}; + return fakeResult; + } + + const { open: originOpen, prefixCls, hashId } = holderRef.value; + const noticePrefixCls = `${prefixCls}-notice`; + const { content, icon, type, key, className, onClose, ...restConfig } = config; + + let mergedKey: Key = key!; + if (mergedKey === undefined || mergedKey === null) { + keyIndex += 1; + mergedKey = `antd-message-${keyIndex}`; + } + + return wrapPromiseFn(resolve => { + originOpen({ + ...restConfig, + key: mergedKey, + content: ( + + {content} + + ), + placement: 'top', + // @ts-ignore + class: classNames(type && `${noticePrefixCls}-${type}`, hashId, className), + onClose: () => { + onClose?.(); + resolve(); + }, + }); + + // Return close function + return () => { + close(mergedKey); + }; + }); + }; + + // >>> destroy + const destroy = (key?: Key) => { + if (key !== undefined) { + close(key); + } else { + holderRef.value?.destroy(); + } + }; + + const clone = { + open, + destroy, + } as MessageInstance; + + const keys: NoticeType[] = ['info', 'success', 'warning', 'error', 'loading']; + keys.forEach(type => { + const typeOpen: TypeOpen = (jointContent, duration, onClose) => { + let config: ArgsProps; + if (jointContent && typeof jointContent === 'object' && 'content' in jointContent) { + config = jointContent; + } else { + config = { + content: jointContent as VNode, + }; + } + + // Params + let mergedDuration: number | undefined; + let mergedOnClose: VoidFunction | undefined; + if (typeof duration === 'function') { + mergedOnClose = duration; + } else { + mergedDuration = duration; + mergedOnClose = onClose; + } + + const mergedConfig = { + onClose: mergedOnClose, + duration: mergedDuration, + ...config, + type, + }; + + return open(mergedConfig); + }; + + clone[type] = typeOpen; + }); + + return clone; + }); + + // ============================== Return =============================== + return [ + wrapAPI.value, + () => , + ] as const; +} + +export default function useMessage(messageConfig?: ConfigOptions) { + return useInternalMessage(messageConfig); +} From 295e5afe37795670da7a3c824e621cd22daeddb6 Mon Sep 17 00:00:00 2001 From: CCherry07 <2405693142@qq.com> Date: Thu, 4 May 2023 17:13:53 +0800 Subject: [PATCH 2/8] feat(Notification): add useNotification hook --- components/notification/PurePanel.tsx | 109 +++++++++ components/notification/index.tsx | 4 + components/notification/interface.ts | 61 +++++ components/notification/useNotification.tsx | 154 ++++++++++++ components/notification/util.ts | 73 ++++++ .../vc-notification/HookNotification.tsx | 230 ++++++++++++++++++ components/vc-notification/Notification.tsx | 13 +- components/vc-notification/index.ts | 5 +- .../vc-notification/useNotification.tsx | 184 ++++++++++++++ 9 files changed, 829 insertions(+), 4 deletions(-) create mode 100644 components/notification/PurePanel.tsx create mode 100644 components/notification/interface.ts create mode 100644 components/notification/useNotification.tsx create mode 100644 components/notification/util.ts create mode 100644 components/vc-notification/HookNotification.tsx create mode 100644 components/vc-notification/useNotification.tsx diff --git a/components/notification/PurePanel.tsx b/components/notification/PurePanel.tsx new file mode 100644 index 0000000000..c014509f39 --- /dev/null +++ b/components/notification/PurePanel.tsx @@ -0,0 +1,109 @@ +import { computed } from 'vue'; +import useStyle from './style'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; +import type { IconType } from './interface'; +import Notice from '../vc-notification/Notice'; +import classNames from '../_util/classNames'; +import type { NoticeProps } from '../vc-notification/Notice'; +import type { VueNode } from '../_util/type'; +import { + CheckCircleOutlined, + CloseCircleOutlined, + CloseOutlined, + ExclamationCircleOutlined, + InfoCircleOutlined, +} from '@ant-design/icons-vue'; +import { renderHelper } from '../_util/util'; + +export function getCloseIcon(prefixCls: string, closeIcon?: VueNode) { + return ( + closeIcon || ( + + + + ) + ); +} + +export interface PureContentProps { + prefixCls: string; + icon?: VueNode; + message?: VueNode; + description?: VueNode; + btn?: VueNode; + type?: IconType; +} + +const typeToIcon = { + success: CheckCircleOutlined, + info: InfoCircleOutlined, + error: CloseCircleOutlined, + warning: ExclamationCircleOutlined, +}; + +export function PureContent({ + prefixCls, + icon, + type, + message, + description, + btn, +}: PureContentProps) { + let iconNode = null; + if (icon) { + iconNode = {renderHelper(icon)}; + } else if (type) { + const Icon = typeToIcon[type]; + iconNode = ; + } + + return ( + + ); +} + +export interface PurePanelProps + extends Omit, + Omit { + prefixCls?: string; +} + +/** @private Internal Component. Do not use in your production. */ +export default function PurePanel(props: PurePanelProps) { + const { getPrefixCls } = useConfigInject('notification', props); + const prefixCls = computed(() => props.prefixCls || getPrefixCls('notification')); + const noticePrefixCls = `${prefixCls.value}-notice`; + + const [, hashId] = useStyle(prefixCls); + + return ( + + + + ); +} diff --git a/components/notification/index.tsx b/components/notification/index.tsx index e6fb47f2e5..4179cc31ef 100644 --- a/components/notification/index.tsx +++ b/components/notification/index.tsx @@ -11,6 +11,8 @@ import { globalConfig } from '../config-provider'; import type { NotificationInstance as VCNotificationInstance } from '../vc-notification/Notification'; import classNames from '../_util/classNames'; import useStyle from './style'; +import useNotification from './useNotification'; + export type NotificationPlacement = | 'top' | 'topLeft' @@ -284,6 +286,7 @@ iconTypes.forEach(type => { }); api.warn = api.warning; +api.useNotification = useNotification; export interface NotificationInstance { success(args: NotificationArgsProps): void; @@ -298,6 +301,7 @@ export interface NotificationApi extends NotificationInstance { close(key: string): void; config(options: ConfigProps): void; destroy(): void; + useNotification: typeof useNotification; } /** @private test Only function. Not work on production */ diff --git a/components/notification/interface.ts b/components/notification/interface.ts new file mode 100644 index 0000000000..a46d6245a2 --- /dev/null +++ b/components/notification/interface.ts @@ -0,0 +1,61 @@ +import type { CSSProperties } from 'vue'; +import type { Key, VueNode } from '../_util/type'; + +export type NotificationPlacement = + | 'top' + | 'topLeft' + | 'topRight' + | 'bottom' + | 'bottomLeft' + | 'bottomRight'; + +export type IconType = 'success' | 'info' | 'error' | 'warning'; + +export interface ArgsProps { + message: VueNode; + description?: VueNode; + btn?: VueNode; + key?: Key; + onClose?: () => void; + duration?: number | null; + icon?: VueNode; + placement?: NotificationPlacement; + style?: CSSProperties; + className?: string; + readonly type?: IconType; + onClick?: () => void; + closeIcon?: VueNode; +} + +type StaticFn = (args: ArgsProps) => void; + +export interface NotificationInstance { + success: StaticFn; + error: StaticFn; + info: StaticFn; + warning: StaticFn; + open: StaticFn; + destroy(key?: Key): void; +} + +export interface GlobalConfigProps { + top?: number; + bottom?: number; + duration?: number; + prefixCls?: string; + getContainer?: () => HTMLElement; + placement?: NotificationPlacement; + closeIcon?: VueNode; + rtl?: boolean; + maxCount?: number; +} + +export interface NotificationConfig { + top?: number; + bottom?: number; + prefixCls?: string; + getContainer?: () => HTMLElement; + placement?: NotificationPlacement; + maxCount?: number; + rtl?: boolean; +} diff --git a/components/notification/useNotification.tsx b/components/notification/useNotification.tsx new file mode 100644 index 0000000000..52923a35a4 --- /dev/null +++ b/components/notification/useNotification.tsx @@ -0,0 +1,154 @@ +import type { VNode } from 'vue'; +import { shallowRef, computed, defineComponent } from 'vue'; +import { useNotification as useVcNotification } from '../vc-notification'; +import type { NotificationAPI } from '../vc-notification'; +import type { + NotificationInstance, + ArgsProps, + NotificationPlacement, + NotificationConfig, +} from './interface'; + +import useStyle from './style'; +import { getCloseIcon, PureContent } from './PurePanel'; +import { getMotion, getPlacementStyle } from './util'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; +import classNames from '../_util/classNames'; +import type { Key } from '../_util/type'; + +const DEFAULT_OFFSET = 24; +const DEFAULT_DURATION = 4.5; + +// ============================================================================== +// == Holder == +// ============================================================================== +type HolderProps = NotificationConfig & { + onAllRemoved?: VoidFunction; + getPopupContainer?: () => HTMLElement; +}; + +interface HolderRef extends NotificationAPI { + prefixCls: string; + hashId: string; +} + +const Holder = defineComponent({ + name: 'Holder', + inheritAttrs: false, + props: ['prefixCls', 'class', 'type', 'icon', 'content', 'onAllRemoved'], + setup(props: HolderProps, { expose }) { + const { getPrefixCls, getPopupContainer } = useConfigInject('notification', props); + const prefixCls = computed(() => props.prefixCls || getPrefixCls('notification')); + // =============================== Style =============================== + const getStyles = (placement: NotificationPlacement) => + getPlacementStyle(placement, props.top ?? DEFAULT_OFFSET, props.bottom ?? DEFAULT_OFFSET); + + // Style + const [, hashId] = useStyle(prefixCls); + + const getClassName = () => classNames(hashId.value, { [`${prefixCls.value}-rtl`]: props.rtl }); + + // ============================== Motion =============================== + const getNotificationMotion = () => getMotion(prefixCls.value); + + // ============================== Origin =============================== + const [api, holder] = useVcNotification({ + prefixCls: prefixCls.value, + getStyles, + getClassName, + motion: getNotificationMotion, + closable: true, + closeIcon: getCloseIcon(prefixCls.value), + duration: DEFAULT_DURATION, + getContainer: () => + props.getPopupContainer?.() || getPopupContainer.value?.() || document.body, + maxCount: props.maxCount, + hashId: hashId.value, + onAllRemoved: props.onAllRemoved, + }); + + // ================================ Ref ================================ + expose({ + ...api, + prefixCls: prefixCls.value, + hashId, + }); + return holder; + }, +}); + +// ============================================================================== +// == Hook == +// ============================================================================== +export function useInternalNotification( + notificationConfig?: HolderProps, +): readonly [NotificationInstance, () => VNode] { + const holderRef = shallowRef(null); + + // ================================ API ================================ + const wrapAPI = computed(() => { + // Wrap with notification content + + // >>> Open + const open = (config: ArgsProps) => { + if (!holderRef.value) { + return; + } + const { open: originOpen, prefixCls, hashId } = holderRef.value; + const noticePrefixCls = `${prefixCls}-notice`; + + const { message, description, icon, type, btn, className, ...restConfig } = config; + return originOpen({ + placement: 'topRight', + ...restConfig, + content: ( + + ), + // @ts-ignore + class: classNames(type && `${noticePrefixCls}-${type}`, hashId, className), + }); + }; + + // >>> destroy + const destroy = (key?: Key) => { + if (key !== undefined) { + holderRef.value?.close(key); + } else { + holderRef.value?.destroy(); + } + }; + + const clone = { + open, + destroy, + } as NotificationInstance; + + const keys = ['success', 'info', 'warning', 'error'] as const; + keys.forEach(type => { + clone[type] = config => + open({ + ...config, + type, + }); + }); + + return clone; + }); + + // ============================== Return =============================== + return [ + wrapAPI.value, + () => , + ] as const; +} + +export default function useNotification(notificationConfig?: NotificationConfig) { + return useInternalNotification(notificationConfig); +} diff --git a/components/notification/util.ts b/components/notification/util.ts new file mode 100644 index 0000000000..93d61ab991 --- /dev/null +++ b/components/notification/util.ts @@ -0,0 +1,73 @@ +import type { CSSProperties } from 'vue'; +import type { NotificationPlacement } from './interface'; +import type { CSSMotionProps } from '../_util/transition'; + +export function getPlacementStyle( + placement: NotificationPlacement, + top: number | string, + bottom: number | string, +) { + let style: CSSProperties; + top = typeof top === 'number' ? `${top}px` : top; + bottom = typeof bottom === 'number' ? `${bottom}px` : bottom; + switch (placement) { + case 'top': + style = { + left: '50%', + transform: 'translateX(-50%)', + right: 'auto', + top, + bottom: 'auto', + }; + break; + + case 'topLeft': + style = { + left: 0, + top, + bottom: 'auto', + }; + break; + + case 'topRight': + style = { + right: 0, + top, + bottom: 'auto', + }; + break; + + case 'bottom': + style = { + left: '50%', + transform: 'translateX(-50%)', + right: 'auto', + top: 'auto', + bottom, + }; + break; + + case 'bottomLeft': + style = { + left: 0, + top: 'auto', + bottom, + }; + break; + + default: + style = { + right: 0, + top: 'auto', + bottom, + }; + break; + } + return style; +} + +export function getMotion(prefixCls: string): CSSMotionProps { + return { + name: `${prefixCls}-fade`, + }; +} diff --git a/components/vc-notification/HookNotification.tsx b/components/vc-notification/HookNotification.tsx new file mode 100644 index 0000000000..597589e823 --- /dev/null +++ b/components/vc-notification/HookNotification.tsx @@ -0,0 +1,230 @@ +import type { CSSProperties } from 'vue'; +import { watch, computed, defineComponent, ref, TransitionGroup } from 'vue'; +import type { NoticeProps } from './Notice'; +import Notice from './Notice'; +import type { CSSMotionProps } from '../_util/transition'; +import { getTransitionGroupProps } from '../_util/transition'; +import type { Key, VueNode } from '../_util/type'; +import classNames from '../_util/classNames'; +import Portal from '../_util/Portal'; + +let seed = 0; +const now = Date.now(); + +export function getUuid() { + const id = seed; + seed += 1; + return `rcNotification_${now}_${id}`; +} + +export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight'; + +export interface OpenConfig extends NoticeProps { + key: Key; + placement?: Placement; + content?: VueNode; + duration?: number | null; +} + +export type Placements = Partial>; + +export interface NoticeContent extends Omit { + prefixCls?: string; + key?: Key; + updateMark?: string; + content?: any; + onClose?: () => void; + style?: CSSProperties; + class?: String; + placement?: Placement; +} + +export type NoticeFunc = (noticeProps: NoticeContent) => void; +export type HolderReadyCallback = ( + div: HTMLDivElement, + noticeProps: NoticeProps & { key: Key }, +) => void; + +export interface NotificationInstance { + notice: NoticeFunc; + removeNotice: (key: Key) => void; + destroy: () => void; + add: (noticeProps: NoticeContent) => void; + component: Notification; +} + +export interface HookNotificationProps { + prefixCls?: string; + transitionName?: string; + animation?: string | CSSMotionProps | ((placement?: Placement) => CSSMotionProps); + maxCount?: number; + closeIcon?: any; + hashId?: string; + // Hook Notification + remove: (key: Key) => void; + notices: NotificationState; + getStyles?: (placement?: Placement) => CSSProperties; + getClassName?: (placement?: Placement) => string; + onAllRemoved?: VoidFunction; + getContainer?: () => HTMLElement; +} + +type NotificationState = { + notice: NoticeContent & { + userPassKey?: Key; + }; + holderCallback?: HolderReadyCallback; +}[]; + +const Notification = defineComponent({ + name: 'HookNotification', + inheritAttrs: false, + props: [ + 'prefixCls', + 'transitionName', + 'animation', + 'maxCount', + 'closeIcon', + 'hashId', + 'remove', + 'notices', + 'getStyles', + 'getClassName', + 'onAllRemoved', + 'getContainer', + ] as any, + setup(props, { attrs, slots }) { + const hookRefs = new Map(); + const notices = computed(() => props.notices); + const transitionProps = computed(() => { + let name = props.transitionName; + if (!name && props.animation) { + switch (typeof props.animation) { + case 'string': + name = props.animation; + break; + case 'function': + name = props.animation().name; + break; + case 'object': + name = props.animation.name; + break; + default: + name = `${props.prefixCls}-fade`; + break; + } + } + return getTransitionGroupProps(name); + }); + + const remove = (key: Key) => props.remove(key); + const placements = ref({} as Record); + watch(notices, () => { + const nextPlacements = {} as any; + // init placements with animation + Object.keys(placements.value).forEach(placement => { + nextPlacements[placement] = []; + }); + props.notices.forEach(config => { + const { placement = 'topRight' } = config.notice; + if (placement) { + nextPlacements[placement] = nextPlacements[placement] || []; + nextPlacements[placement].push(config); + } + }); + placements.value = nextPlacements; + }); + + const placementList = computed(() => Object.keys(placements.value) as Placement[]); + + return () => { + const { prefixCls, closeIcon = slots.closeIcon?.({ prefixCls }) } = props; + const noticeNodes = placementList.value.map(placement => { + const noticesForPlacement = placements.value[placement]; + const classes = props.getClassName?.(placement); + const styles = props.getStyles?.(placement); + const noticeNodesForPlacement = noticesForPlacement.map( + ({ notice, holderCallback }, index) => { + const updateMark = index === notices.value.length - 1 ? notice.updateMark : undefined; + const { key, userPassKey } = notice; + const { content } = notice; + const noticeProps = { + prefixCls, + closeIcon: typeof closeIcon === 'function' ? closeIcon({ prefixCls }) : closeIcon, + ...(notice as any), + ...notice.props, + key, + noticeKey: userPassKey || key, + updateMark, + onClose: (noticeKey: Key) => { + remove(noticeKey); + notice.onClose?.(); + }, + onClick: notice.onClick, + }; + + if (holderCallback) { + return ( +
{ + if (typeof key === 'undefined') { + return; + } + if (div) { + hookRefs.set(key, div); + holderCallback(div, noticeProps); + } else { + hookRefs.delete(key); + } + }} + /> + ); + } + + return ( + + {typeof content === 'function' ? content({ prefixCls }) : content} + + ); + }, + ); + const className = { + [prefixCls]: 1, + [`${prefixCls}-${placement}`]: 1, + [attrs.class as string]: !!attrs.class, + [props.hashId]: true, + [classes]: !!classes, + }; + function onAfterLeave() { + if (noticesForPlacement.length > 0) { + return; + } + Reflect.deleteProperty(placements.value, placement); + props.onAllRemoved?.(); + } + return ( +
+ + {noticeNodesForPlacement} + +
+ ); + }); + return {noticeNodes}; + }; + }, +}); + +export default Notification; diff --git a/components/vc-notification/Notification.tsx b/components/vc-notification/Notification.tsx index 0f422194ee..01662fd417 100644 --- a/components/vc-notification/Notification.tsx +++ b/components/vc-notification/Notification.tsx @@ -1,7 +1,8 @@ import { getTransitionGroupProps } from '../_util/transition'; -import type { Key } from '../_util/type'; +import type { Key, VueNode } from '../_util/type'; import type { CSSProperties } from 'vue'; import { + shallowRef, createVNode, computed, defineComponent, @@ -33,6 +34,14 @@ export interface NoticeContent extends Omit void; export type HolderReadyCallback = ( @@ -220,7 +229,7 @@ Notification.newInstance = function newNotificationInstance(properties, callback compatConfig: { MODE: 3 }, name: 'NotificationWrapper', setup(_props, { attrs }) { - const notiRef = ref(); + const notiRef = shallowRef(); const prefixCls = computed(() => globalConfigForApi.getPrefixCls(name, customizePrefixCls)); const [, hashId] = useStyle(prefixCls); onMounted(() => { diff --git a/components/vc-notification/index.ts b/components/vc-notification/index.ts index 759e48825f..3380e36a41 100644 --- a/components/vc-notification/index.ts +++ b/components/vc-notification/index.ts @@ -1,4 +1,5 @@ -// based on rc-notification 4.5.7 import Notification from './Notification'; - +import useNotification from './useNotification'; +import type { NotificationAPI } from './useNotification'; +export { useNotification, NotificationAPI }; export default Notification; diff --git a/components/vc-notification/useNotification.tsx b/components/vc-notification/useNotification.tsx new file mode 100644 index 0000000000..77da44b646 --- /dev/null +++ b/components/vc-notification/useNotification.tsx @@ -0,0 +1,184 @@ +import type { CSSProperties } from 'vue'; +import { shallowRef, watch, ref, computed } from 'vue'; +import HookNotification, { getUuid } from './HookNotification'; +import type { NotificationInstance, OpenConfig, Placement } from './Notification'; +import type { CSSMotionProps } from '../_util/transition'; +import type { Key, VueNode } from '../_util/type'; +import type { HolderReadyCallback, NoticeContent } from './HookNotification'; + +const defaultGetContainer = () => document.body; + +type OptionalConfig = Partial; + +export interface NotificationConfig { + prefixCls?: string; + /** Customize container. It will repeat call which means you should return same container element. */ + getContainer?: () => HTMLElement; + motion?: CSSMotionProps | ((placement?: Placement) => CSSMotionProps); + closeIcon?: VueNode; + closable?: boolean; + maxCount?: number; + duration?: number; + /** @private. Config for notification holder style. Safe to remove if refactor */ + getClassName?: (placement?: Placement) => string; + /** @private. Config for notification holder style. Safe to remove if refactor */ + getStyles?: (placement?: Placement) => CSSProperties; + /** @private Trigger when all the notification closed. */ + onAllRemoved?: VoidFunction; + hashId?: string; +} + +export interface NotificationAPI { + open: (config: OptionalConfig) => void; + close: (key: Key) => void; + destroy: () => void; +} + +interface OpenTask { + type: 'open'; + config: OpenConfig; +} + +interface CloseTask { + type: 'close'; + key: Key; +} + +interface DestroyTask { + type: 'destroy'; +} + +type Task = OpenTask | CloseTask | DestroyTask; + +let uniqueKey = 0; + +function mergeConfig(...objList: Partial[]): T { + const clone: T = {} as T; + + objList.forEach(obj => { + if (obj) { + Object.keys(obj).forEach(key => { + const val = obj[key]; + + if (val !== undefined) { + clone[key] = val; + } + }); + } + }); + + return clone; +} + +export default function useNotification(rootConfig: NotificationConfig = {}) { + const { + getContainer = defaultGetContainer, + motion, + prefixCls, + maxCount, + getClassName, + getStyles, + onAllRemoved, + ...shareConfig + } = rootConfig; + + const notices = ref([]); + const notificationsRef = shallowRef(); + const add = (originNotice: NoticeContent, holderCallback?: HolderReadyCallback) => { + const key = originNotice.key || getUuid(); + const notice: NoticeContent & { key: Key; userPassKey?: Key } = { + ...originNotice, + key, + }; + const noticeIndex = notices.value.map(v => v.notice.key).indexOf(key); + const updatedNotices = notices.value.concat(); + if (noticeIndex !== -1) { + updatedNotices.splice(noticeIndex, 1, { notice, holderCallback } as any); + } else { + if (maxCount && notices.value.length >= maxCount) { + notice.key = updatedNotices[0].notice.key as Key; + notice.updateMark = getUuid(); + notice.userPassKey = key; + updatedNotices.shift(); + } + updatedNotices.push({ notice, holderCallback } as any); + } + notices.value = updatedNotices; + }; + const removeNotice = (removeKey: Key) => { + notices.value = notices.value.filter(({ notice: { key, userPassKey } }) => { + const mergedKey = userPassKey || key; + return mergedKey !== removeKey; + }); + }; + + const destroy = () => { + notices.value = []; + }; + + const contextHolder = computed(() => ( + + )); + + const taskQueue = ref([] as Task[]); + // ========================= Refs ========================= + const api = computed(() => { + return { + open: (config: OpenConfig) => { + const mergedConfig = mergeConfig(shareConfig, config); + //@ts-ignore + if (mergedConfig.key === null || mergedConfig.key === undefined) { + //@ts-ignore + mergedConfig.key = `vc-notification-${uniqueKey}`; + uniqueKey += 1; + } + + taskQueue.value = [...taskQueue.value, { type: 'open', config: mergedConfig as any }]; + }, + close: key => { + taskQueue.value = [...taskQueue.value, { type: 'close', key }]; + }, + destroy: () => { + taskQueue.value = [...taskQueue.value, { type: 'destroy' }]; + }, + }; + }); + + // ======================== Effect ======================== + watch(taskQueue, () => { + // Flush task when node ready + if (taskQueue.value.length) { + taskQueue.value.forEach(task => { + switch (task.type) { + case 'open': + // @ts-ignore + add(task.config); + break; + + case 'close': + removeNotice(task.key); + break; + case 'destroy': + destroy(); + break; + } + }); + taskQueue.value = []; + } + }); + + // ======================== Return ======================== + return [api.value, () => contextHolder.value] as const; +} From acfd684ea2886c3e8abc8d53b728ecb1fdc7cab8 Mon Sep 17 00:00:00 2001 From: CCherry07 <2405693142@qq.com> Date: Thu, 4 May 2023 17:14:37 +0800 Subject: [PATCH 3/8] feat(Message): add Hook demo --- components/message/demo/hook.vue | 31 +++++++++++++++++++++++++++++++ components/message/demo/index.vue | 3 +++ 2 files changed, 34 insertions(+) create mode 100644 components/message/demo/hook.vue diff --git a/components/message/demo/hook.vue b/components/message/demo/hook.vue new file mode 100644 index 0000000000..1b7fe0608f --- /dev/null +++ b/components/message/demo/hook.vue @@ -0,0 +1,31 @@ + +--- +order: 10 +title: + zh-CN: Hooks 调用(推荐) + en-US: Hooks Usage (Recommend) +--- + +## zh-CN + +通过 `message.useMessage` 创建支持读取 context 的 `contextHolder`。请注意,我们推荐通过顶层注册的方式代替 `message` 静态方法,因为静态方法无法消费上下文,因而 ConfigProvider 的数据也不会生效。 + +## en-US + +Use `message.useMessage` to get `contextHolder` with context accessible issue. Please note that, we recommend to use top level registration instead of `message` static method, because static method cannot consume context, and ConfigProvider data will not work. + + + + + + diff --git a/components/message/demo/index.vue b/components/message/demo/index.vue index 6ad3af47cf..9d79991d58 100644 --- a/components/message/demo/index.vue +++ b/components/message/demo/index.vue @@ -1,5 +1,6 @@