Skip to content

Instantly share code, notes, and snippets.

@kgcreative
Last active August 14, 2017 16:57
Show Gist options
  • Save kgcreative/88aad8c13d1bad38bab07b00cc26f2ea to your computer and use it in GitHub Desktop.
Save kgcreative/88aad8c13d1bad38bab07b00cc26f2ea to your computer and use it in GitHub Desktop.
WIP - Accessible, responsive navigation with mobile toggle
'use strict';
// a11nav Module
var a11ynav = (function (e) {
// Config variables are specifically javascript objects (not jQuery) - We need these when calling Mousetrap bindings.
var config = {
menuToggle: document.getElementById('menu-toggle')
, main: document.querySelector('main, #main')
, primaryNavContainer: document.getElementById('primary-nav')
, primaryNav: document.querySelector('#primary-nav > .nav-menu')
, subNavToggle: document.querySelector('a[aria-controls]')
, Mousetrap: require('mousetrap')
}
, bindEscPrimaryNav = null
, bindEscSubnav = null
;
//private methods
// Main Nav
var openPrimaryNav = function(e) {
$(config.menuToggle).attr('aria-expanded', true).html('Close');
$(config.main).addClass('nav-open');
$(config.primaryNavContainer).attr('aria-expanded', true).addClass('open');
$(config.primaryNav).find('> li:first-of-type > a').focus();
// We bind 'esc' to close the menu.
bindEscPrimaryNav = new config.Mousetrap().bind('esc', function(e) {
closePrimaryNav();
$(config.menuToggle).focus();
});
};
var closePrimaryNav = function(e) {
$(config.primaryNavContainer).attr('aria-expanded', false).removeClass('open');
$(config.menuToggle).attr('aria-expanded', false).html('Menu').focus();
$(config.main).removeClass('nav-open');
bindEscPrimaryNav.unbind('esc');
};
var togglePrimaryNav = function(e) {
e.preventDefault();
var el = this;
if ( $(el).attr('aria-expanded') == 'true' ) {
closePrimaryNav();
} else {
openPrimaryNav();
}
};
// SubNav
var openSubnav = function(subnavToggle) {
var $target = $('#' + $(subnavToggle).attr('aria-controls'))
, $subnavToggle = $(subnavToggle)
;
// First close any open subnavs
$(config.primaryNav).find('>li > a.open').each(function() {
closeSubnav(this);
});
// Open the selected subnav
$subnavToggle.addClass('open');
$target.attr('aria-expanded', true).find('>li:first-of-type > a').focus();
// If we opened the menu via the mobile trigger,
// then we unbind the primary nav event listener
if (bindEscPrimaryNav) {
bindEscPrimaryNav.unbind('esc');
}
// we bind a new unique listener to close the subnav
bindEscSubnav = new config.Mousetrap().bind('esc', function() {
closeSubnav(subnavToggle);
$subnavToggle.focus();
});
// Then we bind click outside of the subnav element to close the subnav
$target.on('click.closeSelf', function(e) {
e.stopPropagation();
console.log('1. click');
});
$(subnavToggle).on('click.closeSelf', function(e) {
e.stopPropagation();
console.log('2. click');
});
$(document).on('click.closeSelf', function(){
if ($target.attr('aria-expanded') == 'true') {
closeSubnav(subnavToggle);
}
console.log('3. click');
});
};
var closeSubnav = function(subnavToggle) {
var $target = $('#' + $(subnavToggle).attr('aria-controls'))
$(subnavToggle).removeClass('open');
$target.attr('aria-expanded', false);
bindEscSubnav.unbind('esc');
if (bindEscPrimaryNav != "initial") {
bindEscPrimaryNav = new config.Mousetrap().bind('esc', function () {
closePrimaryNav();
$(config.menuToggle).focus();
});
}
$(document).off('click.closeSelf');
$target.off('click.closeSelf');
$(subnavToggle).off('click.closeSelf');
}
var toggleSubnav = function(e) {
e.stopPropagation();
e.preventDefault();
if ( $(this).hasClass('open')) {
closeSubnav(this);
} else {
openSubnav(this);
}
};
return {
// aliases to private functions
togglePrimaryNav: togglePrimaryNav,
toggleSubnav: toggleSubnav,
// todo: Figure out how to expose a public configuration api
}
})();
// Bind primary Navigation Button
$('#menu-toggle').click(a11ynav.togglePrimaryNav);
// We find all the sub nav toggles
$('#primary-nav .nav-menu a[aria-controls]').each(function(e) {
var $this = $(this);
$this.click(a11ynav.toggleSubnav);
});
console.log('%c ✔ Accessible Navigation', 'color: green');
body::before {
display: none;
content: "unknown";
@media screen {
content: "screen";
}
@media print {
content: "print";
}
@include grid-media($media-xs-only) {
content: "xs";
}
@include grid-media($media-sm-only) {
content: "sm";
}
@include grid-media($media-md-only) {
content: "md";
}
@include grid-media($media-lg-only) {
content: "lg";
}
@include grid-media($media-xl) {
content: "xl";
}
}
'use strict';
exports.breakpointDetect = function() {
var body = document.querySelector('body')
, result = getComputedStyle(body, ':before').content;
// returns contents without quotes
return result.replace(/\"/g, "");
}
exports.isMobileMenu = function() {
var currentBreakpoint = exports.breakpointDetect()
return ( currentBreakpoint === 'xs' || currentBreakpoint === 'sm' ||
currentBreakpoint === 'md' )
}
'use strict';
var feature = require('./features')
;
function resizeEnd() {
var isMobileMenu = feature.isMobileMenu()
, primaryNav = document.querySelector('#primary-nav > .nav-menu')
, primaryNavContainer = document.getElementById('primary-nav')
, menuToggle = document.getElementById('menu-toggle')
, main = document.querySelector('main, #main')
, closeMainNavigation
;
//synchronous actions
if (isMobileMenu === true) {
$(primaryNav).attr('role', 'menu');
$(primaryNav).find('> li > a').each(function(e) {
$(this).attr('role', 'menuitem');
});
if ($(primaryNavContainer).hasClass('open')) {
$(menuToggle).attr('aria-expanded', true);
$(primaryNavContainer).attr('aria-expanded', true);
$(main).addClass('nav-open');
} else {
$(menuToggle).attr('aria-expanded', false);
$(primaryNavContainer).attr('aria-expanded', false);
}
} else {
$(primaryNav).find('> li > a').each(function(e) {
$(this).removeAttr('role');
});
$(primaryNav).attr('role', 'menubar');
if ($(primaryNavContainer).hasClass('open')) {
$(menuToggle).removeAttr('aria-expanded');
$(primaryNavContainer).removeAttr('aria-expanded');
$(main).removeClass('nav-open');
} else {
$(menuToggle).removeAttr('aria-expanded');
$(primaryNavContainer).removeAttr('aria-expanded');
}
}
console.log('%c ✔ resizeEnd', 'color: green');
//asynchronous triggers
};
// note resize events and trigger resizeEnd event when resizing stops
window.onresize = function() {
if(this.resizeTo) clearTimeout(this.resizeTo);
this.resizeTo = setTimeout(function() {
resizeEnd();
}, 250);
};
resizeEnd();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment