Add ARIA attributes and focus management to cookie banner
Improve accessibility across the cookie banner components: add aria-modal/aria-labelledby/aria-describedby on dialog cards, aria-label on icon-only buttons and category toggles, aria-expanded on cookie detail disclosure, aria-hidden on decorative SVGs, and auto-focus into dialogs when they become visible. Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -27,6 +27,9 @@ export class ProboBanner extends ProboElement {
|
|||||||
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 !== "banner";
|
this.hidden = state !== "banner";
|
||||||
|
if (state === "banner") {
|
||||||
|
this.focusFirst();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
|
|||||||
@@ -20,7 +20,16 @@ export interface ConsentDraft {
|
|||||||
[category: string]: boolean;
|
[category: string]: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
||||||
|
|
||||||
export class ProboElement extends HTMLElement {
|
export class ProboElement extends HTMLElement {
|
||||||
|
protected focusFirst(): void {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const el = this.querySelector<HTMLElement>(FOCUSABLE);
|
||||||
|
el?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected findAncestor<T extends HTMLElement>(tagName: string): T | null {
|
protected findAncestor<T extends HTMLElement>(tagName: string): T | null {
|
||||||
let el: HTMLElement | null = this.parentElement;
|
let el: HTMLElement | null = this.parentElement;
|
||||||
while (el) {
|
while (el) {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export class ProboCategoryToggle extends ProboElement {
|
|||||||
if (!this.category || !this.root) return;
|
if (!this.category || !this.root) return;
|
||||||
|
|
||||||
const name = this.category.categoryName;
|
const name = this.category.categoryName;
|
||||||
|
this.checkbox.setAttribute("aria-label", name);
|
||||||
const isRequired = this.category.kind === "NECESSARY";
|
const isRequired = this.category.kind === "NECESSARY";
|
||||||
|
|
||||||
if (isRequired) {
|
if (isRequired) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export class ProboPreferencePanel extends ProboElement {
|
|||||||
this.hidden = state !== "panel";
|
this.hidden = state !== "panel";
|
||||||
if (state === "panel") {
|
if (state === "panel") {
|
||||||
(this.root as ProboCookieBannerRoot).resetDraft();
|
(this.root as ProboCookieBannerRoot).resetDraft();
|
||||||
|
this.focusFirst();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ export class ProboSettingsButton extends HTMLElement {
|
|||||||
.icon { display: flex; flex-shrink: 0; }
|
.icon { display: flex; flex-shrink: 0; }
|
||||||
::slotted(*) { display: contents; }
|
::slotted(*) { display: contents; }
|
||||||
</style>
|
</style>
|
||||||
<button part="button">
|
<button part="button" aria-label="Cookie settings">
|
||||||
<span class="icon" part="icon">${COOKIE_ICON}</span>
|
<span class="icon" part="icon" aria-hidden="true">${COOKIE_ICON}</span>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import type { ProboCookieBannerRoot } from "../components/cookie-banner-root";
|
|||||||
import type { BannerConfig } from "../client";
|
import type { BannerConfig } from "../client";
|
||||||
import { THEMED_STYLES } from "./styles";
|
import { THEMED_STYLES } from "./styles";
|
||||||
|
|
||||||
const CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
const CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
||||||
const CHEVRON_DOWN = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`;
|
const CHEVRON_DOWN = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>`;
|
||||||
|
|
||||||
export class ProboThemedBanner extends HTMLElement {
|
export class ProboThemedBanner extends HTMLElement {
|
||||||
private shadow: ShadowRoot;
|
private shadow: ShadowRoot;
|
||||||
@@ -52,9 +52,9 @@ export class ProboThemedBanner extends HTMLElement {
|
|||||||
<probo-cookie-banner-root banner-id="${this.esc(bannerId)}" base-url="${this.esc(baseUrl)}"${reopenAttr}>
|
<probo-cookie-banner-root banner-id="${this.esc(bannerId)}" base-url="${this.esc(baseUrl)}"${reopenAttr}>
|
||||||
<probo-banner>
|
<probo-banner>
|
||||||
<div class="floating" data-position="${this.esc(position)}">
|
<div class="floating" data-position="${this.esc(position)}">
|
||||||
<div class="card" role="dialog" aria-label="Cookie consent">
|
<div class="card" role="dialog" aria-modal="true" aria-labelledby="probo-banner-title" aria-describedby="probo-banner-desc">
|
||||||
<p class="title">Cookie Preferences</p>
|
<p class="title" id="probo-banner-title">Cookie Preferences</p>
|
||||||
<p class="description" data-description>
|
<p class="description" id="probo-banner-desc" data-description>
|
||||||
We use cookies to improve your experience and analyze site traffic.
|
We use cookies to improve your experience and analyze site traffic.
|
||||||
</p>
|
</p>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
@@ -68,16 +68,16 @@ export class ProboThemedBanner extends HTMLElement {
|
|||||||
|
|
||||||
<probo-preference-panel>
|
<probo-preference-panel>
|
||||||
<div class="floating" data-position="${this.esc(position)}">
|
<div class="floating" data-position="${this.esc(position)}">
|
||||||
<div class="card" role="dialog" aria-label="Cookie preferences">
|
<div class="card" role="dialog" aria-modal="true" aria-labelledby="probo-panel-title">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<p class="title" style="margin:0">Preferences</p>
|
<p class="title" id="probo-panel-title" style="margin:0">Preferences</p>
|
||||||
<button class="panel-close" data-action="back" aria-label="Close">
|
<button class="panel-close" data-action="back" aria-label="Close">
|
||||||
${CLOSE_ICON}
|
${CLOSE_ICON}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<probo-category-list>
|
<probo-category-list>
|
||||||
<template>
|
<template>
|
||||||
<button class="cookie-toggle" data-action="toggle-cookies">
|
<button class="cookie-toggle" data-action="toggle-cookies" aria-expanded="false" aria-label="Show cookie details">
|
||||||
${CHEVRON_DOWN}
|
${CHEVRON_DOWN}
|
||||||
</button>
|
</button>
|
||||||
<div class="category-header">
|
<div class="category-header">
|
||||||
@@ -141,6 +141,8 @@ export class ProboThemedBanner extends HTMLElement {
|
|||||||
cookieList.setAttribute("hidden", "");
|
cookieList.setAttribute("hidden", "");
|
||||||
btn.classList.remove("open");
|
btn.classList.remove("open");
|
||||||
}
|
}
|
||||||
|
btn.setAttribute("aria-expanded", String(open));
|
||||||
|
btn.setAttribute("aria-label", open ? "Hide cookie details" : "Show cookie details");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +152,7 @@ export class ProboThemedBanner extends HTMLElement {
|
|||||||
|
|
||||||
let html = "We use cookies to improve your experience and analyze site traffic.";
|
let html = "We use cookies to improve your experience and analyze site traffic.";
|
||||||
if (config.privacy_policy_url) {
|
if (config.privacy_policy_url) {
|
||||||
html += ` <a href="${this.esc(config.privacy_policy_url)}" target="_blank" rel="noopener">Privacy Policy</a>`;
|
html += ` <a href="${this.esc(config.privacy_policy_url)}" target="_blank" rel="noopener noreferrer">Privacy Policy</a>`;
|
||||||
}
|
}
|
||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user