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 = { module.exports = {
'@tags': ['core', 'olivero'], '@tags': ['core', 'olivero'],
before(browser) { before(browser) {
@ -18,14 +21,19 @@ module.exports = {
'#block-olivero-content h2', '#block-olivero-content h2',
'Congratulations and welcome to the Drupal community!', 'Congratulations and welcome to the Drupal community!',
) )
.assert.not.visible('button.wide-nav-expand') .assert.not.visible(buttonSelector)
.getLocationInView('footer.site-footer', () => { .getLocationInView('footer.site-footer', () => {
browser.assert.visible('button.wide-nav-expand'); browser.assert.visible(buttonSelector);
browser.assert.not.visible('#site-header__inner'); browser.assert.not.visible('#site-header__inner');
}) })
.assert.not.visible('#block-olivero-main-menu') .assert.not.visible(mainMenuSelector)
.click('button.wide-nav-expand', () => { .click(buttonSelector)
browser.assert.visible('#block-olivero-main-menu'); .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) { @media (min-width: 75rem) {
html.js body:not(.is-always-mobile-nav) .site-header__inner { 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) { @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 */ box-shadow: -36px 1px 36px rgba(0, 0, 0, 0.08) /* LTR */
} }
} }
@media (min-width: 75rem) { @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) 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 { html.js body:not(.is-always-mobile-nav) .site-header__inner {
@media (--nav) { @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) { @media (--nav) {
box-shadow: -36px 1px 36px rgba(0, 0, 0, 0.08); /* LTR */ 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) { @media (--nav) {
box-shadow: 36px 1px 36px rgba(0, 0, 0, 0.08); box-shadow: 36px 1px 36px rgba(0, 0, 0, 0.08);
} }

View File

@ -16,26 +16,75 @@
Drupal.olivero.isDesktopNav = isDesktopNav; 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'); const siteHeaderFixable = document.querySelector('.site-header__fixable');
function wideNavIsOpen() { function stickyHeaderIsEnabled() {
return wideNavButton.getAttribute('aria-expanded') === 'true'; 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()) { if (isDesktopNav()) {
wideNavButton.setAttribute('aria-expanded', 'true'); if (pinnedState === true) {
siteHeaderFixable.classList.add('is-expanded'); 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() { * Return the sticky header's stored state from localStorage.
if (isDesktopNav()) { *
wideNavButton.setAttribute('aria-expanded', 'false'); * @return {boolean} Stored state of the sticky header.
siteHeaderFixable.classList.remove('is-expanded'); */
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. // Only enable scroll effects if the browser supports Intersection Observer.
@ -93,38 +142,27 @@
observer.observe(primaryNav); observer.observe(primaryNav);
} }
wideNavButton.addEventListener('click', () => { stickyHeaderToggleButton.addEventListener('click', () => {
if (!wideNavIsOpen()) { toggleStickyHeaderState(!stickyHeaderIsEnabled());
showWideNav();
} else {
hideWideNav();
}
}); });
siteHeaderFixable // If header is pinned open and a header element gains focus, scroll to the
.querySelector('.site-header__inner') // top of the page to ensure that the header elements can be seen.
.addEventListener('focusin', showWideNav); document
.querySelector('#site-header__inner')
// If skip link is clicked, ensure that the wide navigation closes so the header will not be covered up. .addEventListener('focusin', () => {
document.querySelector('.skip-link').addEventListener('click', hideWideNav); 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(); 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); })(Drupal);

View File

@ -14,25 +14,47 @@
} }
Drupal.olivero.isDesktopNav = isDesktopNav; 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'); var siteHeaderFixable = document.querySelector('.site-header__fixable');
function wideNavIsOpen() { function stickyHeaderIsEnabled() {
return wideNavButton.getAttribute('aria-expanded') === 'true'; 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()) { if (isDesktopNav()) {
wideNavButton.setAttribute('aria-expanded', 'true'); if (pinnedState === true) {
siteHeaderFixable.classList.add('is-expanded'); siteHeaderFixable.classList.add('is-expanded');
} else {
siteHeaderFixable.classList.remove('is-expanded');
}
stickyHeaderToggleButton.setAttribute('aria-checked', pinnedState);
setStickyHeaderStorage(pinnedState);
} }
} }
function hideWideNav() { function getStickyHeaderStorage() {
if (isDesktopNav()) { var stickyHeaderState = localStorage.getItem('Drupal.olivero.stickyHeaderState');
wideNavButton.setAttribute('aria-expanded', 'false'); if (!stickyHeaderState) return null;
siteHeaderFixable.classList.remove('is-expanded'); 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) { if ('IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
@ -79,25 +101,22 @@
observer.observe(primaryNav); observer.observe(primaryNav);
} }
wideNavButton.addEventListener('click', function () { stickyHeaderToggleButton.addEventListener('click', function () {
if (!wideNavIsOpen()) { toggleStickyHeaderState(!stickyHeaderIsEnabled());
showWideNav(); });
} else { document.querySelector('#site-header__inner').addEventListener('focusin', function () {
hideWideNav(); 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(); 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); })(Drupal);

View File

@ -36,11 +36,11 @@ global-styling:
css/components/header-buttons-mobile.css: {} css/components/header-buttons-mobile.css: {}
css/components/header-navigation.css: {} css/components/header-navigation.css: {}
css/components/header-site-branding.css: {} css/components/header-site-branding.css: {}
css/components/header-sticky-toggle.css: {}
css/components/hero.css: {} css/components/hero.css: {}
css/components/links.css: {} css/components/links.css: {}
css/components/messages.css: {} css/components/messages.css: {}
css/components/navigation/nav-button-mobile.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-button.css: {}
css/components/navigation/nav-primary.css: {} css/components/navigation/nav-primary.css: {}
css/components/navigation/nav-primary-wide.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 %} {% if page.header or page.primary_menu or page.secondary_menu %}
<header id="header" class="site-header" role="banner"> <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__fixable fixable">
<div class="site-header__initial"> <div class="site-header__initial">
<button class="wide-nav-expand" aria-controls="site-header__inner" aria-label="{{ 'Toggle navigation'|t }}" aria-expanded="false"> <button class="sticky-header-toggle" role="switch" aria-controls="site-header__inner" aria-label="{{ 'Sticky header'|t }}" aria-checked="false">
<span class="wide-nav-expand__icon"> <span class="sticky-header-toggle__icon">
<span></span> <span></span>
<span></span> <span></span>
<span></span> <span></span>