Issue #3186349 by mherchel, anmolgoyal74, bnjmnm, andrewmacpherson, jwitkowski79, rainbreaw, proeung, benjifisher, lauriii: Major accessibility problems with Olivero header show/hide feature
parent
c2241ab659
commit
bc8ea2e8fb
|
@ -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');
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
if (pinnedState === true) {
|
||||
siteHeaderFixable.classList.add('is-expanded');
|
||||
}
|
||||
}
|
||||
|
||||
// Resets the wide nav button to be closed (its default state).
|
||||
function hideWideNav() {
|
||||
if (isDesktopNav()) {
|
||||
wideNavButton.setAttribute('aria-expanded', 'false');
|
||||
} else {
|
||||
siteHeaderFixable.classList.remove('is-expanded');
|
||||
}
|
||||
|
||||
stickyHeaderToggleButton.setAttribute('aria-checked', pinnedState);
|
||||
setStickyHeaderStorage(pinnedState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
|
|
@ -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');
|
||||
if (pinnedState === true) {
|
||||
siteHeaderFixable.classList.add('is-expanded');
|
||||
}
|
||||
}
|
||||
|
||||
function hideWideNav() {
|
||||
if (isDesktopNav()) {
|
||||
wideNavButton.setAttribute('aria-expanded', 'false');
|
||||
} else {
|
||||
siteHeaderFixable.classList.remove('is-expanded');
|
||||
}
|
||||
|
||||
stickyHeaderToggleButton.setAttribute('aria-checked', pinnedState);
|
||||
setStickyHeaderStorage(pinnedState);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
});
|
||||
siteHeaderFixable.querySelector('.site-header__inner').addEventListener('focusin', showWideNav);
|
||||
document.querySelector('.skip-link').addEventListener('click', hideWideNav);
|
||||
monitorNavPosition();
|
||||
}
|
||||
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;
|
||||
|
||||
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();
|
||||
if (window.scrollY > headerMargin) {
|
||||
window.scrollTo(0, headerMargin);
|
||||
}
|
||||
}
|
||||
});
|
||||
monitorNavPosition();
|
||||
setStickyHeaderStorage(getStickyHeaderStorage());
|
||||
toggleStickyHeaderState(getStickyHeaderStorage());
|
||||
}
|
||||
})(Drupal);
|
|
@ -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: {}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue