Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion themes/osi/assets/js/build/theme.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array(), 'version' => '1768336b7b2ec73f9a1a');
<?php return array('dependencies' => array(), 'version' => '0e8d0c5357eb7a01b537');
2 changes: 1 addition & 1 deletion themes/osi/assets/js/build/theme.js

Large diffs are not rendered by default.

87 changes: 62 additions & 25 deletions themes/osi/assets/js/src/theme/mega-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,34 @@ License: GPLv2
Desktop behavior for .menu-item.megamenu items: keeps aria-expanded in sync,
enforces one open panel at a time, closes on Escape or press outside, and
mirrors the open state onto the header (`is-nav-open`) as the no-:has()
fallback for the CSS opaque-header rule, with a close-delay grace period.
fallback for the CSS opaque-header rule, with close and switch grace periods.
Placeholder (href="#") triggers also toggle on click because taps don't
reliably fire mouseenter/focusin at desktop widths (iPadOS).
*/

// keep in sync with $break-nav (assets/scss/_1_settings.breakpoints.scss)
const desktopNav = window.matchMedia( '(min-width: 1200px)' );
const CLOSE_DELAY = 120;
const SWITCH_DELAY = 150;
const HOLD_LIMIT = 600;

const header = document.querySelector( '.header-main' );
const megaItems = document.querySelectorAll( '.nav-main--menu > .menu-item.megamenu' );
const MEGA_ITEM = '.nav-main--menu > .menu-item.megamenu';
const megaItems = document.querySelectorAll( MEGA_ITEM );

if ( header && megaItems.length ) {
const triggerOf = ( item ) => item.querySelector( ':scope > a' );
const findOpen = () => document.querySelector( `${ MEGA_ITEM }.is-open` );

const syncHeader = () => {
header.classList.toggle(
'is-nav-open',
Boolean( document.querySelector( '.nav-main--menu > .menu-item.megamenu.is-open' ) )
);
header.classList.toggle( 'is-nav-open', Boolean( findOpen() ) );
};

const dismiss = ( item ) => {
release();
closeItem( item );
item.classList.add( 'is-dismissed' );
document.querySelector( `${ MEGA_ITEM }:hover` )?.classList.add( 'is-dismissed' );
syncHeader();
};

Expand All @@ -48,9 +51,46 @@ if ( header && megaItems.length ) {
syncHeader();
};

let holdTimer = null;
let held = null;
let lastY = 0;
let pointerType = 'mouse';

const hold = ( action, delay ) => {
const start = Date.now();
const arm = () => {
window.clearTimeout( holdTimer );
holdTimer = window.setTimeout( () => {
held = null;
action();
}, delay );
};
held = () => {
if ( Date.now() - start < HOLD_LIMIT ) {
arm();
}
};
arm();
};

const release = () => {
window.clearTimeout( holdTimer );
held = null;
};

header.addEventListener( 'pointerover', ( event ) => {
pointerType = event.pointerType;
} );

header.addEventListener( 'mousemove', ( event ) => {
if ( held && event.clientY > lastY ) {
held();
}
lastY = event.clientY;
} );

megaItems.forEach( ( item ) => {
const trigger = triggerOf( item );
let closeTimer = null;
let suppressOpen = false;

if ( trigger ) {
Expand All @@ -62,7 +102,7 @@ if ( header && megaItems.length ) {
return;
}
item.classList.remove( 'is-dismissed' );
window.clearTimeout( closeTimer );
release();
megaItems.forEach( ( other ) => {
if ( other !== item ) {
closeItem( other );
Expand All @@ -75,16 +115,12 @@ if ( header && megaItems.length ) {
syncHeader();
};

const close = ( immediately ) => {
window.clearTimeout( closeTimer );
const doClose = () => {
closeItem( item );
syncHeader();
};
if ( immediately ) {
doClose();
const openOnHover = () => {
const panelOpen = findOpen();
if ( 'mouse' === pointerType && panelOpen && panelOpen !== item && item.offsetTop > panelOpen.offsetTop ) {
hold( open, SWITCH_DELAY );
} else {
closeTimer = window.setTimeout( doClose, CLOSE_DELAY );
open();
}
};

Expand All @@ -95,22 +131,23 @@ if ( header && megaItems.length ) {
}
event.preventDefault();
if ( item.classList.contains( 'is-open' ) ) {
close( true );
release();
closeAll();
} else {
open();
}
} );
}

item.addEventListener( 'mouseenter', open );
item.addEventListener( 'mouseenter', openOnHover );
item.addEventListener( 'mouseleave', () => {
item.classList.remove( 'is-dismissed' );
close( false );
hold( closeAll, CLOSE_DELAY );
} );
item.addEventListener( 'focusin', open );
item.addEventListener( 'focusout', ( event ) => {
if ( ! item.contains( event.relatedTarget ) ) {
close( false );
if ( ! item.contains( event.relatedTarget ) && item.classList.contains( 'is-open' ) && ! held ) {
hold( closeAll, CLOSE_DELAY );
}
} );
// returning focus to the trigger fires focusin, which would reopen the panel
Expand All @@ -128,8 +165,8 @@ if ( header && megaItems.length ) {

document.addEventListener( 'pointerdown', ( event ) => {
if (
! event.target.closest( '.nav-main--menu > .menu-item.megamenu' ) &&
document.querySelector( '.nav-main--menu > .menu-item.megamenu.is-open' )
! event.target.closest( MEGA_ITEM ) &&
findOpen()
) {
closeAll();
}
Expand All @@ -140,7 +177,7 @@ if ( header && megaItems.length ) {
if ( 'Escape' !== event.key ) {
return;
}
const openItem = document.querySelector( '.nav-main--menu > .menu-item.megamenu.is-open' );
const openItem = findOpen();
if ( openItem ) {
dismiss( openItem );
}
Expand Down
3 changes: 2 additions & 1 deletion themes/osi/assets/scss/_6_components.navigation--subnav.scss
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,8 @@ $chevronDown: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/sv

// Escape sets .is-dismissed: without this the pointer still satisfies :hover below
// and the panel stays up, since JS cannot clear a CSS hover state
&.is-dismissed:hover > ul.sub-menu {
&.is-dismissed:hover > ul.sub-menu,
.header-main.is-nav-open &:not(.is-open):hover > ul.sub-menu {
opacity: 0;
visibility: hidden;
}
Expand Down
2 changes: 1 addition & 1 deletion themes/osi/style.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion themes/osi/style.css.map

Large diffs are not rendered by default.

Loading