From efd734b0f188235b2893196d58fe8635c95db650 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 11 Feb 2025 13:53:13 +0100 Subject: [PATCH] fix: [#1722] The slotchange event should be fired after the element has been connected to the DOM (#1723) --- .../CustomElementReactionStack.ts | 4 +- .../happy-dom/src/nodes/element/Element.ts | 37 +++++++++++++++- .../src/nodes/html-element/HTMLElement.ts | 43 ------------------- .../html-slot-element/HTMLSlotElement.test.ts | 38 ++++++++++++++++ 4 files changed, 76 insertions(+), 46 deletions(-) diff --git a/packages/happy-dom/src/custom-element/CustomElementReactionStack.ts b/packages/happy-dom/src/custom-element/CustomElementReactionStack.ts index f720d4dc6..659733f3b 100644 --- a/packages/happy-dom/src/custom-element/CustomElementReactionStack.ts +++ b/packages/happy-dom/src/custom-element/CustomElementReactionStack.ts @@ -1,7 +1,7 @@ -import HTMLElement from '../nodes/html-element/HTMLElement.js'; import BrowserWindow from '../window/BrowserWindow.js'; import * as PropertySymbol from '../PropertySymbol.js'; import WindowBrowserContext from '../window/WindowBrowserContext.js'; +import Element from '../nodes/element/Element.js'; /** * Custom element reaction stack. @@ -29,7 +29,7 @@ export default class CustomElementReactionStack { * @param callbackName Callback name. * @param [args] Arguments. */ - public enqueueReaction(element: HTMLElement, callbackName: string, args?: any[]): void { + public enqueueReaction(element: Element, callbackName: string, args?: any[]): void { // If a polyfill is used, [PropertySymbol.registry] may be undefined const definition = this.window.customElements[PropertySymbol.registry]?.get(element.localName); diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index cb5e3d3b8..cd58e3423 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -953,6 +953,25 @@ export default class Element return null; } + /** + * Connected callback. + */ + public connectedCallback?(): void; + + /** + * Disconnected callback. + */ + public disconnectedCallback?(): void; + + /** + * Attribute changed callback. + * + * @param name Name. + * @param oldValue Old value. + * @param newValue New value. + */ + public attributeChangedCallback?(name: string, oldValue: string, newValue: string): void; + /** * Query CSS selector to find matching nodes. * @@ -1449,6 +1468,17 @@ export default class Element } super[PropertySymbol.connectedToDocument](); + + this[PropertySymbol.window][PropertySymbol.customElementReactionStack].enqueueReaction( + this, + 'connectedCallback' + ); + + if (this[PropertySymbol.shadowRoot]) { + for (const childNode of this[PropertySymbol.nodeArray]) { + this.#onSlotChange(childNode); + } + } } /** @@ -1461,6 +1491,11 @@ export default class Element if (id) { this.#removeIdentifierFromWindow(id); } + + this[PropertySymbol.window][PropertySymbol.customElementReactionStack].enqueueReaction( + this, + 'disconnectedCallback' + ); } /** @@ -1563,7 +1598,7 @@ export default class Element #onSlotChange(addedOrRemovedNode: Node): void { const shadowRoot = this[PropertySymbol.shadowRoot]; - if (!shadowRoot) { + if (!shadowRoot || !this[PropertySymbol.isConnected]) { return; } diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 40b9ac9fb..85ee75214 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -498,25 +498,6 @@ export default class HTMLElement extends Element { this.setAttribute('popover', value); } - /** - * Connected callback. - */ - public connectedCallback?(): void; - - /** - * Disconnected callback. - */ - public disconnectedCallback?(): void; - - /** - * Attribute changed callback. - * - * @param name Name. - * @param oldValue Old value. - * @param newValue New value. - */ - public attributeChangedCallback?(name: string, oldValue: string, newValue: string): void; - /** * Triggers a click event. */ @@ -612,30 +593,6 @@ export default class HTMLElement extends Element { super[PropertySymbol.disconnectedFromNode](); } - /** - * @override - */ - public override [PropertySymbol.connectedToDocument](): void { - super[PropertySymbol.connectedToDocument](); - - this[PropertySymbol.window][PropertySymbol.customElementReactionStack].enqueueReaction( - this, - 'connectedCallback' - ); - } - - /** - * @override - */ - public override [PropertySymbol.disconnectedFromDocument](): void { - super[PropertySymbol.disconnectedFromDocument](); - - this[PropertySymbol.window][PropertySymbol.customElementReactionStack].enqueueReaction( - this, - 'disconnectedCallback' - ); - } - /** * @override */ diff --git a/packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts b/packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts index e827ea291..8db6a4af8 100644 --- a/packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts +++ b/packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts @@ -462,5 +462,43 @@ describe('HTMLSlotElement', () => { expect(dispatchedEvent2).toBe(null); expect(dispatchedEvent3).toBe(null); }); + + it('Fires slotchange after the element is connected to the document', () => { + const lifecycle: string[] = []; + /* eslint-disable jsdoc/require-jsdoc */ + class CustomElement extends HTMLElement { + constructor() { + super(); + this.attachShadow({ + mode: 'open' + }); + + (this.shadowRoot).innerHTML = `
`; + const slot = (this.shadowRoot).children[0].children[0]; + slot.addEventListener('slotchange', () => { + lifecycle.push('slotchange.' + this.isConnected); + }); + } + + public connectedCallback(): void { + lifecycle.push('connected'); + } + + public disconnectedCallback(): void { + lifecycle.push('disconnected'); + } + } + /* eslint-enable jsdoc/require-jsdoc */ + + window.customElements.define('custom-element', CustomElement); + + const customElement = document.createElement('custom-element'); + + customElement.innerHTML = ''; + + document.body.appendChild(customElement); + + expect(lifecycle).toEqual(['connected', 'slotchange.true']); + }); }); });