Skip to content

Commit

Permalink
feat(shadow-dom): isInDOM to traverse up the shadow root host
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Feb 12, 2022
1 parent 158c690 commit 485a736
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 2 deletions.
4 changes: 4 additions & 0 deletions src/dom-node-safe-guards.ts
Expand Up @@ -5,3 +5,7 @@
export function isElement(node: Node | null): node is Element {
return node != null && node.nodeType === Node.ELEMENT_NODE;
}

export function isShadowRoot(node: Node | null): node is ShadowRoot {
return node != null && node instanceof ShadowRoot;
}
24 changes: 24 additions & 0 deletions src/queries.ts
@@ -0,0 +1,24 @@
import { getConfig } from './config';
import { isShadowRoot } from './dom-node-safe-guards';

/**
* `Element.closest` which traverses tree up when `ShadowRoot` is encountered
*/
export function closest(
element: Element,
...args: Parameters<Element['closest']>
): ReturnType<Element['closest']> {
const result = element.closest(...args);

if (result || !getConfig().includeShadowDom) {
return result;
}

const rootNode = element.getRootNode();

if (isShadowRoot(rootNode)) {
return closest(rootNode.host, ...args);
}

return null;
}
5 changes: 4 additions & 1 deletion src/utils.ts
@@ -1,4 +1,5 @@
import { isElement } from './dom-node-safe-guards';
import * as queries from './queries';

export type PolitenessSetting = 'polite' | 'assertive' | 'off';

Expand Down Expand Up @@ -57,7 +58,9 @@ export function isLiveRegionAttribute(
}

export function isInDOM(node: Node): boolean {
return isElement(node) && node.closest('html') != null;
const element = getClosestElement(node);

return element != null && queries.closest(element, 'html') != null;
}

// TODO: Support `hidden` and CSS attributes:
Expand Down
122 changes: 121 additions & 1 deletion test/utils.test.ts
@@ -1,4 +1,6 @@
import { getTextContent } from '../src/utils';
import { configure } from '../src/config';
import { getTextContent, isInDOM } from '../src/utils';
import { appendToRoot } from './utils';

describe('getTextContent', () => {
let root: HTMLElement;
Expand Down Expand Up @@ -93,3 +95,121 @@ describe('getTextContent', () => {
expect(getTextContent(document.getElementById('temp'))).toBe(null);
});
});

describe('isInDOM', () => {
test('mounted element is in DOM', () => {
const element = document.createElement('div');
appendToRoot(element);

expect(isInDOM(element)).toBe(true);
});

test('unmounted element is not in DOM', () => {
const element = document.createElement('div');

expect(isInDOM(element)).toBe(false);
});

test('document body is in DOM', () => {
expect(isInDOM(document.body)).toBe(true);
});

test('mounted text node is in DOM', () => {
const element = document.createElement('div');
const text = document.createTextNode('Hello world');
element.appendChild(text);
appendToRoot(element);

expect(isInDOM(text)).toBe(true);
});

test('unmounted text node is not in DOM', () => {
const text = document.createTextNode('Hello world');

expect(isInDOM(text)).toBe(false);
});

test('element inside mounted shadow root is in DOM', () => {
configure({ includeShadowDom: true });

const parent = document.createElement('div');
const shadowRoot = parent.attachShadow({ mode: 'open' });
appendToRoot(parent);

const element = document.createElement('div');
shadowRoot.appendChild(element);

expect(isInDOM(element)).toBe(true);
});

test('does not traverse shadow dom when config.includeShadowDom is false', () => {
configure({ includeShadowDom: false });

const parent = document.createElement('div');
const shadowRoot = parent.attachShadow({ mode: 'open' });
appendToRoot(parent);

const element = document.createElement('div');
shadowRoot.appendChild(element);

expect(isInDOM(element)).toBe(false);
});

test('element inside unmounted shadow root is not in DOM', () => {
configure({ includeShadowDom: true });

const parent = document.createElement('div');
const shadowRoot = parent.attachShadow({ mode: 'open' });

const element = document.createElement('div');
shadowRoot.appendChild(element);

expect(isInDOM(element)).toBe(false);
});

test('text node inside mounted shadow root is in DOM', () => {
configure({ includeShadowDom: true });

const parent = document.createElement('div');
const shadowRoot = parent.attachShadow({ mode: 'open' });
appendToRoot(parent);

const text = document.createTextNode('Hello world');
const element = document.createElement('div');
element.appendChild(text);
shadowRoot.appendChild(element);

expect(isInDOM(text)).toBe(true);
});

test('text node inside unmounted shadow root is not in DOM', () => {
configure({ includeShadowDom: true });

const parent = document.createElement('div');
const shadowRoot = parent.attachShadow({ mode: 'open' });

const text = document.createTextNode('Hello world');
const element = document.createElement('div');
element.appendChild(text);
shadowRoot.appendChild(element);

expect(isInDOM(text)).toBe(false);
});

test('element inside multiple nested mounted shadow roots is in DOM', () => {
configure({ includeShadowDom: true });

const firstParent = document.createElement('div');
firstParent.attachShadow({ mode: 'open' });
appendToRoot(firstParent);

const secondParent = document.createElement('div');
secondParent.attachShadow({ mode: 'open' });
firstParent.shadowRoot!.appendChild(secondParent);

const element = document.createElement('div');
secondParent.shadowRoot!.appendChild(element);

expect(isInDOM(element)).toBe(true);
});
});

0 comments on commit 485a736

Please sign in to comment.