diff --git a/network-api/networkapi/templates/fragments/buyersguide/category_dropdown.html b/network-api/networkapi/templates/fragments/buyersguide/category_dropdown.html index f7e238fd10a..9f7b4c92faa 100644 --- a/network-api/networkapi/templates/fragments/buyersguide/category_dropdown.html +++ b/network-api/networkapi/templates/fragments/buyersguide/category_dropdown.html @@ -1,51 +1,32 @@ {% load static wagtailcore_tags localization bg_nav_tags wagtailimages_tags i18n %} - - + + diff --git a/source/js/buyers-guide/template-js-handler/pni-category-dropdown.js b/source/js/buyers-guide/template-js-handler/pni-category-dropdown.js index cca7c70eb06..9198ea89990 100644 --- a/source/js/buyers-guide/template-js-handler/pni-category-dropdown.js +++ b/source/js/buyers-guide/template-js-handler/pni-category-dropdown.js @@ -1,26 +1,24 @@ -// TODO Refactor to ES6 Class in future to match other components and include aria/keyboard shortcuts - export default () => { const dropdown = document.querySelector(".pni-category-dropdown"); + const dropdownButton = dropdown.querySelector( + ".pni-category-dropdown-button" + ); + const dropdownButtonContent = dropdownButton.querySelector("span"); + const dropdownButtonChevron = dropdownButton.querySelector("svg"); + const dropdownSelect = dropdown.querySelector( + "#pni-category-dropdown-select" + ); + let defaultDropdownHeaderText; // Keep track of dropdown button's default text for different locales - // Need this to keep track of the default text of the dropdown button when using different locales - let defaultDropdownHeaderText; - - // Needed for calculating which items go into the dropdown - const navLinkMargin = 20; + const navLinkMargin = 20; // Calculate which items go into the dropdown const categoryWrapper = document.querySelector("#pni-category-wrapper"); const categoryNav = document.querySelector("#product-review"); + // Calculate width and margin const calculateWidthAndMargin = (ele) => ele.clientWidth + navLinkMargin; - const dropdownSelect = document.querySelector( - "#pni-category-dropdown-select" - ); - const dropdownSelectItems = document.querySelectorAll( - "#pni-category-dropdown-select > li, #pni-category-dropdown-select > li > a" - ); - - function resizeCategoryNavigation() { + // Resize category navigation + const resizeCategoryNavigation = () => { const categoryLinks = [ ...document.querySelectorAll( "#buyersguide-category-link-container > .multipage-link" @@ -45,9 +43,10 @@ export default () => { } linksForDropdown.forEach((e) => addCategoryToDropdown(e)); - } + }; - function addCategoryToDropdown(category) { + // Add category to dropdown + const addCategoryToDropdown = (category) => { const el = document.createElement("li"); el.classList.add( "tw-bg-white", @@ -66,29 +65,50 @@ export default () => { category.classList.remove("tw-block"); el.append(category); dropdownSelect.append(el); - } + }; - function highlightSelectedCategory() { + // Highlight selected category + const highlightSelectedCategory = () => { const activeCategory = document.querySelector( "#pni-category-dropdown-select .active" ); - const dropdownHeaderText = document.querySelector( - ".pni-category-dropdown > span" - ); - + const dropdownHeaderText = dropdownButtonContent.querySelector("span"); if (activeCategory) { dropdownHeaderText.innerText = activeCategory.innerText; dropdownHeaderText.classList.add("tw-text-black"); + dropdownHeaderText.setAttribute("aria-current", "page"); } else { dropdownHeaderText.innerText = defaultDropdownHeaderText; dropdownHeaderText.classList.remove("tw-text-black"); + dropdownHeaderText.removeAttribute("aria-current"); } - } - - if (dropdown) { - // removing styling that are exclusive used when JS is disabled or before it is loaded - dropdown.classList.add("tw-inline-flex"); + }; + + // Open menu + const openMenu = (withFocus = false) => { + dropdownSelect.classList.remove("tw-hidden"); + dropdownButton.setAttribute("aria-expanded", "true"); + dropdownButtonChevron.classList.add("tw-stroke-black", "tw-rotate-180"); + + if (withFocus) { + const firstOption = dropdownSelect.querySelector("li > a"); + if (firstOption) { + firstOption.focus(); + } + } + }; + + // Close menu + const closeMenu = () => { + dropdownSelect.classList.add("tw-hidden"); + dropdownButton.setAttribute("aria-expanded", "false"); + dropdownButtonChevron.classList.remove("tw-stroke-black", "tw-rotate-180"); + }; + + if (dropdown && dropdownButton && dropdownSelect) { + // removing styling that are exclusively used when JS is disabled or before it is loaded + dropdown.classList.add("tw-block"); dropdown.classList.remove("tw-hidden"); categoryWrapper.classList.add("tw-w-max", "tw-min-w-full"); document @@ -100,9 +120,8 @@ export default () => { "tw-mr-8" ); - defaultDropdownHeaderText = document.querySelector( - ".pni-category-dropdown > span" - ).innerText; + defaultDropdownHeaderText = + dropdownButtonContent.querySelector("span").innerText; // If there is an overflow of categories lets start moving them to the category dropdown if (categoryWrapper.clientWidth > categoryNav.clientWidth) { @@ -110,7 +129,7 @@ export default () => { highlightSelectedCategory(); } - // Using to detect classes changes within the links + // Detect class changes within links new MutationObserver(() => { highlightSelectedCategory(); }).observe(dropdownSelect, { @@ -119,7 +138,7 @@ export default () => { attributes: true, }); - // So people can test going to smaller screen sizes without having to refresh the page + // Support adjusting screen size (avoid having to refresh the page) window.onresize = function () { if (categoryWrapper.clientWidth > categoryNav.clientWidth) { resizeCategoryNavigation(); @@ -127,15 +146,77 @@ export default () => { } }; - dropdown.addEventListener("click", function (event) { + // Event listener for keyboard events on dropdown button + dropdownButton.addEventListener("keydown", function (event) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + if (dropdownButton.getAttribute("aria-expanded") === "false") { + openMenu(true); + } else { + closeMenu(); + } + } + + if (event.key === "ArrowDown") { + if (dropdownButton.getAttribute("aria-expanded") === "true") { + event.preventDefault(); + const firstOption = dropdownSelect.querySelector("li > a"); + if (firstOption) { + firstOption.focus(); + } + } else { + openMenu(true); + } + } + }); + + // Event listener for click events on dropdown button + dropdownButton.addEventListener("click", function (event) { event.stopPropagation(); - dropdownSelect.classList.remove("tw-hidden"); + if (dropdownButton.getAttribute("aria-expanded") === "false") { + openMenu(); + } else { + closeMenu(); + } }); - dropdownSelectItems.forEach((item) => { - item.addEventListener("click", function (event) { - event.stopPropagation(); - dropdownSelect.classList.add("tw-hidden"); + // Event listener for keyboard events on dropdown container + dropdownSelect.addEventListener("keydown", function (event) { + if (event.key === "Escape") { + closeMenu(); + } + }); + + // Event listener for keyboard and click events on dropdown options + dropdownSelect.querySelectorAll("li").forEach(function (option) { + // Event listener for click events on dropdown options + option.querySelector("a")?.addEventListener("click", function () { + closeMenu(); + }); + + // Event listener for keyboard events on dropdown options + option.addEventListener("keydown", function (event) { + if (event.key === " " || event.key === "Enter") { + event.preventDefault(); + option.querySelector("a").click(); + closeMenu(); + } + + if (event.key === "ArrowDown") { + event.preventDefault(); + const nextOption = option?.nextElementSibling?.querySelector("a"); + if (nextOption) { + nextOption.focus(); + } + } else if (event.key === "ArrowUp") { + event.preventDefault(); + const prevOption = option?.previousElementSibling?.querySelector("a"); + if (prevOption) { + prevOption.focus(); + } else { + dropdownButton.focus(); + } + } }); }); }