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:
@@ -30,14 +30,6 @@ export class ProboBanner extends ProboElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
this.shadow.innerHTML = `
|
|
||||||
<style>
|
|
||||||
:host { display: block; }
|
|
||||||
:host([hidden]) { display: none; }
|
|
||||||
</style>
|
|
||||||
<slot></slot>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.hidden = true;
|
this.hidden = true;
|
||||||
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
||||||
|
|
||||||
|
|||||||
@@ -21,25 +21,13 @@ export interface ConsentDraft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ProboElement extends HTMLElement {
|
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 {
|
protected findAncestor<T extends HTMLElement>(tagName: string): T | null {
|
||||||
let node: Node | null = this as Node;
|
let el: HTMLElement | null = this.parentElement;
|
||||||
while (node) {
|
while (el) {
|
||||||
const root = node.getRootNode();
|
if (el.tagName.toLowerCase() === tagName) {
|
||||||
if (root instanceof ShadowRoot) {
|
return el as T;
|
||||||
node = root.host;
|
|
||||||
} else {
|
|
||||||
node = (node as HTMLElement).parentElement;
|
|
||||||
}
|
|
||||||
if (node instanceof HTMLElement && node.tagName.toLowerCase() === tagName) {
|
|
||||||
return node as T;
|
|
||||||
}
|
}
|
||||||
|
el = el.parentElement;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,6 @@ class ProboActionButton extends ProboElement {
|
|||||||
protected root: ProboRootElement | null = null;
|
protected root: ProboRootElement | null = null;
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
this.shadow.innerHTML = `
|
|
||||||
<style>:host { display: contents; }</style>
|
|
||||||
<slot></slot>
|
|
||||||
`;
|
|
||||||
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
||||||
this.addEventListener("click", this.handleClick);
|
this.addEventListener("click", this.handleClick);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,6 @@ export class ProboCategoryList extends ProboElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
this.shadow.innerHTML = `
|
|
||||||
<style>:host { display: contents; }</style>
|
|
||||||
<slot name="items"></slot>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.template = this.querySelector("template");
|
this.template = this.querySelector("template");
|
||||||
if (!this.template) {
|
if (!this.template) {
|
||||||
this.warn("<probo-category-list> requires a <template> child");
|
this.warn("<probo-category-list> requires a <template> child");
|
||||||
@@ -62,7 +57,6 @@ export class ProboCategoryList extends ProboElement {
|
|||||||
for (const cat of categories) {
|
for (const cat of categories) {
|
||||||
const wrapper = document.createElement("probo-category");
|
const wrapper = document.createElement("probo-category");
|
||||||
wrapper.setAttribute("name", cat.name);
|
wrapper.setAttribute("name", cat.name);
|
||||||
wrapper.setAttribute("slot", "items");
|
|
||||||
if (cat.required) {
|
if (cat.required) {
|
||||||
wrapper.setAttribute("required", "");
|
wrapper.setAttribute("required", "");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,6 @@ export class ProboCategoryToggle extends ProboElement {
|
|||||||
private checkbox: HTMLInputElement | null = null;
|
private checkbox: HTMLInputElement | null = null;
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
this.shadow.innerHTML = `
|
|
||||||
<style>
|
|
||||||
:host { display: inline-block; }
|
|
||||||
</style>
|
|
||||||
<slot></slot>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
||||||
this.category = this.findAncestor<ProboCategory>("probo-category");
|
this.category = this.findAncestor<ProboCategory>("probo-category");
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ import type { CookieItem } from "../client";
|
|||||||
import { ProboElement } from "./base";
|
import { ProboElement } from "./base";
|
||||||
|
|
||||||
export class ProboCategory extends ProboElement {
|
export class ProboCategory extends ProboElement {
|
||||||
connectedCallback(): void {
|
|
||||||
this.shadow.innerHTML = `
|
|
||||||
<style>:host { display: contents; }</style>
|
|
||||||
<slot></slot>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get categoryName(): string {
|
get categoryName(): string {
|
||||||
return this.getAttribute("name") ?? "";
|
return this.getAttribute("name") ?? "";
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
this.shadow.innerHTML = `<style>:host { display: contents; }</style><slot></slot>`;
|
|
||||||
this.initClient();
|
this.initClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,6 @@ export class ProboCookieList extends ProboElement {
|
|||||||
private template: HTMLTemplateElement | null = null;
|
private template: HTMLTemplateElement | null = null;
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
this.shadow.innerHTML = `
|
|
||||||
<style>:host { display: contents; }</style>
|
|
||||||
<slot name="items"></slot>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.template = this.querySelector("template");
|
this.template = this.querySelector("template");
|
||||||
if (!this.template) {
|
if (!this.template) {
|
||||||
this.warn("<probo-cookie-list> requires a <template> child");
|
this.warn("<probo-cookie-list> requires a <template> child");
|
||||||
@@ -51,8 +46,6 @@ export class ProboCookieList extends ProboElement {
|
|||||||
|
|
||||||
const wrapper = document.createElement("probo-cookie");
|
const wrapper = document.createElement("probo-cookie");
|
||||||
wrapper.setAttribute("name", cookie.name);
|
wrapper.setAttribute("name", cookie.name);
|
||||||
wrapper.setAttribute("slot", "items");
|
|
||||||
|
|
||||||
const clone = this.template.content.cloneNode(true) as DocumentFragment;
|
const clone = this.template.content.cloneNode(true) as DocumentFragment;
|
||||||
this.fillSlots(clone, {
|
this.fillSlots(clone, {
|
||||||
name: cookie.name,
|
name: cookie.name,
|
||||||
@@ -77,11 +70,4 @@ export class ProboCookieList extends ProboElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProboCookie extends ProboElement {
|
export class ProboCookie extends ProboElement {}
|
||||||
connectedCallback(): void {
|
|
||||||
this.shadow.innerHTML = `
|
|
||||||
<style>:host { display: contents; }</style>
|
|
||||||
<slot></slot>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,14 +29,6 @@ export class ProboPreferencePanel extends ProboElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
this.shadow.innerHTML = `
|
|
||||||
<style>
|
|
||||||
:host { display: block; }
|
|
||||||
:host([hidden]) { display: none; }
|
|
||||||
</style>
|
|
||||||
<slot></slot>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.hidden = true;
|
this.hidden = true;
|
||||||
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
||||||
|
|
||||||
@@ -77,10 +69,6 @@ export class ProboSaveButton extends ProboElement {
|
|||||||
private root: ProboRootElement | null = null;
|
private root: ProboRootElement | null = null;
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
this.shadow.innerHTML = `
|
|
||||||
<style>:host { display: contents; }</style>
|
|
||||||
<slot></slot>
|
|
||||||
`;
|
|
||||||
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
||||||
this.addEventListener("click", this.handleClick);
|
this.addEventListener("click", this.handleClick);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,20 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import { ProboElement } from "./base";
|
|
||||||
import type { ProboRootElement } from "./base";
|
import type { ProboRootElement } from "./base";
|
||||||
import type { ProboCookieBannerRoot } from "./cookie-banner-root";
|
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>`;
|
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;
|
private root: ProboRootElement | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.shadow = this.attachShadow({ mode: "open" });
|
||||||
|
}
|
||||||
|
|
||||||
static get observedAttributes(): string[] {
|
static get observedAttributes(): string[] {
|
||||||
return ["position"];
|
return ["position"];
|
||||||
}
|
}
|
||||||
@@ -69,7 +74,7 @@ export class ProboSettingsButton extends ProboElement {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
this.hidden = true;
|
this.hidden = true;
|
||||||
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
this.root = this.findRoot();
|
||||||
|
|
||||||
if (this.root) {
|
if (this.root) {
|
||||||
this.root.addEventListener("probo-state", this.onStateChange);
|
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 => {
|
private onStateChange = (e: Event): void => {
|
||||||
const { state } = (e as CustomEvent).detail;
|
const { state } = (e as CustomEvent).detail;
|
||||||
this.hidden = state !== "hidden";
|
this.hidden = state !== "hidden";
|
||||||
|
|||||||
@@ -54,6 +54,3 @@ export {
|
|||||||
} from "./errors";
|
} from "./errors";
|
||||||
export { fetchJSON } from "./http";
|
export { fetchJSON } from "./http";
|
||||||
export type { RequestOptions } from "./http";
|
export type { RequestOptions } from "./http";
|
||||||
|
|
||||||
import { registerComponents } from "./components";
|
|
||||||
registerComponents();
|
|
||||||
|
|||||||
Reference in New Issue
Block a user