UNPKG

@material/web

Version:
914 lines 36.7 kB
/** * @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