Skip to content

Commit 38287d3

Browse files
committed
Always check if element is visible on page before we try to scroll to it
1 parent 9940772 commit 38287d3

File tree

11 files changed

+139
-90
lines changed

11 files changed

+139
-90
lines changed

@types/Helpers.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type Helpers = {
77
clamp(value: number, min: number, max?: number): number;
88
decorate(text: string, step: AbstractStep): string;
99
getMaxZIndex(): number;
10+
isElementVisibleOnPage(element: Element | HTMLElement | undefined): boolean;
1011
Color: typeof Color;
1112
Style: typeof Style;
1213
Scroll: typeof Scroll;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tourguidejs",
3-
"version": "2.0.5",
3+
"version": "2.0.6",
44
"src": "src/Tour.ts",
55
"main": "tourguide.js",
66
"module": "tourguide.esm.js",

src/step/PopoverStep.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,13 @@ class PopoverStep<AdditionalStepData = object> extends Step<PopoverStepData & Ad
193193
}
194194
_position($target: U) {
195195
const _target = $target?.first();
196-
if (!_target) this.$arrow.addClass("no-arrow ");
196+
const isElementVisible = this.context.helpers.isElementVisibleOnPage(_target as HTMLElement);
197+
if (!isElementVisible) this.$arrow.addClass("no-arrow ");
197198
else this.$arrow.removeClass("no-arrow ");
198199
this.context.helpers.Position.position(
199200
_target || document.body,
200201
this.$tooltip?.first() as HTMLElement,
201-
_target
202+
isElementVisible
202203
? [
203204
this.context.helpers.Position.positionabsolute(),
204205
this.context.helpers.Position.autoPlacement({

src/utils/dom.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,15 @@ export function getDataContents<T>(data = "", defaults: Record<string, string> =
1616
}
1717
});
1818
return result as unknown as T;
19+
}
20+
21+
export function isElementVisibleOnPage(element: HTMLElement | undefined): boolean {
22+
if (!element) return false;
23+
const rect = element.getBoundingClientRect();
24+
return rect.width !== 0
25+
|| rect.height !== 0
26+
|| rect.top !== 0
27+
|| rect.left !== 0
28+
|| rect.bottom !== 0
29+
|| rect.right !== 0;
1930
}

src/utils/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export * from "./color";
1414
export * from "./style";
1515
export * from "./zindex";
1616
export * from "./scroll";
17-
export * from "./position";
17+
export * from "./position";
18+
export * from "./dom";

src/utils/scroll.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import u, { Umbrella as U } from "umbrellajs";
22
import { Element } from "@types";
3+
import { isElementVisibleOnPage } from "./dom";
34

45
// eslint-disable-next-line @typescript-eslint/no-namespace
56
export namespace Scroll {
@@ -116,7 +117,7 @@ export namespace Scroll {
116117
*/
117118
export function smoothScroll(element: HTMLElement | undefined, options: ScrollIntoViewOptions) {
118119
return new Promise<boolean>((resolve) => {
119-
if (!element) return resolve(false);
120+
if (!element || !isElementVisibleOnPage(element)) return resolve(false);
120121
const observer = new IntersectionObserver(function (entries) {
121122
entries.forEach(function (entry) {
122123
if (entry.isIntersecting) {

tests/utils/scroll.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ describe("Scroll", () => {
3131
"width": 100
3232
}));
3333
element = target.first();
34+
(element as any).getBoundingClientRect = jest.fn(() => ({
35+
"x": 0,
36+
"y": 0,
37+
"bottom": 0,
38+
"height": 100,
39+
"left": 0,
40+
"right": 0,
41+
"top": 0,
42+
"width": 100
43+
}));
3444
element.scrollTo = jest.fn();
3545
element.scrollIntoView = jest.fn();
3646
document.body.appendChild(element);

tourguide.esm.js

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,35 @@ function getMaxZIndex() {
289289
return Math.max(...Array.from(document.querySelectorAll('body *'), el => parseFloat(window.getComputedStyle(el).zIndex)).filter(zIndex => !Number.isNaN(zIndex)), 0);
290290
}
291291

292+
/**
293+
* Parses a data string into an object with default values.
294+
* @param data - The input data string, where key-value pairs are separated by ";".
295+
* @param defaults - An optional object containing default key-value pairs.
296+
* @returns A new object constructed from the parsed data and defaults.
297+
*/
298+
function getDataContents() {
299+
let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
300+
let defaults = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
301+
const parts = data.split(";");
302+
const result = {
303+
...defaults
304+
};
305+
parts.forEach(part => {
306+
const entries = (part || "").split(":");
307+
if (entries[0]) {
308+
result[(entries[0] || "").trim()] = (entries[1] || "").trim();
309+
} else {
310+
console.warn("Invalid key-value pair found in data string:", part);
311+
}
312+
});
313+
return result;
314+
}
315+
function isElementVisibleOnPage(element) {
316+
if (!element) return false;
317+
const rect = element.getBoundingClientRect();
318+
return rect.width !== 0 || rect.height !== 0 || rect.top !== 0 || rect.left !== 0 || rect.bottom !== 0 || rect.right !== 0;
319+
}
320+
292321
// eslint-disable-next-line @typescript-eslint/no-namespace
293322
let Scroll;
294323
(function (_Scroll) {
@@ -362,7 +391,7 @@ let Scroll;
362391
_Scroll.animateScroll = animateScroll;
363392
function smoothScroll(element, options) {
364393
return new Promise(resolve => {
365-
if (!element) return resolve(false);
394+
if (!element || !isElementVisibleOnPage(element)) return resolve(false);
366395
const observer = new IntersectionObserver(function (entries) {
367396
entries.forEach(function (entry) {
368397
if (entry.isIntersecting) {
@@ -2548,7 +2577,9 @@ var Utils = /*#__PURE__*/Object.freeze({
25482577
get Style () { return Style; },
25492578
getMaxZIndex: getMaxZIndex,
25502579
get Scroll () { return Scroll; },
2551-
get Position () { return Position; }
2580+
get Position () { return Position; },
2581+
getDataContents: getDataContents,
2582+
isElementVisibleOnPage: isElementVisibleOnPage
25522583
});
25532584

25542585
/**
@@ -2785,8 +2816,9 @@ class PopoverStep extends Step {
27852816
}
27862817
_position($target) {
27872818
const _target = $target?.first();
2788-
if (!_target) this.$arrow.addClass("no-arrow ");else this.$arrow.removeClass("no-arrow ");
2789-
this.context.helpers.Position.position(_target || document.body, this.$tooltip?.first(), _target ? [this.context.helpers.Position.positionabsolute(), this.context.helpers.Position.autoPlacement({
2819+
const isElementVisible = this.context.helpers.isElementVisibleOnPage(_target);
2820+
if (!isElementVisible) this.$arrow.addClass("no-arrow ");else this.$arrow.removeClass("no-arrow ");
2821+
this.context.helpers.Position.position(_target || document.body, this.$tooltip?.first(), isElementVisible ? [this.context.helpers.Position.positionabsolute(), this.context.helpers.Position.autoPlacement({
27902822
alignment: this.data.alignment,
27912823
padding: 24
27922824
}), this.context.helpers.Position.highlight({
@@ -3118,30 +3150,6 @@ class CardStep extends PopoverStep {
31183150

31193151
const Tour$1 = ":host {\n position: absolute;\n overflow: visible;\n top: 0;\n left: 0;\n width: 0;\n height: 0;\n box-sizing: border-box;\n line-height: 1.4;\n text-align: left;\n text-rendering: optimizespeed;\n font-family: var(--tourguide-font-family);\n font-size: var(--tourguide-font-size);\n color: var(--tourguide-text-color);\n /* 1 */\n -webkit-text-size-adjust: 100%;\n /* 2 */\n -moz-tab-size: 4;\n /* 3 */\n tab-size: 4;\n /* 3 */\n}\n:host * {\n margin: 0;\n padding: 0;\n background: none;\n border: none;\n border-width: 0;\n border-style: none;\n border-color: currentColor;\n box-shadow: none;\n color: inherit;\n appearance: none;\n font-size: inherit;\n font-weight: inherit;\n text-decoration: none;\n}\n:host a,\n:host button {\n cursor: pointer;\n}\n:host a:hover, :host a:focus,\n:host button:hover,\n:host button:focus {\n outline: 5px auto var(--tourguide-focus-color);\n}";
31203152

3121-
/**
3122-
* Parses a data string into an object with default values.
3123-
* @param data - The input data string, where key-value pairs are separated by ";".
3124-
* @param defaults - An optional object containing default key-value pairs.
3125-
* @returns A new object constructed from the parsed data and defaults.
3126-
*/
3127-
function getDataContents() {
3128-
let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
3129-
let defaults = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
3130-
const parts = data.split(";");
3131-
const result = {
3132-
...defaults
3133-
};
3134-
parts.forEach(part => {
3135-
const entries = (part || "").split(":");
3136-
if (entries[0]) {
3137-
result[(entries[0] || "").trim()] = (entries[1] || "").trim();
3138-
} else {
3139-
console.warn("Invalid key-value pair found in data string:", part);
3140-
}
3141-
});
3142-
return result;
3143-
}
3144-
31453153
const defaultKeyNavOptions = {
31463154
next: "ArrowRight",
31473155
prev: "ArrowLeft",

tourguide.js

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,35 @@ var Tourguide = (function (exports) {
292292
return Math.max(...Array.from(document.querySelectorAll('body *'), el => parseFloat(window.getComputedStyle(el).zIndex)).filter(zIndex => !Number.isNaN(zIndex)), 0);
293293
}
294294

295+
/**
296+
* Parses a data string into an object with default values.
297+
* @param data - The input data string, where key-value pairs are separated by ";".
298+
* @param defaults - An optional object containing default key-value pairs.
299+
* @returns A new object constructed from the parsed data and defaults.
300+
*/
301+
function getDataContents() {
302+
let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
303+
let defaults = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
304+
const parts = data.split(";");
305+
const result = {
306+
...defaults
307+
};
308+
parts.forEach(part => {
309+
const entries = (part || "").split(":");
310+
if (entries[0]) {
311+
result[(entries[0] || "").trim()] = (entries[1] || "").trim();
312+
} else {
313+
console.warn("Invalid key-value pair found in data string:", part);
314+
}
315+
});
316+
return result;
317+
}
318+
function isElementVisibleOnPage(element) {
319+
if (!element) return false;
320+
const rect = element.getBoundingClientRect();
321+
return rect.width !== 0 || rect.height !== 0 || rect.top !== 0 || rect.left !== 0 || rect.bottom !== 0 || rect.right !== 0;
322+
}
323+
295324
// eslint-disable-next-line @typescript-eslint/no-namespace
296325
let Scroll;
297326
(function (_Scroll) {
@@ -365,7 +394,7 @@ var Tourguide = (function (exports) {
365394
_Scroll.animateScroll = animateScroll;
366395
function smoothScroll(element, options) {
367396
return new Promise(resolve => {
368-
if (!element) return resolve(false);
397+
if (!element || !isElementVisibleOnPage(element)) return resolve(false);
369398
const observer = new IntersectionObserver(function (entries) {
370399
entries.forEach(function (entry) {
371400
if (entry.isIntersecting) {
@@ -2551,7 +2580,9 @@ var Tourguide = (function (exports) {
25512580
get Style () { return Style; },
25522581
getMaxZIndex: getMaxZIndex,
25532582
get Scroll () { return Scroll; },
2554-
get Position () { return Position; }
2583+
get Position () { return Position; },
2584+
getDataContents: getDataContents,
2585+
isElementVisibleOnPage: isElementVisibleOnPage
25552586
});
25562587

25572588
/**
@@ -2788,8 +2819,9 @@ var Tourguide = (function (exports) {
27882819
}
27892820
_position($target) {
27902821
const _target = $target?.first();
2791-
if (!_target) this.$arrow.addClass("no-arrow ");else this.$arrow.removeClass("no-arrow ");
2792-
this.context.helpers.Position.position(_target || document.body, this.$tooltip?.first(), _target ? [this.context.helpers.Position.positionabsolute(), this.context.helpers.Position.autoPlacement({
2822+
const isElementVisible = this.context.helpers.isElementVisibleOnPage(_target);
2823+
if (!isElementVisible) this.$arrow.addClass("no-arrow ");else this.$arrow.removeClass("no-arrow ");
2824+
this.context.helpers.Position.position(_target || document.body, this.$tooltip?.first(), isElementVisible ? [this.context.helpers.Position.positionabsolute(), this.context.helpers.Position.autoPlacement({
27932825
alignment: this.data.alignment,
27942826
padding: 24
27952827
}), this.context.helpers.Position.highlight({
@@ -3121,30 +3153,6 @@ var Tourguide = (function (exports) {
31213153

31223154
const Tour$1 = ":host {\n position: absolute;\n overflow: visible;\n top: 0;\n left: 0;\n width: 0;\n height: 0;\n box-sizing: border-box;\n line-height: 1.4;\n text-align: left;\n text-rendering: optimizespeed;\n font-family: var(--tourguide-font-family);\n font-size: var(--tourguide-font-size);\n color: var(--tourguide-text-color);\n /* 1 */\n -webkit-text-size-adjust: 100%;\n /* 2 */\n -moz-tab-size: 4;\n /* 3 */\n tab-size: 4;\n /* 3 */\n}\n:host * {\n margin: 0;\n padding: 0;\n background: none;\n border: none;\n border-width: 0;\n border-style: none;\n border-color: currentColor;\n box-shadow: none;\n color: inherit;\n appearance: none;\n font-size: inherit;\n font-weight: inherit;\n text-decoration: none;\n}\n:host a,\n:host button {\n cursor: pointer;\n}\n:host a:hover, :host a:focus,\n:host button:hover,\n:host button:focus {\n outline: 5px auto var(--tourguide-focus-color);\n}";
31233155

3124-
/**
3125-
* Parses a data string into an object with default values.
3126-
* @param data - The input data string, where key-value pairs are separated by ";".
3127-
* @param defaults - An optional object containing default key-value pairs.
3128-
* @returns A new object constructed from the parsed data and defaults.
3129-
*/
3130-
function getDataContents() {
3131-
let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
3132-
let defaults = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
3133-
const parts = data.split(";");
3134-
const result = {
3135-
...defaults
3136-
};
3137-
parts.forEach(part => {
3138-
const entries = (part || "").split(":");
3139-
if (entries[0]) {
3140-
result[(entries[0] || "").trim()] = (entries[1] || "").trim();
3141-
} else {
3142-
console.warn("Invalid key-value pair found in data string:", part);
3143-
}
3144-
});
3145-
return result;
3146-
}
3147-
31483156
const defaultKeyNavOptions = {
31493157
next: "ArrowRight",
31503158
prev: "ArrowLeft",

tourguide.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)