diff --git a/src/_data/meta.js b/src/_data/meta.js index 6d7cda0..c71faa8 100644 --- a/src/_data/meta.js +++ b/src/_data/meta.js @@ -68,7 +68,8 @@ export const navigation = { ariaTop: 'Main', ariaBottom: 'Complementary', ariaPlatforms: 'Platforms', - drawerNav: false + drawerNav: false, + subMenu: false }; export const themeSwitch = { title: 'Theme', diff --git a/src/_includes/partials/main-nav.njk b/src/_includes/partials/main-nav.njk index dfc18e2..9d25bc0 100644 --- a/src/_includes/partials/main-nav.njk +++ b/src/_includes/partials/main-nav.njk @@ -1,21 +1,49 @@ - + {% set drawerNav = meta.navigation.drawerNav %} +{% set subMenu = meta.navigation.subMenu %} @@ -26,7 +54,7 @@ -{% 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 %} diff --git a/src/assets/css/global/blocks/main-nav.css b/src/assets/css/global/blocks/main-nav.css index 3d73393..f8033fe 100644 --- a/src/assets/css/global/blocks/main-nav.css +++ b/src/assets/css/global/blocks/main-nav.css @@ -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; + } + } +} diff --git a/src/assets/css/global/compositions/cluster.css b/src/assets/css/global/compositions/cluster.css index ca5dc9b..59ae0bc 100644 --- a/src/assets/css/global/compositions/cluster.css +++ b/src/assets/css/global/compositions/cluster.css @@ -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); diff --git a/src/assets/scripts/bundle/nav-drawer.js b/src/assets/scripts/bundle/nav-drawer.js index 4372759..7778a16 100644 --- a/src/assets/scripts/bundle/nav-drawer.js +++ b/src/assets/scripts/bundle/nav-drawer.js @@ -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(); } diff --git a/src/assets/scripts/bundle/nav-sub.js b/src/assets/scripts/bundle/nav-sub.js new file mode 100644 index 0000000..257bda7 --- /dev/null +++ b/src/assets/scripts/bundle/nav-sub.js @@ -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; +} diff --git a/src/assets/svg/misc/chev-down.svg b/src/assets/svg/misc/chev-down.svg new file mode 100644 index 0000000..3173c01 --- /dev/null +++ b/src/assets/svg/misc/chev-down.svg @@ -0,0 +1,12 @@ + + + diff --git a/src/docs/navigation.md b/src/docs/navigation.md index c1269c6..55d9cb2 100644 --- a/src/docs/navigation.md +++ b/src/docs/navigation.md @@ -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 + ] +}, ``` \ No newline at end of file