Remove shadow DOM from headless components

Headless components are behavioral wrappers meant to be styled
by the consumer with regular CSS. Shadow DOM on each child
created unnecessary style boundaries with no visual content to
protect. Only ProboSettingsButton retains its own shadow root
since it renders encapsulated styled markup.

Also removes the auto-registerComponents() side effect from
the main entrypoint so consumers opt in explicitly.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-16 16:59:45 +04:00
parent f0b5a13f6f
commit 2e066c82ad
11 changed files with 25 additions and 82 deletions

View File

@@ -30,14 +30,6 @@ export class ProboBanner extends ProboElement {
};
connectedCallback(): void {
this.shadow.innerHTML = `
<style>
:host { display: block; }
:host([hidden]) { display: none; }
</style>
<slot></slot>
`;
this.hidden = true;
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");

View File

@@ -21,25 +21,13 @@ export interface ConsentDraft {
}
export class ProboElement extends HTMLElement {
protected shadow: ShadowRoot;
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
protected findAncestor<T extends HTMLElement>(tagName: string): T | null {
let node: Node | null = this as Node;
while (node) {
const root = node.getRootNode();
if (root instanceof ShadowRoot) {
node = root.host;
} else {
node = (node as HTMLElement).parentElement;
}
if (node instanceof HTMLElement && node.tagName.toLowerCase() === tagName) {
return node as T;
let el: HTMLElement | null = this.parentElement;
while (el) {
if (el.tagName.toLowerCase() === tagName) {
return el as T;
}
el = el.parentElement;
}
return null;
}

View File

@@ -20,10 +20,6 @@ class ProboActionButton extends ProboElement {
protected root: ProboRootElement | null = null;
connectedCallback(): void {
this.shadow.innerHTML = `
<style>:host { display: contents; }</style>
<slot></slot>
`;
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
this.addEventListener("click", this.handleClick);
}

View File

@@ -26,11 +26,6 @@ export class ProboCategoryList extends ProboElement {
};
connectedCallback(): void {
this.shadow.innerHTML = `
<style>:host { display: contents; }</style>
<slot name="items"></slot>
`;
this.template = this.querySelector("template");
if (!this.template) {
this.warn("<probo-category-list> requires a <template> child");
@@ -62,7 +57,6 @@ export class ProboCategoryList extends ProboElement {
for (const cat of categories) {
const wrapper = document.createElement("probo-category");
wrapper.setAttribute("name", cat.name);
wrapper.setAttribute("slot", "items");
if (cat.required) {
wrapper.setAttribute("required", "");
}

View File

@@ -23,13 +23,6 @@ export class ProboCategoryToggle extends ProboElement {
private checkbox: HTMLInputElement | null = null;
connectedCallback(): void {
this.shadow.innerHTML = `
<style>
:host { display: inline-block; }
</style>
<slot></slot>
`;
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
this.category = this.findAncestor<ProboCategory>("probo-category");

View File

@@ -16,12 +16,6 @@ import type { CookieItem } from "../client";
import { ProboElement } from "./base";
export class ProboCategory extends ProboElement {
connectedCallback(): void {
this.shadow.innerHTML = `
<style>:host { display: contents; }</style>
<slot></slot>
`;
}
get categoryName(): string {
return this.getAttribute("name") ?? "";

View File

@@ -50,7 +50,6 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem
}
connectedCallback(): void {
this.shadow.innerHTML = `<style>:host { display: contents; }</style><slot></slot>`;
this.initClient();
}

View File

@@ -21,11 +21,6 @@ export class ProboCookieList extends ProboElement {
private template: HTMLTemplateElement | null = null;
connectedCallback(): void {
this.shadow.innerHTML = `
<style>:host { display: contents; }</style>
<slot name="items"></slot>
`;
this.template = this.querySelector("template");
if (!this.template) {
this.warn("<probo-cookie-list> requires a <template> child");
@@ -51,8 +46,6 @@ export class ProboCookieList extends ProboElement {
const wrapper = document.createElement("probo-cookie");
wrapper.setAttribute("name", cookie.name);
wrapper.setAttribute("slot", "items");
const clone = this.template.content.cloneNode(true) as DocumentFragment;
this.fillSlots(clone, {
name: cookie.name,
@@ -77,11 +70,4 @@ export class ProboCookieList extends ProboElement {
}
}
export class ProboCookie extends ProboElement {
connectedCallback(): void {
this.shadow.innerHTML = `
<style>:host { display: contents; }</style>
<slot></slot>
`;
}
}
export class ProboCookie extends ProboElement {}

View File

@@ -29,14 +29,6 @@ export class ProboPreferencePanel extends ProboElement {
};
connectedCallback(): void {
this.shadow.innerHTML = `
<style>
:host { display: block; }
:host([hidden]) { display: none; }
</style>
<slot></slot>
`;
this.hidden = true;
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
@@ -77,10 +69,6 @@ export class ProboSaveButton extends ProboElement {
private root: ProboRootElement | null = null;
connectedCallback(): void {
this.shadow.innerHTML = `
<style>:host { display: contents; }</style>
<slot></slot>
`;
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
this.addEventListener("click", this.handleClick);
}

View File

@@ -12,15 +12,20 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { ProboElement } from "./base";
import type { ProboRootElement } from "./base";
import type { ProboCookieBannerRoot } from "./cookie-banner-root";
const COOKIE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="8" cy="9" r="1" fill="currentColor"/><circle cx="15" cy="11" r="1" fill="currentColor"/><circle cx="10" cy="15" r="1" fill="currentColor"/><circle cx="13" cy="7" r="1" fill="currentColor"/></svg>`;
export class ProboSettingsButton extends ProboElement {
export class ProboSettingsButton extends HTMLElement {
private shadow: ShadowRoot;
private root: ProboRootElement | null = null;
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
static get observedAttributes(): string[] {
return ["position"];
}
@@ -69,7 +74,7 @@ export class ProboSettingsButton extends ProboElement {
`;
this.hidden = true;
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
this.root = this.findRoot();
if (this.root) {
this.root.addEventListener("probo-state", this.onStateChange);
@@ -88,6 +93,17 @@ export class ProboSettingsButton extends ProboElement {
}
}
private findRoot(): ProboCookieBannerRoot | null {
let el: HTMLElement | null = this.parentElement;
while (el) {
if (el.tagName.toLowerCase() === "probo-cookie-banner-root") {
return el as ProboCookieBannerRoot;
}
el = el.parentElement;
}
return null;
}
private onStateChange = (e: Event): void => {
const { state } = (e as CustomEvent).detail;
this.hidden = state !== "hidden";

View File

@@ -54,6 +54,3 @@ export {
} from "./errors";
export { fetchJSON } from "./http";
export type { RequestOptions } from "./http";
import { registerComponents } from "./components";
registerComponents();