Toolbars and menus ​
Build the strip of tools next to a viewport, open a menu from one of its buttons, and keep viewport shortcuts quiet while a dialog or a menu is open.
1. Register the icons ​
ts
// toolIcons.ts
import { svg } from "lit";
import { registerIcon } from "@jolly-pixel/ui";
registerIcon("brush", svg`
<path class="tone-fill" d="M4 20c4 0 6-2 6-5l-3-3c-3 0-5 2-3 8Z" />
<path d="m10 15 9-9-3-3-9 9" fill="none" stroke="currentColor" stroke-width="2" />
`, { tone: "lime" });ts
import "./toolIcons.ts";- Import the module for its side effect, before the toolbar renders. A
jolly-icondrawn before its glyph is registered does not update. - The root entry point loads the registry without any element, so a module that only registers glyphs stays cheap in unit tests.
tone-filltakes the tone given at registration. Ajolly-tool-buttondrawing the icon tints its hover and active fill with it.
2. Lay out the tools ​
html
<jolly-toolbar label="Sculpt tools" orientation="vertical">
<jolly-tool-button icon="brush" label="Raise" active></jolly-tool-button>
<jolly-tool-button icon="smooth" label="Smooth"></jolly-tool-button>
<jolly-tool-button icon="shape" label="Brush shape" flyout-side="right">
<jolly-tool-button slot="flyout" icon="circle" label="Circle"></jolly-tool-button>
<jolly-tool-button slot="flyout" icon="square" label="Square"></jolly-tool-button>
</jolly-tool-button>
</jolly-toolbar>jolly-tool-buttonis a square icon button.labelis its accessible name and its tooltip.activemarks the current tool. The toolbar does not track it: set it from the application state, one tool at a time.- Content in the
flyoutslot turns the button into a trigger. The flyout opens on hover, and on click for touch, pen and keyboard. Clicking a button inside closes it. jolly-toolbargives the strip the toolbar role.jolly-raillays out the same strip with no role, for controls that are not a toolbar.- To pick one value out of a few, a
jolly-button-groupwithicon-onlyis a field: it emitsjolly-changeand takes part in an inspector.
3. Open a menu from a button ​
A jolly-context-menu opens at a point. menuPointBelow(event) gives the bottom-left corner of the clicked button:
ts
import {
ContextMenuController,
MenuSession,
menuPointBelow
} from "@jolly-pixel/ui";
#menu = new ContextMenuController(() => this.menu, () => MenuSession.EMPTY);
#openLighting = (event: MouseEvent) => {
this.#menu.open(
MenuSession.picker(kLightingOptions, "No lighting modes", (mode) => {
this.view.update({ lighting: mode });
}),
menuPointBelow(event)
);
};ts
html`
<jolly-button aria-haspopup="menu" @click=${this.#openLighting}>Lighting</jolly-button>
<jolly-context-menu
label="Lighting"
@jolly-context-action=${this.#menu.onContextAction}
></jolly-context-menu>
`;MenuSession.pickerbuilds one item per{ label, value }option and passes the chosen value. With no option, it shows one disabled item with the second argument as label.MenuSession.from(items, run)takes any entries, icons and submenus included.- The menu flips above the point when there is no room below.
- Closing it returns focus to the button.
() => MenuSession.EMPTYtells the controller that this menu only opens throughopen().
4. Run a menu action from a button ​
With the row menu controller of Hierarchy panels, a toolbar button runs the same action without opening the menu:
ts
html`
<jolly-button
icon="close"
label="Delete layer"
icon-only
@click=${(event: MouseEvent) => this.#menu.run(this.selectedId, "delete", {
x: event.clientX,
y: event.clientY
})}
></jolly-button>
`;run() asks openSession(id) for the session, so a disabled entry stays disabled from the button too.
5. Keep viewport keys out of overlays ​
An open jolly-dialog, jolly-context-menu or field popover holds an input layer. While one is open, every keydown belongs to the UI. Tell the viewport keyboard to skip those keys:
ts
import { inputLayers } from "@jolly-pixel/ui";
const dispose = input.keyboard.addGuard(inputLayers);The guard has the shape @jolly-pixel/controls expects. A plain listener asks the same question:
ts
window.addEventListener("keydown", (event) => {
if (inputLayers.blocks(event)) {
return;
}
shortcuts.handle(event);
});6. Claim the keyboard for an overlay of your own ​
ts
show(): void {
if (!inputLayers.dismissAll()) {
return;
}
this.panel.hidden = false;
this.#release = inputLayers.push({
dismiss: () => {
this.hide();
return true;
}
});
}
hide(): void {
this.panel.hidden = true;
this.#release?.();
this.#release = null;
}dismissAll()closes every open layer, newest first. Onfalse, one of them refused, such as a dialog that is notdismissible: do not open over it.- The release function returned by
push()can be called more than once. - Hover flyouts, such as a
jolly-tool-buttonflyout, hold no layer.