diff --git a/src/components/Step.tsx b/src/components/Step.tsx index 740bc0dc..2a7176bd 100644 --- a/src/components/Step.tsx +++ b/src/components/Step.tsx @@ -17,13 +17,9 @@ import Overlay from './Overlay'; import Portal from './Portal'; import Tooltip from './Tooltip/index'; -type PopperData = Parameters>[0]; - export default class JoyrideStep extends React.Component { - beaconPopper: PopperData | null = null; scope: Scope | null = null; tooltip: HTMLElement | null = null; - tooltipPopper: PopperData | null = null; componentDidMount() { const { debug, index } = this.props; @@ -47,7 +43,7 @@ export default class JoyrideStep extends React.Component { size, status, step, - update, + store, } = this.props; const { changed, changedFrom } = treeChanges(previousProps, this.props); const state = { action, controlled, index, lifecycle, size, status }; @@ -82,7 +78,7 @@ export default class JoyrideStep extends React.Component { action !== ACTIONS.START && lifecycle === LIFECYCLE.INIT ) { - update({ lifecycle: LIFECYCLE.READY }); + store.update({ lifecycle: LIFECYCLE.READY }); } if (hasStoreChanged) { @@ -111,13 +107,15 @@ export default class JoyrideStep extends React.Component { }); if (!controlled) { - update({ index: index + (action === ACTIONS.PREV ? -1 : 1) }); + store.update({ index: index + (action === ACTIONS.PREV ? -1 : 1) }); } } } if (changedFrom('lifecycle', LIFECYCLE.INIT, LIFECYCLE.READY)) { - update({ lifecycle: hideBeacon(step) || skipBeacon ? LIFECYCLE.TOOLTIP : LIFECYCLE.BEACON }); + store.update({ + lifecycle: hideBeacon(step) || skipBeacon ? LIFECYCLE.TOOLTIP : LIFECYCLE.BEACON, + }); } if (changed('index')) { @@ -152,8 +150,7 @@ export default class JoyrideStep extends React.Component { if (changedFrom('lifecycle', [LIFECYCLE.TOOLTIP, LIFECYCLE.INIT], LIFECYCLE.INIT)) { this.scope?.removeScope(); - this.beaconPopper = null; - this.tooltipPopper = null; + store.cleanupPoppers(); } } @@ -165,13 +162,13 @@ export default class JoyrideStep extends React.Component { * Beacon click/hover event listener */ handleClickHoverBeacon = (event: React.MouseEvent) => { - const { step, update } = this.props; + const { step, store } = this.props; if (event.type === 'mouseenter' && step.event !== 'hover') { return; } - update({ lifecycle: LIFECYCLE.TOOLTIP }); + store.update({ lifecycle: LIFECYCLE.TOOLTIP }); }; handleClickOverlay = () => { @@ -187,18 +184,16 @@ export default class JoyrideStep extends React.Component { }; setPopper: FloaterProps['getPopper'] = (popper, type) => { - const { action, setPopper, update } = this.props; + const { action, store } = this.props; if (type === 'wrapper') { - this.beaconPopper = popper; + store.setPopper('beacon', popper); } else { - this.tooltipPopper = popper; + store.setPopper('tooltip', popper); } - setPopper?.(popper, type); - - if (this.beaconPopper && this.tooltipPopper) { - update({ + if (store.getPopper('beacon') && store.getPopper('tooltip')) { + store.update({ action, lifecycle: LIFECYCLE.READY, }); diff --git a/src/components/index.tsx b/src/components/index.tsx index 5dd924a0..3e1e7ac6 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { Props as FloaterProps } from 'react-floater'; import isEqual from '@gilbarbara/deep-equal'; import is from 'is-lite'; import treeChanges from 'tree-changes'; @@ -26,8 +25,6 @@ import Step from './Step'; class Joyride extends React.Component { private readonly helpers: StoreHelpers; private readonly store: ReturnType; - private beaconPopper: any; - private tooltipPopper: any; static defaultProps = defaultProps; @@ -251,14 +248,6 @@ class Joyride extends React.Component { this.setState(state); }; - setPopper: FloaterProps['getPopper'] = (popper, type) => { - if (type === 'wrapper') { - this.beaconPopper = popper; - } else { - this.tooltipPopper = popper; - } - }; - scrollToStep(previousState: State) { const { index, lifecycle, status } = this.state; const { @@ -296,19 +285,22 @@ class Joyride extends React.Component { debug, }); + const beaconPopper = this.store.getPopper('beacon'); + const tooltipPopper = this.store.getPopper('tooltip'); + /* istanbul ignore else */ - if (lifecycle === LIFECYCLE.BEACON && this.beaconPopper) { - const { placement, popper } = this.beaconPopper; + if (lifecycle === LIFECYCLE.BEACON && beaconPopper) { + const { offsets, placement } = beaconPopper; /* istanbul ignore else */ if (!['bottom'].includes(placement) && !hasCustomScroll) { - scrollY = Math.floor(popper.top - scrollOffset); + scrollY = Math.floor(offsets.popper.top - scrollOffset); } - } else if (lifecycle === LIFECYCLE.TOOLTIP && this.tooltipPopper) { - const { flipped, placement, popper } = this.tooltipPopper; + } else if (lifecycle === LIFECYCLE.TOOLTIP && tooltipPopper) { + const { flipped, offsets, placement } = tooltipPopper; if (['top', 'right', 'left'].includes(placement) && !flipped && !hasCustomScroll) { - scrollY = Math.floor(popper.top - scrollOffset); + scrollY = Math.floor(offsets.popper.top - scrollOffset); } else { scrollY -= step.spotlightPadding; } @@ -349,10 +341,9 @@ class Joyride extends React.Component { debug={debug} helpers={this.helpers} nonce={nonce} - setPopper={this.setPopper} shouldScroll={!step.disableScrolling && (index !== 0 || scrollToFirstStep)} step={step} - update={this.store.update} + store={this.store} /> ); } diff --git a/src/modules/store.ts b/src/modules/store.ts index d2b760c1..17de18d4 100644 --- a/src/modules/store.ts +++ b/src/modules/store.ts @@ -1,3 +1,4 @@ +import { Props as FloaterProps } from 'react-floater'; import is from 'is-lite'; import { ACTIONS, LIFECYCLE, STATUS } from '~/literals'; @@ -6,6 +7,10 @@ import { AnyObject, State, Status, Step, StoreHelpers, StoreOptions } from '~/ty import { hasValidKeys } from './helpers'; +type StateWithContinuous = State & { continuous: boolean }; +type Listener = (state: State) => void; +type PopperData = Parameters>[0]; + const defaultState: State = { action: 'init', controlled: false, @@ -14,14 +19,11 @@ const defaultState: State = { size: 0, status: STATUS.IDLE, }; - -type StateWithContinuous = State & { continuous: boolean }; - const validKeys = ['action', 'index', 'lifecycle', 'status']; -type Listener = (state: State) => void; - class Store { + private beaconPopper: PopperData | null; + private tooltipPopper: PopperData | null; private data: Map = new Map(); private listener: Listener | null; private store: Map = new Map(); @@ -41,6 +43,8 @@ class Store { true, ); + this.beaconPopper = null; + this.tooltipPopper = null; this.listener = null; this.setSteps(steps); } @@ -146,6 +150,27 @@ class Store { }; } + public getPopper = (name: 'beacon' | 'tooltip') => { + if (name === 'beacon') { + return this.beaconPopper; + } + + return this.tooltipPopper; + }; + + public setPopper = (name: 'beacon' | 'tooltip', popper: PopperData) => { + if (name === 'beacon') { + this.beaconPopper = popper; + } else { + this.tooltipPopper = popper; + } + }; + + public cleanupPoppers = () => { + this.beaconPopper = null; + this.tooltipPopper = null; + }; + public close = () => { const { index, status } = this.getState(); @@ -282,6 +307,8 @@ class Store { }; } +export type StoreInstance = ReturnType; + export default function createStore(options?: StoreOptions) { return new Store(options); } diff --git a/src/types/components.ts b/src/types/components.ts index 671f34a1..874f4bd7 100644 --- a/src/types/components.ts +++ b/src/types/components.ts @@ -2,6 +2,8 @@ import { ElementType, MouseEventHandler, ReactNode, RefCallback } from 'react'; import { Props as FloaterProps } from 'react-floater'; import { PartialDeep, SetRequired, Simplify, ValueOf } from 'type-fest'; +import type { StoreInstance } from '~/modules/store'; + import { Actions, Events, Lifecycle, Locale, Placement, Status, Styles } from './common'; export type BaseProps = { @@ -136,10 +138,9 @@ export type StepProps = Simplify< debug: boolean; helpers: StoreHelpers; nonce?: string; - setPopper: FloaterProps['getPopper']; shouldScroll: boolean; step: StepMerged; - update: (state: Partial) => void; + store: StoreInstance; } >; diff --git a/test/modules/store.spec.ts b/test/modules/store.spec.ts index 75131fee..0a93d798 100644 --- a/test/modules/store.spec.ts +++ b/test/modules/store.spec.ts @@ -1,3 +1,5 @@ +import { fromPartial } from '@total-typescript/shoehorn'; + import createStore from '~/modules/store'; import { LIFECYCLE, STATUS } from '~/literals'; @@ -217,4 +219,26 @@ describe('store', () => { expect(mockSyncStore).toHaveBeenCalledTimes(3); }); }); + + describe('with popper', () => { + const store = createStore(); + const popperData = { placement: 'top' } as const; + + it('should set/get both poppers', () => { + store.setPopper('beacon', fromPartial(popperData)); + expect(store.getPopper('beacon')).toEqual(popperData); + + store.setPopper('tooltip', fromPartial(popperData)); + expect(store.getPopper('tooltip')).toEqual(popperData); + }); + + it('should clear both poppers', () => { + expect(store.getPopper('beacon')).toEqual(popperData); + expect(store.getPopper('tooltip')).toEqual(popperData); + + store.cleanupPoppers(); + expect(store.getPopper('beacon')).toBeNull(); + expect(store.getPopper('tooltip')).toBeNull(); + }); + }); });