diff --git a/apps/client/src/menus/context_menu.spec.ts b/apps/client/src/menus/context_menu.spec.ts index a6f385de02..def6d108ed 100644 --- a/apps/client/src/menus/context_menu.spec.ts +++ b/apps/client/src/menus/context_menu.spec.ts @@ -132,6 +132,44 @@ describe("contextMenu", () => { expect(menu?.querySelectorAll(".dropdown-item .use-note-color")).toHaveLength(1); }); + it("puts itself away once a submenu's parent acts, and stays up for one that only folds", async () => { + const menu = buildPage(); + const contextMenu = await buildContextMenu(); + const picked: string[] = []; + + const show = () => contextMenu.show({ + x: 10, + y: 10, + selectMenuItemHandler: (item) => { picked.push(String(item.title)); }, + items: [ + { title: "Open note", command: "openNoteInNewTab", items: [ { title: "New tab" } ] }, + { title: "More", items: [ { title: "Archived" } ] } + ] + }); + const pressRow = (title: string) => { + // Read off the row's own line: its text also holds whatever its submenu lists. + const row = [ ...menu?.querySelectorAll("li.dropdown-item") ?? [] ] + .find((item) => item.querySelector(":scope > span")?.textContent?.trim() === title); + expect(row, title).toBeTruthy(); + const press = new MouseEvent("mousedown", { bubbles: true, button: 0 }); + // happy-dom leaves the legacy `which` unset, which is what the menu reads for the + // primary button. + Object.defineProperty(press, "which", { value: 1 }); + row?.dispatchEvent(press); + }; + + await show(); + pressRow("Open note"); + expect(picked).toEqual([ "Open note" ]); + expect(contextMenu.isShown()).toBe(false); + + await show(); + pressRow("More"); + expect(picked).toEqual([ "Open note", "More" ]); + // Nothing ran, so the menu is left standing for the submenu to be reached from. + expect(contextMenu.isShown()).toBe(true); + }); + it("says whether it is up, for a host whose own press would otherwise not know", async () => { buildPage(); const contextMenu = await buildContextMenu(); diff --git a/apps/client/src/menus/context_menu.ts b/apps/client/src/menus/context_menu.ts index be9f511bb9..06ac965e6d 100644 --- a/apps/client/src/menus/context_menu.ts +++ b/apps/client/src/menus/context_menu.ts @@ -396,8 +396,12 @@ class ContextMenu { return false; } - // Prevent submenu from failing to expand on mobile - if (!("items" in item && item.items)) { + // A submenu's parent stays open so that it can still be expanded. One carrying a + // command or handler of its own is dismissed like any other item once it has run. + const opensSubmenu = "items" in item && !!item.items; + const acts = ("handler" in item && !!item.handler) + || ("command" in item && !!item.command); + if (!opensSubmenu || acts) { this.hide(); } diff --git a/apps/client/src/menus/link_context_menu.spec.ts b/apps/client/src/menus/link_context_menu.spec.ts index a0d3d2c2ac..ca34f29fdc 100644 --- a/apps/client/src/menus/link_context_menu.spec.ts +++ b/apps/client/src/menus/link_context_menu.spec.ts @@ -92,7 +92,11 @@ describe("getItems", () => { it("folds the three places into one submenu, quick edit standing on its own", () => { const open = linkContextMenu.getOpenNoteItem(contextMenuEvent()); - expect(open).toMatchObject({ title: "link_context_menu.open_note" }); + // The entry acts as well as folding: picking it opens the note where it is opened most. + expect(open).toMatchObject({ + title: "link_context_menu.open_note", + command: "openNoteInNewTab" + }); expect("items" in open && open.items?.map((item) => "command" in item && item.command)) .toEqual([ "openNoteInNewTab", "openNoteInNewSplit", "openNoteInNewWindow" ]); expect(linkContextMenu.getQuickEditItem()).toMatchObject({ diff --git a/apps/client/src/menus/link_context_menu.ts b/apps/client/src/menus/link_context_menu.ts index e8c61320f6..1e84f46b06 100644 --- a/apps/client/src/menus/link_context_menu.ts +++ b/apps/client/src/menus/link_context_menu.ts @@ -41,12 +41,14 @@ function getQuickEditItem(): MenuItem { * The same places, folded into one submenu, for a menu that lists entries of its own beside them. * * The items keep their commands, so `handleLinkContextMenuItem` handles them from a submenu as it - * does from the top level. + * does from the top level. The entry carries the first of those commands itself, so that picking it + * opens a new tab without going into the submenu for the place it is opened in most often. */ function getOpenNoteItem(e: ContextMenuEvent | GeoMouseEvent): MenuItem { return { title: t("link_context_menu.open_note"), uiIcon: "bx bx-link-external", + command: "openNoteInNewTab", items: getOpenItems(e) }; } diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index def6b34fc6..7172334414 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -3429,7 +3429,7 @@ "insert-above": "Insert new above", "insert-below": "Insert new below", "move-to-top": "Move to top", - "more-columns": "More states…", + "more-columns": "More…", "delete-column": "Delete column...", "archive-column": "Archive column", "unarchive-column": "Unarchive column",