Issue #3186349 by mherchel, anmolgoyal74, bnjmnm, andrewmacpherson, jwitkowski79, rainbreaw, proeung, benjifisher, lauriii: Major accessibility problems with Olivero header show/hide feature

merge-requests/539/head
Lauri Eskola 2021-04-08 17:41:01 +03:00
parent c2241ab659
commit bc8ea2e8fb
No known key found for this signature in database
GPG Key ID: 382FC0F5B0DF53F8
9 changed files with 408 additions and 83 deletions

View File

@ -1,3 +1,6 @@
const buttonSelector = 'button.sticky-header-toggle';
const mainMenuSelector = '#block-olivero-main-menu';
module.exports = {
'@tags': ['core', 'olivero'],
before(browser) {
@ -18,14 +21,19 @@ module.exports = {
'#block-olivero-content h2',
'Congratulations and welcome to the Drupal community!',
)
.assert.not.visible('button.wide-nav-expand')
.assert.not.visible(buttonSelector)
.getLocationInView('footer.site-footer', () => {
browser.assert.visible('button.wide-nav-expand');
browser.assert.visible(buttonSelector);
browser.assert.not.visible('#site-header__inner');
})
.assert.not.visible('#block-olivero-main-menu')
.click('button.wide-nav-expand', () => {
browser.assert.visible('#block-olivero-main-menu');
});
.assert.not.visible(mainMenuSelector)
.click(buttonSelector)
.assert.visible(mainMenuSelector)
.assert.attributeEquals(buttonSelector, 'aria-checked', 'true')
// Sticky header should remain open after page reload in open state.
.drupalRelativeURL('/node')
.assert.visible(mainMenuSelector)
.assert.attributeEquals(buttonSelector, 'aria-checked', 'true');
},
};

View File

@ -0,0 +1,145 @@
/*
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/3084859
* @preserve
*/
/**
* @file
* Sticky Header Toggle Button.
*
* This button shows on the left hand side of the header (in LTR layouts), and
* toggles fixing the header to the top of the viewport.
*/
.sticky-header-toggle {
display: none
}
@media (min-width: 75rem) {
.sticky-header-toggle {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 5.625rem;
height: 6.75rem;
pointer-events: none;
opacity: 0;
border: 0;
outline: 0;
background-color: #2494db
}
.sticky-header-toggle:focus {
cursor: pointer;
pointer-events: auto;
opacity: 1;
outline: solid 2px #fff;
outline-offset: -4px;
}
}
@media (min-width: 75rem) {
body:not(.is-always-mobile-nav) .js-fixed .sticky-header-toggle {
visibility: visible
}
}
@media (min-width: 75rem) {
body.is-always-mobile-nav .sticky-header-toggle {
visibility: hidden
}
}
.sticky-header-toggle__icon {
position: relative;
width: 2.25rem;
height: 1.3125rem;
transition: opacity 0.2s;
pointer-events: none;
transform-style: preserve-3d
}
.sticky-header-toggle__icon > span {
display: block;
height: 0;
/* Intentionally not using CSS logical properties. */
border-top: solid 3px #fff
}
[dir="ltr"] .sticky-header-toggle__icon > span:nth-child(1) {
left: 0
}
[dir="rtl"] .sticky-header-toggle__icon > span:nth-child(1) {
right: 0
}
.sticky-header-toggle__icon > span:nth-child(1) {
position: absolute;
top: 0;
width: 100%;
height: 0;
transition: transform 0.2s;
background-color: #fff;
}
[dir="ltr"] .sticky-header-toggle__icon > span:nth-child(2) {
left: 0
}
[dir="rtl"] .sticky-header-toggle__icon > span:nth-child(2) {
right: 0
}
.sticky-header-toggle__icon > span:nth-child(2) {
position: absolute;
top: 0.5625rem;
width: 100%;
height: 0;
transition: opacity 0.2s;
background-color: #fff;
}
[dir="ltr"] .sticky-header-toggle__icon > span:nth-child(3) {
left: 0
}
[dir="rtl"] .sticky-header-toggle__icon > span:nth-child(3) {
right: 0
}
.sticky-header-toggle__icon > span:nth-child(3) {
position: absolute;
top: auto;
bottom: 0;
width: 100%;
height: 0;
transition: transform 0.2s;
background-color: #fff;
}
.js-fixed .sticky-header-toggle {
cursor: pointer;
pointer-events: auto;
opacity: 1;
}
[aria-checked="true"] .sticky-header-toggle__icon > span:nth-child(1) {
top: 0.5625rem;
transform: rotate(-45deg);
}
[aria-checked="true"] .sticky-header-toggle__icon > span:nth-child(2) {
opacity: 0;
}
[aria-checked="true"] .sticky-header-toggle__icon > span:nth-child(3) {
top: 0.5625rem;
transform: rotate(45deg);
}

View File

@ -0,0 +1,115 @@
/**
* @file
* Sticky Header Toggle Button.
*
* This button shows on the left hand side of the header (in LTR layouts), and
* toggles fixing the header to the top of the viewport.
*/
@import "../base/variables.pcss.css";
.sticky-header-toggle {
display: none;
@media (--nav) {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: var(--content-left);
height: var(--sp6);
pointer-events: none;
opacity: 0;
border: 0;
outline: 0;
background-color: var(--color--blue-50);
&:focus {
cursor: pointer;
pointer-events: auto;
opacity: 1;
outline: solid 2px var(--color--white);
outline-offset: -4px;
}
}
}
body:not(.is-always-mobile-nav) .js-fixed .sticky-header-toggle {
@media (--nav) {
visibility: visible;
}
}
body.is-always-mobile-nav .sticky-header-toggle {
@media (--nav) {
visibility: hidden;
}
}
.sticky-header-toggle__icon {
position: relative;
width: var(--sp2);
height: 21px;
transition: opacity 0.2s;
pointer-events: none;
transform-style: preserve-3d;
& > span {
display: block;
height: 0;
/* Intentionally not using CSS logical properties. */
border-top: solid 3px var(--color--white);
&:nth-child(1) {
position: absolute;
inset-block-start: 0;
inset-inline-start: 0;
width: 100%;
height: 0;
transition: transform 0.2s;
background-color: var(--color--white);
}
&:nth-child(2) {
position: absolute;
inset-block-start: 9px;
inset-inline-start: 0;
width: 100%;
height: 0;
transition: opacity 0.2s;
background-color: var(--color--white);
}
&:nth-child(3) {
position: absolute;
inset-block: auto 0;
inset-inline-start: 0;
width: 100%;
height: 0;
transition: transform 0.2s;
background-color: var(--color--white);
}
}
}
.js-fixed .sticky-header-toggle {
cursor: pointer;
pointer-events: auto;
opacity: 1;
}
[aria-checked="true"] .sticky-header-toggle__icon {
& > span:nth-child(1) {
inset-block-start: 9px;
transform: rotate(-45deg);
}
& > span:nth-child(2) {
opacity: 0;
}
& > span:nth-child(3) {
inset-block-start: 9px;
transform: rotate(45deg);
}
}

View File

@ -81,20 +81,20 @@
@media (min-width: 75rem) {
html.js body:not(.is-always-mobile-nav) .site-header__inner {
transition: opacity 0.3s, transform 0.3s
transition: opacity 0.3s, transform 0.3s, box-shadow 0.3s
}
}
@media (min-width: 75rem) {
.site-header__fixable.js-fixed .site-header__inner {
.site-header__fixable.is-expanded .site-header__inner {
box-shadow: -36px 1px 36px rgba(0, 0, 0, 0.08) /* LTR */
}
}
@media (min-width: 75rem) {
[dir="rtl"] .site-header__fixable.js-fixed .site-header__inner {
[dir="rtl"] .site-header__fixable.is-expanded .site-header__inner {
box-shadow: 36px 1px 36px rgba(0, 0, 0, 0.08)
}
}

View File

@ -73,17 +73,17 @@
*/
html.js body:not(.is-always-mobile-nav) .site-header__inner {
@media (--nav) {
transition: opacity 0.3s, transform 0.3s;
transition: opacity 0.3s, transform 0.3s, box-shadow 0.3s;
}
}
.site-header__fixable.js-fixed .site-header__inner {
.site-header__fixable.is-expanded .site-header__inner {
@media (--nav) {
box-shadow: -36px 1px 36px rgba(0, 0, 0, 0.08); /* LTR */
}
}
[dir="rtl"] .site-header__fixable.js-fixed .site-header__inner {
[dir="rtl"] .site-header__fixable.is-expanded .site-header__inner {
@media (--nav) {
box-shadow: 36px 1px 36px rgba(0, 0, 0, 0.08);
}

View File

@ -16,26 +16,75 @@
Drupal.olivero.isDesktopNav = isDesktopNav;
const wideNavButton = document.querySelector('.wide-nav-expand');
const stickyHeaderToggleButton = document.querySelector(
'.sticky-header-toggle',
);
const siteHeaderFixable = document.querySelector('.site-header__fixable');
function wideNavIsOpen() {
return wideNavButton.getAttribute('aria-expanded') === 'true';
function stickyHeaderIsEnabled() {
return stickyHeaderToggleButton.getAttribute('aria-checked') === 'true';
}
function showWideNav() {
/**
* Save the current sticky header expanded state to localStorage, and set
* it to expire after two weeks.
*
* @param {boolean} expandedState - Current state of the sticky header button.
*/
function setStickyHeaderStorage(expandedState) {
const now = new Date();
const item = {
value: expandedState,
expiry: now.getTime() + 20160000, // 2 weeks from now.
};
localStorage.setItem(
'Drupal.olivero.stickyHeaderState',
JSON.stringify(item),
);
}
/**
* Toggle the state of the sticky header between always pinned and
* only pinned when scrolled to the top of the viewport.
*
* @param {boolean} pinnedState - State to change the sticky header to.
*/
function toggleStickyHeaderState(pinnedState) {
if (isDesktopNav()) {
wideNavButton.setAttribute('aria-expanded', 'true');
siteHeaderFixable.classList.add('is-expanded');
if (pinnedState === true) {
siteHeaderFixable.classList.add('is-expanded');
} else {
siteHeaderFixable.classList.remove('is-expanded');
}
stickyHeaderToggleButton.setAttribute('aria-checked', pinnedState);
setStickyHeaderStorage(pinnedState);
}
}
// Resets the wide nav button to be closed (its default state).
function hideWideNav() {
if (isDesktopNav()) {
wideNavButton.setAttribute('aria-expanded', 'false');
siteHeaderFixable.classList.remove('is-expanded');
/**
* Return the sticky header's stored state from localStorage.
*
* @return {boolean} Stored state of the sticky header.
*/
function getStickyHeaderStorage() {
const stickyHeaderState = localStorage.getItem(
'Drupal.olivero.stickyHeaderState',
);
if (!stickyHeaderState) return null;
const item = JSON.parse(stickyHeaderState);
const now = new Date();
// Compare the expiry time of the item with the current time.
if (now.getTime() > item.expiry) {
// If the item is expired, delete the item from storage and return null.
localStorage.removeItem('Drupal.olivero.stickyHeaderState');
return null;
}
return item.value;
}
// Only enable scroll effects if the browser supports Intersection Observer.
@ -93,38 +142,27 @@
observer.observe(primaryNav);
}
wideNavButton.addEventListener('click', () => {
if (!wideNavIsOpen()) {
showWideNav();
} else {
hideWideNav();
}
stickyHeaderToggleButton.addEventListener('click', () => {
toggleStickyHeaderState(!stickyHeaderIsEnabled());
});
siteHeaderFixable
.querySelector('.site-header__inner')
.addEventListener('focusin', showWideNav);
// If skip link is clicked, ensure that the wide navigation closes so the header will not be covered up.
document.querySelector('.skip-link').addEventListener('click', hideWideNav);
// If header is pinned open and a header element gains focus, scroll to the
// top of the page to ensure that the header elements can be seen.
document
.querySelector('#site-header__inner')
.addEventListener('focusin', () => {
if (isDesktopNav() && !stickyHeaderIsEnabled()) {
const header = document.querySelector('#header');
const headerNav = header.querySelector('#header-nav');
const headerMargin = header.clientHeight - headerNav.clientHeight;
if (window.scrollY > headerMargin) {
window.scrollTo(0, headerMargin);
}
}
});
monitorNavPosition();
setStickyHeaderStorage(getStickyHeaderStorage());
toggleStickyHeaderState(getStickyHeaderStorage());
}
document.addEventListener('keyup', (e) => {
if (e.keyCode === 27) {
// Close the search form.
if (
'toggleSearchVisibility' in Drupal.olivero &&
'searchIsVisible' in Drupal.olivero &&
Drupal.olivero.searchIsVisible()
) {
Drupal.olivero.toggleSearchVisibility(false);
}
// Close the wide nav.
else {
hideWideNav();
}
}
});
})(Drupal);

View File

@ -14,25 +14,47 @@
}
Drupal.olivero.isDesktopNav = isDesktopNav;
var wideNavButton = document.querySelector('.wide-nav-expand');
var stickyHeaderToggleButton = document.querySelector('.sticky-header-toggle');
var siteHeaderFixable = document.querySelector('.site-header__fixable');
function wideNavIsOpen() {
return wideNavButton.getAttribute('aria-expanded') === 'true';
function stickyHeaderIsEnabled() {
return stickyHeaderToggleButton.getAttribute('aria-checked') === 'true';
}
function showWideNav() {
function setStickyHeaderStorage(expandedState) {
var now = new Date();
var item = {
value: expandedState,
expiry: now.getTime() + 20160000
};
localStorage.setItem('Drupal.olivero.stickyHeaderState', JSON.stringify(item));
}
function toggleStickyHeaderState(pinnedState) {
if (isDesktopNav()) {
wideNavButton.setAttribute('aria-expanded', 'true');
siteHeaderFixable.classList.add('is-expanded');
if (pinnedState === true) {
siteHeaderFixable.classList.add('is-expanded');
} else {
siteHeaderFixable.classList.remove('is-expanded');
}
stickyHeaderToggleButton.setAttribute('aria-checked', pinnedState);
setStickyHeaderStorage(pinnedState);
}
}
function hideWideNav() {
if (isDesktopNav()) {
wideNavButton.setAttribute('aria-expanded', 'false');
siteHeaderFixable.classList.remove('is-expanded');
function getStickyHeaderStorage() {
var stickyHeaderState = localStorage.getItem('Drupal.olivero.stickyHeaderState');
if (!stickyHeaderState) return null;
var item = JSON.parse(stickyHeaderState);
var now = new Date();
if (now.getTime() > item.expiry) {
localStorage.removeItem('Drupal.olivero.stickyHeaderState');
return null;
}
return item.value;
}
if ('IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
@ -79,25 +101,22 @@
observer.observe(primaryNav);
}
wideNavButton.addEventListener('click', function () {
if (!wideNavIsOpen()) {
showWideNav();
} else {
hideWideNav();
stickyHeaderToggleButton.addEventListener('click', function () {
toggleStickyHeaderState(!stickyHeaderIsEnabled());
});
document.querySelector('#site-header__inner').addEventListener('focusin', function () {
if (isDesktopNav() && !stickyHeaderIsEnabled()) {
var header = document.querySelector('#header');
var headerNav = header.querySelector('#header-nav');
var headerMargin = header.clientHeight - headerNav.clientHeight;
if (window.scrollY > headerMargin) {
window.scrollTo(0, headerMargin);
}
}
});
siteHeaderFixable.querySelector('.site-header__inner').addEventListener('focusin', showWideNav);
document.querySelector('.skip-link').addEventListener('click', hideWideNav);
monitorNavPosition();
setStickyHeaderStorage(getStickyHeaderStorage());
toggleStickyHeaderState(getStickyHeaderStorage());
}
document.addEventListener('keyup', function (e) {
if (e.keyCode === 27) {
if ('toggleSearchVisibility' in Drupal.olivero && 'searchIsVisible' in Drupal.olivero && Drupal.olivero.searchIsVisible()) {
Drupal.olivero.toggleSearchVisibility(false);
} else {
hideWideNav();
}
}
});
})(Drupal);

View File

@ -36,11 +36,11 @@ global-styling:
css/components/header-buttons-mobile.css: {}
css/components/header-navigation.css: {}
css/components/header-site-branding.css: {}
css/components/header-sticky-toggle.css: {}
css/components/hero.css: {}
css/components/links.css: {}
css/components/messages.css: {}
css/components/navigation/nav-button-mobile.css: {}
css/components/navigation/wide-nav-expand.css: {}
css/components/navigation/nav-primary-button.css: {}
css/components/navigation/nav-primary.css: {}
css/components/navigation/nav-primary-wide.css: {}

View File

@ -52,11 +52,11 @@
{% if page.header or page.primary_menu or page.secondary_menu %}
<header id="header" class="site-header" role="banner">
{# Gets fixed by JS at wide widths. #}
{# Gets fixed by JavaScript at wide widths. #}
<div class="site-header__fixable fixable">
<div class="site-header__initial">
<button class="wide-nav-expand" aria-controls="site-header__inner" aria-label="{{ 'Toggle navigation'|t }}" aria-expanded="false">
<span class="wide-nav-expand__icon">
<button class="sticky-header-toggle" role="switch" aria-controls="site-header__inner" aria-label="{{ 'Sticky header'|t }}" aria-checked="false">
<span class="sticky-header-toggle__icon">
<span></span>
<span></span>
<span></span>