[FIX] web: DropdownMenu autoclose behavior should match Bootstrap

Before this commit, with an opened DropdownMenu component, when the user
clicks on a Bootstrap's dropdown the DropdownMenu doesn't close.

This issue is due to a conflict between the global click listener
registered to close the DropdownMenu and the listener registered by the
Bootstrap dropdown:
the last one prevents the event from bubbling up to the `window` element
and be handled by the first one.

This commit fixes it by enabling the `capture` flag on the DropdownMenu
global click listener, giving it the opportunity to handling it properly
 and close the DropdownMenu.

Note: this commit also fixes an issue when a dialog is opened from a
DropdownMenu (like the favorite's delete action) and prevents it from
closing in the background (e.g. when the dialog is still opened).

opw-1879566

closes odoo/odoo#60928

X-original-commit: 7f1bcc8ceb78729725f65585e923b08f94bf75b5
Signed-off-by: Pierre Paridans <pparidans@users.noreply.github.com>
This commit is contained in:
Romain Estievenart
2020-10-28 17:22:48 +00:00
parent f1f3366bc0
commit 9348cca0f2
2 changed files with 64 additions and 1 deletions
@@ -35,7 +35,7 @@ odoo.define('web.DropdownMenu', function (require) {
this.dropdownMenu = useRef('dropdown');
this.state = useState({ open: false });
useExternalListener(window, 'click', this._onWindowClick);
useExternalListener(window, 'click', this._onWindowClick, true);
useExternalListener(window, 'keydown', this._onWindowKeydown);
}
@@ -147,6 +147,13 @@ odoo.define('web.DropdownMenu', function (require) {
!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('.modal:not(.o_inactive_modal)');
if (!modal.contains(this.el)) {
return;
}
}
this.state.open = false;
}
}
@@ -68,6 +68,62 @@ odoo.define('web.dropdown_menu_tests', function (require) {
dropdown.destroy();
});
QUnit.test('only one dropdown rendering at same time (owl vs bootstrap dropdown)', async function (assert) {
assert.expect(12);
const bsDropdown = document.createElement('div');
bsDropdown.innerHTML = `<div class="dropdown">
<button class="btn dropdown-toggle" type="button"
data-toggle="dropdown" aria-expanded="false">
BS Dropdown button
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="#">BS Action</a>
</div>
</div>`;
document.body.append(bsDropdown);
const dropdown = await createComponent(DropdownMenu, {
props: {
items: this.items,
title: "Dropdown",
},
});
await testUtils.dom.click(dropdown.el.querySelector('button'));
assert.hasClass(dropdown.el.querySelector('.dropdown-menu'), 'show');
assert.doesNotHaveClass(bsDropdown.querySelector('.dropdown-menu'), 'show');
assert.isVisible(dropdown.el.querySelector('.dropdown-menu'),
"owl dropdown menu should be visible");
assert.isNotVisible(bsDropdown.querySelector('.dropdown-menu'),
"bs dropdown menu should not be visible");
await testUtils.dom.click(bsDropdown.querySelector('.btn.dropdown-toggle'));
assert.doesNotHaveClass(dropdown.el, 'show');
assert.containsNone(dropdown.el, '.dropdown-menu',
"owl dropdown menu should not be set inside the dom");
assert.hasClass(bsDropdown.querySelector('.dropdown-menu'), 'show');
assert.isVisible(bsDropdown.querySelector('.dropdown-menu'),
"bs dropdown menu should be visible");
await testUtils.dom.click(document.body);
assert.doesNotHaveClass(dropdown.el, 'show');
assert.containsNone(dropdown.el, '.dropdown-menu',
"owl dropdown menu should not be set inside the dom");
assert.doesNotHaveClass(bsDropdown.querySelector('.dropdown-menu'), 'show');
assert.isNotVisible(bsDropdown.querySelector('.dropdown-menu'),
"bs dropdown menu should not be visible");
bsDropdown.remove();
dropdown.destroy();
});
QUnit.test('click on an item without options should toggle it', async function (assert) {
assert.expect(7);