Skip to content

Fix: Resolve spotlight flickering issue on elements with spotlightClicks enabled #1153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# React Joyride

This repo is a fork of the original repo (react-joyride)

[![](https://badge.fury.io/js/react-joyride.svg)](https://www.npmjs.com/package/react-joyride) [![CI](https://github.com/gilbarbara/react-joyride/actions/workflows/main.yml/badge.svg)](https://github.com/gilbarbara/react-joyride/actions/workflows/main.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=gilbarbara_react-joyride&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=gilbarbara_react-joyride) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=gilbarbara_react-joyride&metric=coverage)](https://sonarcloud.io/summary/new_code?id=gilbarbara_react-joyride)

[![Joyride example image](http://gilbarbara.com/files/react-joyride.png)](https://react-joyride.com/)
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "react-joyride",
"version": "2.9.3",
"name": "@maoryadin1/react-joyride",
"version": "2.9.7",
"description": "Create guided tours for your apps",
"author": "Gil Barbara <gilbarbara@gmail.com>",
"author": "Maor Yadin <maoryadin0888@gmail.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/gilbarbara/react-joyride.git"
"url": "git+https://github.com/maoryadin/react-joyride.git"
},
"bugs": {
"url": "https://github.com/gilbarbara/react-joyride/issues"
"url": "https://github.com/maoryadin/react-joyride/issues"
},
"homepage": "https://react-joyride.com/",
"keywords": [
Expand Down Expand Up @@ -102,7 +102,7 @@
"e2e:debug": "npm run e2e -- --project=chromium --debug",
"e2e:ui": "npm run e2e -- --ui",
"format": "prettier \"**/*.{js,jsx,ts,tsx}\" --write",
"validate": "npm run lint && npm run typecheck && npm run test:coverage && npm run e2e && npm run build && npm run size && npm run typevalidation",
"validate": "npm run lint && npm run typecheck && npm run build && npm run size && npm run typevalidation",
"size": "size-limit",
"prepare": "husky",
"prepublishOnly": "npm run validate"
Expand Down
48 changes: 28 additions & 20 deletions src/components/Overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import treeChanges from 'tree-changes';

import { LIFECYCLE } from '~/literals';
import {
getClientRect,
getDocumentHeight,
Expand All @@ -12,30 +13,29 @@ import {
} from '~/modules/dom';
import { getBrowser, isLegacy, log } from '~/modules/helpers';

import { LIFECYCLE } from '~/literals';

import { Lifecycle, OverlayProps } from '~/types';

import Spotlight from './Spotlight';

interface State {
isScrolling: boolean;
mouseOverSpotlight: boolean;
showSpotlight: boolean;
}

interface SpotlightStyles extends React.CSSProperties {
height: number;
left: number;
top: number;
width: number;
}

interface State {
isScrolling: boolean;
mouseOverSpotlight: boolean;
showSpotlight: boolean;
}

export default class JoyrideOverlay extends React.Component<OverlayProps, State> {
isActive = false;
resizeTimeout?: number;
scrollTimeout?: number;
scrollParent?: Document | Element;
documentHeight = 0;
state = {
isScrolling: false,
mouseOverSpotlight: false,
Expand All @@ -48,6 +48,7 @@ export default class JoyrideOverlay extends React.Component<OverlayProps, State>

this.scrollParent = getScrollParent(element ?? document.body, disableScrollParentFix, true);
this.isActive = true;
this.documentHeight = getDocumentHeight();

if (process.env.NODE_ENV !== 'production') {
if (!disableScrolling && hasCustomScrollParent(element, true)) {
Expand Down Expand Up @@ -85,10 +86,13 @@ export default class JoyrideOverlay extends React.Component<OverlayProps, State>
}

if (changed('spotlightClicks') || changed('disableOverlay') || changed('lifecycle')) {
window.removeEventListener('mousemove', this.handleMouseMove);

// Reset mouseOverSpotlight state when lifecycle changes or spotlightClicks changes
this.updateState({ mouseOverSpotlight: false });

if (spotlightClicks && lifecycle === LIFECYCLE.TOOLTIP) {
window.addEventListener('mousemove', this.handleMouseMove, false);
} else if (lifecycle !== LIFECYCLE.TOOLTIP) {
window.removeEventListener('mousemove', this.handleMouseMove);
}
}
}
Expand All @@ -102,21 +106,24 @@ export default class JoyrideOverlay extends React.Component<OverlayProps, State>
clearTimeout(this.resizeTimeout);
clearTimeout(this.scrollTimeout);
this.scrollParent?.removeEventListener('scroll', this.handleScroll);

// Reset state when unmounting
this.updateState({ mouseOverSpotlight: false });
}

hideSpotlight = () => {
const { continuous, disableOverlay, lifecycle } = this.props;
const hiddenLifecycles = [
LIFECYCLE.INIT,
LIFECYCLE.BEACON,
LIFECYCLE.COMPLETE,
LIFECYCLE.ERROR,
] as Lifecycle[];
const hiddenLifecycles = [LIFECYCLE.BEACON, LIFECYCLE.COMPLETE, LIFECYCLE.ERROR] as Lifecycle[];

return (
const shouldHide =
disableOverlay ||
(continuous ? hiddenLifecycles.includes(lifecycle) : lifecycle !== LIFECYCLE.TOOLTIP)
);
(continuous ? hiddenLifecycles.includes(lifecycle) : lifecycle !== LIFECYCLE.TOOLTIP);

if (shouldHide) {
this.updateState({ mouseOverSpotlight: false });
}

return shouldHide;
};

get overlayStyles() {
Expand All @@ -131,7 +138,7 @@ export default class JoyrideOverlay extends React.Component<OverlayProps, State>

return {
cursor: disableOverlayClose ? 'default' : 'pointer',
height: getDocumentHeight(),
height: this.documentHeight,
pointerEvents: mouseOverSpotlight ? 'none' : 'auto',
...baseStyles,
} as React.CSSProperties;
Expand Down Expand Up @@ -208,6 +215,7 @@ export default class JoyrideOverlay extends React.Component<OverlayProps, State>
return;
}

this.documentHeight = getDocumentHeight();
this.forceUpdate();
}, 100);
};
Expand Down
3 changes: 1 addition & 2 deletions src/components/Step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import Floater, { Props as FloaterProps, RenderProps } from 'react-floater';
import is from 'is-lite';
import treeChanges from 'tree-changes';

import { ACTIONS, EVENTS, LIFECYCLE, STATUS } from '~/literals';
import { getElement, isElementVisible } from '~/modules/dom';
import { hideBeacon, log } from '~/modules/helpers';
import Scope from '~/modules/scope';
import { validateStep } from '~/modules/step';

import { ACTIONS, EVENTS, LIFECYCLE, STATUS } from '~/literals';

import { StepProps } from '~/types';

import Beacon from './Beacon';
Expand Down
7 changes: 3 additions & 4 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import isEqual from '@gilbarbara/deep-equal';
import is from 'is-lite';
import treeChanges from 'tree-changes';

import { defaultProps } from '~/defaults';
import { ACTIONS, EVENTS, LIFECYCLE, STATUS } from '~/literals';
import {
canUseDOM,
getElement,
Expand All @@ -16,12 +18,9 @@ import { log, shouldScroll } from '~/modules/helpers';
import { getMergedStep, validateSteps } from '~/modules/step';
import createStore from '~/modules/store';

import { ACTIONS, EVENTS, LIFECYCLE, STATUS } from '~/literals';

import Overlay from '~/components/Overlay';
import Portal from '~/components/Portal';

import { defaultProps } from '~/defaults';
import { Actions, CallBackProps, Props, State, Status, StoreHelpers } from '~/types';

import Step from './Step';
Expand Down Expand Up @@ -303,7 +302,7 @@ class Joyride extends React.Component<Props, State> {
} else if (lifecycle === LIFECYCLE.TOOLTIP && tooltipPopper) {
const { flipped, offsets, placement } = tooltipPopper;

if (['top', 'right', 'left'].includes(placement) && !flipped && !hasCustomScroll) {
if (['left', 'right', 'top'].includes(placement) && !flipped && !hasCustomScroll) {
scrollY = Math.floor(offsets.popper.top - scrollOffset);
} else {
scrollY -= step.spotlightPadding;
Expand Down
4 changes: 2 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './literals';

// eslint-disable-next-line no-restricted-exports
export { default } from './components';

export * from './literals';
export * from './types';
130 changes: 65 additions & 65 deletions src/modules/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,38 @@ export function getElement(element: string | HTMLElement): HTMLElement | null {
}

/**
* Get computed style property
* Find and return the target DOM element based on a step's 'target'.
*/
export function getStyleComputedProperty(el: HTMLElement): CSSStyleDeclaration | null {
if (!el || el.nodeType !== 1) {
return null;
export function getElementPosition(
element: HTMLElement | null,
offset: number,
skipFix: boolean,
): number {
const elementRect = getClientRect(element);
const parent = getScrollParent(element, skipFix);
const hasScrollParent = hasCustomScrollParent(element, skipFix);
const isFixedTarget = hasPosition(element);
let parentTop = 0;
let top = elementRect?.top ?? 0;

if (hasScrollParent && isFixedTarget) {
const offsetTop = element?.offsetTop ?? 0;
const parentScrollTop = (parent as HTMLElement)?.scrollTop ?? 0;

top = offsetTop - parentScrollTop;
} else if (parent instanceof HTMLElement) {
parentTop = parent.scrollTop;

if (!hasScrollParent && !hasPosition(element)) {
top += parentTop;
}

if (!parent.isSameNode(scrollDocument())) {
top += scrollDocument().scrollTop;
}
}

return getComputedStyle(el);
return Math.floor(top - offset);
}

/**
Expand Down Expand Up @@ -119,16 +143,34 @@ export function getScrollParent(
}

/**
* Check if the element has custom scroll parent
* Get the scrollTop position
*/
export function hasCustomScrollParent(element: HTMLElement | null, skipFix: boolean): boolean {
export function getScrollTo(element: HTMLElement | null, offset: number, skipFix: boolean): number {
if (!element) {
return false;
return 0;
}

const parent = getScrollParent(element, skipFix);
const { offsetTop = 0, scrollTop = 0 } = scrollParent(element) ?? {};
let top = element.getBoundingClientRect().top + scrollTop;

return parent ? !parent.isSameNode(scrollDocument()) : false;
if (!!offsetTop && (hasCustomScrollParent(element, skipFix) || hasCustomOffsetParent(element))) {
top -= offsetTop;
}

const output = Math.floor(top - offset);

return output < 0 ? 0 : output;
}

/**
* Get computed style property
*/
export function getStyleComputedProperty(el: HTMLElement): CSSStyleDeclaration | null {
if (!el || el.nodeType !== 1) {
return null;
}

return getComputedStyle(el);
}

/**
Expand All @@ -138,6 +180,19 @@ export function hasCustomOffsetParent(element: HTMLElement): boolean {
return element.offsetParent !== document.body;
}

/**
* Check if the element has custom scroll parent
*/
export function hasCustomScrollParent(element: HTMLElement | null, skipFix: boolean): boolean {
if (!element) {
return false;
}

const parent = getScrollParent(element, skipFix);

return parent ? !parent.isSameNode(scrollDocument()) : false;
}

/**
* Check if an element has fixed/sticky position
*/
Expand Down Expand Up @@ -193,61 +248,6 @@ export function isElementVisible(element: HTMLElement): element is HTMLElement {
return true;
}

/**
* Find and return the target DOM element based on a step's 'target'.
*/
export function getElementPosition(
element: HTMLElement | null,
offset: number,
skipFix: boolean,
): number {
const elementRect = getClientRect(element);
const parent = getScrollParent(element, skipFix);
const hasScrollParent = hasCustomScrollParent(element, skipFix);
const isFixedTarget = hasPosition(element);
let parentTop = 0;
let top = elementRect?.top ?? 0;

if (hasScrollParent && isFixedTarget) {
const offsetTop = element?.offsetTop ?? 0;
const parentScrollTop = (parent as HTMLElement)?.scrollTop ?? 0;

top = offsetTop - parentScrollTop;
} else if (parent instanceof HTMLElement) {
parentTop = parent.scrollTop;

if (!hasScrollParent && !hasPosition(element)) {
top += parentTop;
}

if (!parent.isSameNode(scrollDocument())) {
top += scrollDocument().scrollTop;
}
}

return Math.floor(top - offset);
}

/**
* Get the scrollTop position
*/
export function getScrollTo(element: HTMLElement | null, offset: number, skipFix: boolean): number {
if (!element) {
return 0;
}

const { offsetTop = 0, scrollTop = 0 } = scrollParent(element) ?? {};
let top = element.getBoundingClientRect().top + scrollTop;

if (!!offsetTop && (hasCustomScrollParent(element, skipFix) || hasCustomOffsetParent(element))) {
top -= offsetTop;
}

const output = Math.floor(top - offset);

return output < 0 ? 0 : output;
}

export function scrollDocument(): Element | HTMLElement {
return document.scrollingElement ?? document.documentElement;
}
Expand Down
2 changes: 1 addition & 1 deletion src/modules/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export function hideBeacon(step: Step): boolean {
* @returns {boolean}
*/
export function isLegacy(): boolean {
return !['chrome', 'safari', 'firefox', 'opera'].includes(getBrowser());
return !['chrome', 'firefox', 'opera', 'safari'].includes(getBrowser());
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/modules/step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SetRequired } from 'type-fest';

import { defaultFloaterProps, defaultLocale, defaultStep } from '~/defaults';
import getStyles from '~/styles';

import { Props, Step, StepMerged } from '~/types';

import { getElement, hasCustomScrollParent } from './dom';
Expand Down
Loading