diff --git a/docs/en-US/component/message-box.md b/docs/en-US/component/message-box.md index 3909b62807..342939c31c 100644 --- a/docs/en-US/component/message-box.md +++ b/docs/en-US/component/message-box.md @@ -191,3 +191,4 @@ The corresponding methods are: `ElMessageBox`, `ElMessageBox.alert`, `ElMessageB | draggable | whether MessageBox is draggable | boolean | — | false | | round-button | whether to use round button | boolean | — | false | | button-size | custom size of confirm and cancel buttons | string | small / default / large | default | +| append-to | set the root element for the message box | string \| HTMLElement | — | — | diff --git a/packages/components/message-box/__tests__/message-box.test.ts b/packages/components/message-box/__tests__/message-box.test.ts index 9fe51f06a1..0f8459ca9c 100644 --- a/packages/components/message-box/__tests__/message-box.test.ts +++ b/packages/components/message-box/__tests__/message-box.test.ts @@ -1,7 +1,7 @@ // @ts-nocheck import { markRaw } from 'vue' import { mount } from '@vue/test-utils' -import { afterEach, describe, expect, it, test } from 'vitest' +import { afterEach, describe, expect, it, test, vi } from 'vitest' import { rAF } from '@element-plus/test-utils/tick' import { triggerNativeCompositeClick } from '@element-plus/test-utils/composite-click' import { QuestionFilled as QuestionFilledIcon } from '@element-plus/icons-vue' @@ -11,6 +11,10 @@ import { ElMessageBox } from '..' const selector = '.el-overlay' const QuestionFilled = markRaw(QuestionFilledIcon) +vi.mock('@element-plus/utils/error', () => ({ + debugWarn: vi.fn(), +})) + const _mount = (invoker: () => void) => { return mount( { @@ -28,6 +32,7 @@ const _mount = (invoker: () => void) => { describe('MessageBox', () => { afterEach(async () => { MessageBox.close() + document.body.innerHTML = '' await rAF() }) @@ -275,6 +280,52 @@ describe('MessageBox', () => { }) }) + describe('append to', () => { + it('should append to body if parameter is not provided', () => { + MessageBox({ + title: 'append to test', + message: 'append to test', + }) + const msgbox: HTMLElement = document.querySelector(`body > ${selector}`) + expect(msgbox).toBeDefined() + }) + + it('should append to body if element does not exist', () => { + MessageBox({ + title: 'append to test', + message: 'append to test', + appendTo: '.not-existing-selector', + }) + const msgbox: HTMLElement = document.querySelector(`body > ${selector}`) + expect(msgbox).toBeDefined() + }) + + it('should append to HtmlElement provided', () => { + const htmlElement = document.createElement('div') + document.body.appendChild(htmlElement) + MessageBox({ + title: 'append to test', + message: 'append to test', + appendTo: htmlElement, + }) + const msgbox: HTMLElement = htmlElement.querySelector(selector) + expect(msgbox).toBeDefined() + }) + + it('should append to selector provided', () => { + const htmlElement = document.createElement('div') + htmlElement.className = 'custom-html-element' + document.body.appendChild(htmlElement) + MessageBox({ + title: 'append to test', + message: 'append to test', + appendTo: '.custom-html-element', + }) + const msgbox: HTMLElement = htmlElement.querySelector(selector) + expect(msgbox).toBeDefined() + }) + }) + describe('accessibility', () => { test('title attribute should set aria-label', async () => { const title = 'Hello World' diff --git a/packages/components/message-box/src/message-box.type.ts b/packages/components/message-box/src/message-box.type.ts index 61b37df08f..8b02cab0a3 100644 --- a/packages/components/message-box/src/message-box.type.ts +++ b/packages/components/message-box/src/message-box.type.ts @@ -170,6 +170,9 @@ export interface ElMessageBoxOptions { /** Custom size of confirm and cancel buttons */ buttonSize?: ComponentSize + + /** Custom element to append the message box to */ + appendTo?: HTMLElement | string } export type ElMessageBoxShortcutMethod = (( diff --git a/packages/components/message-box/src/messageBox.ts b/packages/components/message-box/src/messageBox.ts index bc0c5e17a1..73e391ea83 100644 --- a/packages/components/message-box/src/messageBox.ts +++ b/packages/components/message-box/src/messageBox.ts @@ -1,7 +1,9 @@ import { createVNode, render } from 'vue' import { isClient } from '@vueuse/core' import { + debugWarn, hasOwn, + isElement, isFunction, isObject, isString, @@ -33,6 +35,28 @@ const messageInstance = new Map< } >() +const getAppendToElement = (props: any): HTMLElement => { + let appendTo: HTMLElement | null = document.body + if (props.appendTo) { + if (isString(props.appendTo)) { + appendTo = document.querySelector(props.appendTo) + } + if (isElement(props.appendTo)) { + appendTo = props.appendTo + } + + // should fallback to default value with a warning + if (!isElement(appendTo)) { + debugWarn( + 'ElMessageBox', + 'the appendTo option is not an HTMLElement. Falling back to document.body.' + ) + appendTo = document.body + } + } + return appendTo +} + const initInstance = ( props: any, container: HTMLElement, @@ -51,7 +75,7 @@ const initInstance = ( ) vnode.appContext = appContext render(vnode, container) - document.body.appendChild(container.firstElementChild!) + getAppendToElement(props).appendChild(container.firstElementChild!) return vnode.component }