fix(components): [notification] type declaration error and four types of methods are missing context parameters (#18951)

* fix(components): [notification] type declaration error

* feat(components): four types of methods add context parameters

* test: [notification] add test case
This commit is contained in:
伊墨
2024-11-29 09:58:35 +08:00
committed by GitHub
parent 3c734df53e
commit 8363b72be4
3 changed files with 115 additions and 98 deletions

View File

@@ -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)
})
})

View File

@@ -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<typeof Notification>
export type NotificationOptions = Omit<NotificationProps, 'id'> & {
export type NotificationOptions = Omit<NotificationProps, 'id' | 'onClose'> & {
/**
* @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<NotificationOptions, 'type'>
@@ -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 {

View File

@@ -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<Notify> & { _context: AppContext | null } =
function (options = {}, context: AppContext | null = null) {
if (!isClient) return { close: () => undefined }
const notify: NotifyFn & Partial<Notify> = 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<NotificationProps> = {
...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<boolean> }).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<NotificationProps> = {
...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<boolean> }).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)
}
})