Skip to content

Commit 49eaad5

Browse files
committed
feat(replay): Define custom elements (web components)
Define custom elements so that we the `:defined` pseudo-class works as it can be used as a "loading" indicator while the web component is being loaded. Fixes getsentry/sentry-javascript#7988
1 parent b4261e7 commit 49eaad5

File tree

4 files changed

+29
-3
lines changed

4 files changed

+29
-3
lines changed

packages/rrweb-snapshot/src/rebuild.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
INode,
99
BuildCache,
1010
} from './types';
11-
import { isElement } from './utils';
11+
import { defineCustomElement, isElement } from './utils';
1212

1313
const tagMap: tagMap = {
1414
script: 'noscript',
@@ -284,6 +284,9 @@ function buildNode(
284284
*/
285285
if (!node.shadowRoot) {
286286
node.attachShadow({ mode: 'open' });
287+
if (doc.defaultView) {
288+
defineCustomElement(doc.defaultView, tagName);
289+
}
287290
} else {
288291
while (node.shadowRoot.firstChild) {
289292
node.shadowRoot.removeChild(node.shadowRoot.firstChild);

packages/rrweb-snapshot/src/utils.ts

+17
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,20 @@ export function getInputValue(
171171

172172
return el.value;
173173
}
174+
175+
/**
176+
* Ensure we define custom elements as you can have css that targets the
177+
* `:defined` pseudo class (e.g. hide until defined)
178+
*/
179+
export function defineCustomElement(w: Window, elementName: string) {
180+
// We need to define custom elements inside of the correct window (i.e.
181+
// inside of the iframe)
182+
try {
183+
// Can only define custom element once
184+
if (!w.customElements.get(elementName)) {
185+
// @ts-ignore HTMLElement exists on window
186+
const CustomElement = w.HTMLElement as CustomElementConstructor;
187+
w.customElements.define(elementName, class extends CustomElement {});
188+
}
189+
} catch {}
190+
}

packages/rrweb-snapshot/typings/utils.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export declare function maskInputValue({ input, maskInputSelector, unmaskInputSe
2020
export declare function is2DCanvasBlank(canvas: HTMLCanvasElement): boolean;
2121
export declare function getInputType(element: HTMLElement): Lowercase<string> | null;
2222
export declare function getInputValue(el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLOptionElement, tagName: Uppercase<string>, type: attributes[string]): string;
23+
export declare function defineCustomElement(w: Window, elementName: string): void;
2324
export {};

packages/rrweb/src/replay/index.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
NodeType,
66
BuildCache,
77
createCache,
8+
defineCustomElement,
89
} from '@sentry-internal/rrweb-snapshot';
910
import * as mittProxy from 'mitt';
1011
import { polyfill as smoothscrollPolyfill } from './smoothscroll';
@@ -1458,8 +1459,12 @@ export class Replayer {
14581459
if (mutation.node.isShadow) {
14591460
// If the parent is attached a shadow dom after it's created, it won't have a shadow root.
14601461
if (!hasShadowRoot(parent)) {
1461-
((parent as Node) as HTMLElement).attachShadow({ mode: 'open' });
1462-
parent = ((parent as Node) as HTMLElement).shadowRoot!;
1462+
const parentNode = parent as Node;
1463+
(parentNode as HTMLElement).attachShadow({ mode: 'open' });
1464+
parent = (parentNode as HTMLElement).shadowRoot!;
1465+
if (this.iframe.contentWindow) {
1466+
defineCustomElement(this.iframe.contentWindow, parentNode.nodeName);
1467+
}
14631468
} else parent = parent.shadowRoot;
14641469
}
14651470

0 commit comments

Comments
 (0)