The Menu class is very limited: it allows only adding menu items of a predetermined form.
I understand it helps keeping plugin user interfacees consistent across the app and make menu layouts adapt to platform.
However, sometimes I would like to do more within menus.
A good example is the ones in the core Bases plugin, which has a search box, expandable items, toggle boxes, etc.
It would be great if Obsidian API could expose a more general menu class that allows developers to render arbitrary DOMs in it (just like views, setting tabs, etc.).
It could be an abstract class with an abstract method called onOpen() or render(), where all the relevant elements are mounted.
The current Menu could be a subclass of the general class.
Currently, we have to work around it by creating custom menu components, however it makes difficult to keep the UI consistent.
For example, it’s not easy to recreate the exact behavior of the builtin menus on mobile where their appearance automatically adapts to the current platform: for example, on smartphones, the Bases menu looks like this:
(I can kind of reproduce this adaptive behavior by just using .menu, .menu-grabber, .menu-scroll and .suggestion-bg classes, but there are more things to consider, for example swipes and animations)
import { type App, Menu, Scope } from 'obsidian';
export abstract class GeneralMenu extends Menu {
constructor(app: App) {
super();
// use DOM-based menu instead of system-native menu
this.setUseNativeMenu(false);
// this dummy empty item is necessary because
// if the menu had no item, it cannot be displayed
// by showAtPosition()
this.addItem(() => { });
// discard the existing scope and replace it with a new one
// that only handles the escape key;
// other existing handlers assume the menu is composed of MenuItems,
// which is not the case for general menus.
this.scope = new Scope(app.scope);
this.scope.register([], 'Escape', () => this.hide());
}
onload() {
// the dummy item is cleared here
this.scrollEl.empty();
this.onOpen(this.scrollEl);
super.onload();
}
onMenuClick(evt: MouseEvent) {
// override to prevent the menu from being closed when
// clicking on the menu content
}
/* Render your custom UI in scrollEl */
protected abstract onOpen(scrollEl: HTMLElement): void;
}