Thank you for being patient! We're working hard on resolving the issue
When multiple plugins show menus (popovers, context menus, forms), the gesture coordination system ensures only one is visible at a time and dismisses others cleanly.
The coordination system has three parts:
User clicks event in daily view
→ Daily system opens event details
→ coordinator.globalState.dismissOtherSystems(self)
→ All-day system: dismisses its open form
→ Monthly system: dismisses its open popover
→ Daily system registers its state
Any plugin that shows menus should implement GestureSystem:
import type { GestureSystem } from "@tento-lona/sheets-ui";
class TaskGestureHandler implements GestureSystem {
closeAllMenus(): void {
this.dismissTaskDetails();
this.dismissContextMenu();
this.dismissCreateForm();
}
}
Register with the coordinator when your plugin binds to a row:
onBindRow(handle: SheetDelegate, row: SheetRow.Joined.Client): void {
this.coordinator.register(this.gestureHandler);
}
When your gesture handler shows a menu, follow this protocol:
showTaskDetails(
task: TaskItem,
position: { x: ColumnIndex.Key.Resolved.WithSubindex; y: RowLocalId },
): void {
// 1. Create your content (you decide what to show)
const $details = TaskDetails.make(task);
// 2. Dismiss other systems
this.coordinator.globalState.dismissOtherSystems(this);
// 3. Show via the sheet's positioning API
handle.popover.reveal("task-details", $details, position, {
offsetY: this.computeOffset(task),
context: "task-details",
});
// 4. Register your state so others can dismiss you
this.coordinator.globalState.registerState({
source: this,
id: "task-details",
dismiss: () => {
handle.popover.dismiss("task-details");
this.state = null;
},
});
}
Steps 2 and 4 are critical. Without step 2, your menu opens alongside another system's menu. Without step 4, no one can dismiss yours.
When hiding your own menu, unregister the state:
hideTaskDetails(): void {
this.coordinator.globalState.unregisterState("task-details");
handle.popover.dismiss("task-details");
this.state = null;
}
Use blocking: true for unfinished creation or edit flows that should consume
the dismissing interaction.
this.coordinator.globalState.registerState({
source: this,
id: "task-pending-new",
blocking: true,
dismiss: () => {
handle.popover.dismiss("new-task");
this.pendingDraft = null;
},
});
Typical rule:
When creating new items (drag-to-create), the first click after another
system's menu is open should dismiss that menu — not start a new creation.
Use CrossSystemDismissalGuard for this:
import { CrossSystemDismissalGuard } from "@tento-lona/sheets-ui";
class ClickToCreateTask {
private guard = new CrossSystemDismissalGuard(this, this.coordinator.globalState);
canHandle(event: GestureEvent): boolean {
// Empty-space creation should consume the click if any other menu was dismissed.
if (
this.guard.shouldBlock(event.isPointerDown, {
blockOnAnyDismissal: true,
})
) return false;
return event.isEmptyCell;
}
}
The guard tracks pointer interactions. Use the default behavior for gestures
that may continue after dismissing non-blocking state, and
blockOnAnyDismissal: true for empty-space creation where the dismissal itself
should consume the click.
The sheet provides four positioning handles. Choose based on your needs:
For details panels, create forms. Tracks scroll position.
handle.popover.reveal(
"task-details",
$content,
{ x: column, y: rowId }, // Point<WithSubindex, RowLocalId>
{ offsetY: 48, context: "task-details" },
);
x: Column coordinate — ColumnIndex.Key.Resolved.WithSubindexy: Row coordinate — RowLocalIdoffsetY: Pixel offset from row top (e.g., based on time position)For context menus, inline controls. Positioned relative to a column anchor with a pixel inset.
handle.fixedChild.reveal(
"task-context-menu",
$menu,
{ x: column, y: rowId },
{ inset: { left: offset.x, top: offset.y }, widthBehavior: 240 },
);
For content within a single row that doesn't need column tracking. Used by the monthly calendar view.
handle.staticChild.reveal(
"month-popover",
$content,
rowId,
{ offset: { x: pixelX, y: pixelY } },
);
handle.tooltip.showTooltip(
"task-tooltip",
$tooltip,
{ x: column, y: rowId },
"bottom",
);
Use descriptive, namespaced IDs for your states:
// Good
"task-pending-new"
"task-existing-details"
"task-context-menu"
// Bad — collides with other systems
"pending"
"details"
"menu"