import type { PropertyValues } from 'lit' import { css, html } from 'lit' import { property, query } from 'lit/decorators.js' import { when } from 'lit/directives/when.js' import type { Placement } from '@floating-ui/dom' import { autoUpdate, computePosition, flip, offset } from '@floating-ui/dom' import { UsBaseElement } from '../base/UsBaseElement' import { customElementIfNotExist } from '../../utils/customElementIfNotExist' import { dispatchCustomEvent } from '../../utils/dispatchCustomEvent' import type { IMenuProps } from './model/IMenuProps' import UsMenu_css from './menu.pcss' import '../list/UsList' declare global { interface HTMLElementTagNameMap { 'us-menu': UsMenu } } @customElementIfNotExist('us-menu') export class UsMenu extends UsBaseElement implements IMenuProps { static styles = [ UsBaseElement.styles, css([UsMenu_css] as TemplateStringsArray | any), ] @property({ type: Boolean }) open = false @property({ type: String }) corner: Placement = 'bottom' @property({ type: Array }) menuItems: any = [] @property() width: any = '100%' @property({ type: Number }) offsetX = 0 @property({ type: Number }) offsetY = 0 @property({ type: String, attribute: 'data-testid', reflect: true }) dataTestId = 'menu' @query('#anchor') anchor!: HTMLDivElement @query('#menuList') menuList!: HTMLDivElement onWindowClickListener: (event: Event) => void cleanup: ReturnType | null = null constructor() { super() this.onWindowClickListener = this.onWindowClick.bind(this) } connectedCallback() { super.connectedCallback() window.addEventListener('click', this.onWindowClickListener, true) } updated(changedProperties: PropertyValues) { if (changedProperties.has('open') || changedProperties.has('corner')) this.showMenuList() } disconnectedCallback() { this.cleanupFloatingUi() window.removeEventListener('click', this.onWindowClickListener, true) super.disconnectedCallback() } render() { return html` ` } onWindowClick(event: Event) { if (!event.composedPath().includes(this) && this.open) { this.open = !this.open if (!this.open) this.dispatchEvent(new CustomEvent('close')) } } showMenuList() { if (this.anchor && this.menuList) { if (this.cleanup) this.computePosition() else this.cleanup = autoUpdate(this.anchor, this.menuList, () => this.computePosition()) } } cleanupFloatingUi() { this.cleanup && this.cleanup() this.cleanup = null } computePosition() { if (!this.open) return const options = { placement: this.corner || 'bottom', middleware: [ offset({ mainAxis: this.offsetY, crossAxis: this.offsetX }), flip(), ], } computePosition(this.anchor, this.menuList, options).then(() => { computePosition(this.anchor, this.menuList, options).then(({ x, y }) => { Object.assign(this.menuList.style, { left: `${x}px`, top: `${y}px`, }) }) }) } selectListItem(event: CustomEvent) { dispatchCustomEvent('clickListItem', event.detail.data, this) } }