diff --git a/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js b/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js index 9f447e79b04..0d70482c1b4 100644 --- a/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js +++ b/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js @@ -46,9 +46,7 @@ export class AnalyticDistribution extends Component { this.widgetRef = useRef("analyticDistribution"); this.dropdownRef = useRef("analyticDropdown"); this.mainRef = useRef("mainElement"); - usePosition(() => this.widgetRef.el, { - popper: "analyticDropdown", - }); + usePosition("analyticDropdown", () => this.widgetRef.el); this.nextId = 1; this.focusSelector = false; diff --git a/addons/calendar/static/src/views/fields/many2many_attendee_expandable.js b/addons/calendar/static/src/views/fields/many2many_attendee_expandable.js index f12125fe864..a1460e139cb 100644 --- a/addons/calendar/static/src/views/fields/many2many_attendee_expandable.js +++ b/addons/calendar/static/src/views/fields/many2many_attendee_expandable.js @@ -22,8 +22,8 @@ export class Many2ManyAttendeeExpandable extends Many2ManyAttendee { useEffect(() => { const popover = document.querySelector(".o_field_many2manyattendeeexpandable") .closest(".o_popover"); - const targetElement = document.querySelector(`.fc-event[data-event-id="${this.props.record.resId}"]`); - reposition(targetElement, popover, null, { position: "right", margin: 0 }); + const target = document.querySelector(`.fc-event[data-event-id="${this.props.record.resId}"]`); + reposition(popover, target, { position: "right", margin: 0 }); }, () => [ this.state.expanded ]); } } diff --git a/addons/mail/static/src/core/common/navigable_list.js b/addons/mail/static/src/core/common/navigable_list.js index 62f53b8bbe1..3d2a9ad0080 100644 --- a/addons/mail/static/src/core/common/navigable_list.js +++ b/addons/mail/static/src/core/common/navigable_list.js @@ -48,10 +48,7 @@ export class NavigableList extends Component { this.close(); }); // position and size - usePosition(() => this.props.anchorRef, { - popper: "root", - position: this.props.position, - }); + usePosition("root", () => this.props.anchorRef, { position: this.props.position }); useEffect( () => { this.open(); diff --git a/addons/web/static/src/core/autocomplete/autocomplete.js b/addons/web/static/src/core/autocomplete/autocomplete.js index 113ca07deb6..d215658c6de 100644 --- a/addons/web/static/src/core/autocomplete/autocomplete.js +++ b/addons/web/static/src/core/autocomplete/autocomplete.js @@ -64,8 +64,7 @@ export class AutoComplete extends Component { // position and size if (this.props.dropdown) { - usePosition(() => this.inputRef.el, { - popper: "sourcesList", + usePosition("sourcesList", () => this.inputRef.el, { position: "bottom-start", }); } else { diff --git a/addons/web/static/src/core/dropdown/dropdown.js b/addons/web/static/src/core/dropdown/dropdown.js index b6a9e6be754..d2ef3a86d4c 100644 --- a/addons/web/static/src/core/dropdown/dropdown.js +++ b/addons/web/static/src/core/dropdown/dropdown.js @@ -102,7 +102,6 @@ export class Dropdown extends Component { } this.defaultDirection = direction; const positioningOptions = { - popper: "menuRef", position, onPositioned: (el, { direction }) => { this.state.directionCaretClass = DIRECTION_CARET_CLASS[direction]; @@ -149,11 +148,19 @@ export class Dropdown extends Component { ); // Position menu relatively to parent element - this.position = usePosition(() => this.rootRef.el.parentElement, positioningOptions); + this.position = usePosition( + "menuRef", + () => this.rootRef.el.parentElement, + positioningOptions + ); } else { // Position menu relatively to inner toggler const togglerRef = useRef("togglerRef"); - this.position = usePosition(() => togglerRef.el, positioningOptions); + this.position = usePosition( + "menuRef", + () => togglerRef.el, + positioningOptions + ); } useEffect( diff --git a/addons/web/static/src/core/popover/popover.js b/addons/web/static/src/core/popover/popover.js index 6ac26bfdef2..81905ce5fa1 100644 --- a/addons/web/static/src/core/popover/popover.js +++ b/addons/web/static/src/core/popover/popover.js @@ -7,18 +7,20 @@ import { usePosition } from "@web/core/position_hook"; export class Popover extends Component { setup() { useForwardRefToParent("ref"); - const position = usePosition(this.props.target, { - onPositioned: (el, solution) => { - (this.props.onPositioned || this.onPositioned.bind(this))(el, solution); - if (this.props.fixedPosition) { - // Prevent further positioning updates if fixed position is wanted - position.lock(); - } - }, - position: this.props.position, - popper: "ref", - fixedPosition: this.props.fixedPosition, - }); + const position = usePosition( + "ref", + () => this.props.target, + { + onPositioned: (el, solution) => { + (this.props.onPositioned || this.onPositioned.bind(this))(el, solution); + if (this.props.fixedPosition) { + // Prevent further positioning updates if fixed position is wanted + position.lock(); + } + }, + position: this.props.position, + } + ); } onPositioned(el, { direction, variant }) { const position = `${direction[0]}${variant[0]}`; diff --git a/addons/web/static/src/core/position_hook.js b/addons/web/static/src/core/position_hook.js index 93dece34950..f08abfd850b 100644 --- a/addons/web/static/src/core/position_hook.js +++ b/addons/web/static/src/core/position_hook.js @@ -23,7 +23,6 @@ import { localization } from "@web/core/l10n/localization"; /** * @typedef Options - * @property {string} [popper="popper"] useRef reference to the popper element * @property {HTMLElement} [container] container element * @property {number} [margin=0] * margin in pixels between the popper and the target. @@ -82,7 +81,6 @@ const FIT_FLIP_ORDER = { top: "tb", right: "rl", bottom: "bt", left: "lr" }; /** @type {Options} */ const DEFAULTS = { - popper: "popper", margin: 0, position: "bottom", }; @@ -110,15 +108,15 @@ function getIFrame(el) { * The popper will stay at `margin` distance from its target. One could also * use the CSS margins of the popper element to achieve the same result. * - * @param {HTMLElement} target * @param {HTMLElement} popper - * @param {HTMLIFrameElement?} [iframe] + * @param {HTMLElement} target * @param {Options} options + * @param {HTMLIFrameElement} [iframe] * @returns {PositioningSolution} the best positioning solution, relative to * the containing block of the popper. * => can be applied to popper.style.(top|left) */ -function getBestPosition(target, popper, iframe, { container, margin, position }) { +function getBestPosition(popper, target, { container, margin, position }, iframe) { // Retrieve directions and variants const [directionKey, variantKey = "middle"] = position.split("-"); const directions = @@ -252,15 +250,13 @@ function getBestPosition(target, popper, iframe, { container, margin, position } * tried in different direction and variant flip orders (depending on the requested position). * If no position is found that fits the container, the requested position stays used. * - * When the final position is applied, a corresponding CSS class is also added to the popper. - * This could be used to further styling. - * - * @param {HTMLElement} target + * @deprecated too low level, will soon not be exported anymore, use usePosition instead * @param {HTMLElement} popper - * @param {HTMLIFrameElement} [iframe] + * @param {HTMLElement} target * @param {Options} options + * @param {HTMLIFrameElement} [iframe] */ -export function reposition(target, popper, iframe, options) { +export function reposition(popper, target, options, iframe) { let [directionKey, variantKey = "middle"] = options.position.split("-"); if (localization.direction === "rtl") { if (["bottom", "top"].includes(directionKey)) { @@ -279,19 +275,17 @@ export function reposition(target, popper, iframe, options) { popper.style.left = "0px"; // Get best positioning solution and apply it - const position = getBestPosition(target, popper, iframe, options); - const { top, left, variant } = position; + const position = getBestPosition(popper, target, options, iframe); + const { top, left, direction, variant } = position; popper.style.top = `${top}px`; popper.style.left = `${left}px`; if (variant === "fit") { - const styleProperty = ["top", "bottom"].includes(directionKey) ? "width" : "height"; + const styleProperty = ["top", "bottom"].includes(direction) ? "width" : "height"; popper.style[styleProperty] = target.getBoundingClientRect()[styleProperty] + "px"; } - if (options.onPositioned) { - options.onPositioned(popper, position); - } + options.onPositioned?.(popper, position); } const POSITION_BUS = Symbol("position-bus"); @@ -304,29 +298,29 @@ const POSITION_BUS = Symbol("position-bus"); * If all of fallback positions are also clipped off `container`, * the original position is used. * - * Note: The popper element should be indicated in your template with a t-ref reference. - * This could be customized with the `popper` option. + * Note: The popper element should be indicated in your template + * with a t-ref reference matching the refName argument. * - * @param {HTMLElement | (() => HTMLElement)} target - * @param {Options} options + * @param {string} refName + * name of the reference to the popper element in the template. + * @param {() => HTMLElement} getTarget + * @param {Options} [options={}] the options to be used for positioning * @returns {PositioningControl} * control object to lock/unlock the positioning. */ -export function usePosition(target, options) { - const popperRef = useRef(options?.popper || DEFAULTS.popper); - const getTarget = typeof target === "function" ? target : () => target; +export function usePosition(refName, getTarget, options = {}) { + const ref = useRef(refName); let lock = false; const update = () => { const targetEl = getTarget(); - const popperEl = popperRef.el; - if (!targetEl || !popperEl || lock) { + if (!ref.el || !targetEl || lock) { + // No compute needed return; } // Prepare const iframe = getIFrame(targetEl); - const currentOptions = { ...DEFAULTS, ...options }; - reposition(targetEl, popperEl, iframe, currentOptions); + reposition(ref.el, targetEl, { ...DEFAULTS, ...options }, iframe); }; const component = useComponent(); @@ -347,7 +341,7 @@ export function usePosition(target, options) { if (isTopmost) { // Attach listeners to keep the positioning up to date const scrollListener = (e) => { - if (popperRef.el?.contains(e.target)) { + if (ref.el?.contains(e.target)) { // In case the scroll event occurs inside the popper, do not reposition return; } diff --git a/addons/web/static/src/views/fields/properties/properties_field.js b/addons/web/static/src/views/fields/properties/properties_field.js index e0e829f3475..76b853d53d2 100644 --- a/addons/web/static/src/views/fields/properties/properties_field.js +++ b/addons/web/static/src/views/fields/properties/properties_field.js @@ -748,11 +748,11 @@ export class PropertiesField extends Component { const popover = document .querySelector(".o_field_property_definition") .closest(".o_popover"); - const targetElement = document.querySelector( + const target = document.querySelector( `*[property-name="${propertyName}"] .o_field_property_open_popover` ); - reposition(targetElement, popover, null, { position: "top", margin: 10 }); + reposition(popover, target, { position: "top", margin: 10 }); const arrow = popover.querySelector(".popover-arrow"); if (arrow) { diff --git a/addons/web/static/tests/core/popover/popover_tests.js b/addons/web/static/tests/core/popover/popover_tests.js index 968b171745f..f53e6ac0a23 100644 --- a/addons/web/static/tests/core/popover/popover_tests.js +++ b/addons/web/static/tests/core/popover/popover_tests.js @@ -178,12 +178,15 @@ QUnit.test("reposition popover should properly change classNames", async (assert const TestPopover = class extends Popover { setup() { // Don't call super.setup() in order to replace the use of usePosition hook... - usePosition(this.props.target, { - container, - onPositioned: this.onPositioned.bind(this), - position: this.props.position, - popper: "ref", - }); + usePosition( + "ref", + () => this.props.target, + { + container, + onPositioned: this.onPositioned.bind(this), + position: this.props.position, + } + ); } }; @@ -279,7 +282,7 @@ QUnit.test("popover fixed position", async (assert) => { fixture.appendChild(container); const TestPopover = class extends Popover { - onPositioned(el, { direction, variant }) { + onPositioned() { assert.step("onPositioned"); } }; diff --git a/addons/web/static/tests/core/position_hook_tests.js b/addons/web/static/tests/core/position_hook_tests.js index 0d7b0c6b42b..bcf83b36bf0 100644 --- a/addons/web/static/tests/core/position_hook_tests.js +++ b/addons/web/static/tests/core/position_hook_tests.js @@ -51,7 +51,7 @@ function getTestComponent(popperOptions = {}, target = document.createElement("d class TestComp extends Component { setup() { - usePosition(target, popperOptions); + usePosition("popper", () => target, popperOptions); } } TestComp.template = xml`
`; @@ -294,23 +294,6 @@ QUnit.test("popper is an inner element", async (assert) => { await mount(TestComp, container); }); -QUnit.test("can change the popper target name", async (assert) => { - assert.expect(2); - const TestComp = getTestComponent({ - popper: "myRef", - onPositioned: (el) => { - assert.notOk(document.getElementById("not-popper") === el); - assert.ok(document.getElementById("popper") === el); - }, - }); - TestComp.template = xml` -
-
-
- `; - await mount(TestComp, container); -}); - QUnit.test("has no effect when component is destroyed", async (assert) => { mockAnimationFrame(); const TestComp = getTestComponent({ @@ -582,7 +565,7 @@ QUnit.test("popper as child of another", async (assert) => { `; setup() { const ref = useRef("ref"); - usePosition(() => ref.el, { position: "left" }); + usePosition("popper", () => ref.el, { position: "left" }); } } const target = document.createElement("div"); @@ -591,9 +574,9 @@ QUnit.test("popper as child of another", async (assert) => { container.appendChild(target); class Parent extends Component { static components = { Child }; - static template = /* xml */ xml`
`; + static template = /* xml */ xml`
`; setup() { - usePosition(target); + usePosition("popper", () => target); } } diff --git a/addons/web_tour/static/src/tour_pointer/tour_pointer.js b/addons/web_tour/static/src/tour_pointer/tour_pointer.js index 18b4a865470..79f9c73f440 100644 --- a/addons/web_tour/static/src/tour_pointer/tour_pointer.js +++ b/addons/web_tour/static/src/tour_pointer/tour_pointer.js @@ -48,7 +48,7 @@ export class TourPointer extends Component { static height = 28; // in pixels setup() { - const rootRef = useRef("popper"); + const rootRef = useRef("pointer"); /** @type {DOMREct | null} */ let dimensions = null; let lastMeasuredContent = null; @@ -58,8 +58,8 @@ export class TourPointer extends Component { useEffect( () => { - const { el } = rootRef; - if (el) { + const { el: pointer } = rootRef; + if (pointer) { const hasContentChanged = lastMeasuredContent !== this.content; const hasOpenStateChanged = lastOpenState !== this.isOpen; lastOpenState = this.isOpen; @@ -67,9 +67,9 @@ export class TourPointer extends Component { // Content changed: we must re-measure the dimensions of the text. if (hasContentChanged) { lastMeasuredContent = this.content; - el.style.removeProperty("width"); - el.style.removeProperty("height"); - dimensions = el.getBoundingClientRect(); + pointer.style.removeProperty("width"); + pointer.style.removeProperty("height"); + dimensions = pointer.getBoundingClientRect(); } // If the content or the "is open" state changed: we must apply @@ -79,13 +79,13 @@ export class TourPointer extends Component { ? [dimensions.width, dimensions.height] : [this.constructor.width, this.constructor.height]; if (this.isOpen) { - el.style.removeProperty("transition"); + pointer.style.removeProperty("transition"); } else { // No transition if switching from open to closed - el.style.setProperty("transition", "none"); + pointer.style.setProperty("transition", "none"); } - el.style.setProperty("width", `${width}px`); - el.style.setProperty("height", `${height}px`); + pointer.style.setProperty("width", `${width}px`); + pointer.style.setProperty("height", `${height}px`); } if (!this.isOpen) { @@ -103,34 +103,38 @@ export class TourPointer extends Component { } const wouldOverflow = window.innerWidth - x - width / 2 < dimensions?.width; - el.classList.toggle("o_expand_left", wouldOverflow); + pointer.classList.toggle("o_expand_left", wouldOverflow); } lastAnchor = anchor; - el.style.bottom = ""; - el.style.right = ""; - reposition(anchor, el, null, { - position: this.position, - margin: 6, - onPositioned: (popper, position) => { - const popperRect = popper.getBoundingClientRect(); - const { top, left, direction } = position; - if (direction === "top") { - // position from the bottom instead of the top as it is needed - // to ensure the expand animation is properly done - popper.style.bottom = `${ - window.innerHeight - top - popperRect.height - }px`; - popper.style.removeProperty("top"); - } else if (direction === "left") { - // position from the right instead of the left as it is needed - // to ensure the expand animation is properly done - popper.style.right = `${ - window.innerWidth - left - popperRect.width - }px`; - popper.style.removeProperty("left"); - } - }, - }); + pointer.style.bottom = ""; + pointer.style.right = ""; + reposition( + pointer, + anchor, + { + position: this.position, + margin: 6, + onPositioned: (_, position) => { + const popperRect = pointer.getBoundingClientRect(); + const { top, left, direction } = position; + if (direction === "top") { + // position from the bottom instead of the top as it is needed + // to ensure the expand animation is properly done + pointer.style.bottom = `${ + window.innerHeight - top - popperRect.height + }px`; + pointer.style.removeProperty("top"); + } else if (direction === "left") { + // position from the right instead of the left as it is needed + // to ensure the expand animation is properly done + pointer.style.right = `${ + window.innerWidth - left - popperRect.width + }px`; + pointer.style.removeProperty("left"); + } + }, + } + ); } } else { lastMeasuredContent = null; diff --git a/addons/web_tour/static/src/tour_pointer/tour_pointer.xml b/addons/web_tour/static/src/tour_pointer/tour_pointer.xml index 4234ef7f5eb..9b798d606c7 100644 --- a/addons/web_tour/static/src/tour_pointer/tour_pointer.xml +++ b/addons/web_tour/static/src/tour_pointer/tour_pointer.xml @@ -3,7 +3,7 @@