diff --git a/packages/cookie-banner/src/components/base.ts b/packages/cookie-banner/src/components/base.ts index 130524345..ba9e3dd1c 100644 --- a/packages/cookie-banner/src/components/base.ts +++ b/packages/cookie-banner/src/components/base.ts @@ -55,6 +55,7 @@ export interface ProboRootElement extends ProboElement { readonly client: CookieBannerClient; readonly bannerConfig: BannerConfig; readonly state: ProboState; + readonly reopenWidget: string; readonly consentDraft: ConsentDraft; setState(state: ProboState): void; updateDraft(category: string, value: boolean): void; diff --git a/packages/cookie-banner/src/components/cookie-banner-root.ts b/packages/cookie-banner/src/components/cookie-banner-root.ts index c3fbbf68c..2df2fffdf 100644 --- a/packages/cookie-banner/src/components/cookie-banner-root.ts +++ b/packages/cookie-banner/src/components/cookie-banner-root.ts @@ -24,7 +24,7 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem private _draft: ConsentDraft = {}; static get observedAttributes(): string[] { - return ["banner-id", "base-url"]; + return ["banner-id", "base-url", "reopen-widget"]; } get client(): CookieBannerClient { @@ -41,6 +41,10 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem return this._config; } + get reopenWidget(): string { + return this.getAttribute("reopen-widget") ?? "floating"; + } + get state(): ProboState { return this._state; } @@ -49,6 +53,18 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem return this._draft; } + attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void { + if (name === "reopen-widget" && oldValue !== newValue) { + this.dispatchEvent( + new CustomEvent("probo-reopen-widget", { + bubbles: true, + composed: true, + detail: { value: newValue ?? "floating" }, + }), + ); + } + } + connectedCallback(): void { this.initClient(); } diff --git a/packages/cookie-banner/src/components/index.ts b/packages/cookie-banner/src/components/index.ts index f1da3c737..88026ba7e 100644 --- a/packages/cookie-banner/src/components/index.ts +++ b/packages/cookie-banner/src/components/index.ts @@ -27,4 +27,5 @@ export { ProboCookieBannerRoot } from "./cookie-banner-root"; export { ProboCookie, ProboCookieList } from "./cookie-list"; export { ProboPreferencePanel, ProboSaveButton } from "./preference-panel"; export { ProboSettingsButton } from "./settings-button"; +export { ProboSettingsLink } from "./settings-link"; export { registerComponents } from "./register"; diff --git a/packages/cookie-banner/src/components/register.ts b/packages/cookie-banner/src/components/register.ts index fad819324..e4c0abe03 100644 --- a/packages/cookie-banner/src/components/register.ts +++ b/packages/cookie-banner/src/components/register.ts @@ -25,6 +25,7 @@ import { ProboCookieBannerRoot } from "./cookie-banner-root"; import { ProboCookie, ProboCookieList } from "./cookie-list"; import { ProboPreferencePanel, ProboSaveButton } from "./preference-panel"; import { ProboSettingsButton } from "./settings-button"; +import { ProboSettingsLink } from "./settings-link"; const elements: [string, CustomElementConstructor][] = [ ["probo-cookie-banner-root", ProboCookieBannerRoot], @@ -40,6 +41,7 @@ const elements: [string, CustomElementConstructor][] = [ ["probo-cookie", ProboCookie], ["probo-save-button", ProboSaveButton], ["probo-settings-button", ProboSettingsButton], + ["probo-settings-link", ProboSettingsLink], ]; export function registerComponents(): void { diff --git a/packages/cookie-banner/src/components/settings-button.ts b/packages/cookie-banner/src/components/settings-button.ts index b0e5931e7..7b06c6fd5 100644 --- a/packages/cookie-banner/src/components/settings-button.ts +++ b/packages/cookie-banner/src/components/settings-button.ts @@ -77,7 +77,12 @@ export class ProboSettingsButton extends HTMLElement { this.root = this.findRoot(); if (this.root) { + if (this.root.reopenWidget === "custom") { + return; + } + this.root.addEventListener("probo-state", this.onStateChange); + this.root.addEventListener("probo-reopen-widget", this.onReopenWidgetChange); if (this.root.state === "hidden") { this.hidden = false; } @@ -90,6 +95,7 @@ export class ProboSettingsButton extends HTMLElement { disconnectedCallback(): void { if (this.root) { this.root.removeEventListener("probo-state", this.onStateChange); + this.root.removeEventListener("probo-reopen-widget", this.onReopenWidgetChange); } } @@ -109,6 +115,15 @@ export class ProboSettingsButton extends HTMLElement { this.hidden = state !== "hidden"; }; + private onReopenWidgetChange = (e: Event): void => { + const { value } = (e as CustomEvent).detail; + if (value === "custom") { + this.hidden = true; + this.root?.removeEventListener("probo-state", this.onStateChange); + this.root?.removeEventListener("probo-reopen-widget", this.onReopenWidgetChange); + } + }; + private handleClick = (): void => { if (!this.root) return; this.root.setState("panel"); diff --git a/packages/cookie-banner/src/components/settings-link.ts b/packages/cookie-banner/src/components/settings-link.ts new file mode 100644 index 000000000..de0e67edc --- /dev/null +++ b/packages/cookie-banner/src/components/settings-link.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { ProboCookieBannerRoot } from "./cookie-banner-root"; + +export class ProboSettingsLink extends HTMLElement { + private root: ProboCookieBannerRoot | null = null; + + connectedCallback(): void { + this.root = this.findRoot(); + + if (this.root) { + this.attach(this.root); + } else { + document.addEventListener("probo-ready", this.onProboReady, { once: true }); + } + + this.addEventListener("click", this.handleClick); + } + + disconnectedCallback(): void { + this.removeEventListener("click", this.handleClick); + document.removeEventListener("probo-ready", this.onProboReady); + this.root = null; + } + + private attach(root: ProboCookieBannerRoot): void { + this.root = root; + root.setAttribute("reopen-widget", "custom"); + } + + private findRoot(): ProboCookieBannerRoot | null { + const direct = document.querySelector("probo-cookie-banner-root") as ProboCookieBannerRoot | null; + if (direct) return direct; + + const themed = document.querySelector("probo-cookie-banner"); + if (themed?.shadowRoot) { + return themed.shadowRoot.querySelector("probo-cookie-banner-root") as ProboCookieBannerRoot | null; + } + + return null; + } + + private onProboReady = (e: Event): void => { + const root = (e as CustomEvent).target as ProboCookieBannerRoot | null; + if (root?.tagName.toLowerCase() === "probo-cookie-banner-root") { + this.attach(root); + return; + } + + const found = this.findRoot(); + if (found) { + this.attach(found); + } + }; + + private handleClick = (e: Event): void => { + if (!this.root) return; + e.preventDefault(); + this.root.setState("panel"); + }; +} diff --git a/packages/cookie-banner/src/themed-banner/themed-banner.ts b/packages/cookie-banner/src/themed-banner/themed-banner.ts index 1ccc99096..0e9d675ff 100644 --- a/packages/cookie-banner/src/themed-banner/themed-banner.ts +++ b/packages/cookie-banner/src/themed-banner/themed-banner.ts @@ -29,7 +29,7 @@ export class ProboThemedBanner extends HTMLElement { } static get observedAttributes(): string[] { - return ["banner-id", "base-url"]; + return ["banner-id", "base-url", "reopen-widget"]; } connectedCallback(): void { @@ -44,10 +44,12 @@ export class ProboThemedBanner extends HTMLElement { } const position = this.getAttribute("position") ?? "bottom-left"; + const reopenWidget = this.getAttribute("reopen-widget"); + const reopenAttr = reopenWidget ? ` reopen-widget="${this.esc(reopenWidget)}"` : ""; this.shadow.innerHTML = ` - +