/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { __decorate } from "tslib";
import '../../elevation/elevation.js';
import '../../focus/md-focus-ring.js';
import { LitElement, html, isServer, nothing } from 'lit';
import { property, query, queryAssignedElements, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { EASING, createAnimationSignal } from '../../internal/motion/animation.js';
import { ListController, NavigableKeys, } from '../../list/internal/list-controller.js';
import { getActiveItem, getFirstActivatableItem, getLastActivatableItem, } from '../../list/internal/list-navigation-helpers.js';
import { FocusState, isClosableKey, isElementInSubtree, } from './controllers/shared.js';
import { Corner, SurfacePositionController, } from './controllers/surfacePositionController.js';
import { TypeaheadController } from './controllers/typeaheadController.js';
export { Corner } from './controllers/surfacePositionController.js';
/**
* The default value for the typeahead buffer time in Milliseconds.
*/
export const DEFAULT_TYPEAHEAD_BUFFER_TIME = 200;
const submenuNavKeys = new Set([
NavigableKeys.ArrowDown,
NavigableKeys.ArrowUp,
NavigableKeys.Home,
NavigableKeys.End,
]);
const menuNavKeys = new Set([
NavigableKeys.ArrowLeft,
NavigableKeys.ArrowRight,
...submenuNavKeys,
]);
/**
* Gets the currently focused element on the page.
*
* @param activeDoc The document or shadowroot from which to start the search.
* Defaults to `window.document`
* @return Returns the currently deeply focused element or `null` if none.
*/
function getFocusedElement(activeDoc = document) {
let activeEl = activeDoc.activeElement;
// Check for activeElement in the case that an element with a shadow root host
// is currently focused.
while (activeEl && activeEl?.shadowRoot?.activeElement) {
activeEl = activeEl.shadowRoot.activeElement;
}
return activeEl;
}
/**
* @fires opening {Event} Fired before the opening animation begins
* @fires opened {Event} Fired once the menu is open, after any animations
* @fires closing {Event} Fired before the closing animation begins
* @fires closed {Event} Fired once the menu is closed, after any animations
*/
export class Menu extends LitElement {
/**
* Whether the menu is animating upwards or downwards when opening. This is
* helpful for calculating some animation calculations.
*/
get openDirection() {
const menuCornerBlock = this.menuCorner.split('-')[0];
return menuCornerBlock === 'start' ? 'DOWN' : 'UP';
}
/**
* The element which the menu should align to. If `anchor` is set to a
* non-empty idref string, then `anchorEl` will resolve to the element with
* the given id in the same root node. Otherwise, `null`.
*/
get anchorElement() {
if (this.anchor) {
return this.getRootNode().querySelector(`#${this.anchor}`);
}
return this.currentAnchorElement;
}
set anchorElement(element) {
this.currentAnchorElement = element;
this.requestUpdate('anchorElement');
}
constructor() {
super();
/**
* The ID of the element in the same root node in which the menu should align
* to. Overrides setting `anchorElement = elementReference`.
*
* __NOTE__: anchor or anchorElement must either be an HTMLElement or resolve
* to an HTMLElement in order for menu to open.
*/
this.anchor = '';
/**
* Whether the positioning algorithm should calculate relative to the parent
* of the anchor element (`absolute`), relative to the window (`fixed`), or
* relative to the document (`document`). `popover` will use the popover API
* to render the menu in the top-layer. If your browser does not support the
* popover API, it will fall back to `fixed`.
*
* __Examples for `position = 'fixed'`:__
*
* - If there is no `position:relative` in the given parent tree and the
* surface is `position:absolute`
* - If the surface is `position:fixed`
* - If the surface is in the "top layer"
* - The anchor and the surface do not share a common `position:relative`
* ancestor
*
* When using `positioning=fixed`, in most cases, the menu should position
* itself above most other `position:absolute` or `position:fixed` elements
* when placed inside of them. e.g. using a menu inside of an `md-dialog`.
*
* __NOTE__: Fixed menus will not scroll with the page and will be fixed to
* the window instead.
*
* __Examples for `position = 'document'`:__
*
* - There is no parent that creates a relative positioning context e.g.
* `position: relative`, `position: absolute`, `transform: translate(x, y)`,
* etc.
* - You put the effort into hoisting the menu to the top of the DOM like the
* end of the `<body>` to render over everything or in a top-layer.
* - You are reusing a single `md-menu` element that dynamically renders
* content.
*
* __Examples for `position = 'popover'`:__
*
* - Your browser supports `popover`.
* - Most cases. Once popover is in browsers, this will become the default.
*/
this.positioning = 'absolute';
/**
* Skips the opening and closing animations.
*/
this.quick = false;
/**
* Displays overflow content like a submenu. Not required in most cases when
* using `positioning="popover"`.
*
* __NOTE__: This may cause adverse effects if you set
* `md-menu {max-height:...}`
* and have items overflowing items in the "y" direction.
*/
this.hasOverflow = false;
/**
* Opens the menu and makes it visible. Alternative to the `.show()` and
* `.close()` methods
*/
this.open = false;
/**
* Offsets the menu's inline alignment from the anchor by the given number in
* pixels. This value is direction aware and will follow the LTR / RTL
* direction.
*
* e.g. LTR: positive -> right, negative -> left
* RTL: positive -> left, negative -> right
*/
this.xOffset = 0;
/**
* Offsets the menu's block alignment from the anchor by the given number in
* pixels.
*
* e.g. positive -> down, negative -> up
*/
this.yOffset = 0;
/**
* Disable the `flip` behavior that usually happens on the horizontal axis
* when the surface would render outside the viewport.
*/
this.noHorizontalFlip = false;
/**
* Disable the `flip` behavior that usually happens on the vertical axis when
* the surface would render outside the viewport.
*/
this.noVerticalFlip = false;
/**
* The max time between the keystrokes of the typeahead menu behavior before
* it clears the typeahead buffer.
*/
this.typeaheadDelay = DEFAULT_TYPEAHEAD_BUFFER_TIME;
/**
* The corner of the anchor which to align the menu in the standard logical
* property style of <block>-<inline> e.g. `'end-start'`.
*
* NOTE: This value may not be respected by the menu positioning algorithm
* if the menu would render outisde the viewport.
* Use `no-horizontal-flip` or `no-vertical-flip` to force the usage of the value
*/
this.anchorCorner = Corner.END_START;
/**
* The corner of the menu which to align the anchor in the standard logical
* property style of <block>-<inline> e.g. `'start-start'`.
*
* NOTE: This value may not be respected by the menu positioning algorithm
* if the menu would render outisde the viewport.
* Use `no-horizontal-flip` or `no-vertical-flip` to force the usage of the value
*/
this.menuCorner = Corner.START_START;
/**
* Keeps the user clicks outside the menu.
*
* NOTE: clicking outside may still cause focusout to close the menu so see
* `stayOpenOnFocusout`.
*/
this.stayOpenOnOutsideClick = false;
/**
* Keeps the menu open when focus leaves the menu's composed subtree.
*
* NOTE: Focusout behavior will stop propagation of the focusout event. Set
* this property to true to opt-out of menu's focusout handling altogether.
*/
this.stayOpenOnFocusout = false;
/**
* After closing, does not restore focus to the last focused element before
* the menu was opened.
*/
this.skipRestoreFocus = false;
/**
* The element that should be focused by default once opened.
*
* NOTE: When setting default focus to 'LIST_ROOT', remember to change
* `tabindex` to `0` and change md-menu's display to something other than
* `display: contents` when necessary.
*/
this.defaultFocus = FocusState.FIRST_ITEM;
/**
* Turns off navigation wrapping. By default, navigating past the end of the
* menu items will wrap focus back to the beginning and vice versa. Use this
* for ARIA patterns that do not wrap focus, like combobox.
*/
this.noNavigationWrap = false;
this.typeaheadActive = true;
/**
* Whether or not the current menu is a submenu and should not handle specific
* navigation keys.
*
* @export
*/
this.isSubmenu = false;
/**
* The event path of the last window pointerdown event.
*/
this.pointerPath = [];
/**
* Whether or not the menu is repositoining due to window / document resize
*/
this.isRepositioning = false;
this.openCloseAnimationSignal = createAnimationSignal();
this.listController = new ListController({
isItem: (maybeItem) => {
return maybeItem.hasAttribute('md-menu-item');
},
getPossibleItems: () => this.slotItems,
isRtl: () => getComputedStyle(this).direction === 'rtl',
deactivateItem: (item) => {
item.selected = false;
item.tabIndex = -1;
},
activateItem: (item) => {
item.selected = true;
item.tabIndex = 0;
},
isNavigableKey: (key) => {
if (!this.isSubmenu) {
return menuNavKeys.has(key);
}
const isRtl = getComputedStyle(this).direction === 'rtl';
// we want md-submenu to handle the submenu's left/right arrow exit
// key so it can close the menu instead of navigate the list.
// Therefore we need to include all keys but left/right arrow close
// key
const arrowOpen = isRtl
? NavigableKeys.ArrowLeft
: NavigableKeys.ArrowRight;
if (key === arrowOpen) {
return true;
}
return submenuNavKeys.has(key);
},
wrapNavigation: () => !this.noNavigationWrap,
});
/**
* The element that was focused before the menu opened.
*/
this.lastFocusedElement = null;
/**
* Handles typeahead navigation through the menu.
*/
this.typeaheadController = new TypeaheadController(() => {
return {
getItems: () => this.items,
typeaheadBufferTime: this.typeaheadDelay,
active: this.typeaheadActive,
};
});
this.currentAnchorElement = null;
this.internals =
// Cast needed for closure
this.attachInternals();
/**
* Handles positioning the surface and aligning it to the anchor as well as
* keeping it in the viewport.
*/
this.menuPositionController = new SurfacePositionController(this, () => {
return {
anchorCorner: this.anchorCorner,
surfaceCorner: this.menuCorner,
surfaceEl: this.surfaceEl,
anchorEl: this.anchorElement,
positioning: this.positioning === 'popover' ? 'document' : this.positioning,
isOpen: this.open,
xOffset: this.xOffset,
yOffset: this.yOffset,
disableBlockFlip: this.noVerticalFlip,
disableInlineFlip: this.noHorizontalFlip,
onOpen: this.onOpened,
beforeClose: this.beforeClose,
onClose: this.onClosed,
// We can't resize components that have overflow like menus with
// submenus because the overflow-y will show menu items / content
// outside the bounds of the menu. Popover API fixes this because each
// submenu is hoisted to the top-layer and are not considered overflow
// content.
repositionStrategy: this.hasOverflow && this.positioning !== 'popover'
? 'move'
: 'resize',
};
});
this.onWindowResize = () => {
if (this.isRepositioning ||
(this.positioning !== 'document' &&
this.positioning !== 'fixed' &&
this.positioning !== 'popover')) {
return;
}
this.isRepositioning = true;
this.reposition();
this.isRepositioning = false;
};
this.handleFocusout = async (event) => {
const anchorEl = this.anchorElement;
// Do not close if we focused out by clicking on the anchor element. We
// can't assume anchor buttons can be the related target because of iOS does
// not focus buttons.
if (this.stayOpenOnFocusout ||
!this.open ||
this.pointerPath.includes(anchorEl)) {
return;
}
if (event.relatedTarget) {
// Don't close the menu if we are switching focus between menu,
// md-menu-item, and md-list or if the anchor was click focused, but check
// if length of pointerPath is 0 because that means something was at least
// clicked (shift+tab case).
if (isElementInSubtree(event.relatedTarget, this) ||
(this.pointerPath.length !== 0 &&
isElementInSubtree(event.relatedTarget, anchorEl))) {
return;
}
}
else if (this.pointerPath.includes(this)) {
// If menu tabindex == -1 and the user clicks on the menu or a divider, we
// want to keep the menu open.
return;
}
const oldRestoreFocus = this.skipRestoreFocus;
// allow focus to continue to the next focused object rather than returning
this.skipRestoreFocus = true;
this.close();
// await for close
await this.updateComplete;
// return to previous behavior
this.skipRestoreFocus = oldRestoreFocus;
};
/**
* Saves the last focused element focuses the new element based on
* `defaultFocus`, and animates open.
*/
this.onOpened = async () => {
this.lastFocusedElement = getFocusedElement();
const items = this.items;
const activeItemRecord = getActiveItem(items);
if (activeItemRecord && this.defaultFocus !== FocusState.NONE) {
activeItemRecord.item.tabIndex = -1;
}
let animationAborted = !this.quick;
if (this.quick) {
this.dispatchEvent(new Event('opening'));
}
else {
animationAborted = !!(await this.animateOpen());
}
// This must come after the opening animation or else it may focus one of
// the items before the animation has begun and causes the list to slide
// (block-padding-of-the-menu)px at the end of the animation
switch (this.defaultFocus) {
case FocusState.FIRST_ITEM:
const first = getFirstActivatableItem(items);
if (first) {
first.tabIndex = 0;
first.focus();
await first.updateComplete;
}
break;
case FocusState.LAST_ITEM:
const last = getLastActivatableItem(items);
if (last) {
last.tabIndex = 0;
last.focus();
await last.updateComplete;
}
break;
case FocusState.LIST_ROOT:
this.focus();
break;
default:
case FocusState.NONE:
// Do nothing.
break;
}
if (!animationAborted) {
this.dispatchEvent(new Event('opened'));
}
};
/**
* Animates closed.
*/
this.beforeClose = async () => {
this.open = false;
if (!this.skipRestoreFocus) {
this.lastFocusedElement?.focus?.();
}
if (!this.quick) {
await this.animateClose();
}
};
/**
* Focuses the last focused element.
*/
this.onClosed = () => {
if (this.quick) {
this.dispatchEvent(new Event('closing'));
this.dispatchEvent(new Event('closed'));
}
};
this.onWindowPointerdown = (event) => {
this.pointerPath = event.composedPath();
};
/**
* We cannot listen to window click because Safari on iOS will not bubble a
* click event on window if the item clicked is not a "clickable" item such as
* <body>
*/
this.onDocumentClick = (event) => {
if (!this.open) {
return;
}
const path = event.composedPath();
if (!this.stayOpenOnOutsideClick &&
!path.includes(this) &&
!path.includes(this.anchorElement)) {
this.open = false;
}
};
if (!isServer) {
this.internals.role = 'menu';
this.addEventListener('keydown', this.handleKeydown);
// Capture so that we can grab the event before it reaches the menu item
// istelf. Specifically useful for the case where typeahead encounters a
// space and we don't want the menu item to close the menu.
this.addEventListener('keydown', this.captureKeydown, { capture: true });
this.addEventListener('focusout', this.handleFocusout);
}
}
/**
* The menu items associated with this menu. The items must be `MenuItem`s and
* have both the `md-menu-item` and `md-list-item` attributes.
*/
get items() {
return this.listController.items;
}
willUpdate(changed) {
if (!changed.has('open')) {
return;
}
if (this.open) {
this.removeAttribute('aria-hidden');
return;
}
this.setAttribute('aria-hidden', 'true');
}
update(changed) {
if (changed.has('open')) {
if (this.open) {
this.setUpGlobalEventListeners();
}
else {
this.cleanUpGlobalEventListeners();
}
}
// Firefox does not support popover. Fall-back to using fixed.
if (changed.has('positioning') &&
this.positioning === 'popover' &&
// type required for Google JS conformance
!this.showPopover) {
this.positioning = 'fixed';
}
super.update(changed);
}
connectedCallback() {
super.connectedCallback();
if (this.open) {
this.setUpGlobalEventListeners();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.cleanUpGlobalEventListeners();
}
getBoundingClientRect() {
if (!this.surfaceEl) {
return super.getBoundingClientRect();
}
return this.surfaceEl.getBoundingClientRect();
}
getClientRects() {
if (!this.surfaceEl) {
return super.getClientRects();
}
return this.surfaceEl.getClientRects();
}
render() {
return this.renderSurface();
}
/**
* Renders the positionable surface element and its contents.
*/
renderSurface() {
return html `
<div
class="menu ${classMap(this.getSurfaceClasses())}"
style=${styleMap(this.menuPositionController.surfaceStyles)}
popover=${this.positioning === 'popover' ? 'manual' : nothing}>
${this.renderElevation()}
<div class="items">
<div class="item-padding"> ${this.renderMenuItems()} </div>
</div>
</div>
`;
}
/**
* Renders the menu items' slot
*/
renderMenuItems() {
return html `<slot
@close-menu=${this.onCloseMenu}
@deactivate-items=${this.onDeactivateItems}
@request-activation=${this.onRequestActivation}
@deactivate-typeahead=${this.handleDeactivateTypeahead}
@activate-typeahead=${this.handleActivateTypeahead}
@stay-open-on-focusout=${this.handleStayOpenOnFocusout}
@close-on-focusout=${this.handleCloseOnFocusout}
@slotchange=${this.listController.onSlotchange}></slot>`;
}
/**
* Renders the elevation component.
*/
renderElevation() {
return html `<md-elevation part="elevation"></md-elevation>`;
}
getSurfaceClasses() {
return {
open: this.open,
fixed: this.positioning === 'fixed',
'has-overflow': this.hasOverflow,
};
}
captureKeydown(event) {
if (event.target === this &&
!event.defaultPrevented &&
isClosableKey(event.code)) {
event.preventDefault();
this.close();
}
this.typeaheadController.onKeydown(event);
}
/**
* Performs the opening animation:
*
* https://direct.googleplex.com/#/spec/295000003+271060003
*
* @return A promise that resolve to `true` if the animation was aborted,
* `false` if it was not aborted.
*/
async animateOpen() {
const surfaceEl = this.surfaceEl;
const slotEl = this.slotEl;
if (!surfaceEl || !slotEl)
return true;
const openDirection = this.openDirection;
this.dispatchEvent(new Event('opening'));
// needs to be imperative because we don't want to mix animation and Lit
// render timing
surfaceEl.classList.toggle('animating', true);
const signal = this.openCloseAnimationSignal.start();
const height = surfaceEl.offsetHeight;
const openingUpwards = openDirection === 'UP';
const children = this.items;
const FULL_DURATION = 500;
const SURFACE_OPACITY_DURATION = 50;
const ITEM_OPACITY_DURATION = 250;
// We want to fit every child fade-in animation within the full duration of
// the animation.
const DELAY_BETWEEN_ITEMS = (FULL_DURATION - ITEM_OPACITY_DURATION) / children.length;
const surfaceHeightAnimation = surfaceEl.animate([{ height: '0px' }, { height: `${height}px` }], {
duration: FULL_DURATION,
easing: EASING.EMPHASIZED,
});
// When we are opening upwards, we want to make sure the last item is always
// in view, so we need to translate it upwards the opposite direction of the
// height animation
const upPositionCorrectionAnimation = slotEl.animate([
{ transform: openingUpwards ? `translateY(-${height}px)` : '' },
{ transform: '' },
], { duration: FULL_DURATION, easing: EASING.EMPHASIZED });
const surfaceOpacityAnimation = surfaceEl.animate([{ opacity: 0 }, { opacity: 1 }], SURFACE_OPACITY_DURATION);
const childrenAnimations = [];
for (let i = 0; i < children.length; i++) {
// If we are animating upwards, then reverse the children list.
const directionalIndex = openingUpwards ? children.length - 1 - i : i;
const child = children[directionalIndex];
const animation = child.animate([{ opacity: 0 }, { opacity: 1 }], {
duration: ITEM_OPACITY_DURATION,
delay: DELAY_BETWEEN_ITEMS * i,
});
// Make them all initially hidden and then clean up at the end of each
// animation.
child.classList.toggle('md-menu-hidden', true);
animation.addEventListener('finish', () => {
child.classList.toggle('md-menu-hidden', false);
});
childrenAnimations.push([child, animation]);
}
let resolveAnimation = (value) => { };
const animationFinished = new Promise((resolve) => {
resolveAnimation = resolve;
});
signal.addEventListener('abort', () => {
surfaceHeightAnimation.cancel();
upPositionCorrectionAnimation.cancel();
surfaceOpacityAnimation.cancel();
childrenAnimations.forEach(([child, animation]) => {
child.classList.toggle('md-menu-hidden', false);
animation.cancel();
});
resolveAnimation(true);
});
surfaceHeightAnimation.addEventListener('finish', () => {
surfaceEl.classList.toggle('animating', false);
this.openCloseAnimationSignal.finish();
resolveAnimation(false);
});
return await animationFinished;
}
/**
* Performs the closing animation:
*
* https://direct.googleplex.com/#/spec/295000003+271060003
*/
animateClose() {
let resolve;
// This promise blocks the surface position controller from setting
// display: none on the surface which will interfere with this animation.
const animationEnded = new Promise((res) => {
resolve = res;
});
const surfaceEl = this.surfaceEl;
const slotEl = this.slotEl;
if (!surfaceEl || !slotEl) {
resolve(false);
return animationEnded;
}
const openDirection = this.openDirection;
const closingDownwards = openDirection === 'UP';
this.dispatchEvent(new Event('closing'));
// needs to be imperative because we don't want to mix animation and Lit
// render timing
surfaceEl.classList.toggle('animating', true);
const signal = this.openCloseAnimationSignal.start();
const height = surfaceEl.offsetHeight;
const children = this.items;
const FULL_DURATION = 150;
const SURFACE_OPACITY_DURATION = 50;
// The surface fades away at the very end
const SURFACE_OPACITY_DELAY = FULL_DURATION - SURFACE_OPACITY_DURATION;
const ITEM_OPACITY_DURATION = 50;
const ITEM_OPACITY_INITIAL_DELAY = 50;
const END_HEIGHT_PERCENTAGE = 0.35;
// We want to fit every child fade-out animation within the full duration of
// the animation.
const DELAY_BETWEEN_ITEMS = (FULL_DURATION - ITEM_OPACITY_INITIAL_DELAY - ITEM_OPACITY_DURATION) /
children.length;
// The mock has the animation shrink to 35%
const surfaceHeightAnimation = surfaceEl.animate([
{ height: `${height}px` },
{ height: `${height * END_HEIGHT_PERCENTAGE}px` },
], {
duration: FULL_DURATION,
easing: EASING.EMPHASIZED_ACCELERATE,
});
// When we are closing downwards, we want to make sure the last item is
// always in view, so we need to translate it upwards the opposite direction
// of the height animation
const downPositionCorrectionAnimation = slotEl.animate([
{ transform: '' },
{
transform: closingDownwards
? `translateY(-${height * (1 - END_HEIGHT_PERCENTAGE)}px)`
: '',
},
], { duration: FULL_DURATION, easing: EASING.EMPHASIZED_ACCELERATE });
const surfaceOpacityAnimation = surfaceEl.animate([{ opacity: 1 }, { opacity: 0 }], { duration: SURFACE_OPACITY_DURATION, delay: SURFACE_OPACITY_DELAY });
const childrenAnimations = [];
for (let i = 0; i < children.length; i++) {
// If the animation is closing upwards, then reverse the list of
// children so that we animate in the opposite direction.
const directionalIndex = closingDownwards ? i : children.length - 1 - i;
const child = children[directionalIndex];
const animation = child.animate([{ opacity: 1 }, { opacity: 0 }], {
duration: ITEM_OPACITY_DURATION,
delay: ITEM_OPACITY_INITIAL_DELAY + DELAY_BETWEEN_ITEMS * i,
});
// Make sure the items stay hidden at the end of each child animation.
// We clean this up at the end of the overall animation.
animation.addEventListener('finish', () => {
child.classList.toggle('md-menu-hidden', true);
});
childrenAnimations.push([child, animation]);
}
signal.addEventListener('abort', () => {
surfaceHeightAnimation.cancel();
downPositionCorrectionAnimation.cancel();
surfaceOpacityAnimation.cancel();
childrenAnimations.forEach(([child, animation]) => {
animation.cancel();
child.classList.toggle('md-menu-hidden', false);
});
resolve(false);
});
surfaceHeightAnimation.addEventListener('finish', () => {
surfaceEl.classList.toggle('animating', false);
childrenAnimations.forEach(([child]) => {
child.classList.toggle('md-menu-hidden', false);
});
this.openCloseAnimationSignal.finish();
this.dispatchEvent(new Event('closed'));
resolve(true);
});
return animationEnded;
}
handleKeydown(event) {
// At any key event, the pointer interaction is done so we need to clear our
// cached pointerpath. This handles the case where the user clicks on the
// anchor, and then hits shift+tab
this.pointerPath = [];
this.listController.handleKeydown(event);
}
setUpGlobalEventListeners() {
document.addEventListener('click', this.onDocumentClick, { capture: true });
window.addEventListener('pointerdown', this.onWindowPointerdown);
document.addEventListener('resize', this.onWindowResize, { passive: true });
window.addEventListener('resize', this.onWindowResize, { passive: true });
}
cleanUpGlobalEventListeners() {
document.removeEventListener('click', this.onDocumentClick, {
capture: true,
});
window.removeEventListener('pointerdown', this.onWindowPointerdown);
document.removeEventListener('resize', this.onWindowResize);
window.removeEventListener('resize', this.onWindowResize);
}
onCloseMenu() {
this.close();
}
onDeactivateItems(event) {
event.stopPropagation();
this.listController.onDeactivateItems();
}
onRequestActivation(event) {
event.stopPropagation();
this.listController.onRequestActivation(event);
}
handleDeactivateTypeahead(event) {
// stopPropagation so that this does not deactivate any typeaheads in menus
// nested above it e.g. md-sub-menu
event.stopPropagation();
this.typeaheadActive = false;
}
handleActivateTypeahead(event) {
// stopPropagation so that this does not activate any typeaheads in menus
// nested above it e.g. md-sub-menu
event.stopPropagation();
this.typeaheadActive = true;
}
handleStayOpenOnFocusout(event) {
event.stopPropagation();
this.stayOpenOnFocusout = true;
}
handleCloseOnFocusout(event) {
event.stopPropagation();
this.stayOpenOnFocusout = false;
}
close() {
this.open = false;
const maybeSubmenu = this.slotItems;
maybeSubmenu.forEach((item) => {
item.close?.();
});
}
show() {
this.open = true;
}
/**
* Activates the next item in the menu. If at the end of the menu, the first
* item will be activated.
*
* @return The activated menu item or `null` if there are no items.
*/
activateNextItem() {
return this.listController.activateNextItem() ?? null;
}
/**
* Activates the previous item in the menu. If at the start of the menu, the
* last item will be activated.
*
* @return The activated menu item or `null` if there are no items.
*/
activatePreviousItem() {
return this.listController.activatePreviousItem() ?? null;
}
/**
* Repositions the menu if it is open.
*
* Useful for the case where document or window-positioned menus have their
* anchors moved while open.
*/
reposition() {
if (this.open) {
this.menuPositionController.position();
}
}
}
__decorate([
query('.menu')
], Menu.prototype, "surfaceEl", void 0);
__decorate([
query('slot')
], Menu.prototype, "slotEl", void 0);
__decorate([
property()
], Menu.prototype, "anchor", void 0);
__decorate([
property()
], Menu.prototype, "positioning", void 0);
__decorate([
property({ type: Boolean })
], Menu.prototype, "quick", void 0);
__decorate([
property({ type: Boolean, attribute: 'has-overflow' })
], Menu.prototype, "hasOverflow", void 0);
__decorate([
property({ type: Boolean, reflect: true })
], Menu.prototype, "open", void 0);
__decorate([
property({ type: Number, attribute: 'x-offset' })
], Menu.prototype, "xOffset", void 0);
__decorate([
property({ type: Number, attribute: 'y-offset' })
], Menu.prototype, "yOffset", void 0);
__decorate([
property({ type: Boolean, attribute: 'no-horizontal-flip' })
], Menu.prototype, "noHorizontalFlip", void 0);
__decorate([
property({ type: Boolean, attribute: 'no-vertical-flip' })
], Menu.prototype, "noVerticalFlip", void 0);
__decorate([
property({ type: Number, attribute: 'typeahead-delay' })
], Menu.prototype, "typeaheadDelay", void 0);
__decorate([
property({ attribute: 'anchor-corner' })
], Menu.prototype, "anchorCorner", void 0);
__decorate([
property({ attribute: 'menu-corner' })
], Menu.prototype, "menuCorner", void 0);
__decorate([
property({ type: Boolean, attribute: 'stay-open-on-outside-click' })
], Menu.prototype, "stayOpenOnOutsideClick", void 0);
__decorate([
property({ type: Boolean, attribute: 'stay-open-on-focusout' })
], Menu.prototype, "stayOpenOnFocusout", void 0);
__decorate([
property({ type: Boolean, attribute: 'skip-restore-focus' })
], Menu.prototype, "skipRestoreFocus", void 0);
__decorate([
property({ attribute: 'default-focus' })
], Menu.prototype, "defaultFocus", void 0);
__decorate([
property({ type: Boolean, attribute: 'no-navigation-wrap' })
], Menu.prototype, "noNavigationWrap", void 0);
__decorate([
queryAssignedElements({ flatten: true })
], Menu.prototype, "slotItems", void 0);
__decorate([
state()
], Menu.prototype, "typeaheadActive", void 0);
//# sourceMappingURL=menu.js.map