Skip to content
Open
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' => '48775f60a0024fbe4d0a');
<?php return array('dependencies' => array(), 'version' => '37ad048f908f1e2ac736');
2 changes: 1 addition & 1 deletion themes/osi/assets/js/build/theme.js

Large diffs are not rendered by default.

130 changes: 130 additions & 0 deletions themes/osi/assets/js/src/theme/mega-menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
Name: Mega Menu
Author: Team 51
Version: 0.1
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.
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 header = document.querySelector( '.header-main' );
const megaItems = document.querySelectorAll( '.nav-main--menu > .menu-item.megamenu' );

if ( header && megaItems.length ) {
const triggerOf = ( item ) => item.querySelector( ':scope > a' );

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

const closeItem = ( item ) => {
item.classList.remove( 'is-open' );
const trigger = triggerOf( item );
if ( trigger ) {
trigger.setAttribute( 'aria-expanded', 'false' );
}
};

const closeAll = () => {
megaItems.forEach( closeItem );
syncHeader();
};

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

if ( trigger ) {
trigger.setAttribute( 'aria-haspopup', 'true' );
trigger.setAttribute( 'aria-expanded', 'false' );
}

const open = () => {
if ( ! desktopNav.matches ) {
return;
}
window.clearTimeout( closeTimer );
megaItems.forEach( ( other ) => {
if ( other !== item ) {
closeItem( other );
}
} );
item.classList.add( 'is-open' );
if ( trigger ) {
trigger.setAttribute( 'aria-expanded', 'true' );
}
syncHeader();
};

const close = ( immediately ) => {
window.clearTimeout( closeTimer );
const doClose = () => {
closeItem( item );
syncHeader();
};
if ( immediately ) {
doClose();
} else {
closeTimer = window.setTimeout( doClose, CLOSE_DELAY );
}
};

if ( trigger && '#' === trigger.getAttribute( 'href' ) ) {
trigger.addEventListener( 'click', ( event ) => {
if ( ! desktopNav.matches ) {
return;
}
event.preventDefault();
if ( item.classList.contains( 'is-open' ) ) {
close( true );
} else {
open();
}
} );
}

item.addEventListener( 'mouseenter', open );
item.addEventListener( 'mouseleave', () => close( false ) );
item.addEventListener( 'focusin', open );
item.addEventListener( 'focusout', ( event ) => {
if ( ! item.contains( event.relatedTarget ) ) {
close( false );
}
} );
item.addEventListener( 'keydown', ( event ) => {
if ( 'Escape' === event.key && item.classList.contains( 'is-open' ) ) {
close( true );
if ( trigger ) {
trigger.focus();
}
}
} );
} );

document.addEventListener( 'pointerdown', ( event ) => {
if (
! event.target.closest( '.nav-main--menu > .menu-item.megamenu' ) &&
document.querySelector( '.nav-main--menu > .menu-item.megamenu.is-open' )
) {
closeAll();
}
} );

desktopNav.addEventListener( 'change', ( event ) => {
if ( ! event.matches ) {
closeAll();
}
} );
}
49 changes: 34 additions & 15 deletions themes/osi/assets/js/src/theme/mobile-menu-toggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,48 @@ License: GPLv2
http://www.gnu.org/licenses/gpl-2.0.html
*/

// toggle buttons are inserted as siblings of the links: a <button> inside an <a> is invalid markup
(function($) {

$('.sub-menu').addClass('menu-collapse');
$('.menu-item-has-children > a').append('<button aria-label="Toggle Submenu" class="menu-toggle"></button>');
$('.menu-item-has-children > a').each(function( index ) {
var $link = $(this);
var $submenu = $link.siblings('.sub-menu').first();

if (!$submenu.length) {
return;
}

var id = $submenu.attr('id') || 'osi-submenu-' + index;
$submenu.attr('id', id);
$('<button class="menu-toggle"></button>')
.attr({
'aria-label': 'Toggle submenu for ' + $link.text().trim(),
'aria-expanded': 'false',
'aria-controls': id
})
.insertAfter($link);
});

$('.menu-toggle').on('click', function(e) {

e.preventDefault();
e.stopPropagation();

var $button = $(this);
var $item = $button.closest('.menu-item-has-children');
var $submenu = $button.siblings('.sub-menu').first();
var opening = $submenu.hasClass('menu-collapse');

if ($(this).parent().next('.sub-menu').hasClass('menu-collapse')) {
$(this).parent().next('.sub-menu').removeClass('menu-collapse');
$(this).parent().parent('.menu-item-has-children').addClass('tab-active');
$(this).parent().parent().parent('.sub-menu').addClass('can-overflow');
$(this).addClass('menu-toggle-active');
e.stopPropagation();
} else {
$(this).parent().next('.sub-menu').addClass('menu-collapse');
$(this).parent().parent('.menu-item-has-children').removeClass('tab-active');
$(this).parent().parent().parent('.sub-menu').removeClass('can-overflow');
$(this).parent().parent().find('.sub-menu').removeClass('can-overflow');
$(this).removeClass('menu-toggle-active');
e.stopPropagation();
}
$submenu.toggleClass('menu-collapse', !opening);
$item.toggleClass('tab-active', opening);
$item.parent().closest('.sub-menu').toggleClass('can-overflow', opening);
if (!opening) {
$item.find('.sub-menu').addClass('menu-collapse').removeClass('can-overflow');
$item.find('.tab-active').removeClass('tab-active');
$item.find('.menu-toggle').removeClass('menu-toggle-active').attr('aria-expanded', 'false');
}
$button.toggleClass('menu-toggle-active', opening).attr('aria-expanded', String(opening));
});

})( jQuery );
4 changes: 2 additions & 2 deletions themes/osi/assets/js/src/theme/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
return;
}

button = container.getElementsByTagName( 'button' )[0];
if ( 'undefined' === typeof button ) {
button = container.querySelector( 'button:not(.menu-toggle)' );
if ( ! button ) {
return;
}

Expand Down
1 change: 1 addition & 0 deletions themes/osi/assets/js/src/theme/theme.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import './block-styles.js';
import './header-resize.js';
import './mobile-menu-toggle.js';
import './mega-menu.js';
import './navigation.js';
import './sortable-table.js';
import './toggle-sections.js';
Expand Down
27 changes: 27 additions & 0 deletions themes/osi/assets/scss/_6_components.header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,33 @@
clear: right;
padding: 0;
}
// white header while a panel is open; on .header--inner because a stacking-
// context root's own background paints below its negative-z ::before dim.
// Kept as separate rules: an unparseable :has() would drop the JS fallback
.header-main.is-nav-open .header--inner {
background-color: $Nwhite;
}
.header-main:has(.menu-item.megamenu:hover) .header--inner {
background-color: $Nwhite;
}
// page dim behind an open panel: z-index -1 inside the header's stacking
// context paints above page content but below the header and panel
.header-main.is-nav-open::before {
background: rgba(0, 0, 0, .35);
content: '';
inset: 0;
pointer-events: none;
position: fixed;
z-index: -1;
}
.header-main:has(.menu-item.megamenu:hover)::before {
background: rgba(0, 0, 0, .35);
content: '';
inset: 0;
pointer-events: none;
position: fixed;
z-index: -1;
}
.header-main-small {
background-color: $Nwhite;
.nav-main {
Expand Down
Loading
Loading