diff --git a/packages/components/notification/__tests__/notify.test.tsx b/packages/components/notification/__tests__/notify.test.tsx index ab00805419..80b82d8749 100644 --- a/packages/components/notification/__tests__/notify.test.tsx +++ b/packages/components/notification/__tests__/notify.test.tsx @@ -1,16 +1,17 @@ -import { nextTick } from 'vue' +import { createApp, nextTick } from 'vue' import { afterEach, describe, expect, it, vi } from 'vitest' import { rAF } from '@element-plus/test-utils/tick' import Notification, { closeAll } from '../src/notify' -import { ElNotification } from '..' import type { NotificationHandle } from '../src/notification' +import type { VNode } from 'vue' const selector = '.el-notification' describe('Notification on command', () => { afterEach(() => { closeAll() + Notification._context = null }) it('it should get component handle', async () => { @@ -111,20 +112,29 @@ describe('Notification on command', () => { await nextTick() expect(htmlElement.querySelector(selector)).toBeNull() }) - describe('context inheritance', () => { - it('should globally inherit context correctly', () => { - expect(ElNotification._context).toBe(null) - const testContext = { - config: { - globalProperties: {}, - }, - _context: {}, - } - ElNotification.install?.(testContext as any) - expect(ElNotification._context).not.toBe(null) - expect(ElNotification._context).toBe(testContext._context) - // clean up - ElNotification._context = null - }) + + it('should globally inherit context correctly', async () => { + const globalContext = createApp({})._context + Notification._context = globalContext + const onClose = vi.fn((vm: VNode) => vm.appContext) + const handle = Notification({ duration: 0, onClose }) + await nextTick() + handle.close() + await nextTick() + expect(onClose).toHaveBeenCalledTimes(1) + expect(onClose).toHaveLastReturnedWith(globalContext) + }) + + it('should be possible to set the context individually', async () => { + const globalContext = createApp({})._context + Notification._context = globalContext + const localContext = createApp({})._context + const onClose = vi.fn((vm: VNode) => vm.appContext) + const handle = Notification({ duration: 0, onClose }, localContext) + await nextTick() + handle.close() + await nextTick() + expect(onClose).toHaveBeenCalledTimes(1) + expect(onClose).toHaveLastReturnedWith(localContext) }) }) diff --git a/packages/components/notification/src/notification.ts b/packages/components/notification/src/notification.ts index 1406d97d54..523e7f6e7e 100644 --- a/packages/components/notification/src/notification.ts +++ b/packages/components/notification/src/notification.ts @@ -1,6 +1,6 @@ import { buildProps, definePropType, iconPropType } from '@element-plus/utils' -import type { ExtractPropTypes, VNode } from 'vue' +import type { AppContext, ExtractPropTypes, VNode } from 'vue' import type Notification from './notification.vue' export const notificationTypes = [ @@ -118,11 +118,15 @@ export type NotificationEmits = typeof notificationEmits export type NotificationInstance = InstanceType -export type NotificationOptions = Omit & { +export type NotificationOptions = Omit & { /** * @description set the root element for the notification, default to `document.body` */ appendTo?: HTMLElement | string + /** + * @description callback function when closed + */ + onClose?(vm: VNode): void } export type NotificationOptionsTyped = Omit @@ -136,12 +140,18 @@ export type NotificationParamsTyped = | string | VNode -export type NotifyFn = (( - options?: NotificationParams -) => NotificationHandle) & { closeAll: () => void } +export interface NotifyFn { + ( + options?: NotificationParams, + appContext?: null | AppContext + ): NotificationHandle + closeAll(): void + _context: AppContext | null +} export type NotifyTypedFn = ( - options?: NotificationParamsTyped + options?: NotificationParamsTyped, + appContext?: null | AppContext ) => NotificationHandle export interface Notify extends NotifyFn { diff --git a/packages/components/notification/src/notify.ts b/packages/components/notification/src/notify.ts index c40aa02af1..e1ae1304d6 100644 --- a/packages/components/notification/src/notify.ts +++ b/packages/components/notification/src/notify.ts @@ -5,12 +5,13 @@ import { isElement, isFunction, isString, + isUndefined, isVNode, } from '@element-plus/utils' import NotificationConstructor from './notification.vue' import { notificationTypes } from './notification' -import type { AppContext, Ref, VNode } from 'vue' +import type { Ref, VNode } from 'vue' import type { NotificationOptions, NotificationProps, @@ -34,88 +35,84 @@ const notifications: Record< const GAP_SIZE = 16 let seed = 1 -const notify: NotifyFn & Partial & { _context: AppContext | null } = - function (options = {}, context: AppContext | null = null) { - if (!isClient) return { close: () => undefined } +const notify: NotifyFn & Partial = function (options = {}, context) { + if (!isClient) return { close: () => undefined } - if (isString(options) || isVNode(options)) { - options = { message: options } - } - - const position = options.position || 'top-right' - - let verticalOffset = options.offset || 0 - notifications[position].forEach(({ vm }) => { - verticalOffset += (vm.el?.offsetHeight || 0) + GAP_SIZE - }) - verticalOffset += GAP_SIZE - - const id = `notification_${seed++}` - const userOnClose = options.onClose - const props: Partial = { - ...options, - offset: verticalOffset, - id, - onClose: () => { - close(id, position, userOnClose) - }, - } - - let appendTo: HTMLElement | null = document.body - if (isElement(options.appendTo)) { - appendTo = options.appendTo - } else if (isString(options.appendTo)) { - appendTo = document.querySelector(options.appendTo) - } - - // should fallback to default value with a warning - if (!isElement(appendTo)) { - debugWarn( - 'ElNotification', - 'the appendTo option is not an HTMLElement. Falling back to document.body.' - ) - appendTo = document.body - } - - const container = document.createElement('div') - - const vm = createVNode( - NotificationConstructor, - props, - isFunction(props.message) ? props.message : () => props.message - ) - vm.appContext = context ?? notify._context - - // clean notification element preventing mem leak - vm.props!.onDestroy = () => { - render(null, container) - } - - // instances will remove this item when close function gets called. So we do not need to worry about it. - render(vm, container) - notifications[position].push({ vm }) - appendTo.appendChild(container.firstElementChild!) - - return { - // instead of calling the onClose function directly, setting this value so that we can have the full lifecycle - // for out component, so that all closing steps will not be skipped. - close: () => { - ;(vm.component!.exposed as { visible: Ref }).visible.value = - false - }, - } + if (isString(options) || isVNode(options)) { + options = { message: options } } + + const position = options.position || 'top-right' + + let verticalOffset = options.offset || 0 + notifications[position].forEach(({ vm }) => { + verticalOffset += (vm.el?.offsetHeight || 0) + GAP_SIZE + }) + verticalOffset += GAP_SIZE + + const id = `notification_${seed++}` + const userOnClose = options.onClose + const props: Partial = { + ...options, + offset: verticalOffset, + id, + onClose: () => { + close(id, position, userOnClose) + }, + } + + let appendTo: HTMLElement | null = document.body + if (isElement(options.appendTo)) { + appendTo = options.appendTo + } else if (isString(options.appendTo)) { + appendTo = document.querySelector(options.appendTo) + } + + // should fallback to default value with a warning + if (!isElement(appendTo)) { + debugWarn( + 'ElNotification', + 'the appendTo option is not an HTMLElement. Falling back to document.body.' + ) + appendTo = document.body + } + + const container = document.createElement('div') + + const vm = createVNode( + NotificationConstructor, + props, + isFunction(props.message) ? props.message : () => props.message + ) + vm.appContext = isUndefined(context) ? notify._context : context + + // clean notification element preventing mem leak + vm.props!.onDestroy = () => { + render(null, container) + } + + // instances will remove this item when close function gets called. So we do not need to worry about it. + render(vm, container) + notifications[position].push({ vm }) + appendTo.appendChild(container.firstElementChild!) + + return { + // instead of calling the onClose function directly, setting this value so that we can have the full lifecycle + // for out component, so that all closing steps will not be skipped. + close: () => { + ;(vm.component!.exposed as { visible: Ref }).visible.value = + false + }, + } +} notificationTypes.forEach((type) => { - notify[type] = (options = {}) => { + notify[type] = (options = {}, appContext) => { if (isString(options) || isVNode(options)) { options = { message: options, } } - return notify({ - ...options, - type, - }) + return notify({ ...options, type }, appContext) } })