Skip to content

Commit

Permalink
fix: [#1581] Fixes bug where Node.getRootNode() returned null when it…
Browse files Browse the repository at this point in the history
… was within a ShadowRoot that previously been disconnected from the Document (#1582)
  • Loading branch information
capricorn86 authored Nov 4, 2024
1 parent 38ab960 commit e6f8b13
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 8 deletions.
5 changes: 4 additions & 1 deletion packages/happy-dom/src/nodes/node/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] === <unknown>this) {
this[PropertySymbol.ownerDocument][PropertySymbol.clearCache]();
Expand Down
24 changes: 17 additions & 7 deletions packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 = <ShadowRoot | Document>activeElement?.getRootNode();

if (!rootNode || rootNode === this[PropertySymbol.ownerDocument]) {
return null;
}

if (rootNode === this) {
return activeElement;
}
return null;

while (rootNode && rootNode !== this) {
activeElement = <HTMLElement | SVGElement>(<ShadowRoot>rootNode).host;
rootNode = <ShadowRoot | Document>activeElement.getRootNode();
}

return activeElement;
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/happy-dom/test/nodes/node/Node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,14 @@ describe('Node', () => {
const rootNode = (<ShadowRoot>customElement.shadowRoot).querySelector('span')?.getRootNode();

expect(rootNode === customElement.shadowRoot).toBe(true);

document.body.removeChild(customElement);

document.body.appendChild(customElement);

const rootNode2 = (<ShadowRoot>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".', () => {
Expand Down
23 changes: 23 additions & 0 deletions packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <ShadowRoot>customElement.shadowRoot;
const div = <HTMLElement>document.createElement('div');
const customElementA = document.createElement('custom-element-a');
const shadowRootA = <ShadowRoot>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()', () => {
Expand Down

0 comments on commit e6f8b13

Please sign in to comment.