Skip to content

Commit 34608a6

Browse files
committed
Menu keyboard navigation
1 parent 4a13568 commit 34608a6

File tree

6 files changed

+213
-94
lines changed

6 files changed

+213
-94
lines changed

etc/pages.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
{ "name": "__profilemenu/me", "order": 100, "allow_if": "email", "print_function": "*Conf::print_profilemenu_item", "separator_group": "self" },
88
{ "name": "__profilemenu/other_accounts", "order": 200, "print_function": "*Conf::print_profilemenu_item", "separator_group": "self" },
9-
{ "name": "__profilemenu/profile", "order": 300, "allow_if": "email", "print_function": "*Conf::print_profilemenu_item", "separator_group": "main" },
109
{ "name": "__profilemenu/search", "order": 310, "allow_if": "pc", "print_function": "*Conf::print_profilemenu_item", "separator_group": "main" },
1110
{ "name": "__profilemenu/my_reviews", "order": 320, "allow_if": "reviewer", "print_function": "*Conf::print_profilemenu_item", "separator_group": "main" },
1211
{ "name": "__profilemenu/my_submissions", "order": 330, "allow_if": "author", "print_function": "*Conf::print_profilemenu_item", "separator_group": "main" },

scripts/script.js

Lines changed: 172 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,7 +1090,10 @@ Object.assign(hotcrp.text, {
10901090

10911091
// events
10921092
var event_key = (function () {
1093-
const key_map = {"Spacebar": " ", "Esc": "Escape"},
1093+
const key_map = {
1094+
"Spacebar": " ", "Esc": "Escape", "Left": "ArrowLeft",
1095+
"Right": "ArrowRight", "Up": "ArrowUp", "Down": "ArrowDown"
1096+
},
10941097
charCode_map = {"9": "Tab", "13": "Enter", "27": "Escape"},
10951098
keyCode_map = {
10961099
"9": "Tab", "13": "Enter", "16": "ShiftLeft", "17": "ControlLeft",
@@ -4867,67 +4870,172 @@ $(function () {
48674870
(function ($) {
48684871
const builders = {};
48694872

4870-
function dropmenu_close() {
4871-
const modal = $$("dropmenu-modal");
4872-
modal && modal.remove();
4873-
$(".dropmenu-container").each(function () { this.hidden = true; });
4874-
}
4875-
4876-
handle_ui.on("click.js-dropmenu-open", function (evt) {
4877-
let modal = $$("dropmenu-modal"), esummary = this;
4878-
if (hasClass(esummary, "need-dropmenu")) {
4879-
$.each(classList(esummary), function (i, c) {
4873+
function dropmenu_open(mb, dir) {
4874+
let was_hidden = false;
4875+
if (hasClass(mb, "need-dropmenu")) {
4876+
$.each(classList(mb), function (i, c) {
48804877
if (builders[c])
4881-
builders[c].call(esummary, evt);
4878+
builders[c].call(mb);
48824879
});
4880+
was_hidden = true;
48834881
}
4884-
const edetails = esummary.closest(".dropmenu-details"),
4882+
const edetails = mb.closest(".dropmenu-details"),
48854883
econtainer = edetails.lastElementChild;
4884+
was_hidden = was_hidden || econtainer.hidden;
48864885
hotcrp.tooltip.close();
4887-
if (econtainer.hidden) {
4888-
if (!modal) {
4889-
modal = $e("div", "modal transparent");
4890-
modal.id = "dropmenu-modal";
4891-
edetails.parentElement.insertBefore(modal, edetails.nextSibling);
4892-
modal.addEventListener("click", dropmenu_close, false);
4893-
}
4886+
if (was_hidden) {
4887+
dropmenu_close();
4888+
const modal = $e("div", "modal transparent");
4889+
modal.id = "dropmenu-modal";
4890+
edetails.parentElement.insertBefore(modal, edetails.nextSibling);
4891+
modal.addEventListener("click", dropmenu_close, false);
48944892
econtainer.hidden = false;
4895-
} else if (this.tagName === "BUTTON") {
4896-
modal && modal.remove();
4897-
econtainer.hidden = true;
48984893
}
4899-
evt.preventDefault();
4900-
handle_ui.stopPropagation(evt);
4901-
});
4894+
const emenu = econtainer.querySelector(".dropmenu");
4895+
if (hasClass(emenu, "need-dropmenu-events")) {
4896+
dropmenu_events(emenu);
4897+
}
4898+
dropmenu_focus(emenu, dir || "first");
4899+
}
4900+
4901+
function dropmenu_events(emenu) {
4902+
removeClass(emenu, "need-dropmenu-events");
4903+
emenu.addEventListener("click", dropmenu_click);
4904+
emenu.addEventListener("mouseover", dropmenu_mouseover);
4905+
emenu.addEventListener("keydown", dropmenu_keydown);
4906+
emenu.addEventListener("focusout", dropmenu_focusout);
4907+
}
4908+
4909+
function dropmenu_focus(emenu, which) {
4910+
const items = emenu.querySelectorAll("[role=\"menuitem\"]");
4911+
if (which === "first") {
4912+
which = items[0];
4913+
} else if (which === "last") {
4914+
which = items[items.length - 1];
4915+
} else if (which === "next" || which === "prev") {
4916+
let current = 0;
4917+
while (current < items.length && items[current].tabIndex !== 0) {
4918+
++current;
4919+
}
4920+
if (current >= items.length) {
4921+
which = items[which === "next" ? 0 : items.length - 1];
4922+
} else if (which === "next") {
4923+
which = items[(current + 1) % items.length];
4924+
} else {
4925+
which = items[(current + items.length - 1) % items.length];
4926+
}
4927+
}
4928+
for (const e of items) {
4929+
if (e === which) {
4930+
e.tabIndex = 0;
4931+
const li = e.closest("li");
4932+
addClass(li, "focus");
4933+
if (e.ariaDisabled === "true") {
4934+
addClass(li, "focus-disabled");
4935+
}
4936+
e.focus();
4937+
} else if (e.tabIndex !== -1) {
4938+
e.tabIndex = -1;
4939+
removeClass(e.closest("li"), "focus");
4940+
}
4941+
}
4942+
}
49024943

4903-
handle_ui.on("click.dropmenu", function (evt) {
4904-
var tgt = evt.target, li, es, bs;
4944+
function dropmenu_click(evt) {
4945+
const tgt = evt.target;
4946+
let li, mi;
49054947
if (tgt.tagName === "A"
49064948
|| tgt.tagName === "BUTTON"
4907-
|| tgt.closest("ul") !== this) {
4949+
|| !(li = tgt.closest("li"))
4950+
|| li.parentElement !== this
4951+
|| !(mi = li.querySelector("[role=\"menuitem\"]"))
4952+
|| mi.ariaDisabled === "true") {
4953+
return;
4954+
}
4955+
if (mi.tagName === "A"
4956+
&& mi.href
4957+
&& !event_key.is_default_a(evt)) {
4958+
window.open(mi.href, "_blank", "noopener");
4959+
} else {
4960+
mi.click();
4961+
evt.preventDefault();
4962+
handle_ui.stopPropagation(evt);
4963+
}
4964+
}
4965+
4966+
function dropmenu_mouseover(evt) {
4967+
const li = evt.target.closest("li");
4968+
let mi;
4969+
if (!li
4970+
|| li.parentElement !== this
4971+
|| hasClass(li, "focus")
4972+
|| !(mi = li.querySelector("[role=\"menuitem\"]"))) {
49084973
return;
49094974
}
4910-
li = tgt.closest("li");
4911-
if (!li) {
4975+
dropmenu_focus(this, mi);
4976+
}
4977+
4978+
function dropmenu_keydown(evt) {
4979+
const key = event_key(evt);
4980+
if (key === "ArrowDown") {
4981+
dropmenu_focus(this, "next");
4982+
} else if (key === "ArrowUp") {
4983+
dropmenu_focus(this, "prev");
4984+
} else if (key === "Home" || key === "PageUp") {
4985+
dropmenu_focus(this, "first");
4986+
} else if (key === "End" || key === "PageDown") {
4987+
dropmenu_focus(this, "last");
4988+
} else if (key === "Escape") {
4989+
dropmenu_close(true);
4990+
} else {
49124991
return;
49134992
}
4914-
es = li.querySelectorAll("button");
4915-
if (es.length !== 1
4916-
&& (bs = li.querySelectorAll("a")).length === 1) {
4917-
es = bs;
4993+
evt.preventDefault();
4994+
handle_ui.stopPropagation(evt);
4995+
}
4996+
4997+
function dropmenu_focusout(evt) {
4998+
if (!evt.relatedTarget
4999+
|| evt.relatedTarget.closest(".dropmenu") !== this) {
5000+
dropmenu_close();
49185001
}
4919-
if (es.length !== 1) {
5002+
}
5003+
5004+
function dropmenu_close(focus) {
5005+
const modal = $$("dropmenu-modal");
5006+
if (!modal) {
49205007
return;
49215008
}
4922-
if (es[0].tagName === "A"
4923-
&& es[0].href
4924-
&& !event_key.is_default_a(evt)) {
4925-
window.open(es[0].href, "_blank", "noopener");
4926-
} else {
4927-
es[0].click();
4928-
evt.preventDefault();
4929-
handle_ui.stopPropagation(evt);
5009+
modal.remove();
5010+
for (const dm of document.querySelectorAll(".dropmenu-container")) {
5011+
if (dm.hidden) {
5012+
continue;
5013+
}
5014+
dm.hidden = true;
5015+
const mb = dm.closest(".dropmenu-details")
5016+
.querySelector(".js-dropmenu-button");
5017+
if (mb) {
5018+
mb.ariaExpanded = "false";
5019+
if (focus) {
5020+
mb.focus();
5021+
}
5022+
}
49305023
}
5024+
}
5025+
5026+
handle_ui.on("click.js-dropmenu-button", function (evt) {
5027+
dropmenu_open(this);
5028+
evt.preventDefault();
5029+
});
5030+
5031+
handle_ui.on("keydown.js-dropmenu-button", function (evt) {
5032+
const k = event_key(evt);
5033+
if ((k !== "ArrowUp" && k !== "ArrowDown")
5034+
|| event_key.modcode(evt) !== 0) {
5035+
return;
5036+
}
5037+
dropmenu_open(this, k === "ArrowUp" ? "last" : "first");
5038+
evt.preventDefault();
49315039
});
49325040

49335041
hotcrp.dropmenu = {
@@ -5756,39 +5864,45 @@ hotcrp.dropmenu.add_builder("row-order-draghandle", function () {
57565864
} else {
57575865
details = $e("div", "dropmenu-details");
57585866
this.replaceWith(details);
5759-
menu = $e("ul", "uic dropmenu");
5867+
menu = $e("ul", "dropmenu need-dropmenu-events");
57605868
menu.setAttribute("role", "menu");
57615869
menu.setAttribute("aria-label", "Reordering menu");
57625870
const menucontainer = $e("div", "dropmenu-container dropmenu-draghandle", menu);
57635871
menucontainer.hidden = true;
57645872
details.append(this, menucontainer);
57655873
}
57665874
menu.append($e("li", "disabled", "(Drag to reorder)"));
5767-
function buttonli(className, attr, text) {
5768-
attr["class"] = className;
5769-
attr["type"] = "button";
5770-
attr["role"] = "menuitem";
5771-
return $e("li", {class: attr.disabled ? "disabled" : "has-link", role: "none"}, $e("button", attr, text));
5875+
function buttonli(className, text, xattr) {
5876+
const attr = {class: className, type: "button", role: "menuitem"};
5877+
if (xattr && xattr.disabled) {
5878+
attr["aria-disabled"] = "true";
5879+
attr.class += " disabled";
5880+
}
5881+
return $e("li", {role: "none"}, $e("button", attr, text));
57725882
}
57735883
let sib = row.previousElementSibling;
5774-
menu.append(buttonli("link ui row-order-dragmenu move-up", {
5884+
menu.append(buttonli("qx ui row-order-dragmenu move-up", "Move up", {
57755885
disabled: !sib || hasClass(sib, "row-order-barrier")
5776-
}, "Move up"));
5886+
}));
57775887
sib = row.nextElementSibling;
5778-
menu.append(buttonli("link ui row-order-dragmenu move-down", {
5888+
menu.append(buttonli("qx ui row-order-dragmenu move-down", "Move down", {
57795889
disabled: !sib || hasClass(sib, "row-order-barrier")
5780-
}, "Move down"));
5890+
}));
57815891
if (group.hasAttribute("data-row-template")) {
57825892
const max_rows = +group.getAttribute("data-max-rows") || 0;
57835893
if (max_rows <= 0 || row_order_count(group) < max_rows) {
5784-
menu.append(buttonli("link ui row-order-dragmenu insert-above", {}, "Insert row above"));
5785-
menu.append(buttonli("link ui row-order-dragmenu insert-below", {}, "Insert row below"));
5894+
menu.append(buttonli("qx ui row-order-dragmenu insert-above", "Insert row above"));
5895+
menu.append(buttonli("qx ui row-order-dragmenu insert-below", "Insert row below"));
57865896
}
57875897
}
5788-
menu.append(buttonli("link ui row-order-dragmenu remove", {disabled: !row_order_allow_remove(group)}, "Remove"));
5898+
menu.append(buttonli("qx ui row-order-dragmenu remove", "Remove", {disabled: !row_order_allow_remove(group)}));
57895899
});
57905900

5791-
handle_ui.on("row-order-dragmenu", function () {
5901+
handle_ui.on("row-order-dragmenu", function (evt) {
5902+
if (this.ariaDisabled === "true") {
5903+
evt.preventDefault();
5904+
return;
5905+
}
57925906
hotcrp.dropmenu.close(this);
57935907
const row = this.closest(".draggable"), group = row.parentElement,
57945908
defaults = row_order_defaults(group);

src/conference.php

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5035,11 +5035,15 @@ function has_active_tracker() {
50355035
* @param string $page
50365036
* @param null|string|array $args */
50375037
private function _print_profilemenu_link_if_enabled($user, $html, $page, $args = null) {
5038-
if (!$user->is_disabled()) {
5039-
echo '<li class="has-link" role="none">', Ht::link($html, $this->hoturl($page, $args), ["role" => "menuitem"]), '</li>';
5038+
$attr = ["role" => "menuitem", "class" => "qx"];
5039+
if ($user->is_disabled()) {
5040+
$attr["aria-disabled"] = "true";
5041+
$attr["class"] .= " disabled";
5042+
$t = Ht::button($html, $attr);
50405043
} else {
5041-
echo '<li class="dim" role="menuitem" aria-disabled="true">', $html, '</li>';
5044+
$t = Ht::link($html, $this->hoturl($page, $args), $attr);
50425045
}
5046+
echo '<li role="none">', $t, '</li>';
50435047
}
50445048

50455049
/** @param ComponentSet $pagecs */
@@ -5051,12 +5055,10 @@ function print_profilemenu_item(Contact $user, Qrequest $qreq, $pagecs, $gj) {
50515055
}
50525056
$ouser = $user;
50535057
if ($user->is_actas_user()) {
5054-
echo '<li class="has-quiet-link" role="none">', Ht::link("Acting as " . htmlspecialchars($user->email), $this->hoturl("profile"), ["role" => "menuitem"]), '</li>';
5055-
echo '<li class="has-link" role="none">', Ht::link("Switch to <strong>" . htmlspecialchars($user->base_user()->email), $this->selfurl($qreq, ["actas" => null]), ["role" => "menuitem"]), '</strong></li>';
5056-
} else if (!$user->is_disabled() && !$user->is_anonymous_user()) {
5057-
echo '<li class="has-quiet-link" role="none">', Ht::link("Signed in as <strong>" . htmlspecialchars($user->email) . "</strong>", $this->hoturl("profile"), ["role" => "menuitem"]), '</li>';
5058-
} else {
5059-
echo '<li>Signed in as <strong>', htmlspecialchars($user->email), '</strong></li>';
5058+
echo '<li role="none"><em>Acting as ', htmlspecialchars($user->email), '</em></li>';
5059+
}
5060+
if ($user->has_email()) {
5061+
$this->_print_profilemenu_link_if_enabled($user, "Account settings", "profile");
50605062
}
50615063
} else if ($itemid === "other_accounts") {
50625064
$base_email = $user->base_user()->email;
@@ -5074,14 +5076,14 @@ function print_profilemenu_item(Contact $user, Qrequest $qreq, $pagecs, $gj) {
50745076
$actas_email = null;
50755077
}
50765078
if ($email !== "" && strcasecmp($email, $base_email) !== 0) {
5077-
echo '<li class="has-link" role="none">', Ht::link("Switch to " . htmlspecialchars($email), "{$nav->base_path_relative}u/{$i}/{$sfx}", ["role" => "menuitem"]), '</li>';
5079+
echo '<li role="none">', Ht::link("Switch to " . htmlspecialchars($email), "{$nav->base_path_relative}u/{$i}/{$sfx}", ["role" => "menuitem", "class" => "qx"]), '</li>';
50785080
}
50795081
}
50805082
if ($actas_email !== null) {
5081-
echo '<li class="has-link" role="none">', Ht::link("Act as ". htmlspecialchars($actas_email), $this->selfurl($qreq, ["actas" => $actas_email]), ["role" => "menuitem"]), '</li>';
5083+
echo '<li role="none">', Ht::link("Act as ". htmlspecialchars($actas_email), $this->selfurl($qreq, ["actas" => $actas_email]), ["role" => "menuitem", "class" => "qx"]), '</li>';
50825084
}
50835085
$t = $user->has_email() ? "Add another account" : "Sign in";
5084-
echo '<li class="has-link" role="none">', Ht::link($t, $this->hoturl("signin"), ["role" => "menuitem"]), '</li>';
5086+
echo '<li role="none">', Ht::link($t, $this->hoturl("signin"), ["role" => "menuitem", "class" => "qx"]), '</li>';
50855087
} else if ($itemid === "profile") {
50865088
if ($user->has_email()) {
50875089
$this->_print_profilemenu_link_if_enabled($user, "Account settings", "profile");
@@ -5107,12 +5109,12 @@ function print_profilemenu_item(Contact $user, Qrequest $qreq, $pagecs, $gj) {
51075109
return;
51085110
}
51095111
if ($user->is_actas_user()) {
5110-
echo '<li class="has-link" role="none">', Ht::link("Return to main account", $this->selfurl($qreq, ["actas" => null]), ["role" => "menuitem"]), '</li>';
5112+
echo '<li role="none">', Ht::link("Return to main account", $this->selfurl($qreq, ["actas" => null]), ["role" => "menuitem"]), '</li>';
51115113
return;
51125114
}
5113-
echo '<li class="has-link" role="none">',
5115+
echo '<li role="none">',
51145116
Ht::form($this->hoturl("=signout", ["cap" => null])),
5115-
Ht::button("Sign out", ["type" => "submit", "class" => "link", "role" => "menuitem"]),
5117+
Ht::button("Sign out", ["type" => "submit", "class" => "qx", "role" => "menuitem"]),
51165118
'</form></li>';
51175119
}
51185120
}
@@ -5137,9 +5139,9 @@ private function print_header_profile($id, Qrequest $qreq, Contact $user) {
51375139
$pagecs = $this->page_components($user, $qreq);
51385140
$old_separator = $pagecs->swap_separator('<li role="separator"></li>');
51395141
echo '<div', $details_id, ' class="dropmenu-details', $details_class, '">',
5140-
'<button type="button" id="h-usermenubutton" class="ui js-dropmenu-open ', $button_class, '" aria-haspopup="menu" aria-controls="h-usermenu">',
5142+
'<button type="button" id="h-usermenubutton" class="ui uikd js-dropmenu-button ', $button_class, '" aria-haspopup="menu" aria-controls="h-usermenu" aria-expanded="false">',
51415143
$details_prefix, $user_html, $details_suffix,
5142-
'</button><div class="dropmenu-container dropmenu-sw" hidden><ul id="h-usermenu" class="uic dropmenu" role="menu" aria-label="Site menu">';
5144+
'</button><div class="dropmenu-container dropmenu-sw" hidden><ul id="h-usermenu" class="dropmenu need-dropmenu-events" role="menu" aria-label="Site menu">';
51435145
$pagecs->print_members("__profilemenu");
51445146
$pagecs->swap_separator($old_separator);
51455147
echo '</ul></div></div>';

src/options/o_authors.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ private function echo_editable_authors_line($pt, $n, $au, $reqau, $shownum) {
301301

302302
echo '<div class="author-entry draggable d-flex">';
303303
if ($shownum) {
304-
echo '<div class="flex-grow-0"><button type="button" class="draghandle ui js-dropmenu-open ui-drag row-order-draghandle need-tooltip need-dropmenu" draggable="true" title="Click or drag to reorder" data-tooltip-anchor="e">&zwnj;</button></div>',
304+
echo '<div class="flex-grow-0"><button type="button" class="draghandle ui uikd js-dropmenu-button ui-drag row-order-draghandle need-tooltip need-dropmenu" draggable="true" title="Click or drag to reorder" data-tooltip-anchor="e" aria-haspopup="menu" aria-expanded="false">&zwnj;</button></div>',
305305
'<div class="flex-grow-0 row-counter">', $n, '.</div>';
306306
}
307307
echo '<div class="flex-grow-1">',

src/pages/p_graph_formula.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ private function echo_formulas_qrow($i, $q, $s, $ms, $field) {
2929
}
3030
$klass = $ms->control_class($field, "need-suggest papersearch want-focus");
3131
echo '<div class="draggable d-flex mb-2">',
32-
'<div class="flex-grow-0 pr-1"><button type="button" class="draghandle ui js-dropmenu-open ui-drag row-order-draghandle need-tooltip need-dropmenu" draggable="true" title="Click or drag to reorder"></button></div>',
32+
'<div class="flex-grow-0 pr-1"><button type="button" class="draghandle ui uikd js-dropmenu-button ui-drag row-order-draghandle need-tooltip need-dropmenu" draggable="true" title="Click or drag to reorder" aria-haspopup="menu" aria-expanded="false"></button></div>',
3333
'<div class="flex-grow-1 lentry">',
3434
$ms->feedback_html_at($field),
3535
Ht::entry("q{$i}", $q, ["size" => 40, "placeholder" => "(All)", "class" => $klass, "id" => "q{$i}", "spellcheck" => false, "autocomplete" => "off", "aria-label" => "Search"]),

0 commit comments

Comments
 (0)