diff --git a/css/components/headernav.css b/css/components/headernav.css index f3dd3ec..0d07064 100644 --- a/css/components/headernav.css +++ b/css/components/headernav.css @@ -1,15 +1,18 @@ /***** Header Nav Component *****/ -/* Read notes in component config */ - .c-headernav { --headernav-section-padding: var(--space3); --headernav-item-margin: var(--space3); - position: relative; + .no-popover & { + ul ul { + display: none; + } + } ul { - all: unset; + margin: unset; + padding: unset; list-style-type: none; li { @@ -18,39 +21,35 @@ } } - a { - display: block; - } - - /* Sections: */ - > ul { - @media (--min-width2) { + @media (--min-width2) { + a { + display: block; + } + + /* Sections: */ + > ul { display: flex; - + > li { flex: 1 1 auto; } } - } - - > ul > li { - @media (--min-width2) { + + > ul > li { /* Nav bar main items: */ &:not(:last-child) { border-inline-end: 1px solid oklch(55% 0 0deg); } } - } - - /* Nav bar links: */ - > ul > li > a { - @media (--min-width2) { + + /* Nav bar links: */ + > ul > li > a { padding: var(--space1); background: linear-gradient(oklch(95% 0 0deg), oklch(88% 0 0deg)); color: oklch(36% 0 0deg); text-align: center; text-decoration: none; - + &:focus, &:hover { color: var(--color-blue); @@ -60,9 +59,31 @@ } /* Panels: */ - > ul > li > ul { - display: none; + + [popover] { + /* position-area: block-end span-inline-end; */ + /* position-try-fallbacks: flip-inline; */ + /* inset: unset; */ position: absolute; + top: anchor(bottom); + left: anchor(left); + 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 { padding: var(--headernav-section-padding); column-gap: calc(var(--headernav-section-padding) * 2); column-rule: 1px solid oklch(88% 0 0deg); @@ -126,48 +147,4 @@ > 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: */ - - li.open ul { - @media (--min-width2) { - display: block; - z-index: 10; - } - } - } } diff --git a/css/components/mobilenav.css b/css/components/mobilenav.css index 53e4ea4..ae6d945 100644 --- a/css/components/mobilenav.css +++ b/css/components/mobilenav.css @@ -42,6 +42,10 @@ mask: url('data-url:npm:fa-light/angle-right.svg') center / 0.7rem no-repeat; } } + + ul { + display: none; + } } } } diff --git a/elements/3-components/headernav.hbs b/elements/3-components/headernav.hbs index c8a8914..4f289e3 100644 --- a/elements/3-components/headernav.hbs +++ b/elements/3-components/headernav.hbs @@ -1,5 +1,4 @@ -{{!-- 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> diff --git a/elements/_template-default.hbs b/elements/_template-default.hbs index 9d52d25..362b8f9 100644 --- a/elements/_template-default.hbs +++ b/elements/_template-default.hbs @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="en" class="no-popover"> <head> <meta charset="utf-8"> <title>CDLIB UI</title> diff --git a/elements/_template-page.hbs b/elements/_template-page.hbs index 58809d3..e872885 100644 --- a/elements/_template-page.hbs +++ b/elements/_template-page.hbs @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="en" class="no-popover"> <head> <meta charset="utf-8"> <title>CDLIB UI</title> diff --git a/js/anchor-positioning.js b/js/anchor-positioning.js new file mode 100644 index 0000000..2439ab8 --- /dev/null +++ b/js/anchor-positioning.js @@ -0,0 +1,19 @@ +// ***** Anchor Positioning Polyfill ***** // + +// Required for positioning elements that use the popover API. + +// https://github.com/oddbird/css-anchor-positioning + +const anchorPositioningPolyfill = async () => { + const { default: polyfill } = await import('@oddbird/css-anchor-positioning/dist/css-anchor-positioning-fn.js') + + polyfill({ + elements: undefined, + excludeInlineStyles: false, + useAnimationFrame: false + }) +} + +if (!('anchorName' in document.documentElement.style)) { + anchorPositioningPolyfill() +} diff --git a/js/headernav.js b/js/headernav.js index f456dd8..2aa1928 100644 --- a/js/headernav.js +++ b/js/headernav.js @@ -1,62 +1,65 @@ -// Headernav Component: +// Headernav Component // +const headerNav = document.querySelector('.js-headernav') +const subNavs = headerNav.querySelectorAll(':scope > ul > li:has(> ul)') const headerNavMediaQuery = window.matchMedia('(min-width: 760px)') +let counter = 1 -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() -} +if (document.querySelector('.c-headernav')) { + 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++ + + // 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: + subNavSiblingLink.setAttribute('style', 'anchor-name: ' + anchorName) + subNavPopover.setAttribute('style', 'position-anchor: ' + anchorName) + + const headerNavToggles = mq => { + const expandedState = () => { + if (subNavPopover.matches(':popover-open')) { + subNavSiblingLink.setAttribute('aria-expanded', 'true') + } else { + subNavSiblingLink.setAttribute('aria-expanded', 'false') + } + } + + if (mq.matches) { + // Only a <button> as a popover control has built-in accessiblity bindings, so set and toggle sibling link aria expanded state: + subNavSiblingLink.setAttribute('aria-expanded', 'false') // initial state -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') - }) - - el.addEventListener('mouseout', function (event) { - this.classList.remove('open') - this.querySelector('a').setAttribute('aria-expanded', 'false') - }) - - 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 - ) - }) - }) - } else { - const allMenuItems = document.querySelectorAll('.c-headernav > ul > li'); - - [].forEach.call(allMenuItems, function (el) { - el.querySelector('a').removeEventListener('click', clickLink) - }) + // Show/hide subnav on mouse pointer over and out: + subNav.addEventListener('mouseover', () => { + subNavPopover.showPopover() + expandedState() + }) + + subNav.addEventListener('mouseout', () => { + subNavPopover.hidePopover() + expandedState() + }) + + // Toggle subnav if sibling link is clicked by keyboard return key: + subNavSiblingLink.addEventListener('click', (e) => { + subNavPopover.togglePopover() + e.preventDefault() // disable link from going to its URL when clicked + expandedState() + }) + + // Hide subnav when keyboard focus leaves its popover control: + subNav.addEventListener('focusout', (e) => { + if (!e.currentTarget.contains(e.relatedTarget)) { + subNavPopover.hidePopover() + expandedState() + } + }) + } + } + + headerNavMediaQuery.addEventListener('change', headerNavToggles) + headerNavToggles(headerNavMediaQuery) } } - -headerNavMediaQuery.addEventListener('change', headerNavToggles) -headerNavToggles(headerNavMediaQuery) diff --git a/js/main.js b/js/main.js index 8da05c8..842d4e2 100644 --- a/js/main.js +++ b/js/main.js @@ -1,6 +1,8 @@ +import './anchor-positioning.js' import './headernav.js' import './mediaqueries.js' import './newsreel.js' +import './popover-support.js' import './sidebarposts.js' import './slideshow.js' import './toggles.js' diff --git a/js/popover-support.js b/js/popover-support.js new file mode 100644 index 0000000..2360e50 --- /dev/null +++ b/js/popover-support.js @@ -0,0 +1,5 @@ +// ***** Popover Support for CSS ***** // + +if (window.HTMLElement.prototype.hasOwnProperty('popover')) { // eslint-disable-line no-prototype-builtins + document.querySelector('html').classList.remove('no-popover') +} diff --git a/package-lock.json b/package-lock.json index ddf8953..d00672b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "cdlib-ui", "version": "2.0.0", "license": "MIT", + "dependencies": { + "@oddbird/css-anchor-positioning": "^0.3.1" + }, "devDependencies": { "@faker-js/faker": "^8.4.1", "@fortawesome/fontawesome-pro": "^6.6.0", @@ -35,6 +38,10 @@ "stylelint": "^16.8.1", "stylelint-config-property-sort-order-smacss": "^10.0.0", "stylelint-config-standard": "^36.0.1" + }, + "engines": { + "node": "~20", + "npm": "~10" } }, "node_modules/@allmarkedup/fang": { @@ -319,6 +326,28 @@ "npm": ">=6.14.13" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" + }, "node_modules/@fortawesome/fontawesome-pro": { "version": "6.6.0", "resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-pro/-/6.6.0/fontawesome-pro-6.6.0.tgz", @@ -982,6 +1011,51 @@ "node": ">= 8" } }, + "node_modules/@oddbird/css-anchor-positioning": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@oddbird/css-anchor-positioning/-/css-anchor-positioning-0.3.1.tgz", + "integrity": "sha512-qjnCAoUkL7TpB5/QIUD+vqFWRUMa+HvDMT/PU9903jXZA3H9qc8Egtfj2JUh4KJN7RKSJ4ox5uLCarF57ACzGw==", + "dependencies": { + "@floating-ui/dom": "^1.6.11", + "@types/css-tree": "^2.3.8", + "css-tree": "^3.0.0", + "nanoid": "^5.0.7" + } + }, + "node_modules/@oddbird/css-anchor-positioning/node_modules/css-tree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.0.1.tgz", + "integrity": "sha512-8Fxxv+tGhORlshCdCwnNJytvlvq46sOLSYEx2ZIGurahWvMucSRnyjPA3AmrMq4VPRYbHVpWj5VkiVasrM2H4Q==", + "dependencies": { + "mdn-data": "2.12.1", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@oddbird/css-anchor-positioning/node_modules/mdn-data": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.1.tgz", + "integrity": "sha512-rsfnCbOHjqrhWxwt5/wtSLzpoKTzW7OXdT5lLOIH1OTYhWu9rRJveGq0sKvDZODABH7RX+uoR+DYcpFnq4Tf6Q==" + }, + "node_modules/@oddbird/css-anchor-positioning/node_modules/nanoid": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.8.tgz", + "integrity": "sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/@one-ini/wasm": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", @@ -3112,6 +3186,11 @@ "@types/node": "*" } }, + "node_modules/@types/css-tree": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/css-tree/-/css-tree-2.3.8.tgz", + "integrity": "sha512-zABG3nI2UENsx7AQv63tI5/ptoAG/7kQR1H0OvG+WTWYHOR5pfAT3cGgC8SdyCrgX/TTxJBZNmx82IjCXs1juQ==" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -15057,7 +15136,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, "engines": { "node": ">=0.10.0" } diff --git a/package.json b/package.json index bdc876e..b48eb54 100644 --- a/package.json +++ b/package.json @@ -115,5 +115,8 @@ } ] } + }, + "dependencies": { + "@oddbird/css-anchor-positioning": "^0.3.1" } }