Skip to content
Open
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
49 changes: 0 additions & 49 deletions fonts/_config.fonts.scss

This file was deleted.

88 changes: 75 additions & 13 deletions js/FocusManagementService.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class FocusManagementService {
constructor () {
this.$element = null;
this.focusableElementList = 'a[href], input, select, textarea, button';
this.activeFocusTrap = null;
}

/**
Expand All @@ -30,7 +31,7 @@ class FocusManagementService {
}

/**
* Move focus to first focuable element in a collection
* Move focus to first focusable element in a collection
* @param {jQuery} $collection - jQuery collection of elements
*/
focusFirstFocusableElement ($collection) {
Expand All @@ -44,43 +45,104 @@ class FocusManagementService {
}

/**
* Trap focus within a container
* @param {jQuery} $container
* Create a focus trap that includes additional elements
* @param {jQuery} $container - The main container to trap focus within
* @param {jQuery} [$additionalElements] - Additional elements to include in the focus trap
*/
trapFocus ($container) {
const $focusableElements = $container
trapFocus ($container, $additionalElements) {
// Remove any existing focus trap
this.removeFocusTrap();

// Get focusable elements from the container
let $focusableElements = $container
.find(this.focusableElementList)
.not('[tabindex=-1], [disabled], :hidden, [aria-hidden]');

$container.on('keydown', this.trapFocusKeydownListener.bind(this, $focusableElements));
// Add any additional elements to the focus trap
if ($additionalElements && $additionalElements.length) {
$focusableElements = $focusableElements.add($additionalElements);
}

// Ensure all elements are focusable
$focusableElements.each(function() {
if ($(this).attr('tabindex') === undefined) {
$(this).attr('tabindex', '0');
}
});

// Sort elements by their DOM order
$focusableElements = $($focusableElements.toArray().sort((a, b) => {
return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
}));

// Store the current focus trap
this.activeFocusTrap = {
$container: $container,
$focusableElements: $focusableElements
};

// Add focus trap handler to document
$(document).on('keydown.focusTrap', this.trapFocusKeydownListener.bind(this, $focusableElements));

// Add focusout handler to container to ensure focus stays within
$container.add($additionalElements).on('focusout.focusTrap', (event) => {
// Use setTimeout to check focus after the event has completed
setTimeout(() => {
const $focused = $(document.activeElement);
if (!$focusableElements.is($focused)) {
$focusableElements.first().focus();
}
}, 0);
});
}

/**
* Manage focus on keydown
* @param {jQuery} $container
* @param {jQuery} $focusableElements - Collection of focusable elements
* @param {Event} event
*/
trapFocusKeydownListener ($focusableElements, event) {
let keyCode = event.keyCode || event.which;

// If tab key is pressed
if (keyCode === 9) {
const $focused = $(document.activeElement);

// If the focused element is not in our focusable elements collection, focus the first element
if (!$focusableElements.is($focused)) {
event.preventDefault();
$focusableElements.first().focus();
return;
}

// Check for shift tab
if (event.shiftKey) {
// Focus previous, check if first element is is currently in focus, if so focus last element
if ($focusableElements.first().is(':focus')) {
// Focus previous, check if first element is currently in focus, if so focus last element
if ($focused.is($focusableElements.first())) {
event.preventDefault();
$focusableElements.last().trigger('focus');
$focusableElements.last().focus();
}
} else {
// Focus next, check if last element is is currently in focus, if so focus first element
if ($focusableElements.last().is(':focus')) {
// Focus next, check if last element is currently in focus, if so focus first element
if ($focused.is($focusableElements.last())) {
event.preventDefault();
$focusableElements.first().trigger('focus');
$focusableElements.first().focus();
}
}
}
}

/**
* Remove the focus trap
*/
removeFocusTrap() {
if (this.activeFocusTrap) {
this.activeFocusTrap.$container.off('focusout.focusTrap');
this.activeFocusTrap = null;
}
$(document).off('keydown.focusTrap');
}
}

module.exports = FocusManagementService;

67 changes: 51 additions & 16 deletions js/NavMainComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,20 @@ NavMainComponent.prototype.init = function () {
}

// Open navigation on mobile
component.$mobileMenuButton.on('click', function(event) {
component.$mobileMenuButton.on('click keydown', function(event) {
var $self = $(this);
event.stopImmediatePropagation();
var $menuExpanded = $self.attr('aria-expanded');

// Handle both click and Enter key
if (event.type === 'click' || event.keyCode === 13) {
event.preventDefault();
event.stopImmediatePropagation();

if ($self.text() === 'Menu') {
component.showMobileNav(true);
} else {
component.showMobileNav(false);
if ($menuExpanded === 'false') {
component.showMobileNav(true);
} else {
component.showMobileNav(false);
}
}
});

Expand Down Expand Up @@ -223,10 +229,22 @@ NavMainComponent.prototype.openSecondaryNav = function ($triggeringElement, even
component.$navPrimary.find('.is-active').removeClass('is-active');
component.$navPrimary.find('[href="' + target + '"], [data-target="' + target + '"]').addClass('is-active');

// Manage focus
// Store the triggering element for focus management
component.focusManagementService.storeElement($triggeringElement);
component.focusManagementService.focusFirstFocusableElement(component.$navSecondary);
component.focusManagementService.trapFocus(component.$navSecondary);

// Get the close button and menu items
const $closeButton = component.$navSecondary.find('.nav-controls__close');
const $menuItems = component.$navSecondary.find('.nav-secondary__item');

// Ensure the close button is focusable
$closeButton.attr('tabindex', '0');

// Set up focus trap for the secondary navigation
// Pass the close button as an additional element to ensure it's included in the focus trap
component.focusManagementService.trapFocus(component.$navSecondary, $closeButton);

// Focus the first item in the secondary menu
$menuItems.first().focus();
}

/**
Expand Down Expand Up @@ -286,12 +304,18 @@ NavMainComponent.prototype.closeSecondaryNav = function (action) {
var component = this;

component.$navMain.removeClass('is-open');
component.$navMain.find('[aria-expanded=true]').attr( 'aria-expanded', 'false');
component.$navMain.find('[aria-expanded=true]').attr('aria-expanded', 'false');
component.$navSecondary.removeClass('is-open');
component.$primaryNavLinks.removeClass('is-active');
component.$navMain.find('.nav-item.is-active').removeClass('is-active');
component.$navSecondary.find('.nav-list').removeClass('is-active');

// Remove focus trap
component.focusManagementService.removeFocusTrap();

// Remove the focus trap wrapper
$('.focus-trap-wrapper').contents().unwrap();

if (action === undefined) {
return;
}
Expand Down Expand Up @@ -368,8 +392,8 @@ NavMainComponent.prototype.showMobileNav = function (show) {
component.$body.removeClass('open-nav');
component.$mobileMenuButton
.removeClass('open')
.text('Menu')
.attr('aria-expanded', 'false');
.attr('aria-expanded', 'false')
.attr('tabindex', '0');
component.$brandingLink.attr('tabindex', '-1');
component.$primaryNavLinks.attr('tabindex', '-1');
component.$navMain.attr('aria-hidden', 'true');
Expand All @@ -378,23 +402,34 @@ NavMainComponent.prototype.showMobileNav = function (show) {
component.$body.find('.content-main').removeAttr('aria-hidden');
component.$body.find('.footer').removeAttr('aria-hidden');

// Remove focus trap
component.focusManagementService.removeFocusTrap();

// Focus the menu button after closing
component.$mobileMenuButton.focus();
return;
}

component.$body.addClass('open-nav');
component.$mobileMenuButton
.addClass('open')
.text('Close')
.attr('aria-expanded', 'true');
component.$brandingLink.attr('tabindex', '3');
component.$primaryNavLinks.attr('tabindex', '3');
.attr('aria-expanded', 'true')
.attr('tabindex', '0');
component.$brandingLink.attr('tabindex', '0');
component.$primaryNavLinks.attr('tabindex', '0');
component.$navMain.attr('aria-hidden', 'false');

// Hide rest of page from SR while nav open
component.$body.find('.skip-link').attr('aria-hidden', 'true');
component.$body.find('.toolbar > :not(.mobile-menu-button)').attr('aria-hidden', 'true');
component.$body.find('.content-main').attr('aria-hidden', 'true');
component.$body.find('.footer').attr('aria-hidden', 'true');

// Create focus trap with nav elements and menu button
component.focusManagementService.trapFocus(component.$navMain, component.$mobileMenuButton);

// Set focus to first menu item
component.focusManagementService.focusFirstFocusableElement(component.$navMain);
}

/**
Expand Down