diff --git a/docs/en-US/component/message.md b/docs/en-US/component/message.md index 84e1506665..561dfc3165 100644 --- a/docs/en-US/component/message.md +++ b/docs/en-US/component/message.md @@ -77,18 +77,19 @@ In this case you should call `ElMessage(options)`. We have also registered metho ## Options -| Attribute | Description | Type | Accepted Values | Default | -| ------------------------ | ------------------------------------------------------------------------------ | ------------------ | -------------------------- | ------- | -| message | message text | string / VNode | — | — | -| type | message type | string | success/warning/info/error | info | -| icon | custom icon component, overrides `type` | string / Component | — | — | -| dangerouslyUseHTMLString | whether `message` is treated as HTML string | boolean | — | false | -| custom-class | custom class name for Message | string | — | — | -| duration | display duration, millisecond. If set to 0, it will not turn off automatically | number | — | 3000 | -| show-close | whether to show a close button | boolean | — | false | -| center | whether to center the text | boolean | — | false | -| on-close | callback function when closed with the message instance as the parameter | function | — | — | -| offset | set the distance to the top of viewport | number | — | 20 | +| Attribute | Description | Type | Accepted Values | Default | +| ------------------------ | ------------------------------------------------------------------------------ | -------------------- | -------------------------- | ------------- | +| message | message text | string / VNode | — | — | +| type | message type | string | success/warning/info/error | info | +| icon-class | custom icon's class, overrides `type` | string | — | — | +| dangerouslyUseHTMLString | whether `message` is treated as HTML string | boolean | — | false | +| custom-class | custom class name for Message | string | — | — | +| duration | display duration, millisecond. If set to 0, it will not turn off automatically | number | — | 3000 | +| show-close | whether to show a close button | boolean | — | false | +| center | whether to center the text | boolean | — | false | +| on-close | callback function when closed with the message instance as the parameter | function | — | — | +| offset | set the distance to the top of viewport | number | — | 20 | +| appendTo | set the root element for the message | string / HTMLElement | - | document.body | ## Methods diff --git a/docs/en-US/component/notification.md b/docs/en-US/component/notification.md index 4724188a88..d37c654579 100644 --- a/docs/en-US/component/notification.md +++ b/docs/en-US/component/notification.md @@ -85,20 +85,21 @@ In this case you should call `ElNotification(options)`. We have also registered ## Options -| Attribute | Description | Type | Accepted Values | Default | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------- | --------- | -| title | title | string | — | — | -| message | description text | string/Vue.VNode | — | — | -| dangerouslyUseHTMLString | whether `message` is treated as HTML string | boolean | — | false | -| type | notification type | string | success/warning/info/error | — | -| icon | custom icon component. It will be overridden by `type` | string / Component | — | — | -| customClass | custom class name for Notification | string | — | — | -| duration | duration before close. It will not automatically close if set 0 | number | — | 4500 | -| position | custom position | string | top-right/top-left/bottom-right/bottom-left | top-right | -| showClose | whether to show a close button | boolean | — | true | -| onClose | callback function when closed | function | — | — | -| onClick | callback function when notification clicked | function | — | — | -| offset | offset from the top edge of the screen. Every Notification instance of the same moment should have the same offset | number | — | 0 | +| Attribute | Description | Type | Accepted Values | Default | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------ | -------------------- | ------------------------------------------- | ------------- | +| title | title | string | — | — | +| message | description text | string/Vue.VNode | — | — | +| dangerouslyUseHTMLString | whether `message` is treated as HTML string | boolean | — | false | +| type | notification type | string | success/warning/info/error | — | +| iconClass | custom icon's class. It will be overridden by `type` | string | — | — | +| customClass | custom class name for Notification | string | — | — | +| duration | duration before close. It will not automatically close if set 0 | number | — | 4500 | +| position | custom position | string | top-right/top-left/bottom-right/bottom-left | top-right | +| showClose | whether to show a close button | boolean | — | true | +| onClose | callback function when closed | function | — | — | +| onClick | callback function when notification clicked | function | — | — | +| offset | offset from the top edge of the screen. Every Notification instance of the same moment should have the same offset | number | — | 0 | +| appendTo | set the root element for the notification | string / HTMLElement | - | document.body | ## Methods diff --git a/packages/components/message/__tests__/message-manager.spec.ts b/packages/components/message/__tests__/message-manager.spec.ts index d82cf83356..39022301d1 100644 --- a/packages/components/message/__tests__/message-manager.spec.ts +++ b/packages/components/message/__tests__/message-manager.spec.ts @@ -87,4 +87,32 @@ describe('Message on command', () => { expect(Message.info).toBeInstanceOf(Function) expect(Message.error).toBeInstanceOf(Function) }) + + test('it should appendTo specified HTMLElement', async () => { + const htmlElement = document.createElement('div') + const handle = Message({ + appendTo: htmlElement, + }) + await rAF() + expect(htmlElement.querySelector(selector)).toBeTruthy() + handle.close() + await rAF() + await nextTick() + expect(htmlElement.querySelector(selector)).toBeFalsy() + }) + + test('it should appendTo specified selector', async () => { + const htmlElement = document.createElement('div') + htmlElement.classList.add('message-manager') + document.body.appendChild(htmlElement) + const handle = Message({ + appendTo: '.message-manager', + }) + await rAF() + expect(htmlElement.querySelector(selector)).toBeTruthy() + handle.close() + await rAF() + await nextTick() + expect(htmlElement.querySelector(selector)).toBeFalsy() + }) }) diff --git a/packages/components/message/src/message-method.ts b/packages/components/message/src/message-method.ts index bc3a6eac48..7aa1cb4c19 100644 --- a/packages/components/message/src/message-method.ts +++ b/packages/components/message/src/message-method.ts @@ -2,6 +2,7 @@ import { createVNode, render } from 'vue' import { isVNode } from '@element-plus/utils/util' import PopupManager from '@element-plus/utils/popup-manager' import isServer from '@element-plus/utils/isServer' +import { debugWarn } from '@element-plus/utils/error' import MessageConstructor from './message.vue' import { messageTypes } from './message' @@ -38,6 +39,21 @@ const message: MessageFn & Partial = function (options = {}) { }, } + let appendTo: HTMLElement | null = document.body + if (options.appendTo instanceof HTMLElement) { + appendTo = options.appendTo + } else if (typeof options.appendTo === 'string') { + appendTo = document.querySelector(options.appendTo) + } + // should fallback to default value with a warning + if (!(appendTo instanceof HTMLElement)) { + debugWarn( + 'ElMessage', + 'the appendTo option is not an HTMLElement. Falling back to document.body.' + ) + appendTo = document.body + } + const container = document.createElement('div') container.className = `container_${id}` @@ -60,7 +76,7 @@ const message: MessageFn & Partial = function (options = {}) { render(vm, container) // instances will remove this item when close function gets called. So we do not need to worry about it. instances.push({ vm }) - document.body.appendChild(container.firstElementChild!) + appendTo.appendChild(container.firstElementChild!) return { // instead of calling the onClose function directly, setting this value so that we can have the full lifecycle diff --git a/packages/components/message/src/message.ts b/packages/components/message/src/message.ts index 9a146fd9c8..66c128bf7a 100644 --- a/packages/components/message/src/message.ts +++ b/packages/components/message/src/message.ts @@ -62,7 +62,9 @@ export const messageEmits = { } export type MessageEmits = typeof messageEmits -export type MessageOptions = Omit +export type MessageOptions = Omit & { + appendTo?: HTMLElement | string +} export type MessageOptionsTyped = Omit export interface MessageHandle { diff --git a/packages/components/notification/__tests__/notify.spec.ts b/packages/components/notification/__tests__/notify.spec.ts index 8e4cc0653e..df930c3433 100644 --- a/packages/components/notification/__tests__/notify.spec.ts +++ b/packages/components/notification/__tests__/notify.spec.ts @@ -80,4 +80,33 @@ describe('Notification on command', () => { expect(document.querySelector(`.el-icon-${type}`)).toBeDefined() } }) + + test('it should appendTo specified HTMLElement', async () => { + const htmlElement = document.createElement('div') + const handle = Notification({ + appendTo: htmlElement, + }) + await rAF() + expect(htmlElement.querySelector(selector)).toBeDefined() + + handle.close() + await rAF() + await nextTick() + expect(htmlElement.querySelector(selector)).toBeNull() + }) + + test('it should appendTo specified selector', async () => { + const htmlElement = document.createElement('div') + htmlElement.classList.add('notification-manager') + document.body.appendChild(htmlElement) + const handle = Notification({ + appendTo: '.notification-manager', + }) + await rAF() + expect(htmlElement.querySelector(selector)).toBeDefined() + handle.close() + await rAF() + await nextTick() + expect(htmlElement.querySelector(selector)).toBeNull() + }) }) diff --git a/packages/components/notification/src/notification.ts b/packages/components/notification/src/notification.ts index 0d26db0613..2c421a2377 100644 --- a/packages/components/notification/src/notification.ts +++ b/packages/components/notification/src/notification.ts @@ -76,7 +76,9 @@ export const notificationEmits = { } export type NotificationEmits = typeof notificationEmits -export type NotificationOptions = Omit +export type NotificationOptions = Omit & { + appendTo?: HTMLElement | string +} export type NotificationOptionsTyped = Omit export interface NotificationHandle { diff --git a/packages/components/notification/src/notify.ts b/packages/components/notification/src/notify.ts index d5e08f4992..da02b07105 100644 --- a/packages/components/notification/src/notify.ts +++ b/packages/components/notification/src/notify.ts @@ -2,6 +2,7 @@ import { createVNode, render } from 'vue' import isServer from '@element-plus/utils/isServer' import PopupManager from '@element-plus/utils/popup-manager' import { isVNode } from '@element-plus/utils/util' +import { debugWarn } from '@element-plus/utils/error' import NotificationConstructor from './notification.vue' import { notificationTypes } from './notification' @@ -57,6 +58,22 @@ const notify: NotifyFn & Partial = function (options = {}) { }, } + let appendTo: HTMLElement | null = document.body + if (options.appendTo instanceof HTMLElement) { + appendTo = options.appendTo + } else if (typeof options.appendTo === 'string') { + appendTo = document.querySelector(options.appendTo) + } + + // should fallback to default value with a warning + if (!(appendTo instanceof HTMLElement)) { + 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( @@ -77,7 +94,7 @@ const notify: NotifyFn & Partial = function (options = {}) { // 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 }) - document.body.appendChild(container.firstElementChild!) + appendTo.appendChild(container.firstElementChild!) return { // instead of calling the onClose function directly, setting this value so that we can have the full lifecycle