Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert menus to disclosure pattern #1814

Merged
merged 36 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d3b58b0
Begin converting menues to disclosure pattern
whabanks Apr 22, 2024
af076f5
Convert main menu botton to disclosure menu
whabanks Apr 25, 2024
8ab6caa
Handle keyboard navigation in disclosure menu
whabanks Apr 30, 2024
211c569
Merge branch 'main' into a11y/disclosure-menu-pattern
whabanks Apr 30, 2024
451f753
rebuild JS, fix formatting
whabanks May 1, 2024
004f703
Fix tests
whabanks May 1, 2024
7ccd09d
Add button role to element that opens the menu
whabanks May 2, 2024
b40d39e
Remove redundant event.preventDefault() calls
whabanks May 2, 2024
6aadd43
Add comment, test all.min.js regen
whabanks May 2, 2024
3e8adc3
Merge branch 'main' into a11y/disclosure-menu-pattern
whabanks May 2, 2024
e092dd0
Restore e.preventDefault() calls
whabanks May 2, 2024
3f2f2d9
Check if menu is open before executing keypress logic
whabanks May 2, 2024
02353f6
Add default width for disclosure menus
whabanks May 6, 2024
99bce3f
Close previously opened menu when opening a new menu
whabanks May 7, 2024
fb09c46
Add logic to ensure that only one menu can be open at a time
whabanks May 13, 2024
edf27b6
Merge branch 'main' into a11y/disclosure-menu-pattern
whabanks May 13, 2024
cb79561
Merge branch 'main' into a11y/disclosure-menu-pattern
whabanks May 14, 2024
631ed69
Merge branch 'main' into a11y/disclosure-menu-pattern
whabanks May 14, 2024
c4c24a6
Fix focus bugs
whabanks May 14, 2024
42552d9
task: add disclosure menu test suite
andrewleith May 14, 2024
d92c8da
task: add disclosure menu tests to CI
andrewleith May 14, 2024
ca97fc2
chore: formatting
andrewleith May 14, 2024
8d5ac3e
fix: turn on test isolation in cypress
andrewleith May 14, 2024
3194e15
fix(app_pages tests): simplify code and rely on cy.session for login
andrewleith May 14, 2024
cfd7338
fix(disclosure menu tests): use `cy.login` command for login; ensure …
andrewleith May 14, 2024
f0f9453
test(disclosure_menu): add a11y test to suite
andrewleith May 15, 2024
27a7302
Merge branch 'main' into a11y/disclosure-menu-pattern
whabanks May 15, 2024
60966c2
Fix cypress a11y violations
whabanks May 16, 2024
6e21768
Fix typo
whabanks May 16, 2024
c152851
Various cleanups
whabanks May 17, 2024
f387f03
Merge branch 'main' into a11y/disclosure-menu-pattern
whabanks May 21, 2024
21ad3d6
Formatting, regen css
whabanks May 21, 2024
34744ab
Merge branch 'main' into a11y/disclosure-menu-pattern
whabanks May 21, 2024
21d695b
Merge branch 'main' into a11y/disclosure-menu-pattern
whabanks May 21, 2024
a110357
Merge branch 'main' into a11y/disclosure-menu-pattern
whabanks May 22, 2024
ec9dc2e
Merge branch 'main' into a11y/disclosure-menu-pattern
andrewleith May 22, 2024
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
180 changes: 143 additions & 37 deletions app/assets/javascripts/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,179 @@
"use strict";

const registerKeyDownEscape = window.utils.registerKeyDownEscape;
const registerKeyBasedMenuNavigation =
window.utils.registerKeyBasedMenuNavigation;
const registerDisclosureMenuBlur = window.utils.registerDisclosureMenuBlur;

function open($menu, $items) {
// show menu if closed
if (!this.hasFocus) {
$items.toggleClass("hidden", false);
$items.removeAttr("hidden");
const $arrow = $menu.find(".arrow");
if ($arrow.length > 0) {
$arrow.toggleClass("flip", true);
}
$items.toggleClass("hidden", false);
$items.removeAttr("hidden");
const $arrow = $menu.find(".arrow");
if ($arrow.length > 0) {
$arrow.toggleClass("flip", true);
}

$menu.attr("aria-expanded", true);
this.hasFocus = true;
$menu.attr("aria-expanded", true);
$menu.isExpanded = true;
$items.children()[0].querySelector("a").focus();

window.setTimeout(function () {
$items.removeClass("opacity-0");
$items.addClass("opacity-100");
}, 1);
}
window.setTimeout(function () {
$items.removeClass("opacity-0");
$items.addClass("opacity-100");
}, 1);
}

function close($menu, $items) {
// hide menu if open
if (this.hasFocus) {
$items.toggleClass("hidden", true);
$items.removeClass("opacity-100");
$items.addClass("opacity-0");
const $arrow = $menu.find(".arrow");
if ($arrow.length > 0) {
$arrow.toggleClass("flip", false);
}
$items.toggleClass("hidden", true);
$items.removeClass("opacity-100");
$items.addClass("opacity-0");
const $arrow = $menu.find(".arrow");
if ($arrow.length > 0) {
$arrow.toggleClass("flip", false);
}

$menu.attr("aria-expanded", false);
this.hasFocus = false;
$menu.attr("aria-expanded", false);
$menu.isExpanded = false;
$menu.focus();

window.setTimeout(function () {
$items.toggleClass("hidden", true);
$items.attr("hidden");
}, 1);
}
window.setTimeout(function () {
$items.toggleClass("hidden", true);
$items.attr("hidden");
}, 1);
}

/**
* Toggles the aria-expanded state of the menu to show/hide the disclosure menu's items.
* Before opening a menu, it checks for any other menus that may be open and closes them
*
* @param {jQuery} $menu The menu button that controls the disclosure menu items container
* @param {jQuery} $items The unordered list containing menu items
*/
function toggleMenu($menu, $items) {
// Show the menu..
if (!this.hasFocus) {
// Get all the menus on the page that are open excluding the currently open menu and close them
const openMenus = Array.from(
document.querySelectorAll("button[data-module='menu']"),
).filter(
(menu) =>
menu.getAttribute("aria-expanded") == "true" && menu !== $menu[0],
);
openMenus.forEach((menu) => {
close(
$(menu),
$(
document.querySelectorAll(
`ul[id=${$(menu).attr("data-menu-items")}]`,
),
),
whabanks marked this conversation as resolved.
Show resolved Hide resolved
);
});

// We're using aria-expanded to determine the open/close state of the menu.
// The menu object's state does not get updated in time to use $menu.isExpanded
// Open the menu if it's closed.
if ($menu.attr("aria-expanded") === "false") {
open($menu, $items);
}
// Hide the menu..
else {
// Hide the menu if it's open
else if ($menu.attr("aria-expanded") === "true") {
close($menu, $items);
}
}

/**
* Handles closing any open menus when the user clicks outside of the menu with their mouse.
*
* @param {FocusEvent} event The focus event
* @param {jQuery} $menu The menu button that controls the disclosure menu items container
* @param {jQuery} $items The unordered list containing menu items
*/
function handleMenuBlur(event, $menu, $items) {
if (event.relatedTarget === null) {
close($menu, $items);
}
}

/**
* Handles the keydown event for the $menu so the user can navigate the menu items via the keyboard.
* This function supports the following key presses:
* - Home/End to navigate to the first and last items in the menu
* - Up/Left/Shift + Tab to navigate to the previous item in the menu
* - Down/Right/Tab to navigate to the next item in the menu
* - Meta + (Left/Right OR Up/Down) to navigate to the first/last items in the menu
*
* @param {KeyboardEvent} event The keydown event object
* @param {jQuery Object} $menu The menu button that controls the disclosure menu items container
* @param {jQuery Object} $items The unordered list containing menu items
*/
function handleKeyBasedMenuNavigation(event, $menu, $items) {
var menuItems = $items.children();

if ($menu.attr("aria-expanded") == "true") {
// Support for Home/End on Windows and Linux + Cmd + Arrows for Mac
if (event.key == "Home" || (event.metaKey && event.key == "ArrowLeft")) {
event.preventDefault();
$menu.selectedMenuItem = 0;
} else if (
event.key == "End" ||
(event.metaKey && event.key == "ArrowRight")
) {
event.preventDefault();
$menu.selectedMenuItem = menuItems.length - 1;
} else if (
event.key === "ArrowUp" ||
event.key === "ArrowLeft" ||
(event.shiftKey && event.key === "Tab")
) {
event.preventDefault();
$menu.selectedMenuItem =
$menu.selectedMenuItem == 0
? menuItems.length - 1
: Math.max(0, $menu.selectedMenuItem - 1);
} else if (
event.key === "ArrowDown" ||
event.key === "ArrowRight" ||
event.key === "Tab"
) {
event.preventDefault();
$menu.selectedMenuItem =
$menu.selectedMenuItem == menuItems.length - 1
? 0
: Math.min(menuItems.length - 1, $menu.selectedMenuItem + 1);
}
}

// Once we've determined the new selected menu item, we need to focus on it
$($items.children()[$menu.selectedMenuItem]).find("a").focus();
}

function init($menu) {
const itemsId = "#" + $menu.attr("data-menu-items");
const $items = $(itemsId);
this.hasFocus = false;
$menu.isExpanded = false;
$menu.selectedMenuItem = 0;

// Click toggler
$menu.click(() => toggleMenu($menu, $items));

// Register Escape key from anywhere in the window to close the menu
// Bind Keypress events to the window so the user can use the arrow/home/end keys to navigate the drop down menu
registerKeyBasedMenuNavigation($(window), (event) =>
handleKeyBasedMenuNavigation(event, $menu, $items),
);

// Bind blur events to each menu button and it's anchor link items.
registerDisclosureMenuBlur(
[...$items.children().find("a"), ...$menu, window],
(event) => handleMenuBlur(event, $menu, $items),
);

// Bind a Keydown event to the window so the user can use the Escape key from anywhere in the window to close the menu
registerKeyDownEscape($(window), () => close($menu, $items));
}

Modules.Menu = function () {
this.hasFocus;
this.isExpanded;
Fixed Show fixed Hide fixed
this.selectedMenuItem;
Fixed Show fixed Hide fixed
whabanks marked this conversation as resolved.
Show resolved Hide resolved

this.start = function (component) {
let $component = $(component);
Expand Down
60 changes: 0 additions & 60 deletions app/assets/javascripts/menuOverlay.js

This file was deleted.

17 changes: 17 additions & 0 deletions app/assets/javascripts/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@
});
}

function registerKeyBasedMenuNavigation($selector, fn) {
$selector.keydown(function (e) {
var menuVisible = !!$selector.not(":hidden");
if (menuVisible) fn(e);
});
}

function registerDisclosureMenuBlur($selectors, fn) {
$selectors.forEach((selector) => {
selector.addEventListener("blur", function (e) {
fn(e);
});
});
}

/**
* Make branding links automatically go back to the previous page without keeping track of them
*/
Expand All @@ -28,5 +43,7 @@

global.utils = {
registerKeyDownEscape: registerKeyDownEscape,
registerKeyBasedMenuNavigation: registerKeyBasedMenuNavigation,
registerDisclosureMenuBlur: registerDisclosureMenuBlur,
};
})(window);
2 changes: 1 addition & 1 deletion app/assets/stylesheets/index.css

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions app/assets/stylesheets/tailwind/components/menu.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@layer components {
/*! purgecss start ignore */
.mobile-menu-container {
width: 20ch;
@apply text-left max-w-full;
}

.mt-3_4 {
margin-top: 3.3rem;
}
}
16 changes: 13 additions & 3 deletions app/assets/stylesheets/tailwind/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
@import "./components/remaining-messages.css";
@import "./components/empty-list.css";
@import "./components/autocomplete.css";
@import "./components/menu.css";

/* views */
@import "./views/dashboard.css";
Expand Down Expand Up @@ -132,6 +133,7 @@ section[id]:target {
0 0 0 15px var(--white),
0 0 0 20px var(--yellow);
}

100% {
box-shadow:
0 0 0 15px var(--white),
Expand Down Expand Up @@ -231,16 +233,19 @@ label {
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px 1px 1px 1px);
/* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap; /* added line */
white-space: nowrap;
/* added line */
}

.border-box {
font-size: 0.9em;
border: 1px solid #000;
@apply p-8 mb-8;
}

.border-box strong {
@apply font-bold;
}
Expand Down Expand Up @@ -288,6 +293,7 @@ label {
margin: -1px;
@apply absolute overflow-hidden border-0 p-0;
}

.research-mode {
@apply font-bold inline-block bg-blue text-white py-2 px-4 rounded;
}
Expand All @@ -305,7 +311,8 @@ label {
}

.loading-indicator:after {
content: "\2026"; /* ellipsis */
content: "\2026";
/* ellipsis */
@apply overflow-hidden inline-block align-bottom animate-ellipsis w-0;
}

Expand All @@ -317,6 +324,7 @@ label {
.highlight {
@apply font-monospace overflow-x-scroll p-4 pr-0;
}

@screen md {
.inline.block-label {
@apply inline-block float-none;
Expand All @@ -336,6 +344,7 @@ label {
outline: 2px solid #000;
@apply p-2 max-w-full;
}

@screen smaller {
#global-header .header-proposition #proposition-links li {
width: 95%;
Expand All @@ -362,4 +371,5 @@ label {
.overflow-anywhere {
overflow-wrap: anywhere;
}

/*! purgecss end ignore */
Loading