UNPKG

@lumino/widgets

Version:
1,279 lines (1,127 loc) 35.5 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. /*----------------------------------------------------------------------------- | Copyright (c) 2014-2017, PhosphorJS Contributors | | Distributed under the terms of the BSD 3-Clause License. | | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ import { ArrayExt } from '@lumino/algorithm'; import { ElementExt } from '@lumino/domutils'; import { getKeyboardLayout } from '@lumino/keyboard'; import { Message, MessageLoop } from '@lumino/messaging'; import { CommandRegistry } from '@lumino/commands'; import { ElementARIAAttrs, ElementDataset, h, VirtualDOM, VirtualElement } from '@lumino/virtualdom'; import { Menu } from './menu'; import { Title } from './title'; import { Widget } from './widget'; /** * A widget which displays menus as a canonical menu bar. * * #### Notes * See also the related [example](../../examples/menubar/index.html) and * its [source](https://github.com/jupyterlab/lumino/tree/main/examples/example-menubar). */ export class MenuBar extends Widget { /** * Construct a new menu bar. * * @param options - The options for initializing the menu bar. */ constructor(options: MenuBar.IOptions = {}) { super({ node: Private.createNode() }); this.addClass('lm-MenuBar'); this.setFlag(Widget.Flag.DisallowLayout); this.renderer = options.renderer || MenuBar.defaultRenderer; this._forceItemsPosition = options.forceItemsPosition || { forceX: true, forceY: true }; this._overflowMenuOptions = options.overflowMenuOptions || { isVisible: true }; } /** * Dispose of the resources held by the widget. */ dispose(): void { this._closeChildMenu(); this._menus.length = 0; super.dispose(); } /** * The renderer used by the menu bar. */ readonly renderer: MenuBar.IRenderer; /** * The child menu of the menu bar. * * #### Notes * This will be `null` if the menu bar does not have an open menu. */ get childMenu(): Menu | null { return this._childMenu; } /** * The overflow index of the menu bar. */ get overflowIndex(): number { return this._overflowIndex; } /** * The overflow menu of the menu bar. */ get overflowMenu(): Menu | null { return this._overflowMenu; } /** * Get the menu bar content node. * * #### Notes * This is the node which holds the menu title nodes. * * Modifying this node directly can lead to undefined behavior. */ get contentNode(): HTMLUListElement { return this.node.getElementsByClassName( 'lm-MenuBar-content' )[0] as HTMLUListElement; } /** * Get the currently active menu. */ get activeMenu(): Menu | null { return this._menus[this._activeIndex] || null; } /** * Set the currently active menu. * * #### Notes * If the menu does not exist, the menu will be set to `null`. */ set activeMenu(value: Menu | null) { this.activeIndex = value ? this._menus.indexOf(value) : -1; } /** * Get the index of the currently active menu. * * #### Notes * This will be `-1` if no menu is active. */ get activeIndex(): number { return this._activeIndex; } /** * Set the index of the currently active menu. * * #### Notes * If the menu cannot be activated, the index will be set to `-1`. */ set activeIndex(value: number) { // Adjust the value for an out of range index. if (value < 0 || value >= this._menus.length) { value = -1; } // An empty menu cannot be active if (value > -1 && this._menus[value].items.length === 0) { value = -1; } // Bail early if the index will not change. if (this._activeIndex === value) { return; } // Update the active index. this._activeIndex = value; // Schedule an update of the items. this.update(); } /** * A read-only array of the menus in the menu bar. */ get menus(): ReadonlyArray<Menu> { return this._menus; } /** * Open the active menu and activate its first menu item. * * #### Notes * If there is no active menu, this is a no-op. */ openActiveMenu(): void { // Bail early if there is no active item. if (this._activeIndex === -1) { return; } // Open the child menu. this._openChildMenu(); // Activate the first item in the child menu. if (this._childMenu) { this._childMenu.activeIndex = -1; this._childMenu.activateNextItem(); } } /** * Add a menu to the end of the menu bar. * * @param menu - The menu to add to the menu bar. * * #### Notes * If the menu is already added to the menu bar, it will be moved. */ addMenu(menu: Menu, update: boolean = true): void { this.insertMenu(this._menus.length, menu, update); } /** * Insert a menu into the menu bar at the specified index. * * @param index - The index at which to insert the menu. * * @param menu - The menu to insert into the menu bar. * * #### Notes * The index will be clamped to the bounds of the menus. * * If the menu is already added to the menu bar, it will be moved. */ insertMenu(index: number, menu: Menu, update: boolean = true): void { // Close the child menu before making changes. this._closeChildMenu(); // Look up the index of the menu. let i = this._menus.indexOf(menu); // Clamp the insert index to the array bounds. let j = Math.max(0, Math.min(index, this._menus.length)); // If the menu is not in the array, insert it. if (i === -1) { // Insert the menu into the array. ArrayExt.insert(this._menus, j, menu); // Add the styling class to the menu. menu.addClass('lm-MenuBar-menu'); // Connect to the menu signals. menu.aboutToClose.connect(this._onMenuAboutToClose, this); menu.menuRequested.connect(this._onMenuMenuRequested, this); menu.title.changed.connect(this._onTitleChanged, this); // Schedule an update of the items. if (update) { this.update(); } // There is nothing more to do. return; } // Otherwise, the menu exists in the array and should be moved. // Adjust the index if the location is at the end of the array. if (j === this._menus.length) { j--; } // Bail if there is no effective move. if (i === j) { return; } // Move the menu to the new locations. ArrayExt.move(this._menus, i, j); // Schedule an update of the items. if (update) { this.update(); } } /** * Remove a menu from the menu bar. * * @param menu - The menu to remove from the menu bar. * * #### Notes * This is a no-op if the menu is not in the menu bar. */ removeMenu(menu: Menu, update: boolean = true): void { this.removeMenuAt(this._menus.indexOf(menu), update); } /** * Remove the menu at a given index from the menu bar. * * @param index - The index of the menu to remove. * * #### Notes * This is a no-op if the index is out of range. */ removeMenuAt(index: number, update: boolean = true): void { // Close the child menu before making changes. this._closeChildMenu(); // Remove the menu from the array. let menu = ArrayExt.removeAt(this._menus, index); // Bail if the index is out of range. if (!menu) { return; } // Disconnect from the menu signals. menu.aboutToClose.disconnect(this._onMenuAboutToClose, this); menu.menuRequested.disconnect(this._onMenuMenuRequested, this); menu.title.changed.disconnect(this._onTitleChanged, this); // Remove the styling class from the menu. menu.removeClass('lm-MenuBar-menu'); // Schedule an update of the items. if (update) { this.update(); } } /** * Remove all menus from the menu bar. */ clearMenus(): void { // Bail if there is nothing to remove. if (this._menus.length === 0) { return; } // Close the child menu before making changes. this._closeChildMenu(); // Disconnect from the menu signals and remove the styling class. for (let menu of this._menus) { menu.aboutToClose.disconnect(this._onMenuAboutToClose, this); menu.menuRequested.disconnect(this._onMenuMenuRequested, this); menu.title.changed.disconnect(this._onTitleChanged, this); menu.removeClass('lm-MenuBar-menu'); } // Clear the menus array. this._menus.length = 0; // Schedule an update of the items. this.update(); } /** * Handle the DOM events for the menu bar. * * @param event - The DOM event sent to the menu bar. * * #### Notes * This method implements the DOM `EventListener` interface and is * called in response to events on the menu bar's DOM nodes. It * should not be called directly by user code. */ handleEvent(event: Event): void { switch (event.type) { case 'keydown': this._evtKeyDown(event as KeyboardEvent); break; case 'mousedown': this._evtMouseDown(event as MouseEvent); break; case 'mousemove': this._evtMouseMove(event as MouseEvent); break; case 'focusout': this._evtFocusOut(event as FocusEvent); break; case 'contextmenu': event.preventDefault(); event.stopPropagation(); break; } } /** * A message handler invoked on a `'before-attach'` message. */ protected onBeforeAttach(msg: Message): void { this.node.addEventListener('keydown', this); this.node.addEventListener('mousedown', this); this.node.addEventListener('mousemove', this); this.node.addEventListener('focusout', this); this.node.addEventListener('contextmenu', this); } /** * A message handler invoked on an `'after-detach'` message. */ protected onAfterDetach(msg: Message): void { this.node.removeEventListener('keydown', this); this.node.removeEventListener('mousedown', this); this.node.removeEventListener('mousemove', this); this.node.removeEventListener('focusout', this); this.node.removeEventListener('contextmenu', this); this._closeChildMenu(); } /** * A message handler invoked on an `'activate-request'` message. */ protected onActivateRequest(msg: Message): void { if (this.isAttached) { this._focusItemAt(0); } } /** * A message handler invoked on a `'resize'` message. */ protected onResize(msg: Widget.ResizeMessage): void { this.update(); super.onResize(msg); } /** * A message handler invoked on an `'update-request'` message. */ protected onUpdateRequest(msg: Message): void { let menus = this._menus; let renderer = this.renderer; let activeIndex = this._activeIndex; let tabFocusIndex = this._tabFocusIndex >= 0 && this._tabFocusIndex < menus.length ? this._tabFocusIndex : 0; let length = this._overflowIndex > -1 ? this._overflowIndex : menus.length; let totalMenuSize = 0; let isVisible = false; // Check that the overflow menu doesn't count length = this._overflowMenu !== null ? length - 1 : length; let content = new Array<VirtualElement>(length); // Render visible menus for (let i = 0; i < length; ++i) { content[i] = renderer.renderItem({ title: menus[i].title, active: i === activeIndex, tabbable: i === tabFocusIndex, disabled: menus[i].items.length === 0, onfocus: () => { this._tabFocusIndex = i; this.activeIndex = i; } }); // Calculate size of current menu totalMenuSize += this._menuItemSizes[i]; // Check if overflow menu is already rendered if (menus[i].title.label === this._overflowMenuOptions.title) { isVisible = true; length--; } } // Render overflow menu if needed and active if (this._overflowMenuOptions.isVisible) { if (this._overflowIndex > -1 && !isVisible) { // Create overflow menu if (this._overflowMenu === null) { const overflowMenuTitle = this._overflowMenuOptions.title ?? '...'; this._overflowMenu = new Menu({ commands: new CommandRegistry() }); this._overflowMenu.title.label = overflowMenuTitle; this._overflowMenu.title.mnemonic = 0; this.addMenu(this._overflowMenu, false); } // Move menus to overflow menu for (let i = menus.length - 2; i >= length; i--) { const submenu = this.menus[i]; submenu.title.mnemonic = 0; this._overflowMenu.insertItem(0, { type: 'submenu', submenu: submenu }); this.removeMenu(submenu, false); } content[length] = renderer.renderItem({ title: this._overflowMenu.title, active: length === activeIndex && menus[length].items.length !== 0, tabbable: length === tabFocusIndex, disabled: menus[length].items.length === 0, onfocus: () => { this._tabFocusIndex = length; this.activeIndex = length; } }); length++; } else if (this._overflowMenu !== null) { // Remove submenus from overflow menu let overflowMenuItems = this._overflowMenu.items; let screenSize = this.node.offsetWidth; let n = this._overflowMenu.items.length; for (let i = 0; i < n; ++i) { let index = menus.length - 1 - i; if (screenSize - totalMenuSize > this._menuItemSizes[index]) { let menu = overflowMenuItems[0].submenu as Menu; this._overflowMenu.removeItemAt(0); this.insertMenu(length, menu, false); content[length] = renderer.renderItem({ title: menu.title, active: false, tabbable: length === tabFocusIndex, disabled: menus[length].items.length === 0, onfocus: () => { this._tabFocusIndex = length; this.activeIndex = length; } }); length++; } } if (this._overflowMenu.items.length === 0) { this.removeMenu(this._overflowMenu, false); content.pop(); this._overflowMenu = null; this._overflowIndex = -1; } } } VirtualDOM.render(content, this.contentNode); this._updateOverflowIndex(); } /** * Calculate and update the current overflow index. */ private _updateOverflowIndex(): void { if (!this._overflowMenuOptions.isVisible) { return; } // Get elements visible in the main menu bar const itemMenus = this.contentNode.childNodes; let screenSize = this.node.offsetWidth; let totalMenuSize = 0; let index = -1; let n = itemMenus.length; if (this._menuItemSizes.length == 0) { // Check if it is the first resize and get info about menu items sizes for (let i = 0; i < n; i++) { let item = itemMenus[i] as HTMLLIElement; // Add sizes to array totalMenuSize += item.offsetWidth; this._menuItemSizes.push(item.offsetWidth); if (totalMenuSize > screenSize && index === -1) { index = i; } } } else { // Calculate current menu size for (let i = 0; i < this._menuItemSizes.length; i++) { totalMenuSize += this._menuItemSizes[i]; if (totalMenuSize > screenSize) { index = i; break; } } } this._overflowIndex = index; } /** * Handle the `'keydown'` event for the menu bar. * * #### Notes * All keys are trapped except the tab key that is ignored. */ private _evtKeyDown(event: KeyboardEvent): void { // Fetch the key code for the event. let kc = event.keyCode; // Reset the active index on tab, but do not trap the tab key. if (kc === 9) { this.activeIndex = -1; return; } // A menu bar handles all other keydown events. event.preventDefault(); event.stopPropagation(); // Enter, Space, Up Arrow, Down Arrow if (kc === 13 || kc === 32 || kc === 38 || kc === 40) { // The active index may have changed (for example, user hovers over an // item with the mouse), so be sure to use the focus index. this.activeIndex = this._tabFocusIndex; if (this.activeIndex !== this._tabFocusIndex) { // Bail if the setter refused to set activeIndex to tabFocusIndex // because it means that the item at tabFocusIndex cannot be opened (for // example, it has an empty menu) return; } this.openActiveMenu(); return; } // Escape if (kc === 27) { this._closeChildMenu(); this._focusItemAt(this.activeIndex); return; } // Left or Right Arrow if (kc === 37 || kc === 39) { let direction = kc === 37 ? -1 : 1; let start = this._tabFocusIndex + direction; let n = this._menus.length; for (let i = 0; i < n; i++) { let index = (n + start + direction * i) % n; if (this._menus[index].items.length) { this._focusItemAt(index); return; } } return; } // Get the pressed key character. let key = getKeyboardLayout().keyForKeydownEvent(event); // Bail if the key is not valid. if (!key) { return; } // Search for the next best matching mnemonic item. let start = this._activeIndex + 1; let result = Private.findMnemonic(this._menus, key, start); // Handle the requested mnemonic based on the search results. // If exactly one mnemonic is matched, that menu is opened. // Otherwise, the next mnemonic is activated if available, // followed by the auto mnemonic if available. if (result.index !== -1 && !result.multiple) { this.activeIndex = result.index; this.openActiveMenu(); } else if (result.index !== -1) { this.activeIndex = result.index; this._focusItemAt(this.activeIndex); } else if (result.auto !== -1) { this.activeIndex = result.auto; this._focusItemAt(this.activeIndex); } } /** * Handle the `'mousedown'` event for the menu bar. */ private _evtMouseDown(event: MouseEvent): void { // Bail if the mouse press was not on the menu bar. This can occur // when the document listener is installed for an active menu bar. if (!ElementExt.hitTest(this.node, event.clientX, event.clientY)) { return; } // Stop the propagation of the event. Immediate propagation is // also stopped so that an open menu does not handle the event. event.stopPropagation(); event.stopImmediatePropagation(); // Check if the mouse is over one of the menu items. let index = ArrayExt.findFirstIndex(this.contentNode.children, node => { return ElementExt.hitTest(node, event.clientX, event.clientY); }); // If the press was not on an item, close the child menu. if (index === -1) { this._closeChildMenu(); return; } // If the press was not the left mouse button, do nothing further. if (event.button !== 0) { return; } // Otherwise, toggle the open state of the child menu. if (this._childMenu) { this._closeChildMenu(); this.activeIndex = index; } else { // If we don't call preventDefault() here, then the item in the menu // bar will take focus over the menu that is being opened. event.preventDefault(); const position = this._positionForMenu(index); Menu.saveWindowData(); // Begin DOM modifications. this.activeIndex = index; this._openChildMenu(position); } } /** * Handle the `'mousemove'` event for the menu bar. */ private _evtMouseMove(event: MouseEvent): void { // Check if the mouse is over one of the menu items. let index = ArrayExt.findFirstIndex(this.contentNode.children, node => { return ElementExt.hitTest(node, event.clientX, event.clientY); }); // Bail early if the active index will not change. if (index === this._activeIndex) { return; } // Bail early if a child menu is open and the mouse is not over // an item. This allows the child menu to be kept open when the // mouse is over the empty part of the menu bar. if (index === -1 && this._childMenu) { return; } // Get position for the new menu >before< updating active index. const position = index >= 0 && this._childMenu ? this._positionForMenu(index) : null; // Before any modification, update window data. Menu.saveWindowData(); // Begin DOM modifications. // Update the active index to the hovered item. this.activeIndex = index; // Open the new menu if a menu is already open. if (position) { this._openChildMenu(position); } } /** * Find initial position for the menu based on menubar item position. * * NOTE: this should be called before updating active index to avoid * an additional layout and style invalidation as changing active * index modifies DOM. */ private _positionForMenu(index: number): Private.IPosition { let itemNode = this.contentNode.children[index]; let { left, bottom } = (itemNode as HTMLElement).getBoundingClientRect(); return { top: bottom, left }; } /** * Handle the `'focusout'` event for the menu bar. */ private _evtFocusOut(event: FocusEvent): void { // Reset the active index if there is no open menu and the menubar is losing focus. if (!this._childMenu && !this.node.contains(event.relatedTarget as Node)) { this.activeIndex = -1; } } /** * Focus an item in the menu bar. * * #### Notes * Does not open the associated menu. */ private _focusItemAt(index: number): void { const itemNode = this.contentNode.childNodes[index] as HTMLElement | void; if (itemNode) { itemNode.focus(); } } /** * Open the child menu at the active index immediately. * * If a different child menu is already open, it will be closed, * even if there is no active menu. */ private _openChildMenu(options: { left?: number; top?: number } = {}): void { // If there is no active menu, close the current menu. let newMenu = this.activeMenu; if (!newMenu) { this._closeChildMenu(); return; } // Bail if there is no effective menu change. let oldMenu = this._childMenu; if (oldMenu === newMenu) { return; } // Swap the internal menu reference. this._childMenu = newMenu; // Close the current menu, or setup for the new menu. if (oldMenu) { oldMenu.close(); } else { document.addEventListener('mousedown', this, true); } // Update the tab focus index and ensure the menu bar is updated. this._tabFocusIndex = this.activeIndex; MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest); // Get the positioning data for the new menu. let { left, top } = options; if (typeof left === 'undefined' || typeof top === 'undefined') { ({ left, top } = this._positionForMenu(this._activeIndex)); } // Begin DOM modifications if (!oldMenu) { // Continue setup for new menu this.addClass('lm-mod-active'); } // Open the new menu at the computed location. if (newMenu.items.length > 0) { newMenu.open(left, top, this._forceItemsPosition); } } /** * Close the child menu immediately. * * This is a no-op if a child menu is not open. */ private _closeChildMenu(): void { // Bail if no child menu is open. if (!this._childMenu) { return; } // Remove the active class from the menu bar. this.removeClass('lm-mod-active'); // Remove the document listeners. document.removeEventListener('mousedown', this, true); // Clear the internal menu reference. let menu = this._childMenu; this._childMenu = null; // Close the menu. menu.close(); // Reset the active index. this.activeIndex = -1; } /** * Handle the `aboutToClose` signal of a menu. */ private _onMenuAboutToClose(sender: Menu): void { // Bail if the sender is not the child menu. if (sender !== this._childMenu) { return; } // Remove the active class from the menu bar. this.removeClass('lm-mod-active'); // Remove the document listeners. document.removeEventListener('mousedown', this, true); // Clear the internal menu reference. this._childMenu = null; // Reset the active index. this.activeIndex = -1; } /** * Handle the `menuRequested` signal of a child menu. */ private _onMenuMenuRequested(sender: Menu, args: 'next' | 'previous'): void { // Bail if the sender is not the child menu. if (sender !== this._childMenu) { return; } // Look up the active index and menu count. let i = this._activeIndex; let n = this._menus.length; // Active the next requested index. switch (args) { case 'next': this.activeIndex = i === n - 1 ? 0 : i + 1; break; case 'previous': this.activeIndex = i === 0 ? n - 1 : i - 1; break; } // Open the active menu. this.openActiveMenu(); } /** * Handle the `changed` signal of a title object. */ private _onTitleChanged(): void { this.update(); } // Track the index of the item that is currently focused or hovered. -1 means nothing focused or hovered. private _activeIndex = -1; // Track which item can be focused using the TAB key. Unlike _activeIndex will // always point to a menuitem. Whenever you update this value, it's important // to follow it with an "update-request" message so that the `tabindex` // attribute on each menubar item gets properly updated. private _tabFocusIndex = 0; private _forceItemsPosition: Menu.IOpenOptions; private _overflowMenuOptions: IOverflowMenuOptions; private _menus: Menu[] = []; private _childMenu: Menu | null = null; private _overflowMenu: Menu | null = null; private _menuItemSizes: number[] = []; private _overflowIndex: number = -1; } /** * The namespace for the `MenuBar` class statics. */ export namespace MenuBar { /** * An options object for creating a menu bar. */ export interface IOptions { /** * A custom renderer for creating menu bar content. * * The default is a shared renderer instance. */ renderer?: IRenderer; /** * Whether to force the position of the menu. The MenuBar forces the * coordinates of its menus by default. With this option you can disable it. * * Setting to `false` will enable the logic which repositions the * coordinates of the menu if it will not fit entirely on screen. * * The default is `true`. */ forceItemsPosition?: Menu.IOpenOptions; /** * Whether to add a overflow menu if there's overflow. * * Setting to `true` will enable the logic that creates an overflow menu * to show the menu items that don't fit entirely on the screen. * * The default is `true`. */ overflowMenuOptions?: IOverflowMenuOptions; } /** * An object which holds the data to render a menu bar item. */ export interface IRenderData { /** * The title to be rendered. */ readonly title: Title<Widget>; /** * Whether the item is the active item. */ readonly active: boolean; /** * Whether the user can tab to the item. */ readonly tabbable: boolean; /** * Whether the item is disabled. * * #### Notes * A disabled item cannot be active. * A disabled item cannot be focussed. */ readonly disabled?: boolean; readonly onfocus?: (event: FocusEvent) => void; } /** * A renderer for use with a menu bar. */ export interface IRenderer { /** * Render the virtual element for a menu bar item. * * @param data - The data to use for rendering the item. * * @returns A virtual element representing the item. */ renderItem(data: IRenderData): VirtualElement; } /** * The default implementation of `IRenderer`. * * #### Notes * Subclasses are free to reimplement rendering methods as needed. */ export class Renderer implements IRenderer { /** * Render the virtual element for a menu bar item. * * @param data - The data to use for rendering the item. * * @returns A virtual element representing the item. */ renderItem(data: IRenderData): VirtualElement { let className = this.createItemClass(data); let dataset = this.createItemDataset(data); let aria = this.createItemARIA(data); return h.li( { className, dataset, ...(data.disabled ? {} : { tabindex: data.tabbable ? '0' : '-1' }), onfocus: data.onfocus, ...aria }, this.renderIcon(data), this.renderLabel(data) ); } /** * Render the icon element for a menu bar item. * * @param data - The data to use for rendering the icon. * * @returns A virtual element representing the item icon. */ renderIcon(data: IRenderData): VirtualElement { let className = this.createIconClass(data); // If data.title.icon is undefined, it will be ignored. return h.div({ className }, data.title.icon!, data.title.iconLabel); } /** * Render the label element for a menu item. * * @param data - The data to use for rendering the label. * * @returns A virtual element representing the item label. */ renderLabel(data: IRenderData): VirtualElement { let content = this.formatLabel(data); return h.div({ className: 'lm-MenuBar-itemLabel' }, content); } /** * Create the class name for the menu bar item. * * @param data - The data to use for the class name. * * @returns The full class name for the menu item. */ createItemClass(data: IRenderData): string { let name = 'lm-MenuBar-item'; if (data.title.className) { name += ` ${data.title.className}`; } if (data.active && !data.disabled) { name += ' lm-mod-active'; } return name; } /** * Create the dataset for a menu bar item. * * @param data - The data to use for the item. * * @returns The dataset for the menu bar item. */ createItemDataset(data: IRenderData): ElementDataset { return data.title.dataset; } /** * Create the aria attributes for menu bar item. * * @param data - The data to use for the aria attributes. * * @returns The aria attributes object for the item. */ createItemARIA(data: IRenderData): ElementARIAAttrs { return { role: 'menuitem', 'aria-haspopup': 'true', 'aria-disabled': data.disabled ? 'true' : 'false' }; } /** * Create the class name for the menu bar item icon. * * @param data - The data to use for the class name. * * @returns The full class name for the item icon. */ createIconClass(data: IRenderData): string { let name = 'lm-MenuBar-itemIcon'; let extra = data.title.iconClass; return extra ? `${name} ${extra}` : name; } /** * Create the render content for the label node. * * @param data - The data to use for the label content. * * @returns The content to add to the label node. */ formatLabel(data: IRenderData): h.Child { // Fetch the label text and mnemonic index. let { label, mnemonic } = data.title; // If the index is out of range, do not modify the label. if (mnemonic < 0 || mnemonic >= label.length) { return label; } // Split the label into parts. let prefix = label.slice(0, mnemonic); let suffix = label.slice(mnemonic + 1); let char = label[mnemonic]; // Wrap the mnemonic character in a span. let span = h.span({ className: 'lm-MenuBar-itemMnemonic' }, char); // Return the content parts. return [prefix, span, suffix]; } } /** * The default `Renderer` instance. */ export const defaultRenderer = new Renderer(); } /** * Options for overflow menu. */ export interface IOverflowMenuOptions { /** * Determines if a overflow menu appears when the menu items overflow. * * Defaults to `true`. */ isVisible: boolean; /** * Determines the title of the overflow menu. * * Default: `...`. */ title?: string; } /** * The namespace for the module implementation details. */ namespace Private { /** * Create the DOM node for a menu bar. */ export function createNode(): HTMLDivElement { let node = document.createElement('div'); let content = document.createElement('ul'); content.className = 'lm-MenuBar-content'; node.appendChild(content); content.setAttribute('role', 'menubar'); return node; } /** * Position for the menu relative to top-left screen corner. */ export interface IPosition { /** * Pixels right from screen origin. */ left: number; /** * Pixels down from screen origin. */ top: number; } /** * The results of a mnemonic search. */ export interface IMnemonicResult { /** * The index of the first matching mnemonic item, or `-1`. */ index: number; /** * Whether multiple mnemonic items matched. */ multiple: boolean; /** * The index of the first auto matched non-mnemonic item. */ auto: number; } /** * Find the best matching mnemonic item. * * The search starts at the given index and wraps around. */ export function findMnemonic( menus: ReadonlyArray<Menu>, key: string, start: number ): IMnemonicResult { // Setup the result variables. let index = -1; let auto = -1; let multiple = false; // Normalize the key to upper case. let upperKey = key.toUpperCase(); // Search the items from the given start index. for (let i = 0, n = menus.length; i < n; ++i) { // Compute the wrapped index. let k = (i + start) % n; // Look up the menu title. let title = menus[k].title; // Ignore titles with an empty label. if (title.label.length === 0) { continue; } // Look up the mnemonic index for the label. let mn = title.mnemonic; // Handle a valid mnemonic index. if (mn >= 0 && mn < title.label.length) { if (title.label[mn].toUpperCase() === upperKey) { if (index === -1) { index = k; } else { multiple = true; } } continue; } // Finally, handle the auto index if possible. if (auto === -1 && title.label[0].toUpperCase() === upperKey) { auto = k; } } // Return the search results. return { index, multiple, auto }; } }