devextreme
Version:
JavaScript/TypeScript Component Suite for Responsive Web Development
412 lines (411 loc) • 15.1 kB
JavaScript
/**
* DevExtreme (esm/__internal/ui/toolbar/internal/toolbar.menu.js)
* Version: 25.2.7
* Build date: Tue May 05 2026
*
* Copyright (c) 2012 - 2026 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
import devices from "../../../../core/devices";
import $ from "../../../../core/renderer";
import {
ChildDefaultTemplate
} from "../../../../core/templates/child_default_template";
import {
getOuterHeight
} from "../../../../core/utils/size";
import {
getWindow
} from "../../../../core/utils/window";
import {
current,
isFluent,
isMaterialBased
} from "../../../../ui/themes";
import Widget from "../../../core/widget/widget";
import Button from "../../../ui/button/wrapper";
import Popup from "../../../ui/popup/m_popup";
import ToolbarMenuList, {
TOOLBAR_MENU_ACTION_CLASS
} from "../../../ui/toolbar/internal/toolbar.menu.list";
import {
toggleItemFocusableElementTabIndex
} from "../../../ui/toolbar/toolbar.utils";
const DROP_DOWN_MENU_CLASS = "dx-dropdownmenu";
const DROP_DOWN_MENU_POPUP_CLASS = "dx-dropdownmenu-popup";
const DROP_DOWN_MENU_POPUP_WRAPPER_CLASS = "dx-dropdownmenu-popup-wrapper";
const DROP_DOWN_MENU_LIST_CLASS = "dx-dropdownmenu-list";
const DROP_DOWN_MENU_BUTTON_CLASS = "dx-dropdownmenu-button";
const POPUP_BOUNDARY_VERTICAL_OFFSET = 10;
const POPUP_VERTICAL_OFFSET = 3;
export default class DropDownMenu extends Widget {
_supportedKeys() {
var _this$_list;
let extension = {};
const {
opened: opened
} = this.option();
if (!opened || !(null !== (_this$_list = this._list) && void 0 !== _this$_list && _this$_list.option("focusedElement"))) {
extension = this._button._supportedKeys()
}
return Object.assign({}, super._supportedKeys(), extension, {
tab() {
var _this$_popup;
null === (_this$_popup = this._popup) || void 0 === _this$_popup || _this$_popup.hide()
}
})
}
_getDefaultOptions() {
return Object.assign({}, super._getDefaultOptions(), {
items: [],
dataSource: null,
itemTemplate: "item",
activeStateEnabled: true,
hoverStateEnabled: true,
opened: false,
closeOnClick: true,
useInkRipple: false,
container: void 0,
animation: {
show: {
type: "fade",
from: 0,
to: 1
},
hide: {
type: "fade",
to: 0
}
}
})
}
_defaultOptionsRules() {
return super._defaultOptionsRules().concat([{
device: () => "desktop" === devices.real().deviceType && !devices.isSimulator(),
options: {
focusStateEnabled: true
}
}, {
device: () => isMaterialBased(current()),
options: {
useInkRipple: true,
animation: {
show: {
type: "pop",
duration: 200,
from: {
scale: 0
},
to: {
scale: 1
}
},
hide: {
type: "pop",
duration: 200,
from: {
scale: 1
},
to: {
scale: 0
}
}
}
}
}])
}
_init() {
super._init();
this.$element().addClass("dx-dropdownmenu");
this._initItemClickAction();
this._initButtonClickAction()
}
_initItemClickAction() {
this._itemClickAction = this._createActionByOption("onItemClick", {})
}
_initButtonClickAction() {
this._buttonClickAction = this._createActionByOption("onButtonClick", {})
}
_initTemplates() {
this._templateManager.addDefaultTemplates({
content: new ChildDefaultTemplate("content")
});
super._initTemplates()
}
_initMarkup() {
this._renderButton();
super._initMarkup()
}
_render() {
super._render();
const {
opened: opened
} = this.option();
this.setAria({
haspopup: true,
expanded: opened
})
}
_renderContentImpl() {
const {
opened: opened
} = this.option();
if (opened) {
this._renderPopup()
}
}
_clean() {
var _this$_list2, _this$_popup2;
this._cleanFocusState();
null === (_this$_list2 = this._list) || void 0 === _this$_list2 || _this$_list2.$element().remove();
null === (_this$_popup2 = this._popup) || void 0 === _this$_popup2 || _this$_popup2.$element().remove();
delete this._list;
delete this._popup
}
_renderButton() {
const $button = this.$element().addClass("dx-dropdownmenu-button");
const {
useInkRipple: useInkRipple
} = this.option();
this._button = this._createComponent($button, Button, {
icon: "overflow",
template: "content",
stylingMode: isFluent(current()) ? "text" : "contained",
useInkRipple: useInkRipple,
hoverStateEnabled: false,
focusStateEnabled: false,
onClick: e => {
var _this$_buttonClickAct;
this.option("opened", !this.option("opened"));
null === (_this$_buttonClickAct = this._buttonClickAction) || void 0 === _this$_buttonClickAct || _this$_buttonClickAct.call(this, e)
}
})
}
_toggleActiveState($element, value) {
this._button._toggleActiveState($element[0], value)
}
_toggleMenuVisibility(opened) {
var _this$_popup3, _this$_popup4;
const state = opened ?? !(null !== (_this$_popup3 = this._popup) && void 0 !== _this$_popup3 && _this$_popup3.option("visible"));
if (opened) {
this._renderPopup()
}
null === (_this$_popup4 = this._popup) || void 0 === _this$_popup4 || _this$_popup4.toggle(state);
this.setAria("expanded", state)
}
_renderPopup() {
if (this._$popup) {
return
}
this._$popup = $("<div>").appendTo(this.$element());
const {
rtlEnabled: rtlEnabled,
container: container,
animation: animation
} = this.option();
this._popup = this._createComponent(this._$popup, Popup, {
onInitialized(e) {
const {
component: component
} = e;
component.$wrapper().addClass("dx-dropdownmenu-popup-wrapper").addClass("dx-dropdownmenu-popup")
},
deferRendering: false,
preventScrollEvents: false,
_ignorePreventScrollEventsDeprecation: true,
contentTemplate: contentElement => this._renderList(contentElement),
_ignoreFunctionValueDeprecation: true,
maxHeight: () => this._getMaxHeight(),
position: {
my: "top " + (rtlEnabled ? "left" : "right"),
at: "bottom " + (rtlEnabled ? "left" : "right"),
collision: "fit flip",
offset: {
v: 3
},
of: this.$element()
},
animation: animation,
onOptionChanged: _ref => {
let {
name: name,
value: value
} = _ref;
if ("visible" === name) {
this.option("opened", value)
}
},
container: container,
autoResizeEnabled: false,
height: "auto",
width: "auto",
hideOnOutsideClick: e => this._closeOutsideDropDownHandler(e),
hideOnParentScroll: true,
shading: false,
dragEnabled: false,
showTitle: false,
fullScreen: false,
ignoreChildEvents: false,
_fixWrapperPosition: true
});
this._popup.registerKeyHandler("space", e => {
this._popupKeyHandler(e)
});
this._popup.registerKeyHandler("enter", e => {
this._popupKeyHandler(e)
});
this._popup.registerKeyHandler("escape", e => {
var _this$_popup5;
if (null !== (_this$_popup5 = this._popup) && void 0 !== _this$_popup5 && _this$_popup5.$overlayContent().is($(e.target))) {
this.option("opened", false)
}
})
}
_getMaxHeight() {
var _$element$offset;
const $element = this.$element();
const offsetTop = (null === (_$element$offset = $element.offset()) || void 0 === _$element$offset ? void 0 : _$element$offset.top) ?? 0;
const windowHeight = getOuterHeight(getWindow());
const maxHeight = Math.max(offsetTop, windowHeight - offsetTop - getOuterHeight($element));
return Math.min(windowHeight, maxHeight - 3 - 10)
}
_closeOutsideDropDownHandler(e) {
const isOutsideClick = !$(e.target).closest(this.$element()).length;
return isOutsideClick
}
_renderList(contentElement) {
const $content = $(contentElement);
$content.addClass("dx-dropdownmenu-list");
const {
itemTemplate: itemTemplate,
onItemRendered: onItemRendered
} = this.option();
this._list = this._createComponent($content, ToolbarMenuList, {
dataSource: this._getListDataSource(),
pageLoadMode: "scrollBottom",
indicateLoading: false,
noDataText: "",
itemTemplate: itemTemplate,
onItemClick: e => {
this._itemClickHandler(e)
},
tabIndex: -1,
focusStateEnabled: false,
activeStateEnabled: true,
onItemRendered: onItemRendered,
_itemAttributes: {
role: "menuitem"
},
_onItemsRendered: () => {
if (this.option("templatesRenderAsynchronously")) {
var _this$_popup6;
null === (_this$_popup6 = this._popup) || void 0 === _this$_popup6 || _this$_popup6._renderGeometry()
}
}
})
}
_popupKeyHandler(e) {
if ($(e.target).closest(`.${TOOLBAR_MENU_ACTION_CLASS}`).length) {
this._closePopup()
}
}
_closePopup() {
const {
closeOnClick: closeOnClick
} = this.option();
if (closeOnClick) {
this.option("opened", false)
}
}
_itemClickHandler(e) {
var _this$_itemClickActio;
this._closePopup();
null === (_this$_itemClickActio = this._itemClickAction) || void 0 === _this$_itemClickActio || _this$_itemClickActio.call(this, e)
}
_itemOptionChanged(item, property, value) {
var _this$_list3;
null === (_this$_list3 = this._list) || void 0 === _this$_list3 || _this$_list3._itemOptionChanged(item, property, value);
toggleItemFocusableElementTabIndex(this._list, item)
}
_getListDataSource() {
const {
dataSource: dataSource,
items: items = []
} = this.option();
return dataSource ?? items
}
_setListDataSource() {
var _this$_list4;
null === (_this$_list4 = this._list) || void 0 === _this$_list4 || _this$_list4.option("dataSource", this._getListDataSource());
delete this._deferRendering
}
_getKeyboardListeners() {
return super._getKeyboardListeners().concat([this._list])
}
_toggleVisibility(visible) {
var _this$_button;
super._toggleVisibility(visible);
null === (_this$_button = this._button) || void 0 === _this$_button || _this$_button.option("visible", visible)
}
_optionChanged(args) {
var _this$_list5, _this$_list6, _this$_list7, _this$_popup7;
const {
name: name,
value: value
} = args;
switch (name) {
case "items":
case "dataSource":
if (!this.option("opened")) {
this._deferRendering = true
} else {
this._setListDataSource()
}
break;
case "itemTemplate":
null === (_this$_list5 = this._list) || void 0 === _this$_list5 || _this$_list5.option(name, this._getTemplate(value));
break;
case "onItemClick":
this._initItemClickAction();
break;
case "onButtonClick":
this._initButtonClickAction();
break;
case "useInkRipple":
this._invalidate();
break;
case "focusStateEnabled":
null === (_this$_list6 = this._list) || void 0 === _this$_list6 || _this$_list6.option(name, value);
super._optionChanged(args);
break;
case "onItemRendered":
null === (_this$_list7 = this._list) || void 0 === _this$_list7 || _this$_list7.option(name, value);
break;
case "opened":
if (this._deferRendering) {
this._setListDataSource()
}
this._toggleMenuVisibility(value);
this._updateFocusableItemsTabIndex();
break;
case "closeOnClick":
break;
case "container":
null === (_this$_popup7 = this._popup) || void 0 === _this$_popup7 || _this$_popup7.option(name, value);
break;
case "disabled":
if (this._list) {
this._updateFocusableItemsTabIndex()
}
break;
default:
super._optionChanged(args)
}
}
_updateFocusableItemsTabIndex() {
const {
items: items = []
} = this.option();
items.forEach(item => toggleItemFocusableElementTabIndex(this._list, item))
}
}