[FIX] web: sync reposition for nested poppers
This commit will have nested components making use of the usePosition hook to synchronize their repositioning. **Before this commit** Each component handles its own repositioning logic. If it is mounted somehow inside another component using that same logic, conflicts occur in some circumstances. **After this commit** It is now ensured that, in case of nested positioned components, the whole chain of repositioning computations is called in the proper order: from the parent to the children. **Example** - Before ![before] - After ![after] [before]: https://github.com/odoo/odoo/assets/1159815/3c9cfae7-db68-4707-a73c-ad7e2e71d4bf [after]: https://github.com/odoo/odoo/assets/1159815/a21f56e0-d8e3-48c1-9509-8e6ee5040d4b closes odoo/odoo#125531 X-original-commit: 9f947ba3fd3a00a0c80efc40186c36e0de78de16 Signed-off-by: Michaël Mattiello (mcm) <mcm@odoo.com> Signed-off-by: Bruno Boi (boi) <boi@odoo.com>
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { useThrottleForAnimation } from "./utils/timing";
|
||||
import { useEffect, useRef } from "@odoo/owl";
|
||||
import {
|
||||
EventBus,
|
||||
onWillDestroy,
|
||||
useChildSubEnv,
|
||||
useComponent,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "@odoo/owl";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
|
||||
/**
|
||||
@@ -281,6 +288,8 @@ export function reposition(target, popper, iframe, options) {
|
||||
}
|
||||
}
|
||||
|
||||
const POSITION_BUS = Symbol("position-bus");
|
||||
|
||||
/**
|
||||
* Makes sure that the `popper` element is always
|
||||
* placed at `position` from the `target` element.
|
||||
@@ -296,10 +305,9 @@ export function reposition(target, popper, iframe, options) {
|
||||
* @param {Options} options
|
||||
*/
|
||||
export function usePosition(target, options) {
|
||||
const popperRef = useRef(options.popper || DEFAULTS.popper);
|
||||
const popperRef = useRef(options?.popper || DEFAULTS.popper);
|
||||
const getTarget = typeof target === "function" ? target : () => target;
|
||||
const throttledReposition = useThrottleForAnimation(reposition);
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
const targetEl = getTarget();
|
||||
const popperEl = popperRef.el;
|
||||
if (!targetEl || !popperEl) {
|
||||
@@ -307,20 +315,35 @@ export function usePosition(target, options) {
|
||||
}
|
||||
|
||||
// Prepare
|
||||
const targetDocument = targetEl.ownerDocument;
|
||||
const iframe = getIFrame(targetEl);
|
||||
const currentOptions = { ...DEFAULTS, ...options };
|
||||
|
||||
// Reposition
|
||||
reposition(targetEl, popperEl, iframe, currentOptions);
|
||||
};
|
||||
|
||||
// Attach listeners to keep the positioning up to date
|
||||
const listener = () => throttledReposition(targetEl, popperEl, iframe, currentOptions);
|
||||
targetDocument.addEventListener("scroll", listener, { capture: true });
|
||||
window.addEventListener("resize", listener);
|
||||
return () => {
|
||||
targetDocument.removeEventListener("scroll", listener, { capture: true });
|
||||
window.removeEventListener("resize", listener);
|
||||
};
|
||||
const component = useComponent();
|
||||
const bus = component.env[POSITION_BUS] || new EventBus();
|
||||
bus.addEventListener("update", update);
|
||||
onWillDestroy(() => bus.removeEventListener("update", update));
|
||||
|
||||
const isTopmost = !(POSITION_BUS in component.env);
|
||||
if (isTopmost) {
|
||||
useChildSubEnv({ [POSITION_BUS]: bus });
|
||||
}
|
||||
|
||||
const throttledUpdate = useThrottleForAnimation(() => bus.trigger("update"));
|
||||
useEffect(() => {
|
||||
// Reposition
|
||||
bus.trigger("update");
|
||||
|
||||
if (isTopmost) {
|
||||
// Attach listeners to keep the positioning up to date
|
||||
const targetDocument = getTarget()?.ownerDocument;
|
||||
targetDocument?.addEventListener("scroll", throttledUpdate, { capture: true });
|
||||
window.addEventListener("resize", throttledUpdate);
|
||||
return () => {
|
||||
targetDocument?.removeEventListener("scroll", throttledUpdate, { capture: true });
|
||||
window.removeEventListener("resize", throttledUpdate);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
triggerEvent,
|
||||
} from "../helpers/utils";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { Component, useRef, xml } from "@odoo/owl";
|
||||
|
||||
const FLEXBOX_STYLE = {
|
||||
display: "flex",
|
||||
@@ -543,6 +543,66 @@ QUnit.test("iframe: both popper and target inside", async (assert) => {
|
||||
assert.strictEqual(popperBox.left, onPositionedArgs.solution.left);
|
||||
});
|
||||
|
||||
QUnit.test("popper as child of another", async (assert) => {
|
||||
class Child extends Component {
|
||||
static template = /* xml */ xml`
|
||||
<div id="child">
|
||||
<div class="target" t-ref="ref" />
|
||||
<div class="popper" t-ref="popper" />
|
||||
</div>
|
||||
`;
|
||||
setup() {
|
||||
const ref = useRef("ref");
|
||||
usePosition(() => ref.el, { position: "left" });
|
||||
}
|
||||
}
|
||||
const target = document.createElement("div");
|
||||
target.id = "target";
|
||||
Object.assign(target.style, TARGET_STYLE);
|
||||
container.appendChild(target);
|
||||
class Parent extends Component {
|
||||
static components = { Child };
|
||||
static template = /* xml */ xml`<div id="popper"><Child/></div>`;
|
||||
setup() {
|
||||
usePosition(target);
|
||||
}
|
||||
}
|
||||
|
||||
const sheet = document.createElement("style");
|
||||
sheet.textContent = `
|
||||
#child .target {
|
||||
background-color: peachpuff;
|
||||
height: 100px;
|
||||
width: 10px;
|
||||
}
|
||||
#child .popper {
|
||||
background-color: olive;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(sheet);
|
||||
registerCleanup(() => sheet.remove());
|
||||
|
||||
await mount(Parent, container);
|
||||
const parentPopBox1 = container.querySelector("#popper").getBoundingClientRect();
|
||||
const childPopBox1 = container.querySelector("#child .popper").getBoundingClientRect();
|
||||
const spacer = document.createElement("div");
|
||||
spacer.id = "foo";
|
||||
spacer.style.height = "1px";
|
||||
spacer.style.width = "100px";
|
||||
container.prepend(spacer);
|
||||
await triggerEvent(document, null, "scroll");
|
||||
|
||||
const parentPopBox2 = container.querySelector("#popper").getBoundingClientRect();
|
||||
const childPopBox2 = container.querySelector("#child .popper").getBoundingClientRect();
|
||||
|
||||
assert.strictEqual(parentPopBox1.top, parentPopBox2.top);
|
||||
assert.strictEqual(childPopBox1.top, childPopBox2.top);
|
||||
assert.strictEqual(parentPopBox2.left, parentPopBox1.left + spacer.offsetWidth * 0.5);
|
||||
assert.strictEqual(childPopBox2.left, childPopBox1.left + spacer.offsetWidth * 0.5);
|
||||
});
|
||||
|
||||
function getPositionTest(position, positionToCheck) {
|
||||
return async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
Reference in New Issue
Block a user