Files
openclaw/ui/src/components/dropdown-menu-controller.ts
T
Peter Steinberger ad409a8285 fix(ui): preserve session, tool, archive, and focus ownership (#117191)
* fix(ui): unify tool, session, and keyboard interaction owners

* refactor(ui): separate archived session regression coverage

* test(ui): isolate memory plugin mutation mock registries

---------

Co-authored-by: Peter Steinberger <steipete@macos.shared>
2026-07-31 22:45:53 -07:00

68 lines
2.1 KiB
TypeScript

import type { ReactiveController, ReactiveControllerHost } from "lit";
import { trackDropdownKeyboardDismissal } from "./web-awesome.ts";
type DropdownMenuHost = ReactiveControllerHost & HTMLElement;
export class DropdownMenuController implements ReactiveController {
private generation = 0;
constructor(
private readonly host: DropdownMenuHost,
private readonly options: {
getTrigger: () => HTMLElement | null;
onClose: () => void;
onKeydown?: (event: KeyboardEvent) => void;
},
) {
host.addController(this);
}
hostConnected(): void {
document.addEventListener("keydown", this.handleDocumentKeydown, true);
const generation = ++this.generation;
void this.focusFirstItem(generation);
}
hostDisconnected(): void {
this.generation += 1;
document.removeEventListener("keydown", this.handleDocumentKeydown, true);
}
private readonly handleDocumentKeydown = (event: KeyboardEvent) => {
if (event.key !== "Escape") {
// Nested dialogs and outside targets own their own Tab order; only menu
// items need the durable trigger before Web Awesome dismisses them.
if (
event.key === "Tab" &&
event
.composedPath()
.some(
(target) =>
target instanceof Element &&
target.localName === "wa-dropdown-item" &&
this.host.contains(target),
)
) {
trackDropdownKeyboardDismissal(event, () => this.options.getTrigger()?.focus());
}
this.options.onKeydown?.(event);
return;
}
event.preventDefault();
event.stopPropagation();
this.options.getTrigger()?.focus();
this.options.onClose();
};
private async focusFirstItem(generation: number): Promise<void> {
await this.host.updateComplete;
const dropdown = this.host.querySelector<HTMLElement & { updateComplete?: Promise<unknown> }>(
"wa-dropdown",
);
await dropdown?.updateComplete;
if (this.host.isConnected && generation === this.generation) {
this.host.querySelector<HTMLElement>("wa-dropdown-item:not([disabled])")?.focus();
}
}
}