diff --git a/content/body/cookiebanner.php b/content/body/cookiebanner.php new file mode 100644 index 00000000..6b985594 --- /dev/null +++ b/content/body/cookiebanner.php @@ -0,0 +1,148 @@ +

+ Cookie banners are ubiquitous and are often the first element on a page demanding your attention. They appear almost + instantaneously and requires a prompt decision to a lengthy explanation as to why cookies are necessary for the + webpage. +

+ +

+ Unfortunately, many cookie banner implementations do not have the explanation interactive as text, nor do they + automatically announce that the user is now interacting with a cookie banner. Instead, screen reader users are often + guided immediately to one of the buttons—accept or reject—without knowing what it is they are accepting or rejecting. +

+ +

+ The instructions on this page walk through how to implement an accessible cookie banner using the + dialog HTML element. +

+ +

Modal Cookie Banner

+ +

+ Using a modal dialog for cookie banners causes the contents of the webpage to be unavailable—often covered by a dark + overlay—until the visitor of the webpage makes a decision on their cookie preferences. The contents of the webpage + cannot be interacted with until the modal dialog is dismissed, this is true for both sighted users and screen readers. +

+ +
+ + + + + + + + + +

+ With the HTML set up, use the built-in method .showModal() of the dialog HTML tag to show + the cookie banner. +

+ +

Non-Modal Cookie Banner

+ +

+ Using a non-modal dialog for cookie banners allows the contents of the webpage to still be interactive. However, focus + often immediately shifts to the action buttons—accept or reject—without first announcing the contents of the dialog. + The implementation below announces the contents while also automatically focuses on the action buttons. +

+ +
+ + + + + + + + diff --git a/content/bottom/cookiebanner.php b/content/bottom/cookiebanner.php new file mode 100644 index 00000000..55e7c714 --- /dev/null +++ b/content/bottom/cookiebanner.php @@ -0,0 +1,6 @@ + diff --git a/content/head/cookiebanner.php b/content/head/cookiebanner.php new file mode 100644 index 00000000..3556aa15 --- /dev/null +++ b/content/head/cookiebanner.php @@ -0,0 +1 @@ + diff --git a/css-list.txt b/css-list.txt index c80c5aa5..0d4c5986 100644 --- a/css-list.txt +++ b/css-list.txt @@ -7,6 +7,7 @@ css/bottom-fixed-nav.css css/button.css css/checkbox.css css/combobox.css +css/cookiebanner.css css/date.css css/definition-term.css css/deque-table-sortable.css diff --git a/css/cookiebanner.css b/css/cookiebanner.css new file mode 100644 index 00000000..60dc26d5 --- /dev/null +++ b/css/cookiebanner.css @@ -0,0 +1,85 @@ +.cookie-banner { + position: fixed; + bottom: 0; + background-color: #333; + color: white; + padding: 20px; + border: none; + border-top: 2px solid #444; + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); +} +.cookie-banner__close-button { + position: absolute; + top: 0; + right: 0; + background: none; + border: none; + display: inline; +} +.cookie-banner__close-button__icon { + border: 0; + width: 38px; + height: 38px; +} +.cookie-banner__title { + flex: 1; + margin-top: 0; + text-align: center; + color: white; + font-family: "OpenSans", "Helvetica", "Arial", sans-serif; + font-weight: bold; +} +.cookie-banner__action-buttons { + display: flex; + flex-direction: row; + justify-content: flex-end; +} +.cookie-banner__accept-button { + background-color: green; + color: white; + border: none; + padding: 10px 20px; + cursor: pointer; + margin-left: 20px; + border-radius: 5px; + font-size: large; +} +.cookie-banner__reject-button { + margin-left: 16px; + background: none; + color: white; + border: none; + padding: 10px 20px; + cursor: pointer; + margin-left: 20px; + border-radius: 5px; + font-size: large; +} +.cookie-banner .actionButton { + color: white; + border: none; + padding: 10px 20px; + cursor: pointer; + margin-left: 20px; + border-radius: 5px; + font-size: large; +} +.non-modal-cookie-banner { + display: flex; + flex-direction: column; + justify-content: space-between; + position: fixed; + bottom: 0; + left: 0; + width: 100%; + max-width: unset; + margin: unset; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 1em; + text-align: center; + z-index: 1000; +} +.non-modal-cookie-banner__example { + display: none; +} diff --git a/images/main-menu/cookiebanner.png b/images/main-menu/cookiebanner.png new file mode 100644 index 00000000..7d32770a Binary files /dev/null and b/images/main-menu/cookiebanner.png differ diff --git a/images/main-menu/cookiebanner.webp b/images/main-menu/cookiebanner.webp new file mode 100644 index 00000000..6f636f54 Binary files /dev/null and b/images/main-menu/cookiebanner.webp differ diff --git a/images/posters/cookiebanner.jpg b/images/posters/cookiebanner.jpg new file mode 100644 index 00000000..e2f809e0 Binary files /dev/null and b/images/posters/cookiebanner.jpg differ diff --git a/js/enable-libs/dialogFocusManager.js b/js/enable-libs/dialogFocusManager.js new file mode 100644 index 00000000..b3878b48 --- /dev/null +++ b/js/enable-libs/dialogFocusManager.js @@ -0,0 +1,77 @@ +import accessibility from '../../enable-node-libs/accessibility-js-routines/dist/accessibility.module.js'; + +export class DialogFocusManager { + previousFocus; + registeredDialogs; + + constructor() { + this.previousFocus = null; + this.registeredDialogs = new WeakMap(); + this.initFocusOutListener(); + } + + initFocusOutListener() { + document.addEventListener('focusout', (ev) => { + this.previousFocus = ev.target; + }, true); + } + + focusOn(dialog) { + if (!window.WeakMap || !window.MutationObserver) { + return; + } + + if (dialog.localName !== 'dialog') { + throw new Error('Failed to upgrade focus on dialog: The element is not a dialog.'); + } + + if (this.registeredDialogs.has(dialog)) { + return; + } + + this.registeredDialogs.set(dialog, null); + this.overrideShowModal(dialog); + this.observeDialogAttributes(dialog); + this.addCloseEventListener(dialog); + } + + overrideShowModal(dialog) { + const realShowModal = dialog.showModal; + dialog.showModal = () => { + let savedFocus = document.activeElement; + if (savedFocus === document || savedFocus === document.body) { + savedFocus = this.previousFocus; + } + this.registeredDialogs.set(dialog, savedFocus); + realShowModal.call(dialog); + }; + } + + observeDialogAttributes(dialog) { + const mo = new MutationObserver(() => { + if (dialog.hasAttribute('open')) { + accessibility.setKeepFocusInside(dialog, true); + } else { + accessibility.setKeepFocusInside(dialog, false); + } + }); + mo.observe(dialog, { attributes: true, attributeFilter: ['open'] }); + } + + addCloseEventListener(dialog) { + dialog.addEventListener('close', () => { + if (dialog.hasAttribute('open')) { + return; + } + const savedFocus = this.registeredDialogs.get(dialog); + if (document.contains(savedFocus)) { + const wasFocus = document.activeElement; + savedFocus.focus(); + if (document.activeElement !== savedFocus) { + wasFocus.focus(); + } + } + this.registeredDialogs.set(dialog, null); + }); + } +} diff --git a/js/global.js b/js/global.js index 81f4247c..aaea2d18 100644 --- a/js/global.js +++ b/js/global.js @@ -93,7 +93,8 @@ function initEnable() { ], showAsSidebarDefault: true, numberFirstLevelHeadings: true, - selectorToSkipHeadingsWithin: '.enable-example', + selectorToSkipHeadingsWithin: + '.enable-example, .enable-example--no-border', collapseNestedHeadingsAfterLevel: 2, }); } diff --git a/js/modules/cookiebanner.js b/js/modules/cookiebanner.js new file mode 100644 index 00000000..abeffc34 --- /dev/null +++ b/js/modules/cookiebanner.js @@ -0,0 +1,143 @@ +'use-strict' + +import { DialogFocusManager } from "../enable-libs/dialogFocusManager.js"; + +const cookieBanner = new function () { + this.init = function () { + setUpShowModalButton(); + setUpShowNonModalButton(); + + const dialog = document.getElementById(`cookie-banner`); + const manager = new DialogFocusManager(); + manager.focusOn(dialog); + } + + function setUpShowModalButton() { + const showModalButton = document.getElementById('show-modal-button'); + showModalButton.addEventListener('click', () => { + const cookieBanner = document.getElementById('cookie-banner'); + cookieBanner.showModal(); + }); + } + + function setUpShowNonModalButton() { + const showNonModalButton = document.getElementById('show-non-modal-button'); + showNonModalButton.addEventListener('click', (event) => { + appendAside(); + setUpNonModalActionButtons(); + }) + } + + function appendAside() { + const closeButton = createCloseButton(); + const documentDiv = createDocumentDiv(); + const actionButtonsDiv = createActionButtons(); + + const aside = createElement('aside', { + id: 'non-modal-cookie-banner', + class: 'non-modal-cookie-banner', + 'aria-labelledby': 'non-modal-cookie-banner-title' + }, [closeButton, documentDiv, actionButtonsDiv]); + + document.body.appendChild(aside); + } + + function createCloseButton() { + const closeIcon = createElement('img', { + class: 'cookie-banner__close-button__icon', + src: 'images/close-window.svg', + alt: 'close cookie notice' + }); + + return createElement('button', { + id: 'non-modal-cookie-banner-close-button', + class: 'cookie-banner__close-button', + autofocus: 'true' + }, [closeIcon]); + } + + function createDocumentDiv() { + const title = createTitle(); + const message = createMessage(); + + return createElement('div', { + role: 'document' + }, [title, message]); + } + + function createTitle() { + return createElement('h2', { + id: 'non-modal-cookie-banner-title', + class: 'cookie-banner__title', + textContent: 'Cookie Notice' + }); + } + + function createMessage() { + return createElement('p', { + id: 'non-modal-cookie-banner-message', + textContent: 'We use strictly necessary cookies to make our Sites work. In addition, if you consent, we will use optional functional, performance and targeting cookies to help us understand how people use our website, to improve your user experience and to provide you with targeted advertisements. You can accept all cookies, or click to review your cookie preferences.' + }); + } + + function createActionButtons() { + const acceptButton = createElement('button', { + id: 'non-modal-cookie-banner-accept-button', + class: 'cookie-banner__accept-button', + textContent: 'Accept' + }); + + const rejectButton = createElement('button', { + id: 'non-modal-cookie-banner-reject-button', + class: 'cookie-banner__reject-button', + textContent: 'Reject' + }); + + return createElement('div', { + class: 'cookie-banner__action-buttons' + }, [acceptButton, rejectButton]); + } + + function createElement(tag, attributes = {}, children = []) { + const element = document.createElement(tag); + Object.keys(attributes).forEach(key => { + if (key === 'textContent') { + element.textContent = attributes[key]; + } else { + element.setAttribute(key, attributes[key]); + } + }); + children.forEach(child => element.appendChild(child)); + return element; + } + + function setUpNonModalActionButtons() { + const close = document.getElementById('non-modal-cookie-banner-close-button'); + close.addEventListener('click', onNonModalActionButtonClicked); + + const accept = document.getElementById('non-modal-cookie-banner-accept-button'); + accept.addEventListener('click', onNonModalActionButtonClicked); + + const reject = document.getElementById('non-modal-cookie-banner-reject-button') + reject.addEventListener('click', onNonModalActionButtonClicked); + } + + function onNonModalActionButtonClicked() { + cleanUpNonModalActionButtons(); + const aside = document.getElementById(`non-modal-cookie-banner`); + aside.remove(); + } + + function cleanUpNonModalActionButtons() { + const close = document.getElementById('non-modal-cookie-banner-close-button'); + close.removeEventListener('click', onNonModalActionButtonClicked); + + const accept = document.getElementById('non-modal-cookie-banner-accept-button'); + accept.removeEventListener('click', onNonModalActionButtonClicked); + + const reject = document.getElementById('non-modal-cookie-banner-reject-button') + reject.removeEventListener('click', onNonModalActionButtonClicked); + } +} + +export default cookieBanner; diff --git a/js/modules/enable-dialog.js b/js/modules/enable-dialog.js index e77d27c5..8dbeeae5 100644 --- a/js/modules/enable-dialog.js +++ b/js/modules/enable-dialog.js @@ -13,90 +13,11 @@ * Released under the MIT License. ******************************************************************************/ -import accessibility from '../../enable-node-libs/accessibility-js-routines/dist/accessibility.module.js'; +import { DialogFocusManager } from "../enable-libs/dialogFocusManager.js"; const enableDialog = new function() { - /** - * Updates the passed dialog to retain focus and restore it when the dialog is closed. Won't - * upgrade a dialog more than once. Supports IE11+ and is a no-op otherwise. - * @param {!HTMLDialogElement} dialog to upgrade - */ - - this.registerFocusRestoreDialog = (function() { - - if (!window.WeakMap || !window.MutationObserver) { - return function() {}; - } - var registered = new WeakMap(); - - // store previous focused node centrally - var previousFocus = null; - document.addEventListener('focusout', function(ev) { - previousFocus = ev.target; - }, true); - - return function registerFocusRestoreDialog(dialog) { - if (dialog.localName !== 'dialog') { - throw new Error('Failed to upgrade focus on dialog: The element is not a dialog.'); - } - if (registered.has(dialog)) { return; } - registered.set(dialog, null); - - // replace showModal method directly, to save focus - var realShowModal = dialog.showModal; - dialog.showModal = function() { - var savedFocus = document.activeElement; - if (savedFocus === document || savedFocus === document.body) { - // some browsers read activeElement as body - savedFocus = previousFocus; - } - registered.set(dialog, savedFocus); - realShowModal.call(this); - }; - - // Watch for 'open' change and clear saved. - // Note this executes with native implementations, so - // native implementations, like Chrome's will have a focus - // loop and not go into the browser chrome. - var mo = new MutationObserver(function() { - if (dialog.hasAttribute('open')) { - accessibility.setKeepFocusInside(dialog, true); - } else { - accessibility.setKeepFocusInside(dialog, false); - } - }); - mo.observe(dialog, { attributes: true, attributeFilter: ['open'] }); - - // For polyfill on close, try to focus saved, if possible - dialog.addEventListener('close', function() { - if (dialog.hasAttribute('open')) { - return; // in native, this fires the frame later - } - var savedFocus = registered.get(dialog); - if (document.contains(savedFocus)) { - var wasFocus = document.activeElement; - savedFocus.focus(); - if (document.activeElement !== savedFocus) { - wasFocus.focus(); // restore focus, we couldn't focus saved - } - } - savedFocus = null; - registered.set(dialog, null); - }); - - // FIXME: If a modal dialog is readded to the page (either remove/add or .appendChild), it will - // be a non-modal. It will still have its 'close' handler called and try to focus on the saved - // element. - // - // These could basically be solved if 'close' yielded whether it was a modal or non-modal - // being closed. But it doesn't. It could also be solved by a permanent MutationObserver, as is - // done inside the polyfill. - } - }()); - - - this.init = () => { + const focusManager = new DialogFocusManager(); const supportsDialog = !!document.createElement('dialog').show; // If we are using the polyfill, then load it as well // as the polyfill accessibility fixes. @@ -104,15 +25,15 @@ const enableDialog = new function() { import('../../enable-node-libs/dialog-polyfill/index.js') .then((dialogPolyfill) => { dialogPolyfill.default.registerDialog(favDialog); - this.registerFocusRestoreDialog(favDialog); + focusManager.focusOn(favDialog); }); } else { // We load the accessibility fixes since they work even // when the polyfill is loaded. Chrome needs this to // have a focus loop like polyfilled versions. - this.registerFocusRestoreDialog(favDialog); + focusManager.focusOn(favDialog); } } } -export default enableDialog; \ No newline at end of file +export default enableDialog; diff --git a/js/modules/es4/cookiebanner.js b/js/modules/es4/cookiebanner.js new file mode 100644 index 00000000..8b054321 --- /dev/null +++ b/js/modules/es4/cookiebanner.js @@ -0,0 +1,139 @@ +'use-strict' + +const cookieBanner = new (function() { + this.init = function () { + setUpShowModalButton(); + setUpShowNonModalButton(); + + const dialog = document.getElementById(`cookie-banner`); + const manager = new DialogFocusManager(); + manager.focusOn(dialog); + } + + function setUpShowModalButton() { + const showModalButton = document.getElementById('show-modal-button'); + showModalButton.addEventListener('click', () => { + const cookieBanner = document.getElementById('cookie-banner'); + cookieBanner.showModal(); + }); + } + + function setUpShowNonModalButton() { + const showNonModalButton = document.getElementById('show-non-modal-button'); + showNonModalButton.addEventListener('click', (event) => { + appendAside(); + setUpNonModalActionButtons(); + }) + } + + function appendAside() { + const closeButton = createCloseButton(); + const documentDiv = createDocumentDiv(); + const actionButtonsDiv = createActionButtons(); + + const aside = createElement('aside', { + id: 'non-modal-cookie-banner', + class: 'non-modal-cookie-banner', + 'aria-labelledby': 'non-modal-cookie-banner-title' + }, [closeButton, documentDiv, actionButtonsDiv]); + + document.body.appendChild(aside); + } + + function createCloseButton() { + const closeIcon = createElement('img', { + class: 'cookie-banner__close-button__icon', + src: 'images/close-window.svg', + alt: 'close cookie notice' + }); + + return createElement('button', { + id: 'non-modal-cookie-banner-close-button', + class: 'cookie-banner__close-button', + autofocus: 'true' + }, [closeIcon]); + } + + function createDocumentDiv() { + const title = createTitle(); + const message = createMessage(); + + return createElement('div', { + role: 'document' + }, [title, message]); + } + + function createTitle() { + return createElement('h2', { + id: 'non-modal-cookie-banner-title', + class: 'cookie-banner__title', + textContent: 'Cookie Notice' + }); + } + + function createMessage() { + return createElement('p', { + id: 'non-modal-cookie-banner-message', + textContent: 'We use strictly necessary cookies to make our Sites work. In addition, if you consent, we will use optional functional, performance and targeting cookies to help us understand how people use our website, to improve your user experience and to provide you with targeted advertisements. You can accept all cookies, or click to review your cookie preferences.' + }); + } + + function createActionButtons() { + const acceptButton = createElement('button', { + id: 'non-modal-cookie-banner-accept-button', + class: 'cookie-banner__accept-button', + textContent: 'Accept' + }); + + const rejectButton = createElement('button', { + id: 'non-modal-cookie-banner-reject-button', + class: 'cookie-banner__reject-button', + textContent: 'Reject' + }); + + return createElement('div', { + class: 'cookie-banner__action-buttons' + }, [acceptButton, rejectButton]); + } + + function createElement(tag, attributes = {}, children = []) { + const element = document.createElement(tag); + Object.keys(attributes).forEach(key => { + if (key === 'textContent') { + element.textContent = attributes[key]; + } else { + element.setAttribute(key, attributes[key]); + } + }); + children.forEach(child => element.appendChild(child)); + return element; + } + + function setUpNonModalActionButtons() { + const close = document.getElementById('non-modal-cookie-banner-close-button'); + close.addEventListener('click', onNonModalActionButtonClicked); + + const accept = document.getElementById('non-modal-cookie-banner-accept-button'); + accept.addEventListener('click', onNonModalActionButtonClicked); + + const reject = document.getElementById('non-modal-cookie-banner-reject-button') + reject.addEventListener('click', onNonModalActionButtonClicked); + } + + function onNonModalActionButtonClicked() { + cleanUpNonModalActionButtons(); + const aside = document.getElementById(`non-modal-cookie-banner`); + aside.remove(); + } + + function cleanUpNonModalActionButtons() { + const close = document.getElementById('non-modal-cookie-banner-close-button'); + close.removeEventListener('click', onNonModalActionButtonClicked); + + const accept = document.getElementById('non-modal-cookie-banner-accept-button'); + accept.removeEventListener('click', onNonModalActionButtonClicked); + + const reject = document.getElementById('non-modal-cookie-banner-reject-button') + reject.removeEventListener('click', onNonModalActionButtonClicked); + } +}) diff --git a/js/modules/es4/enable-dialog.js b/js/modules/es4/enable-dialog.js index fd4f1067..bf91905f 100644 --- a/js/modules/es4/enable-dialog.js +++ b/js/modules/es4/enable-dialog.js @@ -14,87 +14,8 @@ ******************************************************************************/ const enableDialog = new (function() { - /** - * Updates the passed dialog to retain focus and restore it when the dialog is closed. Won't - * upgrade a dialog more than once. Supports IE11+ and is a no-op otherwise. - * @param {!HTMLDialogElement} dialog to upgrade - */ - - this.registerFocusRestoreDialog = (function() { - - if (!window.WeakMap || !window.MutationObserver) { - return function() {}; - } - var registered = new WeakMap(); - - // store previous focused node centrally - var previousFocus = null; - document.addEventListener('focusout', function(ev) { - previousFocus = ev.target; - }, true); - - return function registerFocusRestoreDialog(dialog) { - if (dialog.localName !== 'dialog') { - throw new Error('Failed to upgrade focus on dialog: The element is not a dialog.'); - } - if (registered.has(dialog)) { return; } - registered.set(dialog, null); - - // replace showModal method directly, to save focus - var realShowModal = dialog.showModal; - dialog.showModal = function() { - var savedFocus = document.activeElement; - if (savedFocus === document || savedFocus === document.body) { - // some browsers read activeElement as body - savedFocus = previousFocus; - } - registered.set(dialog, savedFocus); - realShowModal.call(this); - }; - - // Watch for 'open' change and clear saved. - // Note this executes with native implementations, so - // native implementations, like Chrome's will have a focus - // loop and not go into the browser chrome. - var mo = new MutationObserver(function() { - if (dialog.hasAttribute('open')) { - accessibility.setKeepFocusInside(dialog, true); - } else { - accessibility.setKeepFocusInside(dialog, false); - } - }); - mo.observe(dialog, { attributes: true, attributeFilter: ['open'] }); - - // For polyfill on close, try to focus saved, if possible - dialog.addEventListener('close', function() { - if (dialog.hasAttribute('open')) { - return; // in native, this fires the frame later - } - var savedFocus = registered.get(dialog); - if (document.contains(savedFocus)) { - var wasFocus = document.activeElement; - savedFocus.focus(); - if (document.activeElement !== savedFocus) { - wasFocus.focus(); // restore focus, we couldn't focus saved - } - } - savedFocus = null; - registered.set(dialog, null); - }); - - // FIXME: If a modal dialog is readded to the page (either remove/add or .appendChild), it will - // be a non-modal. It will still have its 'close' handler called and try to focus on the saved - // element. - // - // These could basically be solved if 'close' yielded whether it was a modal or non-modal - // being closed. But it doesn't. It could also be solved by a permanent MutationObserver, as is - // done inside the polyfill. - } - }()); - - - this.init = () => { + const focusManager = new DialogFocusManager(); const supportsDialog = !!document.createElement('dialog').show; // If we are using the polyfill, then load it as well // as the polyfill accessibility fixes. @@ -102,13 +23,13 @@ const enableDialog = new (function() { import('../../enable-node-libs/dialog-polyfill/index.js') .then((dialogPolyfill) => { dialogPolyfill.default.registerDialog(favDialog); - this.registerFocusRestoreDialog(favDialog); + focusManager.focusOn(favDialog); }); } else { // We load the accessibility fixes since they work even // when the polyfill is loaded. Chrome needs this to // have a focus loop like polyfilled versions. - this.registerFocusRestoreDialog(favDialog); + focusManager.focusOn(favDialog); } } -}); \ No newline at end of file +}); diff --git a/js/test/cookiebanner.test.js b/js/test/cookiebanner.test.js new file mode 100644 index 00000000..71fced12 --- /dev/null +++ b/js/test/cookiebanner.test.js @@ -0,0 +1,53 @@ +import config from './test-config'; + +let cookieBanner; + +beforeAll(async () => { + await page.goto(`${config.BASE_URL}/cookiebanner.php`); + cookieBanner = await page.$('.cookie-banner'); +}); + +describe('Tests for cookie banner', () => { + it('Contains div with a document role', async () => { + let result = await cookieBanner.$('div[role="document"]'); + expect(result).not.toBeNull(); + }); + + it('Contains close button', async () => { + let result = await getCloseButton(); + console.log(result); + expect(result).not.toBeNull(); + }); + + it('Has autofocus set to the close button', async () => { + let closeButton = await getCloseButton(); + let result = await page.evaluate((closeButton) => closeButton.hasAttribute('autofocus'), closeButton); + expect(result).toBe(true); + }); + + async function getCloseButton() { + return await cookieBanner.$('button[id$="close-button"]'); + } + + it('Contains form with method of dialog', async () => { + let result = await getForm(); + expect(result).not.toBeNull(); + }); + + it('Has aria-labelledby set for form', async () => { + let form = await getForm(); + let result = await page.evaluate((form) => form.hasAttribute('aria-labelledby'), form); + expect(result).toBe(true); + }); + + it('Contains element for aria-labelledby', async () => { + let form = await getForm(); + let ariaLabelledBy = await page.evaluate((el) => el.getAttribute('aria-labelledby'), form); + let result = await cookieBanner.$(`[id$="${ariaLabelledBy}"]`); + expect(result).not.toBeNull(); + }); + + async function getForm() { + return await cookieBanner.$('form[method="dialog"]'); + } +}); diff --git a/less/cookiebanner.less b/less/cookiebanner.less new file mode 100644 index 00000000..76a53e01 --- /dev/null +++ b/less/cookiebanner.less @@ -0,0 +1,83 @@ +.cookie-banner { + position: fixed; + bottom: 0; + background-color: #333; + color: white; + padding: 20px; + border: none; + border-top: 2px solid #444; + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); + + &__close-button { + position: absolute; + top: 0; + right: 0; + + background: none; + border: none; + display: inline; + + &__icon { + border: 0; + width: 38px; + height: 38px; + } + } + + &__title { + flex: 1; + margin-top: 0; + text-align: center; + color: white; + font-family: "OpenSans", "Helvetica", "Arial", sans-serif; + font-weight: bold; + } + + &__action-buttons { + display: flex; + flex-direction: row; + justify-content: flex-end; + } + + &__accept-button { + background-color: green; + .actionButton(); + } + + &__reject-button { + margin-left: 16px; + background: none; + .actionButton(); + } + + .actionButton { + color: white; + border: none; + padding: 10px 20px; + cursor: pointer; + margin-left: 20px; + border-radius: 5px; + font-size: large; + } +} + +.non-modal-cookie-banner { + display: flex; + flex-direction: column; + justify-content: space-between; + position: fixed; + bottom: 0; + left: 0; + width: 100%; + max-width: unset; + margin: unset; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 1em; + text-align: center; + z-index: 1000; // Ensure the non-modal dialog is above other content + + &__example { + display: none; + } +} diff --git a/sitemap.txt b/sitemap.txt index 7f05ff61..95426c26 100644 --- a/sitemap.txt +++ b/sitemap.txt @@ -6,6 +6,7 @@ https://www.useragentman.com/enable/button.php https://www.useragentman.com/enable/carousel.php https://www.useragentman.com/enable/checkbox.php https://www.useragentman.com/enable/combobox.php +https://www.useragentman.com/enable/cookiebanner.php https://www.useragentman.com/enable/description-list.php https://www.useragentman.com/enable/dialog.php https://www.useragentman.com/enable/dropdown.php diff --git a/templates/data/meta-info.json b/templates/data/meta-info.json index 76c0c8aa..762f4dd5 100644 --- a/templates/data/meta-info.json +++ b/templates/data/meta-info.json @@ -52,6 +52,10 @@ "desc": "One exception to the First Rule of ARIA is HTML5 autocomplete using the datalist tag, which is not as accessible as the ARIA datalist role", "isNPM": "true" }, + "cookiebanner.php": { + "title": "Accessible Cookie Banners", + "desc": "Blocking and non-blocking accessible cookie banners." + }, "date.php": { "title": "Accessible Date Widget", "desc": "Depending on the browser, the HTML5 is not keyboard/screen reader friendly. What is a web developer to do? ", diff --git a/templates/includes/documentation-header.php b/templates/includes/documentation-header.php index 4bc9bae0..db7cb5fd 100755 --- a/templates/includes/documentation-header.php +++ b/templates/includes/documentation-header.php @@ -416,6 +416,14 @@ class="enable-flyout enable-flyout__level enable-flyout__dropdown"> "alt": "" } }, + { + "id": "flyout__link", + "props": { + "label": "Cookie Banner", + "url-slug": "cookiebanner", + "alt": "" + } + }, { "id": "flyout__link", "props": { @@ -696,4 +704,4 @@ class="enable-flyout enable-flyout__level enable-flyout__dropdown"> - \ No newline at end of file +