Skip to content

Commit

Permalink
Replace header nav toggle logic with popover api
Browse files Browse the repository at this point in the history
  • Loading branch information
JoelCDL committed Nov 6, 2024
1 parent 8fa0290 commit b073b52
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 93 deletions.
77 changes: 30 additions & 47 deletions css/components/headernav.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
--headernav-section-padding: var(--space3);
--headernav-item-margin: var(--space3);

position: relative;

ul {
all: unset;
margin: unset;
padding: unset;
list-style-type: none;

li {
Expand Down Expand Up @@ -60,9 +59,28 @@
}

/* Panels: */

[popover] {
position-area: block-end span-inline-end;
position-try-fallbacks: flip-inline;
inset: unset;
margin: 0;
transition-property: opacity, display, overlay;
transition-duration: .3s;
transition-behavior: allow-discrete;
border: unset;
opacity: 0;

&:popover-open {
opacity: 1;

@starting-style {
opacity: 0;
}
}
}

> ul > li > ul {
display: none;
position: absolute;
padding: var(--headernav-section-padding);
column-gap: calc(var(--headernav-section-padding) * 2);
column-rule: 1px solid oklch(88% 0 0deg);
Expand Down Expand Up @@ -126,48 +144,13 @@
> ul > li li {
margin-block-end: var(--headernav-item-margin);
}
}

/**** Panel Toggle ****/

/* If c-headernav JS not supported: */

&.c-headernav-no-js {
> ul > li:hover > ul {
@media (--min-width2) {
display: block;
z-index: 10;
}
}

/* If :focus-within not supported. Only primary links accessible via tabkey: */

> ul > li a:focus {
@media (--min-width2) {
display: block;
z-index: 10;
}
}

/* If :focus-within supported. All links accessbile via focus. No toggle state on primary links, so user must tab through every secondary link in each section to reach the next section: */

> ul > li:focus-within > ul {
@media (--min-width2) {
display: block;
z-index: 10;
}
}
}

/* If c-headernav JS supported: */

&.c-headernav-js {
/* Primary links disabled via JS and used as a toggle for each section. Hover and focus states handled by JS and set via 'open' class: */
.c-header ~ * {
filter: blur(0) contrast(1) grayscale(0);
transition: filter 0.5s;
}

li.open ul {
@media (--min-width2) {
display: block;
z-index: 10;
}
}
}
.c-header:has(:popover-open) ~ * {
filter: blur(0.05rem) contrast(90%) grayscale(10%);
}
2 changes: 1 addition & 1 deletion elements/3-components/headernav.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{!-- Read notes in component config --}}
<nav aria-label="global" class="c-headernav c-headernav-no-js c-mobilenav">
<nav aria-label="global" class="c-headernav c-mobilenav js-headernav">
<ul>
{{> @menu-items}}
</ul>
Expand Down
91 changes: 46 additions & 45 deletions js/headernav.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,61 @@
// Headernav Component:

const headerNavMediaQuery = window.matchMedia('(min-width: 760px)')

function clickLink (event) {
if (this.parentElement.classList.contains('open') === false) {
this.parentElement.classList.add('open')
this.setAttribute('aria-expanded', 'true')
} else {
this.parentElement.classList.remove('open')
this.setAttribute('aria-expanded', 'false')
}
event.preventDefault()
}
const menu = document.querySelector('.js-headernav')
const subNavs = menu.querySelectorAll(':scope > ul > li:has(> ul)')
let counter = 1

const headerNavToggles = e => {
if (document.querySelector('.c-headernav') && e.matches) {
const allMenuItems = document.querySelectorAll('.c-headernav > ul > li');

[].forEach.call(allMenuItems, function (el) {
document.querySelector('.c-headernav').classList.remove('c-headernav-no-js')
document.querySelector('.c-headernav').classList.add('c-headernav-js')
el.querySelector('a').setAttribute('aria-haspopup', 'true')
el.querySelector('a').setAttribute('aria-expanded', 'false')

el.addEventListener('mouseover', function (event) {
this.classList.add('open')
this.querySelector('a').setAttribute('aria-expanded', 'true')
// console.log('page wdith at least 760px')

// Find all lists items containing a nested list (subnav). Set each subnav sibling link and parent list item as controls for the subnav, then set the popover:
for (const subNav of subNavs) {
const subNavSiblingLink = subNav.querySelector('a')
const subNavPopover = subNav.querySelector('ul')
subNavPopover.popover = ''

// Anchor each sibling link to its popover using unique anchor names:
const anchorName = '--anchor' + counter++
subNavSiblingLink.style.setProperty('anchor-name', anchorName)
subNavPopover.style.setProperty('position-anchor', anchorName)

// Only a <button> as a popover control has built-in accessiblity bindings with popovers. So for the sibling links, we need to set and toggle their aria expanded state:
subNavSiblingLink.setAttribute('aria-expanded', 'false') // initial state

const expandedState = () => {
if (subNavPopover.matches(':popover-open')) {
subNavSiblingLink.setAttribute('aria-expanded', 'true')
} else {
subNavSiblingLink.setAttribute('aria-expanded', 'false')
}
}

// Show/hide subnav on mouse pointer over and out. Omit these two event listeners if this effect is not desired:
subNav.addEventListener('mouseover', () => {
subNavPopover.showPopover()
expandedState()
})

el.addEventListener('mouseout', function (event) {
this.classList.remove('open')
this.querySelector('a').setAttribute('aria-expanded', 'false')
subNav.addEventListener('mouseout', () => {
subNavPopover.hidePopover()
expandedState()
})

el.querySelector('a').addEventListener('click', clickLink)
});

[].forEach.call(allMenuItems, function (el) {
el.querySelector('a').addEventListener('focus', function (event) {
[].forEach.call(
allMenuItems,
function (el) {
if (el !== this.parentElement) {
el.classList.remove('open')
el.querySelector('a').setAttribute('aria-expanded', 'false')
}
}, this
)
// Toggle subnav if sibling link is clicked by keyboard return key:
subNavSiblingLink.addEventListener('click', () => {
subNavPopover.togglePopover()
expandedState()
})
})
} else {
const allMenuItems = document.querySelectorAll('.c-headernav > ul > li');

[].forEach.call(allMenuItems, function (el) {
el.querySelector('a').removeEventListener('click', clickLink)
})
// Hide subnav when keyboard focus leaves popover control: Adapted from https://danburzo.ro/focus-within
subNav.addEventListener('focusout', (e) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
subNavPopover.hidePopover()
expandedState()
}
})
}
}
}

Expand Down

0 comments on commit b073b52

Please sign in to comment.