diff options
Diffstat (limited to 'addons/web/static/src/js/components/dropdown_menu.js')
| -rw-r--r-- | addons/web/static/src/js/components/dropdown_menu.js | 174 |
1 files changed, 174 insertions, 0 deletions
diff --git a/addons/web/static/src/js/components/dropdown_menu.js b/addons/web/static/src/js/components/dropdown_menu.js new file mode 100644 index 00000000..890d928b --- /dev/null +++ b/addons/web/static/src/js/components/dropdown_menu.js @@ -0,0 +1,174 @@ +odoo.define('web.DropdownMenu', function (require) { + "use strict"; + + const DropdownMenuItem = require('web.DropdownMenuItem'); + + const { Component, hooks } = owl; + const { useExternalListener, useRef, useState } = hooks; + + /** + * Dropdown menu + * + * Generic component used to generate a list of interactive items. It uses some + * bootstrap classes but most interactions are handled in here or in the dropdown + * menu item class definition, including some keyboard navigation and escaping + * system (click outside to close the dropdown). + * + * The layout of a dropdown menu is as following: + * > a Button (always rendered) with a `title` and an optional `icon`; + * > a Dropdown (rendered when open) containing a collection of given items. + * These items must be objects and can have two shapes: + * 1. item.Component & item.props > will instantiate the given Component with + * the given props. Any additional key will be useless. + * 2. any other shape > will instantiate a DropdownMenuItem with the item + * object being its props. There is no props validation as this object + * will be passed as-is when `selected` and can contain additional meta-keys + * that will not affect the displayed item. For more information regarding + * the behaviour of these items, @see DropdownMenuItem. + * @extends Component + */ + class DropdownMenu extends Component { + constructor() { + super(...arguments); + + this.dropdownMenu = useRef('dropdown'); + this.state = useState({ open: false }); + + useExternalListener(window, 'click', this._onWindowClick, true); + useExternalListener(window, 'keydown', this._onWindowKeydown); + } + + //--------------------------------------------------------------------- + // Getters + //--------------------------------------------------------------------- + + /** + * In desktop, by default, we do not display a caret icon next to the + * dropdown. + * @returns {boolean} + */ + get displayCaret() { + return false; + } + + /** + * In mobile, by default, we display a chevron icon next to the dropdown + * button. Note that when 'displayCaret' is true, we display a caret + * instead of a chevron, no matter the value of 'displayChevron'. + * @returns {boolean} + */ + get displayChevron() { + return this.env.device.isMobile; + } + + /** + * Can be overriden to force an icon on an inheriting class. + * @returns {string} Font Awesome icon class + */ + get icon() { + return this.props.icon; + } + + /** + * Meant to be overriden to provide the list of items to display. + * @returns {Object[]} + */ + get items() { + return this.props.items; + } + + /** + * @returns {string} + */ + get title() { + return this.props.title; + } + + //--------------------------------------------------------------------- + // Handlers + //--------------------------------------------------------------------- + + /** + * @private + * @param {KeyboardEvent} ev + */ + _onButtonKeydown(ev) { + switch (ev.key) { + case 'ArrowLeft': + case 'ArrowRight': + case 'ArrowUp': + case 'ArrowDown': + const firstItem = this.el.querySelector('.dropdown-item'); + if (firstItem) { + ev.preventDefault(); + firstItem.focus(); + } + } + } + + /** + * @private + * @param {OwlEvent} ev + */ + _onItemSelected(/* ev */) { + if (this.props.closeOnSelected) { + this.state.open = false; + } + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onWindowClick(ev) { + if ( + this.state.open && + !this.el.contains(ev.target) && + !this.el.contains(document.activeElement) + ) { + if (document.body.classList.contains("modal-open")) { + // retrieve the active modal and check if the dropdown is a child of this modal + const modal = document.querySelector('body > .modal:not(.o_inactive_modal)'); + if (modal && !modal.contains(this.el)) { + return; + } + const owlModal = document.querySelector('body > .o_dialog > .modal:not(.o_inactive_modal)'); + if (owlModal && !owlModal.contains(this.el)) { + return; + } + } + // check for an active open bootstrap calendar like the filter dropdown inside the search panel) + if (document.querySelector('body > .bootstrap-datetimepicker-widget')) { + return; + } + this.state.open = false; + } + } + + /** + * @private + * @param {KeyboardEvent} ev + */ + _onWindowKeydown(ev) { + if (this.state.open && ev.key === 'Escape') { + this.state.open = false; + } + } + } + + DropdownMenu.components = { DropdownMenuItem }; + DropdownMenu.defaultProps = { items: [] }; + DropdownMenu.props = { + icon: { type: String, optional: 1 }, + items: { + type: Array, + element: Object, + optional: 1, + }, + title: { type: String, optional: 1 }, + closeOnSelected: { type: Boolean, optional: 1 }, + }; + DropdownMenu.template = 'web.DropdownMenu'; + + return DropdownMenu; +}); |
