UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

691 lines (589 loc) 20.4 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides control sap.m.MenuItem. sap.ui.define([ 'sap/m/library', 'sap/ui/core/library', 'sap/ui/core/Element', 'sap/ui/core/Control', 'sap/m/ResponsivePopover', 'sap/m/Button', 'sap/m/Bar', 'sap/m/Title', 'sap/m/MenuWrapper', 'sap/ui/core/Lib', 'sap/m/MenuItemRenderer', 'sap/ui/Device', "sap/ui/core/InvisibleText", 'sap/base/i18n/Localization', 'sap/ui/core/IconPool', 'sap/m/Image' ], function( library, coreLibrary, Element, Control, ResponsivePopover, Button, Bar, Title, MenuWrapper, Lib, MenuItemRenderer, Device, InvisibleText, Localization, IconPool, Image ) { "use strict"; // Shortcut for sap.m.PlacementType const PlacementType = library.PlacementType; // Shortcut for sap.ui.core.ItemSelectionMode const ItemSelectionMode = coreLibrary.ItemSelectionMode; // Shortcut for sap.ui.core.TextDirection var TextDirection = coreLibrary.TextDirection; /** * Constructor for a new <code>MenuItem</code>. * * @param {string} [sId] ID for the new control, generated automatically if no ID is given * @param {object} [mSettings] Initial settings for the new control * * @class * The <code>MenuItem</code> control is used for creating items for the <code>sap.m.Menu</code>. * It is derived from a core <code>sap.ui.core.Control</code>. * @extends sap.ui.core.Control * @implements sap.m.IMenuItem, sap.m.IMenuItemBehavior * * @author SAP SE * @version 1.146.0 * * @constructor * @public * @since 1.38 * @alias sap.m.MenuItem */ const MenuItem = Control.extend("sap.m.MenuItem", /** @lends sap.m.MenuItem.prototype */ { metadata : { interfaces: [ "sap.m.IMenuItem" ], library: "sap.m", properties: { /** * The text to be displayed for the item. */ text: {type: "string", group: "Data", defaultValue: ""}, /** * Defines the icon, which belongs to the item. * This can be a URI to an image or an icon font URI. */ icon: {type: "string", group: "Appearance", defaultValue: null}, /** * Enabled items can be selected. */ enabled: {type: "boolean", group: "Misc", defaultValue: true}, /** * Defines whether the item should be visible on the screen. If set to <code>false</code>, * a placeholder is rendered instead of the real item. */ visible: {type: "boolean", group: "Appearance", defaultValue: true}, /** * Determines whether the <code>MenuItem</code> is selected. * A selected <code>MenuItem</code> has a check mark rendered at its end. * <b>Note: </b> selection functionality works only if the menu item is a member of <code>MenuItemGroup</code> with * <code>itemSelectionMode</code> set to {@link sap.ui.core.ItemSelectionMode.SingleSelect} or {@link sap.ui.core.ItemSelectionMode.MultiSelect}. * @since 1.127.0 */ selected: {type: "boolean", group: "Behavior", defaultValue: false}, /** * Defines the shortcut text that should be displayed on the menu item on non-mobile devices. * <b>Note:</b> The text is only displayed and set as а value of the <code>aria-keyshortcuts</code> attribute. */ shortcutText: {type: "string", group: "Appearance", defaultValue: ''}, /** * Defines whether a visual separator should be rendered before the item. * <b>Note:</b> If an item is invisible its separator is also not displayed. */ startsSection: {type: "boolean", group: "Behavior", defaultValue: false}, /** * Options are RTL and LTR. Alternatively, an item can inherit its text direction from its parent control. */ textDirection: {type: "sap.ui.core.TextDirection", group: "Misc", defaultValue: TextDirection.Inherit}, /** * Can be used as input for subsequent actions. */ key: {type: "string", group: "Data", defaultValue: null}, /** * Determines whether the <code>MenuItem</code> is open or closed when it has a submenu. * @private */ _expanded: {type: "boolean", group: "Misc", visibility: "hidden", defaultValue: undefined} }, defaultAggregation: "items", aggregations: { /** * Defines the subitems contained within this element. */ items: { type: "sap.m.IMenuItem", multiple: true, singularName: "item", bindable: "bindable", forwarding: { idSuffix: "-menuWrapper", aggregation: "items" } }, /** * Defines the content that is displayed at the end of a menu item. This aggregation allows for the addition of custom elements, such as icons and buttons. * * <b>Note:</b> Application developers are responsible for ensuring that interactive <code>endContent</code> * controls have the correct accessibility behaviour, including their enabled or disabled states. * The <code>Menu<code> does not manage these aspects when the menu item state changes. * @since 1.131 */ endContent: { type: "sap.ui.core.Control", multiple: true }, /** * Internal Menu Item icon or image control * @since 1.137.0 */ _icon: { type: "sap.ui.core.Control", multiple: false, visibility: "hidden" }, /** * Internal Menu Wrapper control * @since 1.137.0 */ _menuWrapper: { type: "sap.m.MenuWrapper", multiple: false, visibility: "hidden" }, /** * Internal aggregation that contains the inner <code>sap.m.ResponsivePopover</code> for mobile. * @since 1.137.0 */ _popover: { type: "sap.m.ResponsivePopover", multiple: false, visibility: "hidden" } }, associations: { /** * MenuItemGroup associated with this item. * @since 1.127.0 */ _group: {type : "sap.m.MenuItemGroup", group: "Behavior", visibility: "hidden"} }, events: { /** * Fired after the item has been pressed. */ press: {} }, renderer: MenuItemRenderer }}); MenuItem.prototype.init = function() { const oMenuWrapper = this._createMenuWrapper(true); this._itemSelectionMode = ItemSelectionMode.None; oMenuWrapper.attachClosePopover(this._handleCloseRequest, this); this._openDuration = Device.system.phone ? null : 0; }; MenuItem.prototype.exit = function() { const oMenuWrapper = this._getMenuWrapper(), oPopover = this._getPopover(), oIcon = this.getAggregation("_icon"); oMenuWrapper.detachClosePopover(this._handleCloseRequest, this); oMenuWrapper.destroy(); if (oPopover) { oPopover.detachAfterClose(this._afterPopoverClose, this); oPopover.destroy(); } if (oIcon) { oIcon.destroy(); } }; MenuItem.prototype.onBeforeRendering = function() { let oPopover = this._getPopover(); // Initialize popover if subitems exist but popover hasn't been created yet. if (this._hasSubmenu() && !oPopover) { oPopover = this._createPopover(); oPopover.attachAfterClose(this._afterPopoverClose, this); this._hasSubmenu() && this.setProperty("_expanded", false); } }; MenuItem.prototype.onfocusin = function(oEvent) { if (!this.isFocusable || !this.isFocusable()) { oEvent.preventDefault(); } }; /** * Sets the <code>selected</code> state of the <code>MenuItem</code> if it is allowed. * * @override * @param {boolean} bState Whether the menu item should be selected * @returns {this} Returns <code>this</code> to allow method chaining */ MenuItem.prototype.setSelected = function(bState) { const oGroup = Element.getElementById(this.getAssociation("_group")); // In case of single selection, clear selected state of all other items in the group to ensure that only one item is selected if (bState && oGroup && oGroup.getItemSelectionMode() === ItemSelectionMode.SingleSelect) { oGroup._clearSelectedItems(); } this.setProperty("selected", bState); return this; }; /** * Returns whether the firing of press event is allowed. * <b>Note:</b> This method can be overridden by subclasses to implement custom behavior. * * @public * @returns {boolean} Whether the item is enabled for click/press */ MenuItem.prototype.isInteractive = function() { return true; }; /** * Returns whether the item can be focused. * <b>Note:</b> This method can be overridden by subclasses to implement custom behavior. * * @public * @returns {boolean} Whether the item is enabled for focus */ MenuItem.prototype.isFocusable = function() { return true; }; /** * Returns whether the item can be counted in total items count. * <b>Note:</b> This method can be overridden by subclasses to implement custom behavior. * * @public * @returns {boolean} Whether the item is counted in total items count */ MenuItem.prototype.isCountable = function() { return this.isFocusable(); }; /** * Returns the accessibility attributes of the item. * * @private * @returns {Object} The accessibility attributes of the item */ MenuItem.prototype._getAccessibilityAttributes = function() { const oAccInfo = this._oAccInfo || { bAccessible: false}; if (!oAccInfo.bAccessible) { return {}; } const bHasSubmenu = this._hasSubmenu(), oSubmenu = this._getMenuWrapper(), bIsSelected = this.getSelected() && this._getItemSelectionMode() !== ItemSelectionMode.None, sShortcutText = !bHasSubmenu ? this.getShortcutText() : null; return { role: this._getRole(), disabled: !this.getEnabled(), posinset: oAccInfo['posinset'] || null, setsize: oAccInfo['setsize'] || null, selected: null, checked: bIsSelected || null, keyshortcuts: sShortcutText || null, labelledby: { value: `${this.getId()}-txt`, append: true }, haspopup: bHasSubmenu ? coreLibrary.aria.HasPopup.Menu.toLowerCase() : null, owns: bHasSubmenu ? oSubmenu.getId() : null, expanded: this.getProperty("_expanded") }; }; /** * Returns the item accessibility role. * * @private * @returns {string} The role of the item */ MenuItem.prototype._getRole = function() { let sRole; switch (this._getItemSelectionMode()) { case ItemSelectionMode.SingleSelect: sRole = "menuitemradio"; break; case ItemSelectionMode.MultiSelect: sRole = "menuitemcheckbox"; break; default: sRole = "menuitem"; } return sRole; }; /** * Handles the <code>closePopover</code> event of the menu item. * If the event is not bubbled to the root, the submenu of the item is immediately closed. * If the event bubbles, the <code>CloseItemSubmenu</code> event is triggered, allowing the top-level <code>Menu</code> to handle it. * The final decision on submenu closure depends on whether the <code>beforeClose</code> event of the <code>Menu</code> is prevented or not. * * @param {sap.ui.base.Event} oEvent closePopover event * @private */ MenuItem.prototype._handleCloseRequest = function(oEvent) { if (!oEvent.getParameter("bubbleToRoot")) { this._closeSubmenu(); oEvent.cancelBubble(); } else { this._getMenuWrapper().fireCloseItemSubmenu({ item: this }); } }; /** * Closes the submenu of the item. * * @private */ MenuItem.prototype._closeSubmenu = function() { const aItems = this._getVisibleItems(), oPopover = this._getPopover(); aItems.forEach((oItem) => { if (oItem._hasSubmenu && oItem._hasSubmenu()) { oItem._closeSubmenu(); } }); if (oPopover) { oPopover.removeStyleClass(this._getMenuWrapper()._aStyleClasses.join(" ")); oPopover._getPopup().setDurations(this._openDuration, 0); oPopover.close(); } this.setProperty("_expanded", false); this.removeStyleClass("sapMMenuItemSubMenuOpen"); this._getMenuWrapper().oOpenedSubmenuParent = null; }; /** * Opens the submenu of the item. * * @private */ MenuItem.prototype._openSubmenu = function() { if (!this.getEnabled() || !this._hasSubmenu()) { return; } const oMenuWrapper = this._getMenuWrapper(), oSubmenuPopover = this._getPopover(); if (oSubmenuPopover.isOpen()) { return; } oSubmenuPopover.addStyleClass(oMenuWrapper._aStyleClasses.join(" ")); this.setProperty("_expanded", true); if (Device.system.phone) { oMenuWrapper.setTitle(this.getText()); // Set the title of the menu wrapper according to the item's text // the Title is the only elemnet in the contentMiddle aggregation of the Bar we create as custom header // so we can safely set the text of the first element oSubmenuPopover.getCustomHeader().getContentMiddle()[0].setText(this.getText()); // Set the title of the popover according to the item's text } else { this.addStyleClass("sapMMenuItemSubMenuOpen"); } oSubmenuPopover._getPopup().setDurations(this._openDuration, 0); oSubmenuPopover.openBy(this); }; MenuItem.prototype._getBackButtonTooltipForPageWithParent = function() { return Lib.getResourceBundleFor("sap.m").getText("MENU_PAGE_BACK_BUTTON") + " " + this.getText(); }; /** * Removes "opened" item state on popover close. * @private */ MenuItem.prototype._afterPopoverClose = function() { this.removeStyleClass("sapMMenuItemSubMenuOpen"); }; /** * Returns the list of menu subitems of this item, including items in groups. * * @private * @returns {sap.m.MenuItem} List of menu subitems */ MenuItem.prototype._getItems = function() { const aItems = []; const findItems = (aItemItems) => { aItemItems.forEach((oItem) => { if (!oItem.getItemSelectionMode) { aItems.push(oItem); } else { findItems(oItem.getItems()); } }); }; findItems(this.getItems()); return aItems; }; /** * Returns the list of visible menu subitems of this item, including items in groups. * * @private * @returns {sap.m.MenuItem} List of visible menu subitems */ MenuItem.prototype._getVisibleItems = function() { return this._getItems().filter((oItem) => oItem.getVisible()); }; /** * Returns the icon of the menu item. * * @private * @returns {sap.ui.core.Icon} The icon control */ MenuItem.prototype._getIcon = function() { const sIcon = this.getIcon(); let oIcon = this.getAggregation("_icon"); if (oIcon) { oIcon.destroy(); } if (!sIcon) { return null; } oIcon = IconPool.createControlByURI({ src: sIcon, useIconTooltip: false }, Image); this.setAggregation("_icon", oIcon, true); return oIcon; }; /** * Returns the item selection mode inherited by the parent <code>MenuItemGroup</code> (if any). * * @private * @returns {string} The item selection mode */ MenuItem.prototype._getItemSelectionMode = function() { return this._itemSelectionMode; }; /** * Returns whether the item has subitems that will construct a submenu. * * @private * @returns {boolean} Whether the item has a subitems */ MenuItem.prototype._hasSubmenu = function() { return this._getVisibleItems().length > 0; }; /* ResponsivePopover and MenuWrapper functionality */ /** * Creates the internal MenuWrapper control. * @param {boolean} bIsSubmenu Whether the menu in this wrapper is a sub-menu or not * @returns {sap.m.MenuWrapper} The created MenuWrapper * @private */ MenuItem.prototype._createMenuWrapper = function(bIsSubmenu) { const oMenuWrapper = new MenuWrapper(this.getId() + "-menuWrapper", { isSubmenu: bIsSubmenu }); this.setAggregation("_menuWrapper", oMenuWrapper, true); return oMenuWrapper; }; /** * Creates the ResponsivePopover that contains the actual menu. * @returns {sap.m.ResponsivePopover} The created ResponsivePopover * @private */ MenuItem.prototype._createPopover = function() { let oPopover = this._getPopover(); if (oPopover) { return oPopover; } const sDialogAccessibleNameId = Device.system.phone ? `${this.getId()}-title` : InvisibleText.getStaticId("sap.m", "MENU_POPOVER_ACCESSIBLE_NAME"); const oMenuWrapper = this._getMenuWrapper(), bRTL = Localization.getRTL(), bIsSubmenu = oMenuWrapper.getIsSubmenu(), iOffsetXCorrection = bRTL ? 4 : -4; oPopover = new ResponsivePopover(this.getId() + "-rp", { placement: this._getPopoverPlacement(), showHeader: false, showArrow: false, showCloseButton: false, verticalScrolling: true, horizontalScrolling: false, offsetX: bIsSubmenu ? iOffsetXCorrection : 0, offsetY: bIsSubmenu ? 4 : 0, content: oMenuWrapper, ariaLabelledBy: [sDialogAccessibleNameId] }); oPopover.addStyleClass("sapMMenu"); this.setAggregation("_popover", oPopover, true); if (Device.system.phone) { oPopover.setShowHeader(true); oPopover.setEndButton(this._createCloseButton()); oPopover.setCustomHeader(this._createHeaderBar()); } else if (bIsSubmenu) { oPopover.getAggregation("_popup")._adaptPositionParams = function() { this._marginTop = 0; this._marginLeft = 0; this._marginRight = 0; this._marginBottom = 0; this._arrowOffset = 0; this._offsets = ["0 0", "0 0", "0 0", "0 0"]; this._myPositions = ["begin bottom", "begin top", "begin top", "end top"]; this._atPositions = ["begin top", "end top", "begin bottom", "begin top"]; }; } // this override is needed to fix the issue with the popover position flip oPopover._oControl._getDocHeight = () => window.innerHeight + window.scrollY; return oPopover; }; /** * Creates the back button for the Responsive Popover in mobile view. * @returns {sap.m.Button} The back button */ MenuItem.prototype._createBackButton = function() { return new Button(this.getId() + "-backbutton", { icon: "sap-icon://nav-back", tooltip: this._getBackButtonTooltipForPageWithParent(), press: (oEvent) => { this._getMenuWrapper().fireClosePopover(); } }); }; /** * Creates the custom header bar for the Responsive Popover in mobile view. * @returns {sap.m.Bar} The header bar */ MenuItem.prototype._createHeaderBar = function() { const oMenuWrapper = this._getMenuWrapper(), oHeaderBar = new Bar({ contentMiddle: new Title(this.getId() + "-title", { text: oMenuWrapper.getTitle() }) }), bIsSubmenu = this._getMenuWrapper().getIsSubmenu(); if (bIsSubmenu) { oHeaderBar.addContentLeft(this._createBackButton()); } return oHeaderBar; }; /** * Creates the close button for the Responsive Popover for mobile view. * @returns {sap.m.Button} The close button */ MenuItem.prototype._createCloseButton = function() { const oRB = Lib.getResourceBundleFor("sap.m"); return new Button({ text: oRB.getText("MENU_CLOSE"), press: () => { this._getMenuWrapper().fireClosePopover({ bubbleToRoot: true }); } }); }; /** * Gets the internal MenuWrapper control. * @returns {sap.m.MenuWrapper} The internal _menuWrapper aggregation * @private */ MenuItem.prototype._getMenuWrapper = function() { const oPopover = this._getPopover(); return oPopover ? oPopover.getContent()[0] : this.getAggregation("_menuWrapper"); }; /** * Gets the internal ResponsivePopover. * @private * @returns {sap.m.ResponsivePopover} The internal _popover aggregation */ MenuItem.prototype._getPopover = function() { return this.getAggregation("_popover"); }; /** * Gets the placement type for the popover depending of LTR/RTL setting. * @private * @returns {sap.m.PlacementType} The placement type of the popover */ MenuItem.prototype._getPopoverPlacement = function() { const bIsSubmenu = this._getMenuWrapper().getIsSubmenu(); if (bIsSubmenu) { const bRTL = Localization.getRTL(), sPlacement = bRTL ? PlacementType.HorizontalPreferredLeft : PlacementType.HorizontalPreferredRight; return sPlacement; } return PlacementType.VerticalPreferredBottom; }; /** * Sets extra content to the popover. * @private * @param {HTMLElement} oDomRef The DOM ref to be added as extra content to the popover */ MenuItem.prototype._setExtraContent = function(oDomRef) { this._getPopover()._getPopup().setExtraContent([oDomRef]); }; return MenuItem; });