diff --git a/studio-ui/src/lib/menu.element.ts b/studio-ui/src/lib/menu.element.ts index 2d0bd23..c9bab66 100644 --- a/studio-ui/src/lib/menu.element.ts +++ b/studio-ui/src/lib/menu.element.ts @@ -36,69 +36,6 @@ export class MenuElement extends LitElement { #panelRef = createRef(); #handle?: WeakRef; - protected override update(changes: PropertyValues): void { - super.update(changes); - - if (this.#handle) - this.overlay.reRender(this.#handle); - } - - protected override updated(changes: PropertyValues): void { - if (changes.has("trigger")) { - const prevTrigger = changes.get("trigger"); - if (prevTrigger?.value) { - prevTrigger.value.removeEventListener("click", this.onTriggerClick); - } - - const trigger = this.trigger.value; - if (!trigger) return; - - trigger.setAttribute("aria-owns", this.#menuId); - trigger.ariaHasPopup = "true"; - trigger.ariaExpanded = "false"; - - trigger.addEventListener("click", this.onTriggerClick); - } - - super.updated(changes); - } - - override disconnectedCallback(): void { - this.trigger.value?.removeEventListener("click", this.onTriggerClick); - super.disconnectedCallback(); - } - - onTriggerClick = (): void => { - this.show(); - } - - @on("window:click") - onWindowClick(event: PointerEvent): void { - if (!this.#handle || !this.#panelRef.value) - return; - - const path = event.composedPath(); - if ( - !path.includes(this.#panelRef.value) - && (!this.trigger.value || !path.includes(this.trigger.value)) - ) { - this.hide(); - } - } - - onMenuClick(event: PointerEvent): void { - event.stopPropagation(); - - if ( - this.autoClose - && event.target - && event.target instanceof HTMLElement - && event.target.role === "menuitem" - ) { - this.hide(); - } - } - show(): void { const trigger = this.trigger.value; if (!trigger) return; @@ -118,14 +55,13 @@ export class MenuElement extends LitElement { top: `${rect.bottom + 4}px`, left: `${rect.left}px`, })} - @click=${this.onMenuClick} - > - ${ - // Move the nodes assigned to the 's slot into the - // 's slot - this.#slotRef.value?.assignedNodes() - } - + ?autoClose=${this.autoClose} + @hide=${this.hide} + >${ + // Move the nodes assigned to the 's slot into the + // 's slot + this.#slotRef.value?.assignedNodes() + } `); } @@ -149,6 +85,43 @@ export class MenuElement extends LitElement { this.#handle = undefined; } + protected override update(changes: PropertyValues): void { + super.update(changes); + + if (this.#handle) + this.overlay.reRender(this.#handle); + } + + protected override updated(changes: PropertyValues): void { + if (changes.has("trigger")) { + const prevTrigger = changes.get("trigger"); + if (prevTrigger?.value) { + prevTrigger.value.removeEventListener("click", this.#onTriggerClick); + } + + const trigger = this.trigger.value; + if (!trigger) return; + + trigger.setAttribute("aria-owns", this.#menuId); + trigger.ariaHasPopup = "true"; + trigger.ariaExpanded = "false"; + + trigger.addEventListener("click", this.#onTriggerClick); + } + + super.updated(changes); + } + + override disconnectedCallback(): void { + this.trigger.value?.removeEventListener("click", this.#onTriggerClick); + super.disconnectedCallback(); + } + + #onTriggerClick = (event: Event): void => { + event.stopPropagation(); + this.show(); + } + protected override render = () => html` ` @@ -162,16 +135,33 @@ export class MenuPanelElement extends LitElement { @property({ reflect: true }) override role = "menu"; + @property({ type: Boolean }) + autoClose = false; + #slotRef = createRef(); getSlot(): HTMLSlotElement | undefined { return this.#slotRef.value; } + @on("click") + onClick(event: PointerEvent): void { + event.stopPropagation(); + + if ( + this.autoClose + && event.target + && event.target instanceof HTMLElement + && event.target.role === "menuitem" + ) { + this.dispatchEvent(new CustomEvent("hide")); + } + } + + @on("window:click") + onWindowClick(event: PointerEvent): void { + if (!event.composedPath().includes(this)) + this.dispatchEvent(new CustomEvent("hide")); + } + protected override render = () => html` ` } - -declare global { - interface HTMLElementTagNameMap { - "sts-menu-panel": MenuPanelElement; - } -} diff --git a/studio-ui/src/lib/overlay-provider.element.ts b/studio-ui/src/lib/overlay-provider.element.ts index 878610b..774de97 100644 --- a/studio-ui/src/lib/overlay-provider.element.ts +++ b/studio-ui/src/lib/overlay-provider.element.ts @@ -2,17 +2,24 @@ import { UniqueToken, provide } from "@storyteller/framework"; import { LitElement, TemplateResult, html, render } from "lit"; import { customElement } from "lit/decorators.js"; import { Ref, createRef, ref } from "lit/directives/ref.js"; -import { styleMap } from "lit/directives/style-map.js"; +import { StyleInfo, styleMap } from "lit/directives/style-map.js"; export const OVERLAY_PROVIDER = UniqueToken.create(); // TODO: Document this export interface OverlayProvider { - attach(host: LitElement, template: TemplateResult): WeakRef; + attach( + host: LitElement, + template: TemplateResult, + options?: AttachOptions, + ): WeakRef; detach(handle: WeakRef): void; reRender(handle: WeakRef): void; } +export interface AttachOptions { + containerStyles?: Readonly; +} // TODO: Document this export interface OverlayHandle { @@ -42,10 +49,16 @@ export class OverlayProviderElement #containerTemplates: TemplateResult[] = []; #templates: [LitElement, TemplateResult][] = []; - attach(host: LitElement, template: TemplateResult): WeakRef { + attach( + host: LitElement, + template: TemplateResult, + options: AttachOptions = {}, + ): WeakRef { const index = this.#templates.length; const handle = new OverlayHandleInternal(index); + const containerStyles = options.containerStyles ?? {}; + this.#handles.push(handle); this.#containerRefs.push(createRef()); this.#containerTemplates.push(html` @@ -55,6 +68,7 @@ export class OverlayProviderElement style=${styleMap({ position: "fixed", inset: 0, + ...containerStyles, })} > `);