diff --git a/README.md b/README.md index 6a29c4b90..fc27636fb 100644 --- a/README.md +++ b/README.md @@ -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/) diff --git a/package.json b/package.json index ee0b30878..d81263582 100644 --- a/package.json +++ b/package.json @@ -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 ", + "author": "Maor Yadin ", "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": [ @@ -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" diff --git a/src/components/Overlay.tsx b/src/components/Overlay.tsx index 52e594f4d..04872a0e6 100644 --- a/src/components/Overlay.tsx +++ b/src/components/Overlay.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import treeChanges from 'tree-changes'; +import { LIFECYCLE } from '~/literals'; import { getClientRect, getDocumentHeight, @@ -12,18 +13,10 @@ 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; @@ -31,11 +24,18 @@ interface SpotlightStyles extends React.CSSProperties { width: number; } +interface State { + isScrolling: boolean; + mouseOverSpotlight: boolean; + showSpotlight: boolean; +} + export default class JoyrideOverlay extends React.Component { isActive = false; resizeTimeout?: number; scrollTimeout?: number; scrollParent?: Document | Element; + documentHeight = 0; state = { isScrolling: false, mouseOverSpotlight: false, @@ -48,6 +48,7 @@ export default class JoyrideOverlay extends React.Component 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)) { @@ -85,10 +86,13 @@ export default class JoyrideOverlay extends React.Component } 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); } } } @@ -102,21 +106,24 @@ export default class JoyrideOverlay extends React.Component 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() { @@ -131,7 +138,7 @@ export default class JoyrideOverlay extends React.Component return { cursor: disableOverlayClose ? 'default' : 'pointer', - height: getDocumentHeight(), + height: this.documentHeight, pointerEvents: mouseOverSpotlight ? 'none' : 'auto', ...baseStyles, } as React.CSSProperties; @@ -208,6 +215,7 @@ export default class JoyrideOverlay extends React.Component return; } + this.documentHeight = getDocumentHeight(); this.forceUpdate(); }, 100); }; diff --git a/src/components/Step.tsx b/src/components/Step.tsx index 0a6e8d9dd..4d67b6bcc 100644 --- a/src/components/Step.tsx +++ b/src/components/Step.tsx @@ -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'; diff --git a/src/components/index.tsx b/src/components/index.tsx index 410958085..4d3c1e197 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -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, @@ -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'; @@ -303,7 +302,7 @@ class Joyride extends React.Component { } 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; diff --git a/src/index.tsx b/src/index.tsx index ea8d48d35..e20013509 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,5 @@ -export * from './literals'; - // eslint-disable-next-line no-restricted-exports export { default } from './components'; + +export * from './literals'; export * from './types'; diff --git a/src/modules/dom.ts b/src/modules/dom.ts index f6b1c4adb..25133c18e 100644 --- a/src/modules/dom.ts +++ b/src/modules/dom.ts @@ -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); } /** @@ -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); } /** @@ -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 */ @@ -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; } diff --git a/src/modules/helpers.tsx b/src/modules/helpers.tsx index 15ffef18b..08fcdd5f3 100644 --- a/src/modules/helpers.tsx +++ b/src/modules/helpers.tsx @@ -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()); } /** diff --git a/src/modules/step.ts b/src/modules/step.ts index 4a9d5fde0..4db6bacc7 100644 --- a/src/modules/step.ts +++ b/src/modules/step.ts @@ -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'; diff --git a/src/modules/store.ts b/src/modules/store.ts index f00a6664a..de5024c09 100644 --- a/src/modules/store.ts +++ b/src/modules/store.ts @@ -7,9 +7,9 @@ import { Origin, State, Status, Step, StoreHelpers, StoreOptions } from '~/types import { hasValidKeys, objectKeys, omit } from './helpers'; -type StateWithContinuous = State & { continuous: boolean }; type Listener = (state: State) => void; type PopperData = Parameters>[0]; +type StateWithContinuous = State & { continuous: boolean }; const defaultState: State = { action: 'init', @@ -22,6 +22,8 @@ const defaultState: State = { }; const validKeys = objectKeys(omit(defaultState, 'controlled', 'size')); +export type StoreInstance = ReturnType; + class Store { private beaconPopper: PopperData | null; private tooltipPopper: PopperData | null; @@ -79,7 +81,7 @@ class Store { lifecycle: state.lifecycle ?? LIFECYCLE.INIT, origin: state.origin ?? null, size: state.size ?? size, - status: nextIndex === size ? STATUS.FINISHED : state.status ?? status, + status: nextIndex === size ? STATUS.FINISHED : (state.status ?? status), }; } @@ -319,8 +321,6 @@ class Store { }; } -export type StoreInstance = ReturnType; - export default function createStore(options?: StoreOptions) { return new Store(options); } diff --git a/src/types/common.ts b/src/types/common.ts index 8b04baf58..b814bfab5 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -5,19 +5,33 @@ import { ValueOf } from 'type-fest'; import { ACTIONS, EVENTS, LIFECYCLE, ORIGIN, STATUS } from '~/literals'; export type Actions = ValueOf; +export type AnyObject = Record; export type Events = ValueOf; export type Lifecycle = ValueOf; -export type Origin = ValueOf; -export type Status = ValueOf; - -export type AnyObject = Record; - export type NarrowPlainObject> = Exclude< T, // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type Array | Function | Map | Set >; +export type Origin = ValueOf; + +export type Placement = + | 'top' + | 'top-start' + | 'top-end' + | 'bottom' + | 'bottom-start' + | 'bottom-end' + | 'left' + | 'left-start' + | 'left-end' + | 'right' + | 'right-start' + | 'right-end'; + +export type Status = ValueOf; + export interface Locale { /** * Label for the back button. @@ -57,20 +71,6 @@ export interface Locale { skip?: ReactNode; } -export type Placement = - | 'top' - | 'top-start' - | 'top-end' - | 'bottom' - | 'bottom-start' - | 'bottom-end' - | 'left' - | 'left-start' - | 'left-end' - | 'right' - | 'right-start' - | 'right-end'; - export interface Styles { beacon: CSSProperties; beaconInner: CSSProperties; @@ -93,10 +93,6 @@ export interface Styles { tooltipTitle: CSSProperties; } -export interface StylesWithFloaterStyles extends Styles { - floaterStyles: FloaterStyles; -} - export interface StylesOptions { arrowColor: string; backgroundColor: string; @@ -108,3 +104,7 @@ export interface StylesOptions { width?: string | number; zIndex: number; } + +export interface StylesWithFloaterStyles extends Styles { + floaterStyles: FloaterStyles; +} diff --git a/src/types/components.ts b/src/types/components.ts index 0a6e61a5c..f1fabbf39 100644 --- a/src/types/components.ts +++ b/src/types/components.ts @@ -27,14 +27,14 @@ export type BaseProps = { */ disableOverlayClose?: boolean; /** - * Disable the fix to handle "unused" overflow parents. * @default false */ - disableScrollParentFix?: boolean; + disableScrolling?: boolean; /** + * Disable the fix to handle "unused" overflow parents. * @default false */ - disableScrolling?: boolean; + disableScrollParentFix?: boolean; /** * Options to be passed to react-floater */ diff --git a/test/__fixtures__/Controlled.tsx b/test/__fixtures__/Controlled.tsx index 5204cd4d4..a66d1ae21 100644 --- a/test/__fixtures__/Controlled.tsx +++ b/test/__fixtures__/Controlled.tsx @@ -1,11 +1,11 @@ import { useEffect, useReducer, useRef } from 'react'; -import Beacon from './Beacon'; -import Tooltip from './Tooltip'; - import Joyride, { ACTIONS, EVENTS, STATUS } from '../../src'; import { CallBackProps, Props, Step } from '../../src/types'; +import Beacon from './Beacon'; +import Tooltip from './Tooltip'; + interface ControlledProps extends Omit {} interface State { diff --git a/test/__fixtures__/CustomOptions.tsx b/test/__fixtures__/CustomOptions.tsx index bbda5f3f0..89d2e3872 100644 --- a/test/__fixtures__/CustomOptions.tsx +++ b/test/__fixtures__/CustomOptions.tsx @@ -1,10 +1,10 @@ import { useReducer } from 'react'; -import { standardSteps } from './steps'; - import Joyride, { LIFECYCLE, STATUS, Status } from '../../src'; import { CallBackProps, Props, Step } from '../../src/types'; +import { standardSteps } from './steps'; + interface CustomOptionsProps extends Omit { finishedCallback: () => void; } @@ -15,14 +15,14 @@ interface State { steps: Array; } -function Skip() { - return Do you really want to skip?; -} - function NextWithProgress() { return {`Go ({step} of {steps})`}; } +function Skip() { + return Do you really want to skip?; +} + const tourSteps: Array = [ ...standardSteps.slice(0, 3).map(step => { if (step.target === '.mission button') { diff --git a/test/__fixtures__/Scroll.tsx b/test/__fixtures__/Scroll.tsx index e83d9e807..af63a38d0 100644 --- a/test/__fixtures__/Scroll.tsx +++ b/test/__fixtures__/Scroll.tsx @@ -1,10 +1,10 @@ import { useReducer, useRef } from 'react'; -import { scrollSteps } from './steps'; - import Joyride, { STATUS, StoreHelpers } from '../../src'; import { CallBackProps, Props, Status, Step } from '../../src/types'; +import { scrollSteps } from './steps'; + interface State { run: boolean; steps: Array; diff --git a/test/__fixtures__/Standard.tsx b/test/__fixtures__/Standard.tsx index cdff8d797..9d4dc5d45 100644 --- a/test/__fixtures__/Standard.tsx +++ b/test/__fixtures__/Standard.tsx @@ -1,10 +1,10 @@ import { useReducer, useRef } from 'react'; -import { standardSteps } from './steps'; - import Joyride, { STATUS, StoreHelpers } from '../../src'; import { CallBackProps, Props, Status, Step } from '../../src/types'; +import { standardSteps } from './steps'; + interface State { index: number; run: boolean; diff --git a/test/__fixtures__/test-utils.ts b/test/__fixtures__/test-utils.ts index df70e9eb7..88618fd01 100644 --- a/test/__fixtures__/test-utils.ts +++ b/test/__fixtures__/test-utils.ts @@ -49,4 +49,4 @@ const customRender = (ui: ReactElement, options?: Omit export * from '@testing-library/react'; // override render method -export { customScreen as screen, customWithin as within, customRender as render }; +export { customRender as render, customScreen as screen, customWithin as within }; diff --git a/test/modules/helpers.spec.tsx b/test/modules/helpers.spec.tsx index 70f9050f7..19ddd1c84 100644 --- a/test/modules/helpers.spec.tsx +++ b/test/modules/helpers.spec.tsx @@ -19,14 +19,14 @@ import { const baseObject = { a: 1, b: '', c: [1], d: { a: null }, e: undefined }; -function Skip() { - return Do you really want to skip?; -} - function NextWithProgress() { return {`Go ({step} of {steps})`}; } +function Skip() { + return Do you really want to skip?; +} + describe('helpers', () => { describe('getBrowser', () => { describe('with the default userAgent', () => { diff --git a/test/modules/store.spec.ts b/test/modules/store.spec.ts index 9e1c9781b..040a3d504 100644 --- a/test/modules/store.spec.ts +++ b/test/modules/store.spec.ts @@ -1,8 +1,7 @@ import { fromPartial } from '@total-typescript/shoehorn'; -import createStore from '~/modules/store'; - import { LIFECYCLE, STATUS } from '~/literals'; +import createStore from '~/modules/store'; import { standardSteps } from '../__fixtures__/steps'; diff --git a/test/styles.spec.ts b/test/styles.spec.ts index 7e9bc0fa3..dcbdce211 100644 --- a/test/styles.spec.ts +++ b/test/styles.spec.ts @@ -1,6 +1,6 @@ import { getMergedStep } from '~/modules/step'; - import getStyles from '~/styles'; + import { Props, Step } from '~/types'; const props: Props = {