Skip to content

Commit 0fea07a

Browse files
authored
Merge pull request #223 from OpenSourceOrg/add/t51eng-2081-megamenu
Add a mega menu with per-item featured content to the primary navigation
2 parents bc5e55b + 074f524 commit 0fea07a

27 files changed

Lines changed: 1605 additions & 563 deletions

‎themes/osi/assets/css/editor-style.css‎

Lines changed: 6 additions & 46 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎themes/osi/assets/css/editor-style.css.map‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<?php return array('dependencies' => array(), 'version' => '48775f60a0024fbe4d0a');
1+
<?php return array('dependencies' => array(), 'version' => '42979785231fcdbb3f33');

‎themes/osi/assets/js/build/theme.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
Name: Mega Menu
3+
Author: Team 51
4+
Version: 0.1
5+
License: GPLv2
6+
7+
Desktop behavior for .menu-item.megamenu items: keeps aria-expanded in sync,
8+
enforces one open panel at a time, closes on Escape or press outside, and
9+
mirrors the open state onto the header (`is-nav-open`) as the no-:has()
10+
fallback for the CSS opaque-header rule, with a close-delay grace period.
11+
Placeholder (href="#") triggers also toggle on click because taps don't
12+
reliably fire mouseenter/focusin at desktop widths (iPadOS).
13+
*/
14+
15+
// keep in sync with $break-nav (assets/scss/_1_settings.breakpoints.scss)
16+
const desktopNav = window.matchMedia( '(min-width: 1200px)' );
17+
const CLOSE_DELAY = 120;
18+
19+
const header = document.querySelector( '.header-main' );
20+
const megaItems = document.querySelectorAll( '.nav-main--menu > .menu-item.megamenu' );
21+
22+
if ( header && megaItems.length ) {
23+
const triggerOf = ( item ) => item.querySelector( ':scope > a' );
24+
25+
const syncHeader = () => {
26+
header.classList.toggle(
27+
'is-nav-open',
28+
Boolean( document.querySelector( '.nav-main--menu > .menu-item.megamenu.is-open' ) )
29+
);
30+
};
31+
32+
const dismiss = ( item ) => {
33+
closeItem( item );
34+
item.classList.add( 'is-dismissed' );
35+
syncHeader();
36+
};
37+
38+
const closeItem = ( item ) => {
39+
item.classList.remove( 'is-open' );
40+
const trigger = triggerOf( item );
41+
if ( trigger ) {
42+
trigger.setAttribute( 'aria-expanded', 'false' );
43+
}
44+
};
45+
46+
const closeAll = () => {
47+
megaItems.forEach( closeItem );
48+
syncHeader();
49+
};
50+
51+
megaItems.forEach( ( item ) => {
52+
const trigger = triggerOf( item );
53+
let closeTimer = null;
54+
let suppressOpen = false;
55+
56+
if ( trigger ) {
57+
trigger.setAttribute( 'aria-expanded', 'false' );
58+
}
59+
60+
const open = () => {
61+
if ( suppressOpen || ! desktopNav.matches ) {
62+
return;
63+
}
64+
item.classList.remove( 'is-dismissed' );
65+
window.clearTimeout( closeTimer );
66+
megaItems.forEach( ( other ) => {
67+
if ( other !== item ) {
68+
closeItem( other );
69+
}
70+
} );
71+
item.classList.add( 'is-open' );
72+
if ( trigger ) {
73+
trigger.setAttribute( 'aria-expanded', 'true' );
74+
}
75+
syncHeader();
76+
};
77+
78+
const close = ( immediately ) => {
79+
window.clearTimeout( closeTimer );
80+
const doClose = () => {
81+
closeItem( item );
82+
syncHeader();
83+
};
84+
if ( immediately ) {
85+
doClose();
86+
} else {
87+
closeTimer = window.setTimeout( doClose, CLOSE_DELAY );
88+
}
89+
};
90+
91+
if ( trigger && '#' === trigger.getAttribute( 'href' ) ) {
92+
trigger.addEventListener( 'click', ( event ) => {
93+
if ( ! desktopNav.matches ) {
94+
return;
95+
}
96+
event.preventDefault();
97+
if ( item.classList.contains( 'is-open' ) ) {
98+
close( true );
99+
} else {
100+
open();
101+
}
102+
} );
103+
}
104+
105+
item.addEventListener( 'mouseenter', open );
106+
item.addEventListener( 'mouseleave', () => {
107+
item.classList.remove( 'is-dismissed' );
108+
close( false );
109+
} );
110+
item.addEventListener( 'focusin', open );
111+
item.addEventListener( 'focusout', ( event ) => {
112+
if ( ! item.contains( event.relatedTarget ) ) {
113+
close( false );
114+
}
115+
} );
116+
// returning focus to the trigger fires focusin, which would reopen the panel
117+
item.addEventListener( 'keydown', ( event ) => {
118+
if ( 'Escape' === event.key && item.classList.contains( 'is-open' ) ) {
119+
suppressOpen = true;
120+
dismiss( item );
121+
if ( trigger ) {
122+
trigger.focus();
123+
}
124+
suppressOpen = false;
125+
}
126+
} );
127+
} );
128+
129+
document.addEventListener( 'pointerdown', ( event ) => {
130+
if (
131+
! event.target.closest( '.nav-main--menu > .menu-item.megamenu' ) &&
132+
document.querySelector( '.nav-main--menu > .menu-item.megamenu.is-open' )
133+
) {
134+
closeAll();
135+
}
136+
} );
137+
138+
// a panel opened by hover holds focus nowhere, so Escape has to be caught globally
139+
document.addEventListener( 'keydown', ( event ) => {
140+
if ( 'Escape' !== event.key ) {
141+
return;
142+
}
143+
const openItem = document.querySelector( '.nav-main--menu > .menu-item.megamenu.is-open' );
144+
if ( openItem ) {
145+
dismiss( openItem );
146+
}
147+
} );
148+
149+
desktopNav.addEventListener( 'change', ( event ) => {
150+
if ( ! event.matches ) {
151+
closeAll();
152+
}
153+
} );
154+
}

‎themes/osi/assets/js/src/theme/mobile-menu-toggle.js‎

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,69 @@ License: GPLv2
1717
http://www.gnu.org/licenses/gpl-2.0.html
1818
*/
1919

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

23+
function toggleSubmenu($button) {
24+
var $item = $button.closest('.menu-item-has-children');
25+
var $submenu = $button.siblings('.sub-menu').first();
26+
var opening = $submenu.hasClass('menu-collapse');
27+
28+
// accordion: opening an item closes its open siblings at the same level
29+
if (opening) {
30+
$item.siblings('.tab-active').each(function() {
31+
toggleSubmenu($(this).children('.menu-toggle').first());
32+
});
33+
}
34+
35+
$submenu.toggleClass('menu-collapse', !opening);
36+
$item.toggleClass('tab-active', opening);
37+
$item.parent().closest('.sub-menu').toggleClass('can-overflow', opening);
38+
if (!opening) {
39+
$item.find('.sub-menu').addClass('menu-collapse').removeClass('can-overflow');
40+
$item.find('.tab-active').removeClass('tab-active');
41+
$item.find('.menu-toggle').removeClass('menu-toggle-active').attr('aria-expanded', 'false');
42+
$item.find('a[aria-expanded]').attr('aria-expanded', 'false');
43+
}
44+
$button.toggleClass('menu-toggle-active', opening).attr('aria-expanded', String(opening));
45+
$button.siblings('a[aria-expanded]').first().attr('aria-expanded', String(opening));
46+
}
47+
2248
$('.sub-menu').addClass('menu-collapse');
23-
$('.menu-item-has-children > a').append('<button aria-label="Toggle Submenu" class="menu-toggle"></button>');
49+
$('.menu-item-has-children > a').each(function( index ) {
50+
var $link = $(this);
51+
var $submenu = $link.siblings('.sub-menu').first();
2452

25-
$('.menu-toggle').on('click', function(e) {
53+
if (!$submenu.length) {
54+
return;
55+
}
2656

27-
e.preventDefault();
57+
var id = $submenu.attr('id') || 'osi-submenu-' + index;
58+
$submenu.attr('id', id);
59+
var $button = $('<button class="menu-toggle"></button>')
60+
.attr({
61+
'aria-label': 'Toggle submenu for ' + $link.text().trim(),
62+
'aria-expanded': 'false',
63+
'aria-controls': id
64+
})
65+
.insertAfter($link);
2866

29-
if ($(this).parent().next('.sub-menu').hasClass('menu-collapse')) {
30-
$(this).parent().next('.sub-menu').removeClass('menu-collapse');
31-
$(this).parent().parent('.menu-item-has-children').addClass('tab-active');
32-
$(this).parent().parent().parent('.sub-menu').addClass('can-overflow');
33-
$(this).addClass('menu-toggle-active');
34-
e.stopPropagation();
35-
} else {
36-
$(this).parent().next('.sub-menu').addClass('menu-collapse');
37-
$(this).parent().parent('.menu-item-has-children').removeClass('tab-active');
38-
$(this).parent().parent().parent('.sub-menu').removeClass('can-overflow');
39-
$(this).parent().parent().find('.sub-menu').removeClass('can-overflow');
40-
$(this).removeClass('menu-toggle-active');
41-
e.stopPropagation();
42-
}
67+
// the mobile menu's whole row is the disclosure: the link toggles instead of
68+
// navigating, and the caret stays a visual/tap affordance outside the tab order
69+
if ($link.closest('.nav-mobile--menu').length) {
70+
$link.attr('aria-expanded', 'false');
71+
$button.attr({ 'aria-hidden': 'true', tabindex: '-1' });
72+
$link.on('click', function(e) {
73+
e.preventDefault();
74+
toggleSubmenu($button);
75+
});
76+
}
77+
});
78+
79+
$('.menu-toggle').on('click', function(e) {
80+
e.preventDefault();
81+
e.stopPropagation();
82+
toggleSubmenu($(this));
4383
});
4484

4585
})( jQuery );

0 commit comments

Comments
 (0)