diff --git a/fonts/_config.fonts.scss b/fonts/_config.fonts.scss deleted file mode 100644 index fe53fa2bb..000000000 --- a/fonts/_config.fonts.scss +++ /dev/null @@ -1,49 +0,0 @@ -$font-family-light: 'proxima_novalight', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; -$font-family-regular: 'proxima_nova_rgregular', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; -$font-family-heading: 'code_pro_bold', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; -$font-family-heading-light: 'code_pro_regular', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; -$font-family-icons: 'Font Awesome 6 Free'; -$font-family-brands: 'Font Awesome 6 Brands'; - -// Font awesome -$fa-path: '../fonts/fontawesome-free-6.0.0-web' !default; -$fa-font-path: $fa-path + '/webfonts' !default; -$fa-css-prefix: 'icon'; - -@import '../fonts/fontawesome-free-6.0.0-web/scss/fontawesome'; -@import '../fonts/fontawesome-free-6.0.0-web/scss/regular'; -@import '../fonts/fontawesome-free-6.0.0-web/scss/solid'; -@import '../fonts/fontawesome-free-6.0.0-web/scss/brands'; -@import '../fonts/fontawesome-free-6.0.0-web/scss/v4-shims'; - -@font-face { - font-family: 'proxima_novalight'; - src: url($font-path + 'proxima_nova/proximanova-light.woff2') format('woff2'), - url($font-path + 'proxima_nova/proximanova-light.woff') format('woff'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'proxima_nova_rgregular'; - src: url($font-path + 'proxima_nova/proximanova-regular.woff2') format('woff2'), - url($font-path + 'proxima_nova/proximanova-regular.woff') format('woff'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'code_pro_bold'; - src: url($font-path + 'code_pro/code_pro_bold_lc-webfont.woff2') format('woff2'), - url($font-path + 'code_pro/code_pro_bold_lc-webfont.woff') format('woff'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'code_pro_regular'; - src: url($font-path + 'code_pro/code_pro_lc-webfont.woff2') format('woff2'), - url($font-path + 'code_pro/code_pro_lc-webfont.woff') format('woff'); - font-weight: normal; - font-style: normal; -} diff --git a/js/FocusManagementService.js b/js/FocusManagementService.js index 49c22a6b9..c6cd862f5 100644 --- a/js/FocusManagementService.js +++ b/js/FocusManagementService.js @@ -5,6 +5,7 @@ class FocusManagementService { constructor () { this.$element = null; this.focusableElementList = 'a[href], input, select, textarea, button'; + this.activeFocusTrap = null; } /** @@ -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) { @@ -44,20 +45,60 @@ 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) { @@ -65,22 +106,43 @@ class FocusManagementService { // 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; + diff --git a/js/NavMainComponent.js b/js/NavMainComponent.js index 4bc6c9351..f9264bb32 100644 --- a/js/NavMainComponent.js +++ b/js/NavMainComponent.js @@ -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); + } } }); @@ -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(); } /** @@ -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; } @@ -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'); @@ -378,16 +402,21 @@ 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 @@ -395,6 +424,12 @@ NavMainComponent.prototype.showMobileNav = function (show) { 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); } /**