diff --git a/addons/web/static/src/public/interaction.js b/addons/web/static/src/public/interaction.js index b01d1164a59db..8ac3c2425a9eb 100644 --- a/addons/web/static/src/public/interaction.js +++ b/addons/web/static/src/public/interaction.js @@ -25,6 +25,20 @@ export class Interaction { */ static selector = ""; + /** + * The `selectorHas` attribute, if defined, allows to filter elements found + * through the `selector` attribute by only considering those which contain + * at least an element which matches this `selectorHas` selector. + * + * Note that this is the equivalent of setting up a `selector` using the + * `:has` pseudo-selector but that pseudo-selector is known to not be fully + * supported in all browsers. To prevent useless crashes, using this + * `selectorHas` attribute should be preferred. + * + * @type {string} + */ + static selectorHas = ""; + /** * Note that a dynamic selector is allowed to return a falsy value, for ex * the result of a querySelector. In that case, the directive will simply be diff --git a/addons/web/static/src/public/interaction_service.js b/addons/web/static/src/public/interaction_service.js index 0f348c803ee7f..2a5bf6d3fced7 100644 --- a/addons/web/static/src/public/interaction_service.js +++ b/addons/web/static/src/public/interaction_service.js @@ -109,9 +109,13 @@ class InteractionService { try { const isMatch = el.matches(I.selector); targets = isMatch ? [el] : el.querySelectorAll(I.selector); + if (I.selectorHas) { + targets = [...targets].filter((el) => !!el.querySelector(I.selectorHas)); + } } catch { + const selectorHasError = I.selectorHas ? ` or selectorHas: '${I.selectorHas}'` : ""; const error = new Error( - `Could not start interaction ${I.name} (invalid selector: '${I.selector}')` + `Could not start interaction ${I.name} (invalid selector: '${I.selector}'${selectorHasError})` ); proms.push(Promise.reject(error)); continue; diff --git a/addons/web/static/tests/public/interaction_service.test.js b/addons/web/static/tests/public/interaction_service.test.js index 51b60dd0259ae..e890a0c492c6f 100644 --- a/addons/web/static/tests/public/interaction_service.test.js +++ b/addons/web/static/tests/public/interaction_service.test.js @@ -1,4 +1,4 @@ -import { expect, test } from "@odoo/hoot"; +import { describe, expect, test } from "@odoo/hoot"; import { animationFrame } from "@odoo/hoot-mock"; import { Component, xml } from "@odoo/owl"; @@ -6,6 +6,8 @@ import { makeMockEnv } from "@web/../tests/web_test_helpers"; import { Interaction } from "@web/public/interaction"; import { startInteraction } from "./helpers"; +describe.current.tags("interaction_dev"); + test("properly handles case where we have no match for wrapwrap", async () => { const env = await makeMockEnv(); expect(env.services["public.interactions"]).toBe(null); @@ -116,6 +118,43 @@ test("start interactions even if there is a crash when evaluating selector", asy expect.verifySteps(["start notboom"]); }); +test("start interactions even if there is a crash when evaluating selectorHas", async () => { + class Boom extends Interaction { + static selector = ".test"; + static selectorHas = "div:invalid(coucou)"; + + setup() { + expect.step("start boom"); + } + destroy() { + expect.step("destroy boom"); + } + } + class NotBoom extends Interaction { + static selector = ".test"; + + setup() { + expect.step("start notboom"); + } + } + + const { core } = await startInteraction([Boom, NotBoom], `
`, { + waitForStart: false, + }); + + let e = null; + try { + await core.isReady; + } catch (_e) { + e = _e; + } + expect(e.message).toBe( + "Could not start interaction Boom (invalid selector: '.test' or selectorHas: 'div:invalid(coucou)')" + ); + + expect.verifySteps(["start notboom"]); +}); + test("recover from error as much as possible when applying dynamiccontent", async () => { let a = "a"; let b = "b"; diff --git a/addons/website/static/src/interactions/footer_slideout.js b/addons/website/static/src/interactions/footer_slideout.js index 1071dbbaafc90..9b02fdd88bd27 100644 --- a/addons/website/static/src/interactions/footer_slideout.js +++ b/addons/website/static/src/interactions/footer_slideout.js @@ -2,7 +2,9 @@ import { registry } from "@web/core/registry"; import { Interaction } from "@web/public/interaction"; export class FooterSlideout extends Interaction { - static selector = "#wrapwrap:has(.o_footer_slideout)"; + static selector = "#wrapwrap"; + static selectorHas = ".o_footer_slideout"; + dynamicContent = { "_root": { "t-att-class": () => ({