feat: add submenu opt-in for main navigation
This commit is contained in:
parent
204d05b6d7
commit
d9a5b018af
8 changed files with 280 additions and 55 deletions
|
|
@ -68,7 +68,8 @@ export const navigation = {
|
|||
ariaTop: 'Main',
|
||||
ariaBottom: 'Complementary',
|
||||
ariaPlatforms: 'Platforms',
|
||||
drawerNav: false
|
||||
drawerNav: false,
|
||||
subMenu: false
|
||||
};
|
||||
export const themeSwitch = {
|
||||
title: 'Theme',
|
||||
|
|
|
|||
|
|
@ -1,21 +1,49 @@
|
|||
<!-- toggle drawer menu in _data/meta.js -->
|
||||
<!-- toggle drawer and sub menu in _data/meta.js -->
|
||||
{% set drawerNav = meta.navigation.drawerNav %}
|
||||
{% set subMenu = meta.navigation.subMenu %}
|
||||
|
||||
<nav id="mainnav" class="mainnav" aria-label="{{ meta.navigation.ariaTop }}">
|
||||
<ul class="cluster" role="list" no-flash>
|
||||
{% for item in navigation.top %}
|
||||
<li>
|
||||
<a
|
||||
href="{{ item.url }}"
|
||||
{{
|
||||
helpers.getLinkActiveState(item.url,
|
||||
page.url)
|
||||
|
|
||||
safe
|
||||
}}
|
||||
>{{ item.text }}</a
|
||||
>
|
||||
</li>
|
||||
{% if item.submenu %}
|
||||
<li class="relative">
|
||||
<button
|
||||
data-submenu-toggle
|
||||
aria-expanded="false"
|
||||
aria-labelledby="mainnav-{{ loop.index }}"
|
||||
aria-controls="mainnav-{{ loop.index }}-sub"
|
||||
>
|
||||
{{ item.text }} {% svg "misc/chev-down" %}
|
||||
</button>
|
||||
<ul class="nav-sublist | cluster" id="mainnav-{{ loop.index }}-sub">
|
||||
{% for subitem in item.submenu %}
|
||||
<li>
|
||||
<a
|
||||
href="{{ subitem.url }}"
|
||||
{{
|
||||
helpers.getLinkActiveState(subitem.url, page.url)
|
||||
| safe
|
||||
}}
|
||||
>{{ subitem.text }}</a
|
||||
>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a
|
||||
href="{{ item.url }}"
|
||||
{{
|
||||
helpers.getLinkActiveState(item.url,
|
||||
page.url)
|
||||
|
|
||||
safe
|
||||
}}
|
||||
>{{ item.text }}</a
|
||||
>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
@ -26,7 +54,7 @@
|
|||
<!-- see also: https://kittygiraudel.com/2022/09/30/templating-in-html/ -->
|
||||
|
||||
<template id="burger-template">
|
||||
<button type="button" aria-expanded="false" aria-controls="mainnav" class="cluster">
|
||||
<button data-drawer-toggle class="cluster" type="button" aria-expanded="false" aria-controls="mainnav">
|
||||
<span>{{ meta.navigation.navLabel }}</span>
|
||||
|
||||
<svg
|
||||
|
|
@ -44,9 +72,7 @@
|
|||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
{% endif %}
|
||||
|
||||
{% if drawerNav %}
|
||||
{% css "local" %}
|
||||
{% include "css/nav-main-drawer-cls.css" %}
|
||||
{% endcss %}
|
||||
|
|
@ -55,3 +81,9 @@
|
|||
{% include "scripts/nav-drawer.js" %}
|
||||
{% endjs %}
|
||||
{% endif %}
|
||||
|
||||
{% if subMenu %}
|
||||
{% js "defer" %}
|
||||
{% include "scripts/nav-sub.js" %}
|
||||
{% endjs %}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
.mainnav:has([aria-expanded='true']) {
|
||||
.mainnav:has([data-drawer-toggle][aria-expanded='true']) {
|
||||
--nav-position: fixed;
|
||||
inset-inline-end: var(--gap);
|
||||
}
|
||||
|
|
@ -13,11 +13,13 @@
|
|||
/* configuration */
|
||||
--gutter: var(--space-xs);
|
||||
--cluster-vertical-alignment: normal;
|
||||
--cluster-wrap: wrap;
|
||||
--cluster-direction: column;
|
||||
--nav-list-background: var(--color-bg);
|
||||
--nav-list-shadow: -5px 0 11px 0 hsl(0 0% 0% / 0.2);
|
||||
--nav-list-layout: column;
|
||||
--nav-list-height: 100dvh;
|
||||
--nav-list-padding: var(--space-2xl) var(--space-s);
|
||||
--nav-list-padding-block: var(--space-2xl);
|
||||
--nav-list-padding-inline: var(--space-s);
|
||||
--nav-list-position: fixed;
|
||||
--nav-list-width: min(18rem, 100vw);
|
||||
--nav-list-visibility: hidden;
|
||||
|
|
@ -25,13 +27,11 @@
|
|||
|
||||
background: var(--nav-list-background);
|
||||
box-shadow: var(--nav-list-shadow);
|
||||
display: flex;
|
||||
flex-direction: var(--nav-list-layout);
|
||||
flex-wrap: wrap;
|
||||
block-size: var(--nav-list-height);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: var(--nav-list-padding);
|
||||
padding-block: var(--nav-list-padding-block);
|
||||
padding-inline: var(--nav-list-padding-inline);
|
||||
position: var(--nav-list-position);
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: 0;
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.mainnav ul {
|
||||
.mainnav > ul {
|
||||
--nav-list-transform: translateX(100%);
|
||||
--nav-list-opacity: 1;
|
||||
transform: var(--nav-list-transform);
|
||||
|
|
@ -62,22 +62,29 @@
|
|||
}
|
||||
}
|
||||
|
||||
.mainnav [aria-expanded='true'] + ul {
|
||||
--nav-list-visibility: visible;
|
||||
--nav-list-transform: translateX(0);
|
||||
--nav-list-opacity: 1;
|
||||
}
|
||||
|
||||
.mainnav button {
|
||||
.mainnav [data-drawer-toggle] {
|
||||
--gutter: var(--space-2xs);
|
||||
--cluster-vertical-alignment: center;
|
||||
display: var(--nav-button-display, flex);
|
||||
background: var(--color-bg);
|
||||
display: var(--nav-button-display, inline-flex);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: var(--space-2xs) 0;
|
||||
line-height: var(--leading-flat);
|
||||
}
|
||||
|
||||
.mainnav [data-drawer-toggle][aria-expanded='true'] + ul {
|
||||
--cluster-wrap: nowrap;
|
||||
--nav-list-visibility: visible;
|
||||
--nav-list-transform: translateX(0);
|
||||
--nav-list-opacity: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
body:has([data-drawer-toggle][aria-expanded='true']) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mainnav span {
|
||||
font-weight: var(--font-bold);
|
||||
text-transform: uppercase;
|
||||
|
|
@ -95,15 +102,18 @@
|
|||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.mainnav a {
|
||||
.mainnav :is(a, [data-submenu-toggle]) {
|
||||
/* configuration */
|
||||
--nav-item-background: transparent;
|
||||
--nav-item-text-color: var(--color-text);
|
||||
--nav-item-padding: var(--space-xs) var(--space-2xs);
|
||||
--nav-item-decoration-color: transparent;
|
||||
--nav-item-font-size: var(--size-step-0);
|
||||
--nav-item-font-weight: var(--font-bold);
|
||||
|
||||
background: var(--nav-item-background);
|
||||
color: var(--nav-item-text-color);
|
||||
font-size: var(--nav-item-font-size);
|
||||
padding: var(--nav-item-padding);
|
||||
display: block;
|
||||
border-radius: var(--border-radius-medium);
|
||||
|
|
@ -113,17 +123,68 @@
|
|||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
.mainnav a:where(:hover, :focus) {
|
||||
.mainnav:has(.nav-sublist) :is(a, [data-submenu-toggle]) {
|
||||
font-weight: var(--nav-item-font-weight);
|
||||
}
|
||||
|
||||
.mainnav [data-submenu-toggle] {
|
||||
gap: var(--space-2xs);
|
||||
display: flex;
|
||||
inline-size: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mainnav [data-submenu-toggle] svg {
|
||||
margin-inline-end: calc(var(--gap) - var(--nav-list-padding-inline));
|
||||
}
|
||||
|
||||
.mainnav :is(a, [data-submenu-toggle]):hover {
|
||||
--nav-item-background: transparent;
|
||||
--nav-item-text-color: var(--color-text);
|
||||
--nav-item-decoration-color: var(--color-text);
|
||||
--nav-item-decoration-color: var(--color-bg-accent-2);
|
||||
}
|
||||
|
||||
.mainnav [aria-current='page'],
|
||||
.mainnav [data-state='active'] {
|
||||
--nav-item-background: var(--color-primary);
|
||||
--nav-item-text-color: var(--color-light);
|
||||
--nav-item-decoration-color: transparent;
|
||||
--nav-item-background: var(--color-bg);
|
||||
--nav-item-text-color: var(--color-primary);
|
||||
--nav-item-decoration-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* current parent (if submenu) */
|
||||
li:has(ul a[aria-current='page']) > [data-submenu-toggle] {
|
||||
--nav-item-background: var(--color-bg);
|
||||
--nav-item-text-color: var(--color-text);
|
||||
--nav-item-decoration-color: var(--color-text);
|
||||
}
|
||||
|
||||
/* sub menu */
|
||||
|
||||
.mainnav [data-submenu-toggle][aria-expanded='false'] + ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mainnav .nav-sublist {
|
||||
--gutter: 0;
|
||||
--cluster-direction: column;
|
||||
--nav-sublist-position: relative;
|
||||
--nav-sublist-background: var(--color-bg);
|
||||
--nav-sublist-width: 100%;
|
||||
--nav-list-visibility: visible;
|
||||
--nav-list-opacity: 1;
|
||||
--nav-list-padding-block: 0 var(--space-m);
|
||||
--nav-list-padding-inline: 0;
|
||||
box-shadow: none;
|
||||
position: var(--nav-sublist-position);
|
||||
inline-size: var(--nav-sublist-width);
|
||||
block-size: auto;
|
||||
background: var(--nav-sublist-background);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.mainnav .nav-sublist a {
|
||||
--nav-item-font-weight: var(--font-regular);
|
||||
}
|
||||
|
||||
@media screen(navigation) {
|
||||
|
|
@ -132,10 +193,15 @@
|
|||
--nav-button-display: none;
|
||||
}
|
||||
|
||||
.mainnav :is(a, [data-submenu-toggle]) {
|
||||
--nav-item-font-weight: var(--font-regular);
|
||||
}
|
||||
|
||||
.mainnav ul {
|
||||
--nav-list-layout: row;
|
||||
--cluster-direction: row;
|
||||
--nav-list-position: static;
|
||||
--nav-list-padding: 0;
|
||||
--nav-list-padding-block: 0;
|
||||
--nav-list-padding-inline: 0;
|
||||
--nav-list-height: auto;
|
||||
--nav-list-width: 100%;
|
||||
--nav-list-shadow: none;
|
||||
|
|
@ -145,20 +211,42 @@
|
|||
}
|
||||
|
||||
.mainnav [aria-current='page'],
|
||||
.mainnav [data-state='active'] {
|
||||
.mainnav [data-state='active'],
|
||||
li:has(ul a[aria-current='page']) > [data-submenu-toggle] {
|
||||
--nav-item-background: transparent;
|
||||
--nav-item-text-color: var(--color-primary);
|
||||
--nav-item-decoration-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.mainnav [data-submenu-toggle] {
|
||||
inline-size: auto;
|
||||
}
|
||||
|
||||
.mainnav [data-submenu-toggle] svg {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.mainnav .nav-sublist {
|
||||
--nav-sublist-position: absolute;
|
||||
--nav-sublist-background: var(--color-bg);
|
||||
--nav-sublist-border: var(--color-text);
|
||||
--nav-sublist-width: max-content;
|
||||
--nav-list-padding-block: var(--space-xs);
|
||||
--nav-list-padding-inline: var(--space-xs);
|
||||
border: 3px solid var(--nav-sublist-border);
|
||||
inset-block-start: var(--space-xl);
|
||||
inset-inline-start: var(--space-2xs);
|
||||
}
|
||||
}
|
||||
|
||||
/* Repeat the settings to provide a different styling when JavaScript is disabled or drawerNav is set to false. The selector
|
||||
assumes that the button doesn’t exist without JS, making the list the first child within the navigation. */
|
||||
|
||||
.mainnav ul:first-child {
|
||||
--nav-list-layout: row;
|
||||
--cluster-direction: row;
|
||||
--nav-list-position: static;
|
||||
--nav-list-padding: 0;
|
||||
--nav-list-padding-block: 0;
|
||||
--nav-list-padding-inline: 0;
|
||||
--nav-list-height: auto;
|
||||
--nav-list-width: 100%;
|
||||
--nav-list-shadow: none;
|
||||
|
|
@ -178,3 +266,19 @@ assumes that the button doesn’t exist without JS, making the list the first ch
|
|||
.mainnav:has(ul:first-child) {
|
||||
--nav-position: relative;
|
||||
}
|
||||
|
||||
/* no JS fallback for sub menus */
|
||||
@media (scripting: none) {
|
||||
.mainnav ul:first-child ul.nav-sublist {
|
||||
--cluster-direction: row;
|
||||
--cluster-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (scripting: none) {
|
||||
@media screen(navigation) {
|
||||
.mainnav ul:first-child ul.nav-sublist {
|
||||
--cluster-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ Can be any acceptable flexbox alignment value.
|
|||
|
||||
.cluster {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: var(--cluster-direction, row);
|
||||
flex-wrap: var(--cluster-wrap, wrap);
|
||||
gap: var(--gutter, var(--space-s-l));
|
||||
justify-content: var(--cluster-horizontal-alignment, flex-start);
|
||||
align-items: var(--cluster-vertical-alignment, center);
|
||||
|
|
|
|||
|
|
@ -3,29 +3,30 @@
|
|||
const nav = document.querySelector('nav');
|
||||
const list = nav.querySelector('ul');
|
||||
const burgerClone = document.querySelector('#burger-template').content.cloneNode(true);
|
||||
const button = burgerClone.querySelector('button');
|
||||
const buttonDrawer = burgerClone.querySelector('button[data-drawer-toggle]');
|
||||
|
||||
list.style.setProperty('display', 'flex');
|
||||
|
||||
button.addEventListener('click', e => {
|
||||
const isOpen = button.getAttribute('aria-expanded') === 'true';
|
||||
button.setAttribute('aria-expanded', !isOpen);
|
||||
buttonDrawer.addEventListener('click', e => {
|
||||
const isOpenDrawer = buttonDrawer.getAttribute('aria-expanded') === 'true';
|
||||
buttonDrawer.setAttribute('aria-expanded', !isOpenDrawer);
|
||||
});
|
||||
|
||||
const disableMenu = () => {
|
||||
button.setAttribute('aria-expanded', false);
|
||||
buttonDrawer.setAttribute('aria-expanded', false);
|
||||
};
|
||||
|
||||
// close on escape
|
||||
nav.addEventListener('keyup', e => {
|
||||
if (e.code === 'Escape') {
|
||||
nav.addEventListener('keyup', event => {
|
||||
if (event.code === 'Escape') {
|
||||
disableMenu();
|
||||
buttonDrawer.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// close if clicked outside of event target
|
||||
document.addEventListener('click', e => {
|
||||
const isClickInsideElement = nav.contains(e.target);
|
||||
document.addEventListener('click', event => {
|
||||
const isClickInsideElement = nav.contains(event.target);
|
||||
if (!isClickInsideElement) {
|
||||
disableMenu();
|
||||
}
|
||||
|
|
|
|||
45
src/assets/scripts/bundle/nav-sub.js
Normal file
45
src/assets/scripts/bundle/nav-sub.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
const nav = document.querySelector('nav');
|
||||
const navBreakpoint = '{{ designTokens.viewports.navigation }}';
|
||||
|
||||
// toggle submenu and aria-expanded on button click
|
||||
nav.addEventListener('click', event => {
|
||||
const buttonSub = event.target.closest('button[data-submenu-toggle]');
|
||||
if (!buttonSub) return;
|
||||
|
||||
const isOpenSub = buttonSub.getAttribute('aria-expanded') === 'true';
|
||||
|
||||
// if above nav breakpoint, close any other open submenu first
|
||||
if (window.innerWidth >= navBreakpoint && !isOpenSub) {
|
||||
const openButton = nav.querySelector('button[data-submenu-toggle][aria-expanded="true"]');
|
||||
if (openButton && openButton !== buttonSub) {
|
||||
closeSubmenu(openButton);
|
||||
}
|
||||
}
|
||||
|
||||
buttonSub.setAttribute('aria-expanded', String(!isOpenSub));
|
||||
const submenu = document.getElementById(buttonSub.getAttribute('aria-controls'));
|
||||
submenu.hidden = isOpenSub;
|
||||
});
|
||||
|
||||
// close on click outside nav
|
||||
document.addEventListener('click', event => {
|
||||
const openButton = nav.querySelector('button[data-submenu-toggle][aria-expanded="true"]');
|
||||
if (openButton && !nav.contains(event.target)) {
|
||||
closeSubmenu(openButton);
|
||||
}
|
||||
});
|
||||
|
||||
// close on ESC
|
||||
document.addEventListener('keyup', event => {
|
||||
if (event.code !== 'Escape') return;
|
||||
const openButton = nav.querySelector('button[data-submenu-toggle][aria-expanded="true"]');
|
||||
if (openButton) {
|
||||
closeSubmenu(openButton);
|
||||
}
|
||||
});
|
||||
|
||||
function closeSubmenu(buttonSub) {
|
||||
const submenu = document.getElementById(buttonSub.getAttribute('aria-controls'));
|
||||
buttonSub.setAttribute('aria-expanded', 'false');
|
||||
submenu.hidden = true;
|
||||
}
|
||||
12
src/assets/svg/misc/chev-down.svg
Normal file
12
src/assets/svg/misc/chev-down.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 201 B |
|
|
@ -6,7 +6,9 @@ Edit your navigation items in `src/_data/navigation.js`.
|
|||
|
||||
You have two options for mobile navigation: by default, the navigation on small displays is converted to small pills that wrap. This does not require any additional JavaScript.
|
||||
|
||||
You can activate a drawer menu in `src/_data/meta.js`:
|
||||
**Drawer Menu**
|
||||
|
||||
You can activate a drawer menu for mobile in `src/_data/meta.js`:
|
||||
|
||||
```js
|
||||
navigation: {
|
||||
|
|
@ -25,4 +27,31 @@ Adjust your menu breakpoint in `src/_data/designTokens/viewports.json`
|
|||
"navigation": 662,
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Submenu**
|
||||
|
||||
You can activate submenus in `src/_data/meta.js`:
|
||||
|
||||
```js
|
||||
navigation: {
|
||||
// other settings
|
||||
subMenu: true,
|
||||
},
|
||||
```
|
||||
|
||||
This includes the JavaScript for the submenu functionality. Add your submenu items to `src/_data/navigation.js` using this structure:
|
||||
|
||||
```js
|
||||
{
|
||||
text: 'Unlinked parent',
|
||||
url: '#',
|
||||
submenu: [
|
||||
{
|
||||
text: 'Sub Item',
|
||||
url: '/sub-item/'
|
||||
},
|
||||
... more items
|
||||
]
|
||||
},
|
||||
```
|
||||
Loading…
Reference in a new issue