client/ui/context menu: tweak card context menu

This commit is contained in:
Adorian Doran 2026-09-11 20:00:07 +03:00
parent 56dad66adc
commit eb9fd98d03
5 changed files with 53 additions and 5 deletions

View File

@ -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();

View File

@ -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();
}

View File

@ -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({

View File

@ -41,12 +41,14 @@ function getQuickEditItem(): MenuItem<CommandNames> {
* 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<CommandNames> {
return {
title: t("link_context_menu.open_note"),
uiIcon: "bx bx-link-external",
command: "openNoteInNewTab",
items: getOpenItems(e)
};
}

View File

@ -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",