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

Replace topbar dropdown menus working with JS by <details> #1939

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion src/web/crate_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -991,7 +991,7 @@ mod tests {

let platform_links: Vec<(String, String)> = kuchiki::parse_html()
.one(response.text()?)
.select(r#"a[aria-label="Platform"] + ul li a"#)
.select(r#"summary[aria-label="Platform"] + ul li a"#)
.expect("invalid selector")
.map(|el| {
let attributes = el.attributes.borrow();
Expand Down
2 changes: 1 addition & 1 deletion src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,7 @@ mod test {
let text = web.get("/foo/0.2.0/foo").send()?.text()?;
let platform = kuchiki::parse_html()
.one(text)
.select(r#"ul > li > a[aria-label="Platform"]"#)
.select(r#"form.landing-search-form-nav details > summary[aria-label="Platform"]"#)
.unwrap()
.count();
assert_eq!(platform, 1);
Expand Down
6 changes: 3 additions & 3 deletions src/web/rustdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -861,7 +861,7 @@ mod test {
let dom = kuchiki::parse_html().one(data);

if let Some(elem) = dom
.select("form > ul > li > a.warn")
.select("form.landing-search-form-nav > a.warn.pure-menu-item")
.expect("invalid selector")
.next()
{
Expand Down Expand Up @@ -1257,7 +1257,7 @@ mod test {
let data = web.get(path).send()?.text()?;
Ok(kuchiki::parse_html()
.one(data)
.select("form > ul > li > .warn")
.select("form.landing-search-form-nav > .warn.pure-menu-item")
.expect("invalid selector")
.any(|el| el.text_contents().contains("yanked")))
}
Expand Down Expand Up @@ -1502,7 +1502,7 @@ mod test {
let data = web.get(path).send()?.text()?;
let dom = kuchiki::parse_html().one(data);
Ok(dom
.select(r#"a[aria-label="Platform"] + ul li a"#)
.select(r#"summary[aria-label="Platform"] + ul li a"#)
.expect("invalid selector")
.map(|el| {
let attributes = el.attributes.borrow();
Expand Down
250 changes: 37 additions & 213 deletions static/menu.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
const updateMenuPositionForSubMenu = (currentMenuSupplier) => {
const currentMenu = currentMenuSupplier();
const subMenu = currentMenu?.getElementsByClassName('pure-menu-children')?.[0];
// Improve interactions with dropdown menus.
(function() {
const OPEN_MENU_SELECTOR = ".nav-container details[open]";

subMenu?.style.setProperty('--menu-x', `${currentMenu.getBoundingClientRect().x}px`);
}
function updateMenuPositionForSubMenu() {
const currentMenu = document.querySelector(OPEN_MENU_SELECTOR);
const subMenu = currentMenu?.getElementsByClassName('pure-menu-children')?.[0];

// Allow menus to be open and used by keyboard.
(function() {
var currentMenu;
var backdrop = document.createElement("div");
backdrop.style = "display:none;position:fixed;width:100%;height:100%;z-index:1";
document.documentElement.insertBefore(backdrop, document.querySelector("body"));
subMenu?.style.setProperty('--menu-x', `${currentMenu.getBoundingClientRect().x}px`);
}

addEventListener('resize', () => updateMenuPositionForSubMenu(() => currentMenu));
addEventListener('resize', updateMenuPositionForSubMenu);

function previous(allItems, item) {
var i = 1;
Expand All @@ -37,216 +34,43 @@ const updateMenuPositionForSubMenu = (currentMenuSupplier) => {
function last(allItems) {
return allItems[allItems.length - 1];
}
function closeMenu() {
if (this === backdrop) {
document.documentElement.focus();
} else if (currentMenu.querySelector(".pure-menu-link:focus")) {
currentMenu.firstElementChild.focus();
function closeMenu(ignore) {
const menus = Array.prototype.slice.call(
document.querySelectorAll(OPEN_MENU_SELECTOR));
for (const menu of menus) {
if (menu !== ignore) {
menu.open = false;
}
}
currentMenu.className = currentMenu.className.replace("pure-menu-active", "");
currentMenu = null;
backdrop.style.display = "none";
}
backdrop.onclick = closeMenu;
function openMenu(newMenu) {
updateMenuPositionForSubMenu(() => newMenu);
currentMenu = newMenu;
newMenu.className += " pure-menu-active";
backdrop.style.display = "block";
}
function menuOnClick(e) {
if (this.getAttribute("href") != "#") {
return;
}
if (this.parentNode === currentMenu) {
closeMenu();
this.blur();
if (!this.open) {
this.focus();
} else {
if (currentMenu) closeMenu();

openMenu(this.parentNode);
closeMenu(this);
updateMenuPositionForSubMenu();
}
e.preventDefault();
e.stopPropagation();
};
function menuKeyDown(e) {
if (currentMenu) {
var children = currentMenu.querySelector(".pure-menu-children");
var currentLink = children.querySelector(".pure-menu-link:focus");
var currentItem;
if (currentLink && currentLink.parentNode.className.indexOf("pure-menu-item") !== -1) {
currentItem = currentLink.parentNode;
}
var allItems = [];
if (children) {
allItems = children.querySelectorAll(".pure-menu-item .pure-menu-link");
}
var switchTo = null;
switch (e.key.toLowerCase()) {
case "escape":
case "esc":
closeMenu();
e.preventDefault();
e.stopPropagation();
return;
case "arrowdown":
case "down":
if (currentLink) {
// Arrow down when an item other than the last is focused: focus next item.
// Arrow down when the last item is focused: jump to top.
switchTo = (next(allItems, currentLink) || allItems[0]);
} else {
// Arrow down when a menu is open and nothing is focused: focus first item.
switchTo = allItems[0];
}
break;
case "arrowup":
case "up":
if (currentLink) {
// Arrow up when an item other than the first is focused: focus previous item.
// Arrow up when the first item is focused: jump to bottom.
switchTo = (previous(allItems, currentLink) || last(allItems));
} else {
// Arrow up when a menu is open and nothing is focused: focus last item.
switchTo = last(allItems);
}
break;
case "tab":
if (!currentLink) {
// if the menu is open, we should focus trap into it
// this is the behavior of the WAI example
// it is not the same as GitHub, but GitHub allows you to tab yourself out
// of the menu without closing it (which is horrible behavior)
switchTo = e.shiftKey ? last(allItems) : allItems[0];
} else if (e.shiftKey && currentLink === allItems[0]) {
// if you tab your way out of the menu, close it
// this is neither what GitHub nor the WAI example do,
// but is a rationalization of GitHub's behavior: we don't want users who know how to
// use tab and enter, but don't know that they can close menus with Escape,
// to find themselves completely trapped in the menu
closeMenu();
e.preventDefault();
e.stopPropagation();
} else if (!e.shiftKey && currentLink === last(allItems)) {
// same as above.
// if you tab your way out of the menu, close it
closeMenu();
}
break;
case "enter":
case "return":
// enter and return have the default browser behavior,
// but they also close the menu
// this behavior is identical between both the WAI example, and GitHub's
setTimeout(function() {
closeMenu();
}, 100);
break;
case "space":
case " ":
// space closes the menu, and activates the current link
// this behavior is identical between both the WAI example, and GitHub's
if (document.activeElement instanceof HTMLAnchorElement && !document.activeElement.hasAttribute("aria-haspopup")) {
// It's supposed to copy the behaviour of the WAI Menu Bar
// page, and of GitHub's menus. I've been using these two
// sources to judge what is basically "industry standard"
// behaviour for menu keyboard activity on the web.
//
// On GitHub, here's what I notice:
//
// 1 If you click open a menu, the menu button remains
// focused. If, in this stage, I press space, the menu will
// close.
//
// 2 If I use the arrow keys to focus a menu item, and then
// press space, the menu item will be activated. For
// example, clicking "+", then pressing down, then pressing
// space will open the New Repository page.
//
// Behaviour 1 is why the
// `!document.activeElement.hasAttribute("aria-haspopup")`
// condition is there. It's to make sure the menu-link on
// things like the About dropdown don't get activated.
// Behaviour 2 is why this code is required at all; I want to
// activate the currently highlighted menu item.
document.activeElement.click();
}
setTimeout(function() {
closeMenu();
}, 100);
e.preventDefault();
e.stopPropagation();
break;
case "home":
// home: focus first menu item.
// This is the behavior of WAI, while GitHub scrolls,
// but it's unlikely that a user will try to scroll the page while the menu is open,
// so they won't do it on accident.
switchTo = allItems[0];
break;
case "end":
// end: focus last menu item.
// This is the behavior of WAI, while GitHub scrolls,
// but it's unlikely that a user will try to scroll the page while the menu is open,
// so they won't do it on accident.
switchTo = last(allItems);
break;
case "pageup":
// page up: jump five items up, stopping at the top
// the number 5 is used so that we go one page in the
// inner-scrolled Dependencies and Versions fields
switchTo = currentItem || allItems[0];
for (var n = 0; n < 5; ++n) {
if (switchTo.previousElementSibling && switchTo.previousElementSibling.className == 'pure-menu-item') {
switchTo = switchTo.previousElementSibling;
}
}
break;
case "pagedown":
// page down: jump five items down, stopping at the bottom
// the number 5 is used so that we go one page in the
// inner-scrolled Dependencies and Versions fields
switchTo = currentItem || last(allItems);
for (var n = 0; n < 5; ++n) {
if (switchTo.nextElementSibling && switchTo.nextElementSibling.className == 'pure-menu-item') {
switchTo = switchTo.nextElementSibling;
}
}
break;
}
if (switchTo) {
var switchToLink = switchTo.querySelector("a");
if (switchToLink) {
switchToLink.focus();
} else {
switchTo.focus();
}
e.preventDefault();
e.stopPropagation();
}
} else if (e.target.parentNode.className && e.target.parentNode.className.indexOf("pure-menu-has-children") !== -1) {
switch (e.key.toLowerCase()) {
case "arrowdown":
case "down":
case "space":
case " ":
openMenu(e.target.parentNode);
e.preventDefault();
e.stopPropagation();
break;
}
const key = e.key.toLowerCase();
if ((key === "escape" || key === "esc") &&
document.querySelector(OPEN_MENU_SELECTOR) !== null)
{
closeMenu();
e.preventDefault();
e.stopPropagation();
}
};
var menus = Array.prototype.slice.call(document.querySelectorAll(".pure-menu-has-children"));
var menusLength = menus.length;
var menu;
for (var i = 0; i < menusLength; ++i) {
menu = menus[i];
menu.firstElementChild.setAttribute("aria-haspopup", "menu");
menu.firstElementChild.nextElementSibling.setAttribute("role", "menu");
menu.firstElementChild.addEventListener("click", menuOnClick);
}
document.documentElement.addEventListener("keydown", menuKeyDown);

const setEvents = (menus) => {
menus = Array.prototype.slice.call(menus);
for (const menu of menus) {
menu.addEventListener("toggle", menuOnClick);
menu.addEventListener("keydown", menuKeyDown);
}
};
setEvents(document.querySelectorAll(".nav-container details"));

document.documentElement.addEventListener("keydown", function(ev) {
if (ev.key == "y" && ev.target.tagName != "INPUT") {
let permalink = document.getElementById("permalink");
Expand Down
19 changes: 9 additions & 10 deletions templates/header/topbar_end.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,24 @@
{# The global alert, if there is one #}
{% include "header/global_alert.html" -%}

<ul class="pure-menu-list">
<li class="pure-menu-item pure-menu-has-children">
<a href="#" class="pure-menu-link">
<div class="pure-menu-list">
<details class="pure-menu-item" aria-haspopup="menu">
<summary>
<span title="Releases">{{ "leaf" | fas }}</span>
<span class="title">Releases</span>
</a>

</summary>
<ul class="pure-menu-children">
{{ macros::menu_link(href="/releases", text="All Releases") }}
{{ macros::menu_link(href="/releases/stars", text="Releases by Stars") }}
{{ macros::menu_link(href="/releases/recent-failures", text="Recent Build Failures") }}
{{ macros::menu_link(href="/releases/failures", text="Build Failures by Stars") }}
{{ macros::menu_link(href="/releases/activity", text="Release Activity") }}
</ul>
</li>{#
</details>{#

The Rust dropdown menu
#}<li class="pure-menu-item pure-menu-has-children">
<a href="#" class="pure-menu-link" aria-label="Rust">Rust</a>
#}<details class="pure-menu-item" aria-haspopup="menu">
<summary aria-label="Rust">Rust</summary>
<ul class="pure-menu-children">
{{ macros::menu_link(
href="https://www.rust-lang.org/",
Expand Down Expand Up @@ -59,8 +58,8 @@
target="_blank"
) }}
</ul>
</li>
</ul>
</details>
</div>
{# The search bar #}
<div id="search-input-nav">
<label for="nav-search">
Expand Down
Loading