Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENABLE-140 Component Cookie Banner #174

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4e7f484
Cookie banner page creation and JS initialization.
ocjadan Jul 12, 2024
c6cafe8
Show simple modal and non-modal dialogs.
ocjadan Jul 15, 2024
3aa5997
Cookie banner message announcing for Safari and Chrome. Not Firefox. …
ocjadan Jul 17, 2024
45c7c2f
cookiebanner.php webpage contents explaining modal and non-modal dial…
ocjadan Jul 17, 2024
fee7b17
Cookie banner announcing appropriately when shown as a modal or non-m…
ocjadan Jul 18, 2024
8759730
Provide images for the cookie banner selection under Controls in Main…
ocjadan Jul 18, 2024
3fdcca0
Prevent headers inside <dialog> to get generated Table of Contents ad…
ocjadan Jul 18, 2024
485b882
Code walkthrough.
ocjadan Jul 18, 2024
73a7ff7
Unit tests for cookie banner.
ocjadan Jul 19, 2024
c308ec3
Add steps for enabling an accessible dialog.
ocjadan Jul 19, 2024
9ac6d9c
Add poster jpeg for cookie banner.
ocjadan Jul 19, 2024
eb06df1
Restore original formatting of global.js file.
ocjadan Jul 19, 2024
f91b786
Re-add prevent TOC content being generated for cookie banner dialog.
ocjadan Jul 19, 2024
4b74c83
Small renaming edit.
ocjadan Jul 22, 2024
47e15d5
Styling for cookie banner.
ocjadan Jul 25, 2024
5c82c30
Cookie banner built with <aside> tag.
ocjadan Jul 31, 2024
e72b019
Ensure dialog headers are not generated for table of contents.
ocjadan Jul 31, 2024
cb4e8c2
Remove tabIndex.
ocjadan Jul 31, 2024
88cf056
cookiebanner.php does not show the exampe banner.
ocjadan Aug 6, 2024
3fda260
Aside programmatically appended to end of document.
ocjadan Aug 6, 2024
77007be
Refactor focus loop code into its own class. Focus loop on cookie ban…
ocjadan Aug 6, 2024
8bd5749
Merge main.
ocjadan Aug 6, 2024
9af7456
Update cookie banner images.
ocjadan Aug 6, 2024
553cb0f
Square cookiebanner images for main menu.
ocjadan Aug 6, 2024
77968eb
Fix autofocus issue with automated tests.
ocjadan Aug 6, 2024
83dc65e
Fix aria-labelledby for example so it passes automated tests.
ocjadan Aug 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions content/body/cookiebanner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<p>
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.
</p>

<p>
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.
</p>

<p>
The instructions on this page walk through how to implement an accessible cookie banner using the
<code>dialog</code> HTML element.
</p>

<h2>Modal Cookie Banner</h2>

<p>
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.
</p>

<br />

<button id="show-modal-button" aria-haspopup="dialog">Show Modal Banner</button>

<div id="cookie-banner-example" class="enable-example--no-border">
<dialog id="cookie-banner" class="cookie-banner">
Copy link
Collaborator

@zoltan-dulac zoltan-dulac Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is a non-modal, I think it better to not use the dialog tag to encapsulate the cookie banner in the non-modal case. I think

would be better (with aria-labeledby pointing to the title of the banner.

You should use for the modal case, but make sure you follow all the instructions in dialog.php.

Copy link
Collaborator Author

@ocjadan ocjadan Jul 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @zoltan-dulac , could you share why the <aside> tag would be appropriate for a cookie banner. If we trade <dialog> in favor of <aside> then we also lose the following benefits:

  1. The .show() method which positions the contents above everything else on the page, but "still allowing interaction with content outside of the dialog". MDN link
  2. The combination of using a <form> tag with its method attribute set to "dialog", which results in automatic dismissal of the dialog when a button is pressed.

We'd have to program these functionalities ourselves when using the <aside> tag, and also convince users of Enable to do the same.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zoltan: "Will check with Allison regarding the .show() method."

<form method="dialog" aria-labelledby="cookie-banner-title">
<button id="cookie-banner-close-button" class="cookie-banner__close-button" autofocus>
<img class="cookie-banner__close-button__icon" src="images/close-window.svg" alt="close cookie notice">
</button>

<div role="document">
<h2 id="cookie-banner-title" class="cookie-banner__title">Cookie Notice</h2>
<p id="cookie-banner-message">
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.
</p>
</div>

<div class="cookie-banner__action-buttons">
<button id="cookie-banner-accept-button" class="cookie-banner__accept-button">Accept</button>
<button id="cookie-banner-reject-button" class="cookie-banner__reject-button">Reject</button>
</div>
</form>
</dialog>
</div>

<?php includeShowcode("cookie-banner-example"); ?>

<script type="application/json" id="cookie-banner-example-props">
{
"replaceHtmlRules": {},
"steps": [
{
"label": "Use the HTML dialog tag",
"highlight": "%OPENCLOSECONTENTTAG%dialog",
"notes": "The dialog tag comes with handy functionality such as a <code>.showModal()</code> method."
},
{
"label": "Use a form to encapsulate the contents of the cookie banner",
"highlight": "%OPENCLOSECONTENTTAG%form",
"notes": "Using a form is good practice—<a href=\"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog#usage_notes\">MDN docs</a>—to have the dialog close automatically when a button within it is pressed."
},
{
"label": "Ensure the form element has \"aria-labelledby\" set",
"highlight": "%INLINE%aria-labelledby=\"cookie-banner-title\"",
"notes": "Using aria-labelledby along with a div element with a role of document ensures it's read out when the dialog is opened."
},
{
"label": "Use the document role to enclose the cookie explanation",
"highlight": "%INLINE%role=\"document\"",
"notes": "This presents the content in reading mode for screen readers."
}
]
}
</script>

<p>
With the HTML set up, use the built-in method <code>.showModal()</code> of the <code>dialog</code> HTML tag to show
the cookie banner.
</p>

<h2>Non-Modal Cookie Banner</h2>

<p>
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.
</p>

<br />

<button id="show-non-modal-button" aria-haspopup="dialog">Show Non-Modal Banner</button>

<div id="cookie-banner-example2" class="enable-example--no-border">
<aside id="non-modal-cookie-banner-example" class="non-modal-cookie-banner__example" aria-labelledby="non-modal-cookie-banner-title-example">
<button id="non-modal-cookie-banner-close-button-example" class="cookie-banner__close-button">
<img class="cookie-banner__close-button__icon" src="images/close-window.svg" alt="close cookie notice">
</button>

<div role="document">
<h2 id="non-modal-cookie-banner-title-example" class="cookie-banner__title">Cookie Notice</h2>
<p id="non-modal-cookie-banner-message-example">
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.
</p>
</div>

<div class="cookie-banner__action-buttons">
<button id="non-modal-cookie-banner-accept-button-example" class="cookie-banner__accept-button">Accept</button>
<button id="non-modal-cookie-banner-reject-button-example" class="cookie-banner__reject-button">Reject</button>
</div>
</aside>
</div>

<?php includeShowcode("cookie-banner-example2"); ?>

<script type="application/json" id="cookie-banner-example2-props">
{
"replaceHtmlRules": {},
"steps": [
{
"label": "Use the HTML aside tag",
"highlight": "%OPENCLOSECONTENTTAG%aside",
"notes": ""
},
{
"label": "Ensure the aside element has \"aria-labelledby\" set",
"highlight": "%INLINE%aria-labelledby=\"non-modal-cookie-banner-title\"",
"notes": "Using aria-labelledby along with a div element with a role of document ensures it's read out when the dialog is opened."
},
{
"label": "Use the document role to enclose the cookie explanation",
"highlight": "%INLINE%role=\"document\"",
"notes": "This presents the content in reading mode for screen readers."
}
]
}
</script>
6 changes: 6 additions & 0 deletions content/bottom/cookiebanner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script id="cookie-banner-js" type="module">
import cookieBanner from "./js/modules/cookiebanner.js"
import showCode from "./js/enable-libs/showcode.js";
cookieBanner.init();
showCode.addJsObj('cookie banner', cookieBanner);
</script>
1 change: 1 addition & 0 deletions content/head/cookiebanner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<link id="cookiebanner-css" rel="stylesheet" type="text/css" href="css/cookiebanner.css" >
1 change: 1 addition & 0 deletions css-list.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 85 additions & 0 deletions css/cookiebanner.css
Original file line number Diff line number Diff line change
@@ -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;
}
Binary file added images/main-menu/cookiebanner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/main-menu/cookiebanner.webp
Binary file not shown.
Binary file added images/posters/cookiebanner.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 77 additions & 0 deletions js/enable-libs/dialogFocusManager.js
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
3 changes: 2 additions & 1 deletion js/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ function initEnable() {
],
showAsSidebarDefault: true,
numberFirstLevelHeadings: true,
selectorToSkipHeadingsWithin: '.enable-example',
selectorToSkipHeadingsWithin:
'.enable-example, .enable-example--no-border',
collapseNestedHeadingsAfterLevel: 2,
});
}
Expand Down
Loading
Loading