Skip to content

Commit f06b56b

Browse files
committed
Replace header nav toggle logic with popover api
1 parent 8fa0290 commit f06b56b

11 files changed

+216
-132
lines changed

css/components/headernav.css

+42-71
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
/***** Header Nav Component *****/
22

3-
/* Read notes in component config */
4-
53
.c-headernav {
64
--headernav-section-padding: var(--space3);
75
--headernav-item-margin: var(--space3);
86

9-
position: relative;
7+
.no-popover & {
8+
ul ul {
9+
display: none;
10+
}
11+
}
1012

1113
ul {
12-
all: unset;
14+
margin: unset;
15+
padding: unset;
1316
list-style-type: none;
1417

1518
li {
@@ -18,39 +21,32 @@
1821
}
1922
}
2023

21-
a {
22-
display: block;
23-
}
24-
25-
/* Sections: */
26-
> ul {
27-
@media (--min-width2) {
24+
@media (--min-width2) {
25+
a {
26+
display: block;
27+
}
28+
29+
/* Sections: */
30+
> ul {
2831
display: flex;
29-
30-
> li {
31-
flex: 1 1 auto;
32-
}
3332
}
34-
}
33+
34+
> ul > li {
35+
flex: 1 1 auto;
3536

36-
> ul > li {
37-
@media (--min-width2) {
38-
/* Nav bar main items: */
3937
&:not(:last-child) {
4038
border-inline-end: 1px solid oklch(55% 0 0deg);
4139
}
4240
}
43-
}
44-
45-
/* Nav bar links: */
46-
> ul > li > a {
47-
@media (--min-width2) {
41+
42+
/* Nav bar links: */
43+
> ul > li > a {
4844
padding: var(--space1);
4945
background: linear-gradient(oklch(95% 0 0deg), oklch(88% 0 0deg));
5046
color: oklch(36% 0 0deg);
5147
text-align: center;
5248
text-decoration: none;
53-
49+
5450
&:focus,
5551
&:hover {
5652
color: var(--color-blue);
@@ -60,9 +56,28 @@
6056
}
6157

6258
/* Panels: */
63-
> ul > li > ul {
64-
display: none;
59+
60+
[popover] {
6561
position: absolute;
62+
top: anchor(bottom);
63+
left: anchor(left);
64+
margin: 0;
65+
transition-property: opacity, display, overlay;
66+
transition-duration: .3s;
67+
transition-behavior: allow-discrete;
68+
border: unset;
69+
opacity: 0;
70+
71+
&:popover-open {
72+
opacity: 1;
73+
74+
@starting-style {
75+
opacity: 0;
76+
}
77+
}
78+
}
79+
80+
> ul > li > ul {
6681
padding: var(--headernav-section-padding);
6782
column-gap: calc(var(--headernav-section-padding) * 2);
6883
column-rule: 1px solid oklch(88% 0 0deg);
@@ -126,48 +141,4 @@
126141
> ul > li li {
127142
margin-block-end: var(--headernav-item-margin);
128143
}
129-
130-
/**** Panel Toggle ****/
131-
132-
/* If c-headernav JS not supported: */
133-
134-
&.c-headernav-no-js {
135-
> ul > li:hover > ul {
136-
@media (--min-width2) {
137-
display: block;
138-
z-index: 10;
139-
}
140-
}
141-
142-
/* If :focus-within not supported. Only primary links accessible via tabkey: */
143-
144-
> ul > li a:focus {
145-
@media (--min-width2) {
146-
display: block;
147-
z-index: 10;
148-
}
149-
}
150-
151-
/* 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: */
152-
153-
> ul > li:focus-within > ul {
154-
@media (--min-width2) {
155-
display: block;
156-
z-index: 10;
157-
}
158-
}
159-
}
160-
161-
/* If c-headernav JS supported: */
162-
163-
&.c-headernav-js {
164-
/* 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: */
165-
166-
li.open ul {
167-
@media (--min-width2) {
168-
display: block;
169-
z-index: 10;
170-
}
171-
}
172-
}
173144
}

css/components/mobilenav.css

+4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
mask: url('data-url:npm:fa-light/angle-right.svg') center / 0.7rem no-repeat;
4343
}
4444
}
45+
46+
ul {
47+
display: none;
48+
}
4549
}
4650
}
4751
}

elements/3-components/headernav.hbs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
{{!-- Read notes in component config --}}
2-
<nav aria-label="global" class="c-headernav c-headernav-no-js c-mobilenav">
1+
<nav aria-label="global" class="c-headernav c-mobilenav js-headernav">
32
<ul>
43
{{> @menu-items}}
54
</ul>

elements/_template-default.hbs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!DOCTYPE html>
2-
<html lang="en">
2+
<html lang="en" class="no-popover">
33
<head>
44
<meta charset="utf-8">
55
<title>CDLIB UI</title>

elements/_template-page.hbs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!DOCTYPE html>
2-
<html lang="en">
2+
<html lang="en" class="no-popover">
33
<head>
44
<meta charset="utf-8">
55
<title>CDLIB UI</title>

js/anchor-positioning.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// ***** Anchor Positioning Polyfill ***** //
2+
3+
// Required for positioning elements that use the popover API.
4+
5+
// https://github.com/oddbird/css-anchor-positioning
6+
7+
const anchorPositioningPolyfill = async () => {
8+
const { default: polyfill } = await import('@oddbird/css-anchor-positioning/dist/css-anchor-positioning-fn.js')
9+
10+
polyfill({
11+
elements: undefined,
12+
excludeInlineStyles: false,
13+
useAnimationFrame: false
14+
})
15+
}
16+
17+
if (!('anchorName' in document.documentElement.style)) {
18+
anchorPositioningPolyfill()
19+
}

js/headernav.js

+59-56
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,65 @@
1-
// Headernav Component:
1+
// Headernav Component //
22

3+
const headerNav = document.querySelector('.js-headernav')
4+
const subNavs = headerNav.querySelectorAll(':scope > ul > li:has(> ul)')
35
const headerNavMediaQuery = window.matchMedia('(min-width: 760px)')
6+
let counter = 1
47

5-
function clickLink (event) {
6-
if (this.parentElement.classList.contains('open') === false) {
7-
this.parentElement.classList.add('open')
8-
this.setAttribute('aria-expanded', 'true')
9-
} else {
10-
this.parentElement.classList.remove('open')
11-
this.setAttribute('aria-expanded', 'false')
12-
}
13-
event.preventDefault()
14-
}
8+
if (document.querySelector('.c-headernav')) {
9+
for (const subNav of subNavs) {
10+
const subNavSiblingLink = subNav.querySelector('a')
11+
const subNavPopover = subNav.querySelector('ul')
12+
subNavPopover.popover = ''
13+
14+
// Anchor each sibling link to its popover using unique anchor names:
15+
const anchorName = '--anchor' + counter++
16+
17+
// The properties 'anchor-name' and 'position-anchor' can't be set using style.setProperty in browsers that don't support them. Instead, use setAttribute to force them to appear in so that the anchor positioning polyfill sees them:
18+
subNavSiblingLink.setAttribute('style', 'anchor-name: ' + anchorName)
19+
subNavPopover.setAttribute('style', 'position-anchor: ' + anchorName)
20+
21+
const headerNavToggles = mq => {
22+
const expandedState = () => {
23+
if (subNavPopover.matches(':popover-open')) {
24+
subNavSiblingLink.setAttribute('aria-expanded', 'true')
25+
} else {
26+
subNavSiblingLink.setAttribute('aria-expanded', 'false')
27+
}
28+
}
29+
30+
if (mq.matches) {
31+
// Only a <button> as a popover control has built-in accessiblity bindings, so set and toggle sibling link aria expanded state:
32+
subNavSiblingLink.setAttribute('aria-expanded', 'false') // initial state
1533

16-
const headerNavToggles = e => {
17-
if (document.querySelector('.c-headernav') && e.matches) {
18-
const allMenuItems = document.querySelectorAll('.c-headernav > ul > li');
19-
20-
[].forEach.call(allMenuItems, function (el) {
21-
document.querySelector('.c-headernav').classList.remove('c-headernav-no-js')
22-
document.querySelector('.c-headernav').classList.add('c-headernav-js')
23-
el.querySelector('a').setAttribute('aria-haspopup', 'true')
24-
el.querySelector('a').setAttribute('aria-expanded', 'false')
25-
26-
el.addEventListener('mouseover', function (event) {
27-
this.classList.add('open')
28-
this.querySelector('a').setAttribute('aria-expanded', 'true')
29-
})
30-
31-
el.addEventListener('mouseout', function (event) {
32-
this.classList.remove('open')
33-
this.querySelector('a').setAttribute('aria-expanded', 'false')
34-
})
35-
36-
el.querySelector('a').addEventListener('click', clickLink)
37-
});
38-
39-
[].forEach.call(allMenuItems, function (el) {
40-
el.querySelector('a').addEventListener('focus', function (event) {
41-
[].forEach.call(
42-
allMenuItems,
43-
function (el) {
44-
if (el !== this.parentElement) {
45-
el.classList.remove('open')
46-
el.querySelector('a').setAttribute('aria-expanded', 'false')
47-
}
48-
}, this
49-
)
50-
})
51-
})
52-
} else {
53-
const allMenuItems = document.querySelectorAll('.c-headernav > ul > li');
54-
55-
[].forEach.call(allMenuItems, function (el) {
56-
el.querySelector('a').removeEventListener('click', clickLink)
57-
})
34+
// Show/hide subnav on mouse pointer over and out:
35+
subNav.addEventListener('mouseover', () => {
36+
subNavPopover.showPopover()
37+
expandedState()
38+
})
39+
40+
subNav.addEventListener('mouseout', () => {
41+
subNavPopover.hidePopover()
42+
expandedState()
43+
})
44+
45+
// Toggle subnav if sibling link is clicked by keyboard return key:
46+
subNavSiblingLink.addEventListener('click', (e) => {
47+
subNavPopover.togglePopover()
48+
e.preventDefault() // disable link from going to its URL when clicked
49+
expandedState()
50+
})
51+
52+
// Hide subnav when keyboard focus leaves its popover control:
53+
subNav.addEventListener('focusout', (e) => {
54+
if (!e.currentTarget.contains(e.relatedTarget)) {
55+
subNavPopover.hidePopover()
56+
expandedState()
57+
}
58+
})
59+
}
60+
}
61+
62+
headerNavMediaQuery.addEventListener('change', headerNavToggles)
63+
headerNavToggles(headerNavMediaQuery)
5864
}
5965
}
60-
61-
headerNavMediaQuery.addEventListener('change', headerNavToggles)
62-
headerNavToggles(headerNavMediaQuery)

js/main.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import './anchor-positioning.js'
12
import './headernav.js'
23
import './mediaqueries.js'
34
import './newsreel.js'
5+
import './popover-support.js'
46
import './sidebarposts.js'
57
import './slideshow.js'
68
import './toggles.js'

js/popover-support.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// ***** Popover Support for CSS ***** //
2+
3+
if (window.HTMLElement.prototype.hasOwnProperty('popover')) { // eslint-disable-line no-prototype-builtins
4+
document.querySelector('html').classList.remove('no-popover')
5+
}

0 commit comments

Comments
 (0)