UNPKG

@universal-material/web

Version:
349 lines 11.7 kB
import { __decorate } from "tslib"; import { html, LitElement } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { styles as baseStyles } from '../shared/base.styles.js'; import { styles } from './menu.styles.js'; import '../elevation/elevation.js'; let UmMenu = class UmMenu extends LitElement { constructor() { super(...arguments); this.#open = false; this.#preInitOpen = false; this.autoclose = true; this.positioning = 'relative'; this.manualFocus = false; /** * The corner of the anchor which to align the menu in the standard logical * property style of <block>-<inline> e.g. `'end-start'`. */ this.anchorCorner = 'end-start'; /** * The direction of the menu. e.g. `'down-end'`. * * NOTE: This value may not be respected by the menu positioning algorithm * if the menu would render outside the viewport. */ this.direction = 'down-end'; /** * Don't limit the height of the menu */ this.allowOverflow = false; this.#onOpened = () => this.dispatchEvent(new Event('opened')); this.#onClosed = () => { this.menu.style.display = 'none'; this.dispatchEvent(new Event('closed')); }; this.toggle = () => { if (this.open) { this.close(); return; } this.show(); }; this.close = () => { this.open = false; }; this.#clickClose = () => { if (this.autoclose !== false) { this.open = false; } }; this.#handleMenuClick = (e) => { if (this.autoclose === 'outside') { e.stopPropagation(); } }; } static { this.styles = [baseStyles, styles]; } #open; #preInitOpen; /** * Opens the menu and makes it visible. Alternative to the `.show()`, `.close()` and `.toggle()` methods */ get open() { return this.#open; } set open(open) { if (!this.menu) { this.#preInitOpen = open; return; } if (this.open === open) { return; } this.menu.removeEventListener('transitionend', this.#onClosed, true); this.menu.removeEventListener('transitionend', this.#onOpened, true); if (!open) { const closePrevented = !this.dispatchEvent(new Event('close', { cancelable: true })); if (closePrevented) { return; } this.#open = open; this.#hide(); return; } const openPrevented = !this.dispatchEvent(new Event('open', { cancelable: true })); if (openPrevented) { return; } this.#open = open; this.#show(); } #show() { this.menu.style.display = ''; this.calcDropdownPositioning(); this.menu.addEventListener('transitionend', this.#onOpened, { capture: true, once: true, }); setTimeout(() => document.addEventListener('click', this.#clickClose)); if (this.manualFocus) { return; } setTimeout(() => this.querySelector('u-menu-item:not([disabled])')?.focus()); } #hide() { document.removeEventListener('click', this.#clickClose); this.menu.addEventListener('transitionend', this.#onClosed, { capture: true, once: true, }); } get scrollContainer() { return this.menu; } #onOpened; #onClosed; #anchorElement; get anchorElement() { return this.#anchorElement ?? this.parentElement ?? this.getRootNode().host; } set anchorElement(anchorElement) { this.#anchorElement = anchorElement; } render() { const menuClasses = { open: this.open }; return html ` <div class="ref"></div> <div class="menu ${classMap(menuClasses)}" part="menu" style="display: none" ?inert=${!this.open} @click=${this.#handleMenuClick}> <u-elevation></u-elevation> <div role="menu" class="content" part="content"> <slot></slot> </div> </div> `; } connectedCallback() { super.connectedCallback(); this.role = 'listbox'; this.#setInitOpen(); } show() { this.open = true; } #clickClose; async #setInitOpen() { await this.updateComplete; if (this.#preInitOpen) { this.open = true; } } #handleMenuClick; calcDropdownPositioning() { if (!this.anchorElement) { return; } const menuPosition = this.getMenuPosition(); const menuSize = this.getMenuSize(); this.#resetMenu(); this.#setToOpenUpOrDown(menuPosition, menuSize); this.#setToOpenToStartOrEnd(menuPosition, menuSize); } #resetMenu() { this.menu.className = 'menu'; this.menu.style.top = ''; this.menu.style.bottom = ''; this.menu.style.left = ''; this.menu.style.right = ''; this.menu.style.maxHeight = ''; } #setToOpenUpOrDown(menuPosition, menuSize) { if (this.anchorCorner.startsWith('auto-')) { this.#openBlockAuto(menuPosition, menuSize); return; } const side = this.anchorCorner.startsWith('start-') ? menuPosition.bounds.top : menuPosition.bounds.bottom; if (this.direction.startsWith('up-')) { this.#tryOpenUp(side, menuSize); return; } this.#tryOpenDown(side, menuSize); } #openBlockAuto(menuPosition, menuSize) { const topSide = menuPosition.bounds.top; const bottomSide = menuPosition.bounds.bottom; const viewPortHeight = window.innerHeight; if (bottomSide.bottom >= topSide.top || viewPortHeight - (bottomSide.top + menuSize.height) >= 0) { this.#openDown(bottomSide); return; } this.#openUp(topSide); } #tryOpenUp(side, menuSize) { if (side.top === side.bottom || side.top - menuSize.height >= 0) { this.#openUp(side); return; } this.#openToLargestBlockSide(side); } #tryOpenDown(side, menuSize) { const viewPortHeight = window.innerHeight; if (side.top === side.bottom || viewPortHeight - (side.top + menuSize.height) >= 0) { this.#openDown(side); return; } this.#openToLargestBlockSide(side); } #openToLargestBlockSide(side) { if (side.top > side.bottom) { this.#openUp(side); return; } this.#openDown(side); } #setToOpenToStartOrEnd(menuPosition, menuSize) { const openStart = menuPosition.isRtl ? this.#tryOpenRight.bind(this) : this.#tryOpenLeft.bind(this); const openEnd = menuPosition.isRtl ? this.#tryOpenLeft.bind(this) : this.#tryOpenRight.bind(this); const side = this.anchorCorner.endsWith('-start') ? menuPosition.bounds.start : menuPosition.bounds.end; if (this.direction.endsWith('-start')) { openStart(side, menuSize); return; } openEnd(side, menuSize); } #tryOpenLeft(side, menuSize) { if (side.left === side.right || side.left - menuSize.width >= 0) { this.menu.style.right = `${side.relativeX * -1}px`; return; } this.#openToLargestInlineSide(side); } #tryOpenRight(side, menuSize) { const viewPortWidth = window.innerWidth; if (side.left === side.right || viewPortWidth - (side.left + menuSize.width) >= 0) { this.menu.style.left = `${side.relativeX}px`; return; } this.#openToLargestInlineSide(side); } #openToLargestInlineSide(side) { if (side.left > side.right) { this.menu.style.right = `${side.relativeX * -1}px`; return; } this.menu.style.left = `${side.relativeX}px`; } #openUp(side) { const viewPortHeight = window.innerHeight; this.menu.style.bottom = `${side.relativeY * -1}px`; this.menu.classList.add('up'); this.menu.style.maxHeight = this.allowOverflow ? '' : `${viewPortHeight - side.bottom}px`; } #openDown(side) { const viewPortHeight = window.innerHeight; this.menu.style.top = `${side.relativeY}px`; this.menu.style.maxHeight = this.allowOverflow ? '' : `${viewPortHeight - side.top}px`; } getMenuPosition() { const viewPortWidth = window.innerWidth; const viewPortHeight = window.innerHeight; const anchorElement = this.anchorElement; const anchorRect = anchorElement.getBoundingClientRect(); const refRect = this.ref.getBoundingClientRect(); const anchorStyles = getComputedStyle(anchorElement); const isRtl = anchorStyles.direction === 'rtl'; const width = parseInt(anchorStyles.width, 10); const height = parseInt(anchorStyles.height, 10); const rectX = refRect.left; const rectY = refRect.top; const leftPoint = { left: anchorRect.left, right: viewPortWidth - anchorRect.left, relativeX: anchorRect.left - rectX, }; const rightPoint = { left: anchorRect.right, right: viewPortWidth - anchorRect.right, relativeX: leftPoint.relativeX + width, }; const topPoint = { top: anchorRect.top, relativeY: anchorRect.top - rectY, bottom: viewPortHeight - anchorRect.top, }; const anchorBounds = { top: topPoint, bottom: { top: anchorRect.bottom, relativeY: topPoint.relativeY + height, bottom: viewPortHeight - anchorRect.bottom, }, start: isRtl ? rightPoint : leftPoint, end: isRtl ? leftPoint : rightPoint, width, height, }; return { isRtl, bounds: anchorBounds, }; } getMenuSize() { const menu = this.menu; const menuStyles = getComputedStyle(menu); const width = parseInt(menuStyles.width, 10); const height = parseInt(menuStyles.height, 10); return { width, height, }; } }; __decorate([ property() ], UmMenu.prototype, "autoclose", void 0); __decorate([ property({ type: Boolean, reflect: true }) ], UmMenu.prototype, "open", null); __decorate([ property({ reflect: true }) ], UmMenu.prototype, "positioning", void 0); __decorate([ property({ type: Boolean }) ], UmMenu.prototype, "manualFocus", void 0); __decorate([ property({ attribute: 'anchor-corner', reflect: true }) ], UmMenu.prototype, "anchorCorner", void 0); __decorate([ property({ reflect: true }) ], UmMenu.prototype, "direction", void 0); __decorate([ property({ type: Boolean, attribute: 'allow-overflow', reflect: true }) ], UmMenu.prototype, "allowOverflow", void 0); __decorate([ query('.menu') ], UmMenu.prototype, "menu", void 0); __decorate([ query('.ref') ], UmMenu.prototype, "ref", void 0); UmMenu = __decorate([ customElement('u-menu') ], UmMenu); export { UmMenu }; //# sourceMappingURL=menu.js.map