diff --git a/addons/web/static/src/core/position_hook.js b/addons/web/static/src/core/position_hook.js index d9c86263a77..bf8ddf8288e 100644 --- a/addons/web/static/src/core/position_hook.js +++ b/addons/web/static/src/core/position_hook.js @@ -83,7 +83,9 @@ const DEFAULTS = { * @param {HTMLElement} reference * @param {HTMLElement} popper * @param {Options} options - * @returns {PositioningSolution} the best positioning solution + * @returns {PositioningSolution} the best positioning solution, relative to + * the containing block of the popper. + * => can be applied to popper.style.(top|left) */ function getBestPosition(reference, popper, { container, margin, position }) { // Retrieve directions and variants @@ -172,7 +174,12 @@ function getBestPosition(reference, popper, { container, margin, position }) { left: directionValue, }; return { - ...positioning, + // Subtract the offsets of the containing block (relative to the + // viewport). It can be done like that because the style top and + // left were reset to 0px in `reposition` + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + top: positioning.top - popBox.top, + left: positioning.left - popBox.left, direction: DIRECTIONS[d], variant: VARIANTS[v], }; diff --git a/addons/web/static/tests/core/position_hook_tests.js b/addons/web/static/tests/core/position_hook_tests.js index 6ae3f493235..59d224afc5f 100644 --- a/addons/web/static/tests/core/position_hook_tests.js +++ b/addons/web/static/tests/core/position_hook_tests.js @@ -150,6 +150,43 @@ QUnit.test("has no effect when component is destroyed", async (assert) => { ); }); +QUnit.test("is positioned relative to its containing block", async (assert) => { + const fixtureBox = getFixture().getBoundingClientRect(); + // offset the container + const margin = 15; + container.style.margin = `${margin}px`; + let pos1, pos2; + let TestComp = getTestComponent({ + onPositioned: (el, pos) => { + pos1 = pos; + }, + }); + let popper = await mount(TestComp, container); + + const popBox1 = document.getElementById("popper").getBoundingClientRect(); + destroy(popper); + document.getElementById("reference").remove(); + + // make container the containing block instead of the viewport + container.style.contain = "layout"; + + TestComp = getTestComponent({ + onPositioned: (el, pos) => { + pos2 = pos; + }, + }); + popper = await mount(TestComp, container); + const popBox2 = document.getElementById("popper").getBoundingClientRect(); + destroy(popper); + + // best positions are not the same relative to their containing block + assert.equal(pos1.top, pos2.top + margin + fixtureBox.top); + assert.equal(pos1.left, pos2.left + margin + fixtureBox.left); + // best positions are the same relative to the viewport + assert.equal(popBox1.top, popBox2.top); + assert.equal(popBox1.left, popBox2.left); +}); + function getPositionTest(position, positionToCheck) { return async (assert) => { assert.expect(2);