@@ -1090,7 +1090,10 @@ Object.assign(hotcrp.text, {
10901090
10911091// events
10921092var 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 ($) {
48684871const 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
49335041hotcrp.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);
0 commit comments