@universal-material/web
Version:
Material web components
349 lines • 11.7 kB
JavaScript
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}
=${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