From e6f8b13b5775c5a669d852c834527dab66fb5662 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 5 Nov 2024 00:40:19 +0100 Subject: [PATCH] fix: [#1581] Fixes bug where Node.getRootNode() returned null when it was within a ShadowRoot that previously been disconnected from the Document (#1582) --- packages/happy-dom/src/nodes/node/Node.ts | 5 +++- .../src/nodes/shadow-root/ShadowRoot.ts | 24 +++++++++++++------ .../happy-dom/test/nodes/node/Node.test.ts | 8 +++++++ .../test/nodes/shadow-root/ShadowRoot.test.ts | 23 ++++++++++++++++++ 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 686a0713d..1e5e8a733 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -1025,7 +1025,10 @@ export default class Node extends EventTarget { */ public [PropertySymbol.disconnectedFromDocument](): void { this[PropertySymbol.isConnected] = false; - this[PropertySymbol.rootNode] = null; + + if (this[PropertySymbol.nodeType] !== NodeTypeEnum.documentFragmentNode) { + this[PropertySymbol.rootNode] = null; + } if (this[PropertySymbol.ownerDocument][PropertySymbol.activeElement] === this) { this[PropertySymbol.ownerDocument][PropertySymbol.clearCache](); diff --git a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts index eabfc8046..2723a908d 100644 --- a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts +++ b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts @@ -7,6 +7,7 @@ import CSSStyleSheet from '../../css/CSSStyleSheet.js'; import HTMLElement from '../../nodes/html-element/HTMLElement.js'; import Event from '../../event/Event.js'; import SVGElement from '../svg-element/SVGElement.js'; +import Document from '../document/Document.js'; /** * ShadowRoot. @@ -165,16 +166,25 @@ export default class ShadowRoot extends DocumentFragment { * @returns Active element. */ public get activeElement(): HTMLElement | SVGElement | null { - const activeElement: HTMLElement | SVGElement = + let activeElement: HTMLElement | SVGElement = this[PropertySymbol.ownerDocument][PropertySymbol.activeElement]; - if ( - activeElement && - activeElement[PropertySymbol.isConnected] && - activeElement.getRootNode() === this - ) { + + let rootNode: ShadowRoot | Document = activeElement?.getRootNode(); + + if (!rootNode || rootNode === this[PropertySymbol.ownerDocument]) { + return null; + } + + if (rootNode === this) { return activeElement; } - return null; + + while (rootNode && rootNode !== this) { + activeElement = (rootNode).host; + rootNode = activeElement.getRootNode(); + } + + return activeElement; } /** diff --git a/packages/happy-dom/test/nodes/node/Node.test.ts b/packages/happy-dom/test/nodes/node/Node.test.ts index 0645eb570..d18145aea 100644 --- a/packages/happy-dom/test/nodes/node/Node.test.ts +++ b/packages/happy-dom/test/nodes/node/Node.test.ts @@ -366,6 +366,14 @@ describe('Node', () => { const rootNode = (customElement.shadowRoot).querySelector('span')?.getRootNode(); expect(rootNode === customElement.shadowRoot).toBe(true); + + document.body.removeChild(customElement); + + document.body.appendChild(customElement); + + const rootNode2 = (customElement.shadowRoot).querySelector('span')?.getRootNode(); + + expect(rootNode2 === customElement.shadowRoot).toBe(true); }); it('Returns Document when used on a node inside a ShadowRoot and the option "composed" is set to "true".', () => { diff --git a/packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts b/packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts index 026b0483d..b772fea67 100644 --- a/packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts +++ b/packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts @@ -218,6 +218,29 @@ describe('ShadowRoot', () => { expect(shadowRoot.activeElement === null).toBe(true); }); + + it('Returns the first custom element when the active element is not a child of the ShadowRoot, but is a child of a custom element within it.', () => { + const customElement = document.createElement('custom-element'); + const shadowRoot = customElement.shadowRoot; + const div = document.createElement('div'); + const customElementA = document.createElement('custom-element-a'); + const shadowRootA = customElementA.shadowRoot; + + document.body.appendChild(customElement); + + shadowRoot.appendChild(customElementA); + shadowRootA.appendChild(div); + + expect(shadowRoot.activeElement === null).toBe(true); + + div.focus(); + + expect(shadowRoot.activeElement).toBe(customElementA); + + customElementA.remove(); + + expect(shadowRoot.activeElement === null).toBe(true); + }); }); describe('getAnimations()', () => {