summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/components/dropdown_menu.js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/components/dropdown_menu.js')
-rw-r--r--addons/web/static/src/js/components/dropdown_menu.js174
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;
+});