From d8c0373f336ef3bc4dcea891ff70113b864ea8cc Mon Sep 17 00:00:00 2001 From: "Michael (mcm)" Date: Fri, 13 Oct 2023 15:39:49 +0200 Subject: [PATCH] [IMP] web: link popover together Before this commit, cliking on a popover that was open inside another popover closed both of them. After this commit, the popovers are not closed. them will close only if we click out of them and the popovers they opened. closes odoo/odoo#140258 Signed-off-by: Francois Georis (fge) --- .../src/core/popover/popover_controller.js | 28 +++++++--- .../static/src/core/popover/popover_hook.js | 5 +- .../src/core/popover/popover_service.js | 3 +- .../tests/core/popover/popover_hook_tests.js | 53 ++++++++++++++++++- 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/addons/web/static/src/core/popover/popover_controller.js b/addons/web/static/src/core/popover/popover_controller.js index b82c1fc35ce..41ecddf0ce5 100644 --- a/addons/web/static/src/core/popover/popover_controller.js +++ b/addons/web/static/src/core/popover/popover_controller.js @@ -1,10 +1,12 @@ /** @odoo-module **/ -import { Component, onWillDestroy, useExternalListener, xml } from "@odoo/owl"; +import { Component, onWillDestroy, useExternalListener, useSubEnv, xml } from "@odoo/owl"; import { useHotkey } from "../hotkeys/hotkey_hook"; import { useChildRef } from "../utils/hooks"; import { Popover } from "./popover"; +export const POPOVER_SYMBOL = Symbol("popover"); + export class PopoverController extends Component { static template = xml` @@ -19,28 +21,40 @@ export class PopoverController extends Component { "component", "componentProps", "popoverProps", + "subPopovers?", ]; setup() { + this.props.subPopovers?.add(this); + + this.subPopovers = new Set(); + useSubEnv({ [POPOVER_SYMBOL]: this.subPopovers }); + if (this.props.target.isConnected) { this.popoverRef = useChildRef(); useExternalListener(window, "pointerdown", this.onClickAway, { capture: true }); useHotkey("escape", () => this.props.close()); const targetObserver = new MutationObserver(this.onTargetMutate.bind(this)); targetObserver.observe(this.props.target.parentElement, { childList: true }); - onWillDestroy(() => targetObserver.disconnect()); + onWillDestroy(() => { + targetObserver.disconnect(); + this.props.subPopovers?.delete(this); + }); } else { this.props.close(); } } + isInside(target) { + if (this.props.target.contains(target) || this.popoverRef.el.contains(target)) { + return true; + } + return [...this.subPopovers].some((p) => p.isInside(target)); + } + onClickAway(ev) { const target = ev.composedPath()[0]; - if ( - this.props.closeOnClickAway(target) && - !this.props.target.contains(target) && - !this.popoverRef.el.contains(target) - ) { + if (this.props.closeOnClickAway(target) && !this.isInside(target)) { this.props.close(); ev.preventDefault(); } diff --git a/addons/web/static/src/core/popover/popover_hook.js b/addons/web/static/src/core/popover/popover_hook.js index bc51ba77696..fbaf61d4ea9 100644 --- a/addons/web/static/src/core/popover/popover_hook.js +++ b/addons/web/static/src/core/popover/popover_hook.js @@ -2,7 +2,8 @@ import { useService } from "@web/core/utils/hooks"; -import { onWillUnmount, status, useComponent } from "@odoo/owl"; +import { onWillUnmount, status, useComponent, useEnv } from "@odoo/owl"; +import { POPOVER_SYMBOL } from "./popover_controller"; /** * @typedef PopoverHookReturnType @@ -45,9 +46,11 @@ export function makePopover(popoverService, component, options) { * @returns {PopoverHookReturnType} */ export function usePopover(component, options = {}) { + const env = useEnv(); const popoverService = useService("popover"); const owner = useComponent(); const newOptions = Object.create(options); + newOptions[POPOVER_SYMBOL] = env[POPOVER_SYMBOL]; newOptions.onClose = () => { if (status(owner) !== "destroyed") { options.onClose?.(); diff --git a/addons/web/static/src/core/popover/popover_service.js b/addons/web/static/src/core/popover/popover_service.js index 6608afc5245..63fc779e31d 100644 --- a/addons/web/static/src/core/popover/popover_service.js +++ b/addons/web/static/src/core/popover/popover_service.js @@ -2,7 +2,7 @@ import { markRaw } from "@odoo/owl"; import { registry } from "../registry"; -import { PopoverController } from "./popover_controller"; +import { POPOVER_SYMBOL, PopoverController } from "./popover_controller"; /** * @typedef {{ @@ -40,6 +40,7 @@ export const popoverService = { target, close: () => remove(), closeOnClickAway, + subPopovers: options[POPOVER_SYMBOL], component, componentProps: markRaw(props), popoverProps: { diff --git a/addons/web/static/tests/core/popover/popover_hook_tests.js b/addons/web/static/tests/core/popover/popover_hook_tests.js index d84deae28c0..57875c4ad0f 100644 --- a/addons/web/static/tests/core/popover/popover_hook_tests.js +++ b/addons/web/static/tests/core/popover/popover_hook_tests.js @@ -4,7 +4,7 @@ import { usePopover } from "@web/core/popover/popover_hook"; import { popoverService } from "@web/core/popover/popover_service"; import { registry } from "@web/core/registry"; import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env"; -import { destroy, getFixture, mount, nextTick } from "../../helpers/utils"; +import { click, destroy, getFixture, mount, nextTick } from "../../helpers/utils"; import { Component, xml } from "@odoo/owl"; import { hotkeyService } from "@web/core/hotkeys/hotkey_service"; @@ -85,3 +85,54 @@ QUnit.test("close popover when component is unmounted", async (assert) => { assert.containsNone(target, ".o_popover #comp1"); assert.containsNone(target, ".o_popover #comp2"); }); + +QUnit.test("popover opened from another", async (assert) => { + class Comp extends Component { + static id = 0; + static template = xml` +
+ +
+ `; + setup() { + this.popover = usePopover(Comp, { + popoverClass: `popover-${++Comp.id}`, + }); + } + } + + await mount(Comp, target, { env }); + + await click(target, ".pop-open"); + assert.containsOnce(target, ".popover-1", "open first popover"); + + await click(target, ".popover-1 .pop-open"); + assert.containsN(target, ".o_popover", 2, "open second popover from the first one"); + assert.containsOnce(target, ".popover-1"); + assert.containsOnce(target, ".popover-2"); + + await click(target, ".popover-2 .pop-open"); + assert.containsN(target, ".o_popover", 3, "open third popover from the second one"); + assert.containsOnce(target, ".popover-1"); + assert.containsOnce(target, ".popover-2"); + assert.containsOnce(target, ".popover-3"); + + await click(target, ".popover-3"); + assert.containsN(target, ".o_popover", 3, "clicking inside third popover closes nothing"); + assert.containsOnce(target, ".popover-1"); + assert.containsOnce(target, ".popover-2"); + assert.containsOnce(target, ".popover-3"); + + await click(target, ".popover-2"); + assert.containsN( + target, + ".o_popover", + 2, + "clicking inside second popover closes third popover" + ); + assert.containsOnce(target, ".popover-1"); + assert.containsOnce(target, ".popover-2"); + + await click(target, "#close"); + assert.containsNone(target, ".o_popover", "clicking out of any popover closes them all"); +});