diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index df438d47eee..bffb5ff6e92 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -17,6 +17,7 @@ import { render, renderSlot, useHost, + useHostInternals, useShadowRoot, } from '../src' @@ -1164,6 +1165,21 @@ describe('defineCustomElement', () => { const style = el.shadowRoot?.querySelector('style')! expect(style.textContent).toBe(`div { color: red; }`) }) + + // wait for jsdom to fix https://github.com/jsdom/jsdom/issues/3732 + test.todo('useHostInternals', async () => { + const Foo = defineCustomElement({ + setup() { + const internals = useHostInternals()! + internals.ariaLive = 'polite' + return () => h('div', 'hello') + }, + }) + customElements.define('my-el-use-host-internals', Foo) + container.innerHTML = `` + const el = container.childNodes[0] as VueElement + expect(el._internals?.ariaLive).toBe('polite') + }) }) describe('expose', () => { @@ -1419,6 +1435,20 @@ describe('defineCustomElement', () => { expect(e.shadowRoot!.innerHTML).toBe(`false,boolean`) }) + test('support attachInternals method', () => { + const E = defineCustomElement({ + formAssociated: true, + render() { + return h('div', 'hello') + }, + }) + customElements.define('my-el-attach-internals', E) + container.innerHTML = `` + const e = container.childNodes[0] as VueElement + expect(e.shadowRoot!.innerHTML).toBe(`
hello
`) + expect(e._internals).toBeTruthy() + }) + test('hyphenated attr removal', async () => { const E = defineCustomElement({ props: { diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index aeeaeec9b9f..c6d000feeca 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -176,6 +176,8 @@ export function defineCustomElement( if (isPlainObject(Comp)) extend(Comp, extraOptions) class VueCustomElement extends VueElement { static def = Comp + static formAssociated = !!options.formAssociated + constructor(initialProps?: Record) { super(Comp, initialProps, _createApp) } @@ -204,6 +206,7 @@ export class VueElement implements ComponentCustomElementInterface { _isVueCE = true + _internals: ElementInternals | null = null /** * @internal */ @@ -253,6 +256,9 @@ export class VueElement private _createApp: CreateAppFunction = createApp, ) { super() + + if (this.attachInternals) this._internals = this.attachInternals() + if (this.shadowRoot && _createApp !== createApp) { this._root = this.shadowRoot } else { @@ -710,3 +716,12 @@ export function useShadowRoot(): ShadowRoot | null { const el = __DEV__ ? useHost('useShadowRoot') : useHost() return el && el.shadowRoot } + +/** + * Retrieve the ElementInternals of the current custom element. Only usable in setup() + * of a `defineCustomElement` component. + */ +export function useHostInternals(): ElementInternals | null { + const el = __DEV__ ? useHost('useHostInternals') : useHost() + return el && el._internals +} diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index ca9a307dd98..b388eac11ec 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -256,6 +256,7 @@ export { defineSSRCustomElement, useShadowRoot, useHost, + useHostInternals, VueElement, type VueElementConstructor, type CustomElementOptions,