Skip to content

Commit 66b08bd

Browse files
authored
Merge pull request #2123 from Parvinmh/bugfix/tooltip-scrollable
delay positioning using observer to handle scrollable tooltip
2 parents 92bcfd9 + 1797796 commit 66b08bd

File tree

4 files changed

+152
-7
lines changed

4 files changed

+152
-7
lines changed

src/packages/tour/position.cy.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
describe("Intro.js tooltip position with scrollable container", () => {
2+
beforeEach(() => {
3+
cy.visit("./cypress/setup/index.html");
4+
5+
// intro.js CSS
6+
cy.document().then((doc) => {
7+
if (!doc.getElementById("introjs-css")) {
8+
const link = doc.createElement("link");
9+
link.id = "introjs-css";
10+
link.rel = "stylesheet";
11+
link.href = "https://unpkg.com/intro.js/minified/introjs.min.css";
12+
doc.head.appendChild(link);
13+
}
14+
});
15+
16+
// scrollable container and target element
17+
cy.document().then((doc) => {
18+
const container = doc.createElement("div");
19+
container.id = "scrollable-container";
20+
container.style.cssText =
21+
"height: 600px; overflow-y: auto; border: 2px solid gray; margin-bottom: 20px;";
22+
23+
const inner = doc.createElement("div");
24+
inner.style.height = "1000px";
25+
inner.style.position = "relative";
26+
27+
const target = doc.createElement("div");
28+
target.id = "target-element";
29+
target.style.cssText =
30+
"margin-top: 800px; background-color: yellow; padding: 10px;";
31+
target.setAttribute("data-intro", "Scrollable test element");
32+
target.textContent = "Step Element (scrollable test)";
33+
34+
inner.appendChild(target);
35+
container.appendChild(inner);
36+
doc.body.prepend(container);
37+
});
38+
39+
// intro.js script and initialize tour
40+
cy.window().then((win) => {
41+
return new Promise((resolve) => {
42+
const script = win.document.createElement("script");
43+
script.src = "https://unpkg.com/intro.js/minified/intro.min.js";
44+
script.onload = () => {
45+
const tour = win.introJs.tour();
46+
tour.setOptions({
47+
steps: [
48+
{ element: "#target-element", intro: "Scrollable test tooltip" },
49+
{
50+
element: "#target-element",
51+
intro: "Scrollable test tooltip 2",
52+
},
53+
],
54+
});
55+
win.__testTour = tour;
56+
resolve();
57+
};
58+
win.document.head.appendChild(script);
59+
});
60+
});
61+
});
62+
63+
it("scrolls and ensures tooltip is correctly positioned near target", () => {
64+
cy.get("#scrollable-container").scrollTo("top");
65+
cy.get("#target-element")
66+
.scrollIntoView({ block: "center" })
67+
.should("be.visible");
68+
69+
cy.window().then((win) => {
70+
win.__testTour.start();
71+
});
72+
73+
cy.get(".introjs-tooltip", { timeout: 500 }).should("be.visible");
74+
75+
cy.get("#target-element").then(($target) => {
76+
const targetRect = $target[0].getBoundingClientRect();
77+
78+
cy.get(".introjs-tooltip").then(($tooltip) => {
79+
const tooltipRect = $tooltip[0].getBoundingClientRect();
80+
81+
cy.log("Target Rect:", JSON.stringify(targetRect));
82+
cy.log("Tooltip Rect:", JSON.stringify(tooltipRect));
83+
84+
const horizontallySeparate =
85+
tooltipRect.right < targetRect.left ||
86+
tooltipRect.left > targetRect.right;
87+
const verticallySeparate =
88+
tooltipRect.bottom < targetRect.top ||
89+
tooltipRect.top > targetRect.bottom;
90+
expect(horizontallySeparate || verticallySeparate).to.be.true;
91+
92+
const verticalDistance = Math.min(
93+
Math.abs(tooltipRect.top - targetRect.bottom),
94+
Math.abs(targetRect.top - tooltipRect.bottom)
95+
);
96+
expect(verticalDistance).to.be.lessThan(16);
97+
});
98+
});
99+
});
100+
});

src/packages/tour/position.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { setPositionRelativeToStep } from "./position";
2+
import { setPositionRelativeTo } from "../../util/positionRelativeTo";
3+
import { TourStep } from "./steps";
4+
5+
jest.mock("../../util/positionRelativeTo", () => ({
6+
setPositionRelativeTo: jest.fn(),
7+
}));
8+
9+
beforeAll(() => {
10+
// Mock requestAnimationFrame to call callback immediately
11+
global.requestAnimationFrame = (cb) => {
12+
cb(0);
13+
return 0;
14+
};
15+
});
16+
17+
afterAll(() => {
18+
global.requestAnimationFrame = undefined as any;
19+
});
20+
21+
test("requestAnimationFrame runs and calls setPositionRelativeTo", () => {
22+
const relativeElement = document.createElement("div");
23+
const element = document.createElement("div");
24+
const step: TourStep = {
25+
step: 0,
26+
title: "My Step Title",
27+
intro: "My step description",
28+
element: element,
29+
position: "bottom",
30+
tooltipClass: "my-tooltip-class",
31+
highlightClass: "my-highlight-class",
32+
scrollTo: "element",
33+
disableInteraction: false,
34+
};
35+
36+
setPositionRelativeToStep(relativeElement, element, step, 10);
37+
38+
expect(setPositionRelativeTo).toHaveBeenCalledWith(
39+
relativeElement,
40+
element,
41+
step.element,
42+
10
43+
);
44+
});

src/packages/tour/position.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ export const setPositionRelativeToStep = (
1111
step: TourStep,
1212
padding: number
1313
) => {
14-
setPositionRelativeTo(
15-
relativeElement,
16-
element,
17-
step.element as HTMLElement,
18-
step.position === "floating" ? 0 : padding
19-
);
14+
setTimeout(() => {
15+
setPositionRelativeTo(
16+
relativeElement,
17+
element,
18+
step.element as HTMLElement,
19+
step.position === "floating" ? 0 : padding
20+
);
21+
}, 0);
2022
};

src/util/getOffset.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export default function getOffset(
3030
const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
3131
const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
3232

33-
3433
relativeEl = relativeEl || docEl || body;
3534

3635
const x = element.getBoundingClientRect();

0 commit comments

Comments
 (0)