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 5faea7bf50a..18b4a865470 100644 --- a/addons/web_tour/static/src/tour_pointer/tour_pointer.js +++ b/addons/web_tour/static/src/tour_pointer/tour_pointer.js @@ -19,7 +19,6 @@ export class TourPointer extends Component { shape: { anchor: { type: HTMLElement, optional: true }, content: { type: String, optional: true }, - fixed: { type: Boolean, optional: true }, isOpen: { type: Boolean, optional: true }, isVisible: { type: Boolean, optional: true }, onClick: { type: [Function, { value: null }], optional: true }, @@ -54,6 +53,7 @@ export class TourPointer extends Component { let dimensions = null; let lastMeasuredContent = null; let lastOpenState = this.isOpen; + let lastAnchor; let [anchorX, anchorY] = [0, 0]; useEffect( @@ -90,7 +90,7 @@ export class TourPointer extends Component { if (!this.isOpen) { const { anchor } = this.props.pointerState; - if (anchor) { + if (anchor === lastAnchor) { const { x, y, width } = anchor.getBoundingClientRect(); const [lastAnchorX, lastAnchorY] = [anchorX, anchorY]; [anchorX, anchorY] = [x, y]; @@ -104,35 +104,38 @@ export class TourPointer extends Component { const wouldOverflow = window.innerWidth - x - width / 2 < dimensions?.width; el.classList.toggle("o_expand_left", wouldOverflow); - reposition(anchor, el, null, { - position: this.position, - margin: 6, - onPositioned: (popper, position) => { - const popperRect = popper.getBoundingClientRect(); - const { top, left, direction } = position; - if (direction === "top") { - popper.style.bottom = `${ - window.innerHeight - top - popperRect.height - }px`; - popper.style.removeProperty("top"); - } else { - popper.style.top = `${top}px`; - } - if (direction === "left") { - popper.style.right = `${ - window.innerWidth - left - popperRect.width - }px`; - popper.style.removeProperty("left"); - } else { - popper.style.left = `${left}px`; - } - }, - }); } + 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"); + } + }, + }); } } else { lastMeasuredContent = null; lastOpenState = false; + lastAnchor = null; dimensions = 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 65e2328fffd..4234ef7f5eb 100644 --- a/addons/web_tour/static/src/tour_pointer/tour_pointer.xml +++ b/addons/web_tour/static/src/tour_pointer/tour_pointer.xml @@ -8,7 +8,6 @@ o_tour_pointer o_{{ position }} {{ isOpen ? 'o_open' : (props.bounce ? 'o_bouncing' : '') }} - {{ props.pointerState.fixed ? 'position-fixed' : 'position-absolute' }} {{ props.pointerState.onClick ? 'cursor-pointer' : '' }} " t-attf-style=" diff --git a/addons/web_tour/static/src/tour_service/tour_pointer_state.js b/addons/web_tour/static/src/tour_service/tour_pointer_state.js index 50e169fe36e..b7189f987c9 100644 --- a/addons/web_tour/static/src/tour_service/tour_pointer_state.js +++ b/addons/web_tour/static/src/tour_service/tour_pointer_state.js @@ -15,7 +15,6 @@ import { getScrollParent } from "./tour_utils"; * @typedef TourPointerState * @property {HTMLElement} [anchor] * @property {string} [content] - * @property {boolean} fixed * @property {boolean} [isOpen] * @property {() => {}} [onClick] * @property {() => {}} [onMouseEnter] diff --git a/addons/web_tour/static/tests/tour_service_tests.js b/addons/web_tour/static/tests/tour_service_tests.js index cda8479ee46..66186929af7 100644 --- a/addons/web_tour/static/tests/tour_service_tests.js +++ b/addons/web_tour/static/tests/tour_service_tests.js @@ -60,7 +60,7 @@ QUnit.module("Tour service", (hooks) => { start() { super.start(...arguments); macroEngines.push(this); - } + }, }); registerCleanup(() => { macroEngines.forEach((e) => e.stop()); @@ -76,7 +76,7 @@ QUnit.module("Tour service", (hooks) => { .add("tour_service", tourService); patchWithCleanup(browser.console, { // prevent form logging "tour successful" which would end the qunit suite test - log: () => {} + log: () => {}, }); }); @@ -135,6 +135,60 @@ QUnit.module("Tour service", (hooks) => { assert.strictEqual(target.querySelector("span.value").textContent, "1"); }); + QUnit.test("next step with new anchor at same position", async (assert) => { + registry.category("web_tour.tours").add("tour1", { + sequence: 10, + steps: () => [{ trigger: "button.foo" }, { trigger: "button.bar" }], + }); + const env = await makeTestEnv({}); + + const { Component: OverlayContainer, props: overlayContainerProps } = registry + .category("main_components") + .get("OverlayContainer"); + + class Dummy extends Component { + state = useState({ bool: true }); + static template = xml/*html*/ ` + + + `; + } + class Root extends Component { + static components = { OverlayContainer, Dummy }; + static template = xml/*html*/ ` + + + + + `; + } + + await mount(Root, target, { env, props: { overlayContainerProps } }); + env.services.tour_service.startTour("tour1", { mode: "manual" }); + await mock.advanceTime(100); + assert.containsOnce(document.body, ".o_tour_pointer"); + + // check position of the pointer relative to the foo button + let pointerRect = document.body.querySelector(".o_tour_pointer").getBoundingClientRect(); + let buttonRect = document.body.querySelector("button.foo").getBoundingClientRect(); + const leftValue1 = pointerRect.left - buttonRect.left; + const bottomValue1 = pointerRect.bottom - buttonRect.bottom; + assert.ok(leftValue1 !== 0); + assert.ok(bottomValue1 !== 0); + + await click(target, "button.foo"); + await mock.advanceTime(100); + assert.containsOnce(document.body, ".o_tour_pointer"); + + // check position of the pointer relative to the bar button + pointerRect = document.body.querySelector(".o_tour_pointer").getBoundingClientRect(); + buttonRect = document.body.querySelector("button.bar").getBoundingClientRect(); + const leftValue2 = pointerRect.left - buttonRect.left; + const bottomValue2 = pointerRect.bottom - buttonRect.bottom; + assert.strictEqual(bottomValue1, bottomValue2); + assert.strictEqual(leftValue1, leftValue2); + }); + QUnit.test("scroller pointer to reach next step", async function (assert) { patchWithCleanup(Element.prototype, { scrollIntoView(options) { @@ -222,6 +276,92 @@ QUnit.module("Tour service", (hooks) => { ); }); + QUnit.test("scrolling to next step should update the pointer's height", async (assert) => { + patchWithCleanup(Element.prototype, { + scrollIntoView(options) { + super.scrollIntoView({ ...options, behavior: "instant" }); + }, + }); + + // The fixture should be shown for this test + target.style.position = "fixed"; + target.style.top = "200px"; + target.style.left = "50px"; + + const stepContent = "Click this pretty button to increment this magnificent counter !"; + registry.category("web_tour.tours").add("tour1", { + sequence: 10, + steps: () => [ + { + trigger: "button.inc", + content: stepContent, + }, + ], + }); + const env = await makeTestEnv({}); + + const { Component: OverlayContainer, props: overlayContainerProps } = registry + .category("main_components") + .get("OverlayContainer"); + + class Root extends Component { + static components = { OverlayContainer, Counter }; + static template = xml/*html*/ ` +
+ +
+
+ + `; + } + + await mount(Root, target, { env, props: { overlayContainerProps } }); + env.services.tour_service.startTour("tour1", { mode: "manual" }); + await mock.advanceTime(100); // awaits the macro engine + assert.containsOnce(document.body, ".o_tour_pointer"); + assert.equal(document.body.querySelector(".o_tour_pointer").textContent, stepContent); + + const pointer = document.body.querySelector(".o_tour_pointer"); + assert.doesNotHaveClass(pointer, "o_open"); + assert.strictEqual(pointer.style.height, "28px"); + assert.strictEqual(pointer.style.width, "28px"); + + await triggerEvent(document.body, ".o_tour_pointer", "mouseenter"); + await mock.advanceTime(100); // awaits for the macro engine next check cycle + assert.hasClass(pointer, "o_open"); + const firstOpenHeight = pointer.style.height; + const firstOpenWidth = pointer.style.width; + + await triggerEvent(document.body, ".o_tour_pointer", "mouseleave"); + await mock.advanceTime(100); // awaits for the macro engine next check cycle + assert.doesNotHaveClass(pointer, "o_open"); + + document.querySelector(".scrollable-parent").scrollTop = 1000; + await nextTick(); // awaits the intersection observer to update after the scroll + await mock.advanceTime(100); // awaits for the macro engine next check cycle + // now the scroller pointer should be shown + assert.containsOnce(document.body, ".o_tour_pointer"); + assert.equal( + document.body.querySelector(".o_tour_pointer").textContent, + "Scroll up to reach the next step." + ); + + document.querySelector(".scrollable-parent").scrollTop = 0; + await nextTick(); // awaits the intersection observer to update after the scroll + await mock.advanceTime(100); // awaits for the macro engine next check cycle + // now the true step pointer should be shown again + assert.containsOnce(document.body, ".o_tour_pointer"); + assert.equal(document.body.querySelector(".o_tour_pointer").textContent, stepContent); + + await triggerEvent(document.body, ".o_tour_pointer", "mouseenter"); + await mock.advanceTime(100); // awaits for the macro engine next check cycle + assert.hasClass(pointer, "o_open"); + const secondOpenHeight = pointer.style.height; + const secondOpenWidth = pointer.style.width; + assert.strictEqual(firstOpenHeight, secondOpenHeight); + assert.strictEqual(firstOpenWidth, secondOpenWidth); + }); + QUnit.test("perform edit on next step", async function (assert) { registry.category("web_tour.tours").add("tour1", { sequence: 10,