/** * @license * Copyright 2023 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import '@material/web/button/filled-button.js'; import '@material/web/divider/divider.js'; import '@material/web/icon/icon.js'; import '@material/web/menu/menu.js'; import '@material/web/menu/menu-item.js'; import '@material/web/menu/sub-menu.js'; import {MaterialStoryInit} from './material-collection.js'; import {CloseMenuEvent} from '@material/web/menu/internal/controllers/shared.js'; import {Corner, FocusState, MdMenu, MenuItem} from '@material/web/menu/menu.js'; import {css, html} from 'lit'; /** Knob types for Menu stories. */ export interface StoryKnobs { menu: void; anchorCorner: Corner | undefined; menuCorner: Corner | undefined; defaultFocus: FocusState | undefined; positioning: 'absolute' | 'fixed' | 'document' | 'popover' | undefined; open: boolean; quick: boolean; hasOverflow: boolean; stayOpenOnOutsideClick: boolean; stayOpenOnFocusout: boolean; skipRestoreFocus: boolean; xOffset: number; yOffset: number; noVerticalFlip: boolean; noHorizontalFlip: boolean; typeaheadDelay: number; listTabIndex: number; 'menu-item': void; keepOpen: boolean; disabled: boolean; href: string; 'link icon': string; 'sub-menu': void; 'submenu.anchorCorner': Corner | undefined; 'submenu.menuCorner': Corner | undefined; hoverOpenDelay: number; hoverCloseDelay: number; 'submenu item icon': string; } const fruitNames = [ 'Apple', 'Apricot', 'Avocado', 'Green Apple', 'Green Grapes', 'Olive', 'Orange', ]; const sharedStyle = css` #anchor { display: block; border: 1px solid var(--md-sys-color-on-background); color: var(--md-sys-color-on-background); width: 100px; padding: 16px; text-align: center; } .md-stories-bg-override { display: flex; justify-content: center; width: min(700px, 80vw); } .root { display: flex; flex-direction: column; justify-content: center; align-items: center; } .output { color: var(--md-sys-color-on-background); margin-top: 4px; font-family: sans-serif; } [dir='rtl'] md-icon { transform: scaleX(-1); } [slot='headline'] { white-space: nowrap; } `; const standard: MaterialStoryInit = { name: 'Menu with items', styles: sharedStyle, render(knobs) { return html`
${fruitNames.map( (name, index) => html`
${name}
`, )}

      
`; }, }; const linkable: MaterialStoryInit = { name: 'Menu with links', styles: sharedStyle, render(knobs) { const items = fruitNames.map((name, index) => { return html`
${name}
${knobs['link icon']}
${index === 2 ? html`` : ''}`; }); return html`
${items}

      
`; }, }; const submenu: MaterialStoryInit = { name: 'Menu with sub-menus', styles: sharedStyle, render(knobs) { let currentIndex = -1; // This is the third layer with all menu items which close on selection const layer2 = fruitNames.slice(4).map((name) => { currentIndex++; return html`
${name}
`; }); // This is the second layer with a mix of submenu items and menu items const layer1 = [ ...fruitNames.slice(0, 2).map((name) => { currentIndex++; return html`
${name}
${knobs['submenu item icon']}
${layer2}
`; }), ...fruitNames.slice(2, 5).map((name) => { currentIndex++; return html`
${name}
`; }), ]; // This is the first layer with all sub menu items const layer0 = fruitNames.map((name) => { currentIndex++; return html`
${name}
${knobs['submenu item icon']}
${layer1}
`; }); return html`
${layer0}

      
`; }, }; const menuWithoutButton: MaterialStoryInit = { name: 'Menu without button', styles: [ sharedStyle, css` #anchor { display: block; border: 1px solid var(--md-sys-color-on-background); color: var(--md-sys-color-on-background); width: 100px; } #storyWrapper { display: flex; justify-content: center; width: min(700px, 80vw); } `, ], render(knobs) { return html`
This is the anchor (use the "open" knob)
${fruitNames.map( (name, index) => html`
${name}
`, )}

      
`; }, }; /** * Searches for an MdMenu with the id="menu" in the same shadow root and calls * `menu.show()` to open the menu. If it is a keyboard event, it will call show * only if the key is ArrowDown as is standard a11y practice. This function also * attempts to find a menu button with `#button` set on it and sets * aria-expanded=true. */ function toggleMenu(event: Event | KeyboardEvent) { // get the menu from the event const root = (event.target as HTMLElement).getRootNode() as ShadowRoot; const menu = root.querySelector('#menu') as MdMenu; // determine if is keyboard event const isKeyboardEvent = ( event: KeyboardEvent | Event, ): event is KeyboardEvent => { return (event as KeyboardEvent).key !== undefined; }; const isKeyboard = isKeyboardEvent(event); // if is a click, open the menu if (!isKeyboard) { menu.open = !menu.open; // if is arrow down, open the menu and prevent default to prevent scrolling } else if (event.key === 'ArrowDown') { menu.open = !menu.open; event.preventDefault(); } // set aria-expanded true on the button root.querySelector('#button')?.setAttribute('aria-expanded', 'true'); } /** * Searches for an element with `class="output"` set on it, and updates the * text of that element with the menu-closed event's content. */ function displayCloseEvent(event: CloseMenuEvent) { // get the output element from the shadow root const root = (event.target as HTMLElement).getRootNode() as ShadowRoot; const outputEl = root.querySelector('.output') as HTMLElement; const stringifyItem = (menuItem: MenuItem & HTMLElement) => { const tagName = menuItem.tagName.toLowerCase(); const headline = menuItem.typeaheadText; return `${tagName}${ menuItem.id ? `[id="${menuItem.id}"]` : '' } > [slot="headline"] > ${headline}`; }; // display the event's details in the inner text of that output element outputEl.textContent = `CustomEvent { type: ${event.type}, target: ${stringifyItem(event.target as unknown as MenuItem)}, detail: { initiator: ${stringifyItem(event.detail.initiator)}, itemPath: [ ${event.detail.itemPath.map((item) => stringifyItem(item)).join(`, `)} ], }, reason: ${JSON.stringify(event.detail.reason)} }`; } function setButtonAriaExpandedFalse(e: Event) { const root = (e.target as HTMLElement).getRootNode() as ShadowRoot; // get the button element and remove aria-expaned if exists root.querySelector('#button')?.removeAttribute('aria-expanded'); } /** Menu stories. */ export const stories = [standard, linkable, submenu, menuWithoutButton];