Add reopen-widget attribute and probo-settings-link element

Allow customers to replace the built-in floating settings button with
their own re-open trigger (e.g. a footer link) by either placing a
<probo-settings-link> element anywhere on the page or manually setting
reopen-widget="custom" on the root. The settings-link auto-hides the
floating button on connect via a reopen-widget attribute change event.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-21 18:10:43 +04:00
parent 6391d276d9
commit 68fffbf2f3
7 changed files with 113 additions and 3 deletions

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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");

View File

@@ -0,0 +1,73 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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");
};
}

View File

@@ -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 = `
<style>${THEMED_STYLES}</style>
<probo-cookie-banner-root banner-id="${this.esc(bannerId)}" base-url="${this.esc(baseUrl)}">
<probo-cookie-banner-root banner-id="${this.esc(bannerId)}" base-url="${this.esc(baseUrl)}"${reopenAttr}>
<probo-banner>
<div class="floating" data-position="${this.esc(position)}">
<div class="card" role="dialog" aria-label="Cookie consent">