Files
att-consent/public/js/banner.js
Steve Hanlon 2294c8eda7 Security hardening and accessibility improvements (v1.1.0)
Address findings from security audit: wrap inline JSON.parse in
try/catch to prevent consent flow failure on corrupted cookies,
allowlist admin tab parameter, clamp consent_expiry server-side,
escape all server values in admin JS table builder, fix focus trap
listener cleanup, add aria-labelledby and banner focus for screen
readers, skip frontend loading during AJAX.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:32:16 +00:00

213 lines
4.9 KiB
JavaScript

/**
* ATT Consent Banner UI
*
* Handles banner display, preferences modal, floating widget, anchor link
* triggers, focus trapping, and user interactions.
* Depends on AttConsent (consent-manager.js) for consent state management.
*/
(function() {
'use strict';
var banner = document.getElementById('att-cc-banner');
var modal = document.getElementById('att-cc-modal');
var widget = document.getElementById('att-cc-widget');
if (!banner || !modal) {
return;
}
// --- Modal buttons ---
var saveBtn = modal.querySelector('[data-att-cc="save-preferences"]');
var closeBtn = modal.querySelector('[data-att-cc="close-modal"]');
var overlay = modal.querySelector('.att-cc-modal__overlay');
if (saveBtn) {
saveBtn.addEventListener('click', function() {
var categories = {
functional: document.getElementById('att-cc-functional').checked,
analytics: document.getElementById('att-cc-analytics').checked,
marketing: document.getElementById('att-cc-marketing').checked
};
AttConsent.update(categories);
closeModal();
hideBanner();
showWidget();
});
}
if (closeBtn) {
closeBtn.addEventListener('click', function() {
closeModal();
});
}
if (overlay) {
overlay.addEventListener('click', function() {
closeModal();
});
}
// --- Floating widget click handler ---
if (widget) {
widget.addEventListener('click', function() {
openModal();
});
}
// --- Anchor link listener ---
document.addEventListener('click', function(e) {
var link = e.target.closest('a[href="#att-cc-preferences"]');
if (link) {
e.preventDefault();
openModal();
}
});
// --- Initial state ---
if (AttConsent.hasConsent()) {
hideBanner();
showWidget();
} else {
// Move focus to banner so screen readers announce it.
banner.focus();
// --- Banner buttons (only needed for first-time visitors) ---
var acceptBtn = banner.querySelector('[data-att-cc="accept-all"]');
var rejectBtn = banner.querySelector('[data-att-cc="reject-all"]');
var prefsBtn = banner.querySelector('[data-att-cc="preferences"]');
if (acceptBtn) {
acceptBtn.addEventListener('click', function() {
AttConsent.update({ functional: true, analytics: true, marketing: true });
hideBanner();
showWidget();
});
}
if (rejectBtn) {
rejectBtn.addEventListener('click', function() {
AttConsent.update({ functional: false, analytics: false, marketing: false });
hideBanner();
showWidget();
});
}
if (prefsBtn) {
prefsBtn.addEventListener('click', function() {
openModal();
});
}
}
// --- Helper functions ---
// Tracks the current focus trap handler for cleanup.
var activeTrapHandler = null;
function hideBanner() {
banner.setAttribute('aria-hidden', 'true');
banner.classList.add('att-cc-hidden');
}
function showWidget() {
if (widget) {
widget.style.display = '';
}
}
function openModal() {
// Pre-fill checkboxes with existing consent if available.
var existing = AttConsent.getConsent();
if (existing) {
setCheckbox('att-cc-functional', existing.functional);
setCheckbox('att-cc-analytics', existing.analytics);
setCheckbox('att-cc-marketing', existing.marketing);
}
modal.setAttribute('aria-hidden', 'false');
modal.classList.add('att-cc-modal--open');
document.body.classList.add('att-cc-modal-active');
trapFocus(modal);
}
function closeModal() {
modal.setAttribute('aria-hidden', 'true');
modal.classList.remove('att-cc-modal--open');
document.body.classList.remove('att-cc-modal-active');
// Clean up focus trap listener.
if (activeTrapHandler) {
modal.removeEventListener('keydown', activeTrapHandler);
activeTrapHandler = null;
}
// Return focus to the widget if visible, otherwise the banner prefs button.
if (widget && widget.style.display !== 'none') {
widget.focus();
} else {
var prefsBtn = banner.querySelector('[data-att-cc="preferences"]');
if (prefsBtn) {
prefsBtn.focus();
}
}
}
function setCheckbox(id, value) {
var el = document.getElementById(id);
if (el) {
el.checked = !!value;
}
}
/**
* Trap keyboard focus within an element.
*
* @param {HTMLElement} element The container to trap focus in.
*/
function trapFocus(element) {
var focusable = element.querySelectorAll(
'button, [href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable.length === 0) {
return;
}
var first = focusable[0];
var last = focusable[focusable.length - 1];
first.focus();
function handleKeydown(e) {
if (e.key === 'Escape') {
closeModal();
return;
}
if (e.key !== 'Tab') {
return;
}
if (e.shiftKey) {
if (document.activeElement === first) {
last.focus();
e.preventDefault();
}
} else {
if (document.activeElement === last) {
first.focus();
e.preventDefault();
}
}
}
activeTrapHandler = handleKeydown;
element.addEventListener('keydown', handleKeydown);
}
})();