@robertlinde/react-tour-kit 0.8.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/MIGRATION.md +205 -0
- package/README.md +401 -850
- package/dist/react-native-platform/hooks/use-tour-scroll-view.hook.d.ts +14 -0
- package/dist/react-native-platform/hooks/use-tour-scroll-view.hook.d.ts.map +1 -0
- package/dist/react-native-platform/hooks/use-tour-scroll-view.hook.js +21 -0
- package/dist/react-native-platform/hooks/use-tour-scroll-view.hook.js.map +1 -0
- package/dist/react-native-platform/hooks/use-tour-target.hook.d.ts +15 -0
- package/dist/react-native-platform/hooks/use-tour-target.hook.d.ts.map +1 -0
- package/dist/react-native-platform/hooks/use-tour-target.hook.js +28 -0
- package/dist/react-native-platform/hooks/use-tour-target.hook.js.map +1 -0
- package/dist/react-native-platform/host-store.d.ts +24 -0
- package/dist/react-native-platform/host-store.d.ts.map +1 -0
- package/dist/react-native-platform/host-store.js +63 -0
- package/dist/react-native-platform/host-store.js.map +1 -0
- package/dist/react-native-platform/insets.util.d.ts +11 -0
- package/dist/react-native-platform/insets.util.d.ts.map +1 -0
- package/dist/react-native-platform/insets.util.js +31 -0
- package/dist/react-native-platform/insets.util.js.map +1 -0
- package/dist/react-native-platform/layer/native-renderer.d.ts +7 -0
- package/dist/react-native-platform/layer/native-renderer.d.ts.map +1 -0
- package/dist/react-native-platform/layer/native-renderer.js +496 -0
- package/dist/react-native-platform/layer/native-renderer.js.map +1 -0
- package/dist/react-native-platform/layer/tour-host.d.ts +13 -0
- package/dist/react-native-platform/layer/tour-host.d.ts.map +1 -0
- package/dist/react-native-platform/layer/tour-host.js +70 -0
- package/dist/react-native-platform/layer/tour-host.js.map +1 -0
- package/dist/react-native-platform/measure.util.d.ts +20 -0
- package/dist/react-native-platform/measure.util.d.ts.map +1 -0
- package/dist/react-native-platform/measure.util.js +100 -0
- package/dist/react-native-platform/measure.util.js.map +1 -0
- package/dist/react-native-platform/native-context.d.ts +12 -0
- package/dist/react-native-platform/native-context.d.ts.map +1 -0
- package/dist/react-native-platform/native-context.js +4 -0
- package/dist/react-native-platform/native-context.js.map +1 -0
- package/dist/react-native-platform/native-environment.d.ts +14 -0
- package/dist/react-native-platform/native-environment.d.ts.map +1 -0
- package/dist/react-native-platform/native-environment.js +20 -0
- package/dist/react-native-platform/native-environment.js.map +1 -0
- package/dist/react-native-platform/native-platform.d.ts +19 -0
- package/dist/react-native-platform/native-platform.d.ts.map +1 -0
- package/dist/react-native-platform/native-platform.js +265 -0
- package/dist/react-native-platform/native-platform.js.map +1 -0
- package/dist/react-native-platform/reduced-motion.d.ts +4 -0
- package/dist/react-native-platform/reduced-motion.d.ts.map +1 -0
- package/dist/react-native-platform/reduced-motion.js +44 -0
- package/dist/react-native-platform/reduced-motion.js.map +1 -0
- package/dist/react-native-platform/registry.d.ts +18 -0
- package/dist/react-native-platform/registry.d.ts.map +1 -0
- package/dist/react-native-platform/registry.js +51 -0
- package/dist/react-native-platform/registry.js.map +1 -0
- package/dist/react-native-platform/tour-provider.d.ts +16 -3
- package/dist/react-native-platform/tour-provider.d.ts.map +1 -0
- package/dist/react-native-platform/tour-provider.js +92 -211
- package/dist/react-native-platform/tour-provider.js.map +1 -1
- package/dist/react-native.d.ts +14 -8
- package/dist/react-native.d.ts.map +1 -0
- package/dist/react-native.js +7 -6
- package/dist/react-native.js.map +1 -1
- package/dist/react-platform/dom/elements.util.d.ts +35 -0
- package/dist/react-platform/dom/elements.util.d.ts.map +1 -0
- package/dist/react-platform/dom/elements.util.js +155 -0
- package/dist/react-platform/dom/elements.util.js.map +1 -0
- package/dist/react-platform/dom/occluders.util.d.ts +13 -0
- package/dist/react-platform/dom/occluders.util.d.ts.map +1 -0
- package/dist/react-platform/dom/occluders.util.js +75 -0
- package/dist/react-platform/dom/occluders.util.js.map +1 -0
- package/dist/react-platform/dom/scroll-lock.util.d.ts +13 -0
- package/dist/react-platform/dom/scroll-lock.util.d.ts.map +1 -0
- package/dist/react-platform/dom/scroll-lock.util.js +219 -0
- package/dist/react-platform/dom/scroll-lock.util.js.map +1 -0
- package/dist/react-platform/dom/scroll.util.d.ts +44 -0
- package/dist/react-platform/dom/scroll.util.d.ts.map +1 -0
- package/dist/react-platform/dom/scroll.util.js +250 -0
- package/dist/react-platform/dom/scroll.util.js.map +1 -0
- package/dist/react-platform/dom/viewport.util.d.ts +16 -0
- package/dist/react-platform/dom/viewport.util.d.ts.map +1 -0
- package/dist/react-platform/dom/viewport.util.js +65 -0
- package/dist/react-platform/dom/viewport.util.js.map +1 -0
- package/dist/react-platform/dom/web-platform.d.ts +15 -0
- package/dist/react-platform/dom/web-platform.d.ts.map +1 -0
- package/dist/react-platform/dom/web-platform.js +169 -0
- package/dist/react-platform/dom/web-platform.js.map +1 -0
- package/dist/react-platform/layer/styles.d.ts +12 -0
- package/dist/react-platform/layer/styles.d.ts.map +1 -0
- package/dist/react-platform/layer/styles.js +285 -0
- package/dist/react-platform/layer/styles.js.map +1 -0
- package/dist/react-platform/layer/web-renderer.d.ts +4 -0
- package/dist/react-platform/layer/web-renderer.d.ts.map +1 -0
- package/dist/react-platform/layer/web-renderer.js +390 -0
- package/dist/react-platform/layer/web-renderer.js.map +1 -0
- package/dist/react-platform/tour-provider.d.ts +9 -3
- package/dist/react-platform/tour-provider.d.ts.map +1 -0
- package/dist/react-platform/tour-provider.js +28 -218
- package/dist/react-platform/tour-provider.js.map +1 -1
- package/dist/react.d.ts +10 -8
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +5 -6
- package/dist/react.js.map +1 -1
- package/dist/shared/engine/abort.util.d.ts +17 -0
- package/dist/shared/engine/abort.util.d.ts.map +1 -0
- package/dist/shared/engine/abort.util.js +72 -0
- package/dist/shared/engine/abort.util.js.map +1 -0
- package/dist/shared/engine/report.util.d.ts +12 -0
- package/dist/shared/engine/report.util.d.ts.map +1 -0
- package/dist/shared/engine/report.util.js +51 -0
- package/dist/shared/engine/report.util.js.map +1 -0
- package/dist/shared/engine/tour-adapter.type.d.ts +60 -0
- package/dist/shared/engine/tour-adapter.type.d.ts.map +1 -0
- package/dist/shared/engine/tour-adapter.type.js +2 -0
- package/dist/shared/engine/tour-adapter.type.js.map +1 -0
- package/dist/shared/engine/tour-engine.d.ts +92 -0
- package/dist/shared/engine/tour-engine.d.ts.map +1 -0
- package/dist/shared/engine/tour-engine.js +787 -0
- package/dist/shared/engine/tour-engine.js.map +1 -0
- package/dist/shared/geometry/box.util.d.ts +39 -0
- package/dist/shared/geometry/box.util.d.ts.map +1 -0
- package/dist/shared/geometry/box.util.js +146 -0
- package/dist/shared/geometry/box.util.js.map +1 -0
- package/dist/shared/geometry/compute-tooltip-layout.util.d.ts +60 -0
- package/dist/shared/geometry/compute-tooltip-layout.util.d.ts.map +1 -0
- package/dist/shared/geometry/compute-tooltip-layout.util.js +148 -0
- package/dist/shared/geometry/compute-tooltip-layout.util.js.map +1 -0
- package/dist/shared/geometry/geometry.type.d.ts +34 -0
- package/dist/shared/geometry/geometry.type.d.ts.map +1 -0
- package/dist/shared/geometry/geometry.type.js +2 -0
- package/dist/shared/geometry/geometry.type.js.map +1 -0
- package/dist/shared/geometry/reveal.util.d.ts +38 -0
- package/dist/shared/geometry/reveal.util.d.ts.map +1 -0
- package/dist/shared/geometry/reveal.util.js +135 -0
- package/dist/shared/geometry/reveal.util.js.map +1 -0
- package/dist/shared/geometry/target-visibility.util.d.ts +10 -0
- package/dist/shared/geometry/target-visibility.util.d.ts.map +1 -0
- package/dist/shared/geometry/target-visibility.util.js +29 -0
- package/dist/shared/geometry/target-visibility.util.js.map +1 -0
- package/dist/shared/platform/create-tour-adapter.d.ts +9 -0
- package/dist/shared/platform/create-tour-adapter.d.ts.map +1 -0
- package/dist/shared/platform/create-tour-adapter.js +130 -0
- package/dist/shared/platform/create-tour-adapter.js.map +1 -0
- package/dist/shared/platform/platform.type.d.ts +77 -0
- package/dist/shared/platform/platform.type.d.ts.map +1 -0
- package/dist/shared/platform/platform.type.js +2 -0
- package/dist/shared/platform/platform.type.js.map +1 -0
- package/dist/shared/platform/reveal-in-scroll-containers.d.ts +27 -0
- package/dist/shared/platform/reveal-in-scroll-containers.d.ts.map +1 -0
- package/dist/shared/platform/reveal-in-scroll-containers.js +49 -0
- package/dist/shared/platform/reveal-in-scroll-containers.js.map +1 -0
- package/dist/shared/react/tour-context.d.ts +7 -0
- package/dist/shared/react/tour-context.d.ts.map +1 -0
- package/dist/shared/react/tour-context.js +23 -0
- package/dist/shared/react/tour-context.js.map +1 -0
- package/dist/shared/react/tour-provider-core.d.ts +41 -0
- package/dist/shared/react/tour-provider-core.d.ts.map +1 -0
- package/dist/shared/react/tour-provider-core.js +24 -0
- package/dist/shared/react/tour-provider-core.js.map +1 -0
- package/dist/shared/react/tour-root.d.ts +14 -0
- package/dist/shared/react/tour-root.d.ts.map +1 -0
- package/dist/shared/react/tour-root.js +22 -0
- package/dist/shared/react/tour-root.js.map +1 -0
- package/dist/shared/react/use-isomorphic-layout-effect.hook.d.ts +4 -0
- package/dist/shared/react/use-isomorphic-layout-effect.hook.d.ts.map +1 -0
- package/dist/shared/react/use-isomorphic-layout-effect.hook.js +5 -0
- package/dist/shared/react/use-isomorphic-layout-effect.hook.js.map +1 -0
- package/dist/shared/react/use-tour-view.hook.d.ts +5 -0
- package/dist/shared/react/use-tour-view.hook.d.ts.map +1 -0
- package/dist/shared/react/use-tour-view.hook.js +14 -0
- package/dist/shared/react/use-tour-view.hook.js.map +1 -0
- package/dist/shared/react/use-tour.hook.d.ts +7 -0
- package/dist/shared/react/use-tour.hook.d.ts.map +1 -0
- package/dist/shared/react/use-tour.hook.js +10 -0
- package/dist/shared/react/use-tour.hook.js.map +1 -0
- package/dist/shared/renderer/default-tooltip.d.ts +5 -0
- package/dist/shared/renderer/default-tooltip.d.ts.map +1 -0
- package/dist/shared/renderer/default-tooltip.js +13 -0
- package/dist/shared/renderer/default-tooltip.js.map +1 -0
- package/dist/shared/renderer/motion.d.ts +29 -0
- package/dist/shared/renderer/motion.d.ts.map +1 -0
- package/dist/shared/renderer/motion.js +49 -0
- package/dist/shared/renderer/motion.js.map +1 -0
- package/dist/shared/renderer/renderer-context.d.ts +17 -0
- package/dist/shared/renderer/renderer-context.d.ts.map +1 -0
- package/dist/shared/renderer/renderer-context.js +16 -0
- package/dist/shared/renderer/renderer-context.js.map +1 -0
- package/dist/shared/renderer/renderer.type.d.ts +164 -0
- package/dist/shared/renderer/renderer.type.d.ts.map +1 -0
- package/dist/shared/renderer/renderer.type.js +2 -0
- package/dist/shared/renderer/renderer.type.js.map +1 -0
- package/dist/shared/renderer/tooltip-model.d.ts +32 -0
- package/dist/shared/renderer/tooltip-model.d.ts.map +1 -0
- package/dist/shared/renderer/tooltip-model.js +24 -0
- package/dist/shared/renderer/tooltip-model.js.map +1 -0
- package/dist/shared/renderer/tour-overlay.d.ts +24 -0
- package/dist/shared/renderer/tour-overlay.d.ts.map +1 -0
- package/dist/shared/renderer/tour-overlay.js +228 -0
- package/dist/shared/renderer/tour-overlay.js.map +1 -0
- package/dist/shared/types/tour-context.type.d.ts +21 -11
- package/dist/shared/types/tour-context.type.d.ts.map +1 -0
- package/dist/shared/types/tour-end-info.type.d.ts +19 -2
- package/dist/shared/types/tour-end-info.type.d.ts.map +1 -0
- package/dist/shared/types/tour-i18n.type.d.ts +8 -0
- package/dist/shared/types/tour-i18n.type.d.ts.map +1 -0
- package/dist/shared/types/tour-step.type.d.ts +35 -6
- package/dist/shared/types/tour-step.type.d.ts.map +1 -0
- package/dist/shared/types/tour-theme.type.d.ts +22 -2
- package/dist/shared/types/tour-theme.type.d.ts.map +1 -0
- package/dist/shared/types/tour-theme.type.js +1 -16
- package/dist/shared/types/tour-theme.type.js.map +1 -1
- package/dist/shared/types/tour-tooltip-props.type.d.ts +21 -17
- package/dist/shared/types/tour-tooltip-props.type.d.ts.map +1 -0
- package/dist/shared/utils/frame.util.d.ts +6 -0
- package/dist/shared/utils/frame.util.d.ts.map +1 -0
- package/dist/shared/utils/frame.util.js +20 -0
- package/dist/shared/utils/frame.util.js.map +1 -0
- package/dist/shared/utils/get-truncated-dots.util.d.ts +13 -7
- package/dist/shared/utils/get-truncated-dots.util.d.ts.map +1 -0
- package/dist/shared/utils/get-truncated-dots.util.js +30 -30
- package/dist/shared/utils/get-truncated-dots.util.js.map +1 -1
- package/dist/shared/utils/i18n.util.d.ts +7 -0
- package/dist/shared/utils/i18n.util.d.ts.map +1 -0
- package/dist/shared/utils/i18n.util.js +34 -0
- package/dist/shared/utils/i18n.util.js.map +1 -0
- package/dist/shared/utils/theme.util.d.ts +6 -0
- package/dist/shared/utils/theme.util.d.ts.map +1 -0
- package/dist/shared/utils/theme.util.js +31 -0
- package/dist/shared/utils/theme.util.js.map +1 -0
- package/package.json +72 -55
- package/src/react-native-platform/hooks/use-tour-scroll-view.hook.ts +24 -0
- package/src/react-native-platform/hooks/use-tour-target.hook.ts +30 -0
- package/src/react-native-platform/host-store.ts +92 -0
- package/src/react-native-platform/insets.util.ts +39 -0
- package/src/react-native-platform/layer/native-renderer.tsx +776 -0
- package/src/react-native-platform/layer/tour-host.tsx +105 -0
- package/src/react-native-platform/measure.util.ts +140 -0
- package/src/react-native-platform/native-context.ts +15 -0
- package/src/react-native-platform/native-environment.ts +33 -0
- package/src/react-native-platform/native-platform.ts +368 -0
- package/src/react-native-platform/reduced-motion.ts +47 -0
- package/src/react-native-platform/registry.ts +70 -0
- package/src/react-native-platform/tour-provider.tsx +148 -0
- package/src/react-native.ts +15 -0
- package/src/react-platform/dom/elements.util.ts +191 -0
- package/src/react-platform/dom/occluders.util.ts +88 -0
- package/src/react-platform/dom/scroll-lock.util.ts +261 -0
- package/src/react-platform/dom/scroll.util.ts +310 -0
- package/src/react-platform/dom/viewport.util.ts +74 -0
- package/src/react-platform/dom/web-platform.ts +208 -0
- package/src/react-platform/layer/styles.ts +286 -0
- package/src/react-platform/layer/web-renderer.tsx +594 -0
- package/src/react-platform/tour-provider.tsx +44 -0
- package/src/react.ts +13 -0
- package/src/shared/engine/abort.util.ts +84 -0
- package/src/shared/engine/report.util.ts +58 -0
- package/src/shared/engine/tour-adapter.type.ts +62 -0
- package/src/shared/engine/tour-engine.ts +1068 -0
- package/src/shared/geometry/box.util.ts +185 -0
- package/src/shared/geometry/compute-tooltip-layout.util.ts +238 -0
- package/src/shared/geometry/geometry.type.ts +38 -0
- package/src/shared/geometry/reveal.util.ts +190 -0
- package/src/shared/geometry/target-visibility.util.ts +44 -0
- package/src/shared/platform/create-tour-adapter.ts +151 -0
- package/src/shared/platform/platform.type.ts +81 -0
- package/src/shared/platform/reveal-in-scroll-containers.ts +78 -0
- package/src/shared/react/tour-context.ts +28 -0
- package/src/shared/react/tour-provider-core.ts +67 -0
- package/src/shared/react/tour-root.tsx +34 -0
- package/src/shared/react/use-isomorphic-layout-effect.hook.ts +7 -0
- package/src/shared/react/use-tour-view.hook.ts +17 -0
- package/src/shared/react/use-tour.hook.ts +11 -0
- package/src/shared/renderer/default-tooltip.tsx +52 -0
- package/src/shared/renderer/motion.ts +59 -0
- package/src/shared/renderer/renderer-context.ts +33 -0
- package/src/shared/renderer/renderer.type.ts +183 -0
- package/src/shared/renderer/tooltip-model.ts +44 -0
- package/src/shared/renderer/tour-overlay.tsx +453 -0
- package/src/shared/types/tour-context.type.ts +23 -0
- package/src/shared/types/tour-end-info.type.ts +32 -0
- package/src/shared/types/tour-i18n.type.ts +15 -0
- package/src/shared/types/tour-step.type.ts +40 -0
- package/src/shared/types/tour-theme.type.ts +28 -0
- package/src/shared/types/tour-tooltip-props.type.ts +27 -0
- package/src/shared/utils/frame.util.ts +21 -0
- package/src/shared/utils/get-truncated-dots.util.ts +46 -0
- package/src/shared/utils/i18n.util.ts +44 -0
- package/src/shared/utils/theme.util.ts +38 -0
- package/dist/react-native-platform/components/index.d.ts +0 -2
- package/dist/react-native-platform/components/index.js +0 -3
- package/dist/react-native-platform/components/index.js.map +0 -1
- package/dist/react-native-platform/components/tour-overlay.d.ts +0 -3
- package/dist/react-native-platform/components/tour-overlay.js +0 -26
- package/dist/react-native-platform/components/tour-overlay.js.map +0 -1
- package/dist/react-native-platform/components/tour-tooltip.d.ts +0 -3
- package/dist/react-native-platform/components/tour-tooltip.js +0 -167
- package/dist/react-native-platform/components/tour-tooltip.js.map +0 -1
- package/dist/react-native-platform/hooks/index.d.ts +0 -1
- package/dist/react-native-platform/hooks/index.js +0 -2
- package/dist/react-native-platform/hooks/index.js.map +0 -1
- package/dist/react-native-platform/hooks/use-tour-target.d.ts +0 -2
- package/dist/react-native-platform/hooks/use-tour-target.js +0 -13
- package/dist/react-native-platform/hooks/use-tour-target.js.map +0 -1
- package/dist/react-native-platform/index.d.ts +0 -7
- package/dist/react-native-platform/index.js +0 -7
- package/dist/react-native-platform/index.js.map +0 -1
- package/dist/react-native-platform/platform-adapter.d.ts +0 -2
- package/dist/react-native-platform/platform-adapter.js +0 -84
- package/dist/react-native-platform/platform-adapter.js.map +0 -1
- package/dist/react-native-platform/target-registry.d.ts +0 -10
- package/dist/react-native-platform/target-registry.js +0 -17
- package/dist/react-native-platform/target-registry.js.map +0 -1
- package/dist/react-native-platform/types/tour-provider.props.type.d.ts +0 -12
- package/dist/react-native-platform/types/tour-provider.props.type.js +0 -2
- package/dist/react-native-platform/types/tour-provider.props.type.js.map +0 -1
- package/dist/react-platform/components/index.d.ts +0 -2
- package/dist/react-platform/components/index.js +0 -3
- package/dist/react-platform/components/index.js.map +0 -1
- package/dist/react-platform/components/tour-overlay.d.ts +0 -3
- package/dist/react-platform/components/tour-overlay.js +0 -29
- package/dist/react-platform/components/tour-overlay.js.map +0 -1
- package/dist/react-platform/components/tour-tooltip.d.ts +0 -2
- package/dist/react-platform/components/tour-tooltip.js +0 -155
- package/dist/react-platform/components/tour-tooltip.js.map +0 -1
- package/dist/react-platform/index.d.ts +0 -6
- package/dist/react-platform/index.js +0 -6
- package/dist/react-platform/index.js.map +0 -1
- package/dist/react-platform/platform-adapter.d.ts +0 -2
- package/dist/react-platform/platform-adapter.js +0 -101
- package/dist/react-platform/platform-adapter.js.map +0 -1
- package/dist/react-platform/types/tour-provider.props.type.d.ts +0 -12
- package/dist/react-platform/types/tour-provider.props.type.js +0 -2
- package/dist/react-platform/types/tour-provider.props.type.js.map +0 -1
- package/dist/react-platform/utils/find-visible-element.util.d.ts +0 -1
- package/dist/react-platform/utils/find-visible-element.util.js +0 -11
- package/dist/react-platform/utils/find-visible-element.util.js.map +0 -1
- package/dist/react-platform/utils/index.d.ts +0 -1
- package/dist/react-platform/utils/index.js +0 -2
- package/dist/react-platform/utils/index.js.map +0 -1
- package/dist/shared/context/index.d.ts +0 -1
- package/dist/shared/context/index.js +0 -2
- package/dist/shared/context/index.js.map +0 -1
- package/dist/shared/context/tour-context.d.ts +0 -3
- package/dist/shared/context/tour-context.js +0 -14
- package/dist/shared/context/tour-context.js.map +0 -1
- package/dist/shared/hooks/index.d.ts +0 -1
- package/dist/shared/hooks/index.js +0 -2
- package/dist/shared/hooks/index.js.map +0 -1
- package/dist/shared/hooks/use-tour.hook.d.ts +0 -2
- package/dist/shared/hooks/use-tour.hook.js +0 -7
- package/dist/shared/hooks/use-tour.hook.js.map +0 -1
- package/dist/shared/index.d.ts +0 -5
- package/dist/shared/index.js +0 -5
- package/dist/shared/index.js.map +0 -1
- package/dist/shared/types/index.d.ts +0 -10
- package/dist/shared/types/index.js +0 -2
- package/dist/shared/types/index.js.map +0 -1
- package/dist/shared/types/platform-adapter.type.d.ts +0 -24
- package/dist/shared/types/platform-adapter.type.js +0 -2
- package/dist/shared/types/platform-adapter.type.js.map +0 -1
- package/dist/shared/types/rect.type.d.ts +0 -11
- package/dist/shared/types/rect.type.js +0 -2
- package/dist/shared/types/rect.type.js.map +0 -1
- package/dist/shared/types/tour-overlay-props.type.d.ts +0 -8
- package/dist/shared/types/tour-overlay-props.type.js +0 -2
- package/dist/shared/types/tour-overlay-props.type.js.map +0 -1
- package/dist/shared/utils/calculate-tooltip-position.util.d.ts +0 -14
- package/dist/shared/utils/calculate-tooltip-position.util.js +0 -49
- package/dist/shared/utils/calculate-tooltip-position.util.js.map +0 -1
- package/dist/shared/utils/index.d.ts +0 -3
- package/dist/shared/utils/index.js +0 -3
- package/dist/shared/utils/index.js.map +0 -1
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
import {
|
|
2
|
+
boxesEqual,
|
|
3
|
+
containsBox,
|
|
4
|
+
deflate,
|
|
5
|
+
intersect,
|
|
6
|
+
projectOnto,
|
|
7
|
+
sanitizeBox,
|
|
8
|
+
sanitizeInsets,
|
|
9
|
+
sanitizeSize,
|
|
10
|
+
sizesEqual,
|
|
11
|
+
} from '../geometry/box.util.js';
|
|
12
|
+
import {computeTooltipLayout, getSafeArea, type TooltipLayout} from '../geometry/compute-tooltip-layout.util.js';
|
|
13
|
+
import type {Box, Side, Size, Viewport} from '../geometry/geometry.type.js';
|
|
14
|
+
import {buildRevealMargins} from '../geometry/reveal.util.js';
|
|
15
|
+
import {getTargetVisibility, type TargetVisibility} from '../geometry/target-visibility.util.js';
|
|
16
|
+
import type {TourContextType} from '../types/tour-context.type.js';
|
|
17
|
+
import type {StartTourOptions, TourEndCallback, TourEndInfo, TourEndReason} from '../types/tour-end-info.type.js';
|
|
18
|
+
import type {TourPlacement, TourStep, TourTarget} from '../types/tour-step.type.js';
|
|
19
|
+
import {withTimeout} from './abort.util.js';
|
|
20
|
+
import {devWarn, reportError, safeInvoke} from './report.util.js';
|
|
21
|
+
import type {TargetMeasurement, TourAdapter} from './tour-adapter.type.js';
|
|
22
|
+
|
|
23
|
+
/** Spacing used for layout, shared by both platforms. */
|
|
24
|
+
export const tourLayout = {
|
|
25
|
+
/** Distance between the highlighted area and the tooltip (arrows are at most this long). */
|
|
26
|
+
gap: 12,
|
|
27
|
+
/** Space between the target and the edge of the highlighted area. */
|
|
28
|
+
highlightPadding: 8,
|
|
29
|
+
/** Minimum distance between the tooltip and the edge of the safe area. */
|
|
30
|
+
viewportPadding: 12,
|
|
31
|
+
/** Minimum distance between an arrow and the tooltip's corners. */
|
|
32
|
+
arrowPadding: 16,
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
/** How long to wait for a step's target to appear before skipping it. */
|
|
36
|
+
export const targetTimeoutMs = 5000;
|
|
37
|
+
/** Safety cap for the renderer to report a tooltip's size; it normally reports within the same frame. */
|
|
38
|
+
const measureTimeoutMs = 1000;
|
|
39
|
+
/** Safety cap for scrolling a target into view. */
|
|
40
|
+
const revealTimeoutMs = 2000;
|
|
41
|
+
/**
|
|
42
|
+
* Safety cap for `onBeforeStep`. The tour covers the app meanwhile, so it cannot be waiting for the
|
|
43
|
+
* user; one that never settles would otherwise leave a dimmed screen with no way out.
|
|
44
|
+
*/
|
|
45
|
+
export const beforeStepTimeoutMs = 30_000;
|
|
46
|
+
/** How many tours started from `onTourEnd` callbacks are ended before a new tour starts. */
|
|
47
|
+
const maxChainedEnds = 10;
|
|
48
|
+
/** Size assumed when the renderer never reports one (for example in a test environment without layout). */
|
|
49
|
+
const fallbackTooltipSize: Size = {width: 320, height: 180};
|
|
50
|
+
|
|
51
|
+
export type TourPhase = 'idle' | 'preparing' | 'revealing' | 'showing';
|
|
52
|
+
|
|
53
|
+
/** The step currently on screen. */
|
|
54
|
+
export type ShownStep = {
|
|
55
|
+
/** Changes only when a step is (re)shown: renderers animate when it changes and apply everything else instantly. */
|
|
56
|
+
readonly transitionId: number;
|
|
57
|
+
readonly stepIndex: number;
|
|
58
|
+
readonly step: TourStep;
|
|
59
|
+
/** The highlighted part of the target in layer coordinates, or `undefined` for a centred step. */
|
|
60
|
+
readonly highlight: Box | undefined;
|
|
61
|
+
readonly layout: TooltipLayout;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Asks the renderer to render the pending step's tooltip invisibly and report its size. */
|
|
65
|
+
export type MeasureRequest = {
|
|
66
|
+
readonly id: number;
|
|
67
|
+
readonly stepIndex: number;
|
|
68
|
+
readonly step: TourStep;
|
|
69
|
+
readonly maxWidth: number;
|
|
70
|
+
readonly maxHeight: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Everything a renderer needs. Changes only on structural events, never while idle. */
|
|
74
|
+
export type TourViewState = {
|
|
75
|
+
readonly phase: TourPhase;
|
|
76
|
+
/**
|
|
77
|
+
* While `phase` is `'revealing'`: where the next target currently is, updated every frame while it
|
|
78
|
+
* is scrolled into view, so the highlight can travel with the content.
|
|
79
|
+
*/
|
|
80
|
+
readonly revealHighlight: Box | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* How `revealHighlight` moves: frame by frame with the target (`true`), or straight to where the
|
|
83
|
+
* target will end up (`false`, on platforms whose measurements lag behind scrolling).
|
|
84
|
+
*/
|
|
85
|
+
readonly followsScroll: boolean;
|
|
86
|
+
/** Changes for every tour, so renderers can tell a new tour from a step change. */
|
|
87
|
+
readonly tourKey: number;
|
|
88
|
+
readonly steps: readonly TourStep[];
|
|
89
|
+
readonly shown: ShownStep | undefined;
|
|
90
|
+
readonly measure: MeasureRequest | undefined;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type TourPublicState = Pick<TourContextType, 'isActive' | 'currentStep' | 'steps' | 'currentTourId'>;
|
|
94
|
+
|
|
95
|
+
export type TourActions = Pick<TourContextType, 'startTour' | 'endTour' | 'nextStep' | 'prevStep' | 'goToStep'>;
|
|
96
|
+
|
|
97
|
+
export type TourEngineOptions = {
|
|
98
|
+
readonly adapter: TourAdapter;
|
|
99
|
+
readonly onTourEnd?: TourEndCallback | undefined;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type TourEngine = {
|
|
103
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
104
|
+
readonly getPublicState: () => TourPublicState;
|
|
105
|
+
readonly getViewState: () => TourViewState;
|
|
106
|
+
/** Stable for the lifetime of the engine. */
|
|
107
|
+
readonly actions: TourActions;
|
|
108
|
+
/** Replaces the options (latest props). Never restarts anything. */
|
|
109
|
+
readonly configure: (options: TourEngineOptions) => void;
|
|
110
|
+
/** Call on mount; returns the cleanup. Tolerates StrictMode's simulated unmount. */
|
|
111
|
+
readonly mount: () => () => void;
|
|
112
|
+
/** Reports the rendered size of a measured tooltip (`MeasureRequest.id`) or of the shown one (`transitionId`). */
|
|
113
|
+
readonly reportTooltipSize: (id: number, size: Size) => void;
|
|
114
|
+
/** Ends the tour because of user input in the renderer. */
|
|
115
|
+
readonly dismiss: (reason: Extract<TourEndReason, 'closed' | 'blur' | 'escape'>) => void;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
type Session = {
|
|
119
|
+
readonly key: number;
|
|
120
|
+
readonly tourId: string | undefined;
|
|
121
|
+
readonly steps: readonly TourStep[];
|
|
122
|
+
/** The array passed to `startTour`, to recognise repeated starts of the same tour. */
|
|
123
|
+
readonly source: readonly TourStep[];
|
|
124
|
+
readonly onTourEnd: TourEndCallback | undefined;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
type State = {
|
|
128
|
+
readonly phase: TourPhase;
|
|
129
|
+
readonly session: Session | undefined;
|
|
130
|
+
/** The step on screen, or the start index while the first step is prepared. */
|
|
131
|
+
readonly currentStep: number;
|
|
132
|
+
readonly pendingStepIndex: number | undefined;
|
|
133
|
+
readonly shown: ShownStep | undefined;
|
|
134
|
+
readonly measure: MeasureRequest | undefined;
|
|
135
|
+
/** Where the target being revealed currently is (only meaningful while revealing). */
|
|
136
|
+
readonly revealHighlight: Box | undefined;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
type ShownDetails = {
|
|
140
|
+
readonly measurement: TargetMeasurement | undefined;
|
|
141
|
+
readonly size: Size;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const emptySteps: readonly TourStep[] = Object.freeze([]);
|
|
145
|
+
|
|
146
|
+
const sideOrder: Record<TourPlacement, Side[]> = {
|
|
147
|
+
top: ['top', 'bottom', 'right', 'left'],
|
|
148
|
+
bottom: ['bottom', 'top', 'right', 'left'],
|
|
149
|
+
left: ['left', 'right', 'bottom', 'top'],
|
|
150
|
+
right: ['right', 'left', 'bottom', 'top'],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** The part of the viewport that is not covered by insets (notch, keyboard, system bars). */
|
|
154
|
+
function unobstructed(viewport: Viewport): Box {
|
|
155
|
+
return deflate(sanitizeBox(viewport.rect), sanitizeInsets(viewport.insets));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The visible part of the target: clipped by its scroll containers. */
|
|
159
|
+
/**
|
|
160
|
+
* The visible part of the target: clipped by its scroll containers. A target scrolled completely out
|
|
161
|
+
* of its container collapses onto the container's nearest edge, where it will appear.
|
|
162
|
+
*/
|
|
163
|
+
function highlightOf(measurement: TargetMeasurement): Box {
|
|
164
|
+
if (!measurement.clip) {
|
|
165
|
+
return measurement.rect;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return intersect(measurement.rect, measurement.clip) ?? projectOnto(measurement.rect, measurement.clip);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* How much of the target the user can see: inside its scroll containers and the screen, and not
|
|
173
|
+
* covered by elements that stay put (sticky headers, floating buttons). Covered completely counts as
|
|
174
|
+
* not visible; covered partly as partly visible.
|
|
175
|
+
*/
|
|
176
|
+
function visibilityOf(measurement: TargetMeasurement, screen: Box): TargetVisibility {
|
|
177
|
+
const area = measurement.clip ? intersect(measurement.clip, screen) : screen;
|
|
178
|
+
const visibility = area ? getTargetVisibility(measurement.rect, area) : 'none';
|
|
179
|
+
const occluders = measurement.occluders ?? [];
|
|
180
|
+
if (visibility === 'none' || occluders.length === 0) {
|
|
181
|
+
return visibility;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (occluders.some((occluder) => containsBox(occluder, measurement.rect))) {
|
|
185
|
+
return 'none';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return occluders.some((occluder) => intersect(occluder, measurement.rect) !== undefined) ? 'partial' : visibility;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function layoutsEqual(a: TooltipLayout, b: TooltipLayout): boolean {
|
|
192
|
+
return (
|
|
193
|
+
a.placement === b.placement &&
|
|
194
|
+
Math.abs(a.x - b.x) < 0.01 &&
|
|
195
|
+
Math.abs(a.y - b.y) < 0.01 &&
|
|
196
|
+
Math.abs(a.width - b.width) < 0.01 &&
|
|
197
|
+
Math.abs(a.height - b.height) < 0.01 &&
|
|
198
|
+
Math.abs(a.maxWidth - b.maxWidth) < 0.01 &&
|
|
199
|
+
Math.abs(a.maxHeight - b.maxHeight) < 0.01 &&
|
|
200
|
+
(a.arrowOffset === b.arrowOffset ||
|
|
201
|
+
(a.arrowOffset !== undefined && b.arrowOffset !== undefined && Math.abs(a.arrowOffset - b.arrowOffset) < 0.01))
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Creates the platform-independent tour engine: a small store plus the step lifecycle
|
|
207
|
+
* (`onBeforeStep` → wait for target → measure tooltip → scroll into view → verify → show).
|
|
208
|
+
*
|
|
209
|
+
* Every step change is driven by an action, never by a re-render, so user callbacks run exactly
|
|
210
|
+
* once per navigation no matter how often React renders. Every asynchronous step is cancellable.
|
|
211
|
+
*/
|
|
212
|
+
export function createTourEngine(initialOptions: TourEngineOptions): TourEngine {
|
|
213
|
+
let options = initialOptions;
|
|
214
|
+
const listeners = new Set<() => void>();
|
|
215
|
+
|
|
216
|
+
let state: State = {
|
|
217
|
+
phase: 'idle',
|
|
218
|
+
session: undefined,
|
|
219
|
+
currentStep: 0,
|
|
220
|
+
pendingStepIndex: undefined,
|
|
221
|
+
shown: undefined,
|
|
222
|
+
measure: undefined,
|
|
223
|
+
revealHighlight: undefined,
|
|
224
|
+
};
|
|
225
|
+
let publicState: TourPublicState = {isActive: false, currentStep: 0, steps: emptySteps, currentTourId: undefined};
|
|
226
|
+
let viewState: TourViewState = {
|
|
227
|
+
phase: 'idle',
|
|
228
|
+
revealHighlight: undefined,
|
|
229
|
+
followsScroll: initialOptions.adapter.measuresWhileScrolling,
|
|
230
|
+
tourKey: 0,
|
|
231
|
+
steps: emptySteps,
|
|
232
|
+
shown: undefined,
|
|
233
|
+
measure: undefined,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
let sessionCounter = 0;
|
|
237
|
+
let idCounter = 0;
|
|
238
|
+
/** Preparation of a step in flight. */
|
|
239
|
+
let transition: AbortController | undefined;
|
|
240
|
+
/** Observation of the step on screen. */
|
|
241
|
+
let stepScope: AbortController | undefined;
|
|
242
|
+
let shownDetails: ShownDetails | undefined;
|
|
243
|
+
let measureSequence = 0;
|
|
244
|
+
/** A scroll that brings the shown target back into view is running. */
|
|
245
|
+
let isKeepingInView = false;
|
|
246
|
+
/** The target moved again while keepInView was scrolling: check once more when it is done. */
|
|
247
|
+
let isKeepInViewStale = false;
|
|
248
|
+
/** The scroll of keepInView in flight, stopped when the next step is prepared. */
|
|
249
|
+
let keepInViewScope: AbortController | undefined;
|
|
250
|
+
let lastTooltipSize: Size | undefined;
|
|
251
|
+
const reportedSizes = new Map<number, Size>();
|
|
252
|
+
const sizeWaiters = new Map<number, (size: Size) => void>();
|
|
253
|
+
let lifecycle: 'live' | 'unmounting' | 'suspended' = 'live';
|
|
254
|
+
|
|
255
|
+
// -------------------------------------------------------------------------------------------
|
|
256
|
+
// Store
|
|
257
|
+
// -------------------------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
/** The public state for `useTour()`; the previous object while nothing in it changed. */
|
|
260
|
+
function derivePublicState(previous: TourPublicState): TourPublicState {
|
|
261
|
+
const next: TourPublicState = {
|
|
262
|
+
isActive: state.phase !== 'idle',
|
|
263
|
+
currentStep: state.currentStep,
|
|
264
|
+
steps: state.session?.source ?? emptySteps,
|
|
265
|
+
currentTourId: state.session?.tourId,
|
|
266
|
+
};
|
|
267
|
+
const isUnchanged =
|
|
268
|
+
next.isActive === previous.isActive &&
|
|
269
|
+
next.currentStep === previous.currentStep &&
|
|
270
|
+
next.steps === previous.steps &&
|
|
271
|
+
next.currentTourId === previous.currentTourId;
|
|
272
|
+
return isUnchanged ? previous : next;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The state the renderer draws; the previous object while nothing in it changed. */
|
|
276
|
+
function deriveViewState(previous: TourViewState): TourViewState {
|
|
277
|
+
const next: TourViewState = {
|
|
278
|
+
phase: state.phase,
|
|
279
|
+
revealHighlight: state.phase === 'revealing' ? state.revealHighlight : undefined,
|
|
280
|
+
followsScroll: options.adapter.measuresWhileScrolling,
|
|
281
|
+
tourKey: state.session?.key ?? previous.tourKey,
|
|
282
|
+
steps: state.session?.steps ?? emptySteps,
|
|
283
|
+
shown: state.shown,
|
|
284
|
+
measure: state.measure,
|
|
285
|
+
};
|
|
286
|
+
const isUnchanged =
|
|
287
|
+
next.phase === previous.phase &&
|
|
288
|
+
next.revealHighlight === previous.revealHighlight &&
|
|
289
|
+
next.followsScroll === previous.followsScroll &&
|
|
290
|
+
next.tourKey === previous.tourKey &&
|
|
291
|
+
next.steps === previous.steps &&
|
|
292
|
+
next.shown === previous.shown &&
|
|
293
|
+
next.measure === previous.measure;
|
|
294
|
+
return isUnchanged ? previous : next;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function setState(patch: Partial<State>): void {
|
|
298
|
+
state = {...state, ...patch};
|
|
299
|
+
const nextPublic = derivePublicState(publicState);
|
|
300
|
+
const nextView = deriveViewState(viewState);
|
|
301
|
+
if (nextPublic === publicState && nextView === viewState) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
publicState = nextPublic;
|
|
306
|
+
viewState = nextView;
|
|
307
|
+
// Snapshot: listeners may subscribe or unsubscribe while being notified.
|
|
308
|
+
const snapshot = [...listeners];
|
|
309
|
+
for (const listener of snapshot) {
|
|
310
|
+
listener();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// -------------------------------------------------------------------------------------------
|
|
315
|
+
// Layout
|
|
316
|
+
// -------------------------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
function computeLayout(
|
|
319
|
+
step: TourStep,
|
|
320
|
+
highlight: Box | undefined,
|
|
321
|
+
size: Size,
|
|
322
|
+
previous: TooltipLayout | undefined,
|
|
323
|
+
): TooltipLayout {
|
|
324
|
+
return computeTooltipLayout({
|
|
325
|
+
...tourLayout,
|
|
326
|
+
target: highlight,
|
|
327
|
+
tooltip: size,
|
|
328
|
+
viewport: options.adapter.getViewport(step.target),
|
|
329
|
+
placement: step.placement ?? 'bottom',
|
|
330
|
+
previousPlacement: previous?.placement,
|
|
331
|
+
pixelRatio: options.adapter.getPixelRatio?.(),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function updateShown(measurement: TargetMeasurement | undefined, size: Size): void {
|
|
336
|
+
const {shown} = state;
|
|
337
|
+
if (!shown) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
shownDetails = {measurement, size};
|
|
342
|
+
const highlight = measurement ? highlightOf(measurement) : undefined;
|
|
343
|
+
const layout = computeLayout(shown.step, highlight, size, shown.layout);
|
|
344
|
+
if (boxesEqual(highlight, shown.highlight) && layoutsEqual(layout, shown.layout)) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
setState({shown: {...shown, highlight, layout}});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// -------------------------------------------------------------------------------------------
|
|
352
|
+
// Step lifecycle
|
|
353
|
+
// -------------------------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
async function waitForTooltipSize(id: number, signal: AbortSignal): Promise<Size | undefined> {
|
|
356
|
+
const reported = reportedSizes.get(id);
|
|
357
|
+
if (reported) {
|
|
358
|
+
return reported;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return new Promise<Size | undefined>((resolve) => {
|
|
362
|
+
sizeWaiters.set(id, resolve);
|
|
363
|
+
signal.addEventListener('abort', () => {
|
|
364
|
+
sizeWaiters.delete(id);
|
|
365
|
+
resolve(undefined);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function measureTooltip(index: number, step: TourStep, signal: AbortSignal): Promise<{id: number; size: Size}> {
|
|
371
|
+
const id = ++idCounter;
|
|
372
|
+
const safe = getSafeArea(options.adapter.getViewport(step.target), tourLayout.viewportPadding);
|
|
373
|
+
setState({measure: {id, stepIndex: index, step, maxWidth: safe.width, maxHeight: safe.height}});
|
|
374
|
+
const size = await withTimeout(
|
|
375
|
+
async (childSignal) => waitForTooltipSize(id, childSignal),
|
|
376
|
+
signal,
|
|
377
|
+
measureTimeoutMs,
|
|
378
|
+
);
|
|
379
|
+
reportedSizes.delete(id);
|
|
380
|
+
return {id, size: size ?? lastTooltipSize ?? fallbackTooltipSize};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
type RevealInput = {
|
|
384
|
+
readonly step: TourStep;
|
|
385
|
+
readonly target: TourTarget;
|
|
386
|
+
readonly measurement: TargetMeasurement;
|
|
387
|
+
readonly size: Size;
|
|
388
|
+
readonly signal: AbortSignal;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
/** Scrolls the target into view and returns its measurement if it is (at least partly) visible. */
|
|
392
|
+
async function reveal(input: RevealInput): Promise<TargetMeasurement | undefined> {
|
|
393
|
+
const {target, signal} = input;
|
|
394
|
+
// While scrolling, the highlight follows the target frame by frame so the user sees where the tour goes.
|
|
395
|
+
const following = new AbortController();
|
|
396
|
+
const stopFollowing = (): void => {
|
|
397
|
+
following.abort();
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
signal.addEventListener('abort', stopFollowing);
|
|
401
|
+
let hasStarted = false;
|
|
402
|
+
const startFollowing = (): void => {
|
|
403
|
+
if (!hasStarted && !signal.aborted) {
|
|
404
|
+
hasStarted = true;
|
|
405
|
+
followTarget(target, following.signal).catch(reportError);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
return await scrollIntoViewAndVerify({...input, startFollowing});
|
|
411
|
+
} finally {
|
|
412
|
+
signal.removeEventListener('abort', stopFollowing);
|
|
413
|
+
stopFollowing();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Moves the reveal highlight with the target, once per frame, until `signal` aborts. */
|
|
418
|
+
async function followTarget(target: TourTarget, signal: AbortSignal): Promise<void> {
|
|
419
|
+
const {adapter} = options;
|
|
420
|
+
while (!signal.aborted) {
|
|
421
|
+
// eslint-disable-next-line no-await-in-loop -- one measurement per frame
|
|
422
|
+
await adapter.nextFrame(signal);
|
|
423
|
+
// eslint-disable-next-line no-await-in-loop -- see above
|
|
424
|
+
const current = signal.aborted ? undefined : await adapter.measure(target);
|
|
425
|
+
if (!signal.aborted && state.phase === 'revealing' && current) {
|
|
426
|
+
const highlight = highlightOf(current);
|
|
427
|
+
if (!boxesEqual(highlight, state.revealHighlight)) {
|
|
428
|
+
setState({revealHighlight: highlight});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function scrollIntoViewAndVerify({
|
|
435
|
+
step,
|
|
436
|
+
target,
|
|
437
|
+
measurement,
|
|
438
|
+
size,
|
|
439
|
+
signal,
|
|
440
|
+
startFollowing,
|
|
441
|
+
}: RevealInput & {readonly startFollowing: () => void}): Promise<TargetMeasurement | undefined> {
|
|
442
|
+
const {adapter} = options;
|
|
443
|
+
const margins = buildRevealMargins({
|
|
444
|
+
...tourLayout,
|
|
445
|
+
target: highlightOf(measurement),
|
|
446
|
+
tooltip: size,
|
|
447
|
+
area: unobstructed(adapter.getViewport(target)),
|
|
448
|
+
sides: sideOrder[step.placement ?? 'bottom'],
|
|
449
|
+
});
|
|
450
|
+
// Where the target is now: the second attempt starts where the first one ended.
|
|
451
|
+
const latest = {measurement};
|
|
452
|
+
for (const behavior of ['smooth', 'instant'] as const) {
|
|
453
|
+
// eslint-disable-next-line no-await-in-loop -- the retry depends on the first attempt's outcome
|
|
454
|
+
await withTimeout(
|
|
455
|
+
async (childSignal) => {
|
|
456
|
+
await adapter.reveal(target, {
|
|
457
|
+
margins,
|
|
458
|
+
behavior,
|
|
459
|
+
signal: childSignal,
|
|
460
|
+
onScrollStart(destination) {
|
|
461
|
+
if (signal.aborted) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (adapter.measuresWhileScrolling) {
|
|
466
|
+
setState({phase: 'revealing', revealHighlight: highlightOf(latest.measurement)});
|
|
467
|
+
startFollowing();
|
|
468
|
+
} else {
|
|
469
|
+
// Positions only update once scrolling ends: move the highlight to where the target is
|
|
470
|
+
// going, together with the scroll.
|
|
471
|
+
const screen = unobstructed(adapter.getViewport(target));
|
|
472
|
+
setState({phase: 'revealing', revealHighlight: highlightOf({rect: destination, clip: screen})});
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
return true;
|
|
477
|
+
},
|
|
478
|
+
signal,
|
|
479
|
+
revealTimeoutMs,
|
|
480
|
+
);
|
|
481
|
+
if (signal.aborted) {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// eslint-disable-next-line no-await-in-loop -- see above
|
|
486
|
+
const current = await adapter.measure(target);
|
|
487
|
+
if (signal.aborted || !current) {
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
latest.measurement = current;
|
|
492
|
+
const visibility = visibilityOf(current, unobstructed(adapter.getViewport(target)));
|
|
493
|
+
if (visibility === 'full') {
|
|
494
|
+
return current;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// The first attempt can be cut short (the user scrolled, or the page moved while scrolling);
|
|
498
|
+
// the second one is instant. Partly visible targets are shown, invisible ones are skipped.
|
|
499
|
+
if (behavior === 'instant') {
|
|
500
|
+
return visibility === 'partial' ? current : undefined;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
type PrepareOptions = {
|
|
508
|
+
/** Where the user came from; going back past the first step returns there. */
|
|
509
|
+
readonly origin?: number | undefined;
|
|
510
|
+
/** Skip `onBeforeStep` for this index (used to re-find a target that disappeared). */
|
|
511
|
+
readonly skipBeforeStepFor?: number | undefined;
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
function beginTransition(): AbortController {
|
|
515
|
+
transition?.abort();
|
|
516
|
+
// The step on screen stays observed (and laid out) until the next one is shown; only an
|
|
517
|
+
// automatic scroll of it stops, so it cannot fight the next step's.
|
|
518
|
+
keepInViewScope?.abort();
|
|
519
|
+
keepInViewScope = undefined;
|
|
520
|
+
const controller = new AbortController();
|
|
521
|
+
transition = controller;
|
|
522
|
+
return controller;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Prepares the steps from `requested` on, in the direction of travel, until one can be shown.
|
|
527
|
+
* Steps whose target cannot be found or brought into view are skipped.
|
|
528
|
+
*/
|
|
529
|
+
async function prepare(
|
|
530
|
+
requested: number,
|
|
531
|
+
initialDirection: 1 | -1,
|
|
532
|
+
prepareOptions: PrepareOptions = {},
|
|
533
|
+
): Promise<void> {
|
|
534
|
+
const {session} = state;
|
|
535
|
+
if (!session || lifecycle === 'suspended') {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const controller = beginTransition();
|
|
540
|
+
let index = requested;
|
|
541
|
+
let direction = initialDirection;
|
|
542
|
+
let returnedToOrigin = false;
|
|
543
|
+
while (!controller.signal.aborted) {
|
|
544
|
+
if (index < 0 || index >= session.steps.length) {
|
|
545
|
+
if (direction === -1 && prepareOptions.origin !== undefined && !returnedToOrigin) {
|
|
546
|
+
// Nothing to show before this step: go back to where the user came from.
|
|
547
|
+
returnedToOrigin = true;
|
|
548
|
+
index = prepareOptions.origin;
|
|
549
|
+
direction = 1;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
finish('target-missing', {stepIndex: Math.min(Math.max(index - direction, 0), session.steps.length - 1)});
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// eslint-disable-next-line no-await-in-loop -- steps are tried one after another
|
|
558
|
+
const result = await prepareStep(controller, index, prepareOptions.skipBeforeStepFor !== index);
|
|
559
|
+
if (result === 'done') {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
index += direction;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Runs a step's `onBeforeStep`. Resolves `true` once it has finished, `false` if it did not within
|
|
569
|
+
* {@link beforeStepTimeoutMs} (its signal is then aborted); rejects with what it throws.
|
|
570
|
+
*/
|
|
571
|
+
async function callBeforeStep(
|
|
572
|
+
step: TourStep,
|
|
573
|
+
onBeforeStep: NonNullable<TourStep['onBeforeStep']>,
|
|
574
|
+
parent: AbortSignal,
|
|
575
|
+
): Promise<boolean> {
|
|
576
|
+
const child = new AbortController();
|
|
577
|
+
const abortChild = (): void => {
|
|
578
|
+
child.abort();
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
parent.addEventListener('abort', abortChild);
|
|
582
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
583
|
+
const timedOut = new Promise<false>((resolve) => {
|
|
584
|
+
timer = setTimeout(() => {
|
|
585
|
+
child.abort();
|
|
586
|
+
resolve(false);
|
|
587
|
+
}, beforeStepTimeoutMs);
|
|
588
|
+
});
|
|
589
|
+
try {
|
|
590
|
+
const finished = (async (): Promise<true> => {
|
|
591
|
+
await onBeforeStep(step.target, {signal: child.signal});
|
|
592
|
+
return true;
|
|
593
|
+
})();
|
|
594
|
+
return await Promise.race([finished, timedOut]);
|
|
595
|
+
} finally {
|
|
596
|
+
clearTimeout(timer);
|
|
597
|
+
parent.removeEventListener('abort', abortChild);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Runs one step's lifecycle. Resolves `'skip'` when its target cannot be found or shown. */
|
|
602
|
+
async function prepareStep(
|
|
603
|
+
controller: AbortController,
|
|
604
|
+
index: number,
|
|
605
|
+
runBeforeStep: boolean,
|
|
606
|
+
): Promise<'done' | 'skip'> {
|
|
607
|
+
const {signal} = controller;
|
|
608
|
+
const step = state.session?.steps[index];
|
|
609
|
+
if (!step) {
|
|
610
|
+
return 'done';
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
setState({phase: 'preparing', pendingStepIndex: index});
|
|
614
|
+
try {
|
|
615
|
+
if (runBeforeStep && step.onBeforeStep) {
|
|
616
|
+
const isSettled = await callBeforeStep(step, step.onBeforeStep, signal);
|
|
617
|
+
if (signal.aborted) {
|
|
618
|
+
return 'done';
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!isSettled) {
|
|
622
|
+
finish('error', {
|
|
623
|
+
stepIndex: index,
|
|
624
|
+
error: new Error(
|
|
625
|
+
`onBeforeStep of step ${index + 1} did not finish within ${beforeStepTimeoutMs / 1000} seconds.`,
|
|
626
|
+
),
|
|
627
|
+
});
|
|
628
|
+
return 'done';
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const {adapter} = options;
|
|
633
|
+
const {target} = step;
|
|
634
|
+
let measurement: TargetMeasurement | undefined;
|
|
635
|
+
if (target !== undefined) {
|
|
636
|
+
measurement = await withTimeout(
|
|
637
|
+
async (childSignal) => adapter.waitForTarget(target, childSignal),
|
|
638
|
+
signal,
|
|
639
|
+
targetTimeoutMs,
|
|
640
|
+
);
|
|
641
|
+
if (signal.aborted) {
|
|
642
|
+
return 'done';
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (!measurement) {
|
|
646
|
+
devWarn(`Skipping step ${index + 1}: its target was not found within ${targetTimeoutMs / 1000} seconds.`);
|
|
647
|
+
return 'skip';
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const {id, size} = await measureTooltip(index, step, signal);
|
|
652
|
+
if (signal.aborted) {
|
|
653
|
+
return 'done';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (target !== undefined && measurement) {
|
|
657
|
+
measurement = await reveal({step, target, measurement, size, signal});
|
|
658
|
+
if (signal.aborted) {
|
|
659
|
+
return 'done';
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (!measurement) {
|
|
663
|
+
devWarn(`Skipping step ${index + 1}: its target could not be scrolled into view.`);
|
|
664
|
+
return 'skip';
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
show({controller, id, index, step, measurement, size});
|
|
669
|
+
return 'done';
|
|
670
|
+
} catch (error) {
|
|
671
|
+
if (!signal.aborted) {
|
|
672
|
+
finish('error', {stepIndex: index, error});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return 'done';
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
type ShowInput = {
|
|
680
|
+
readonly controller: AbortController;
|
|
681
|
+
readonly id: number;
|
|
682
|
+
readonly index: number;
|
|
683
|
+
readonly step: TourStep;
|
|
684
|
+
readonly measurement: TargetMeasurement | undefined;
|
|
685
|
+
readonly size: Size;
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
function show({controller, id, index, step, measurement, size}: ShowInput): void {
|
|
689
|
+
if (transition === controller) {
|
|
690
|
+
transition = undefined;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
shownDetails = {measurement, size};
|
|
694
|
+
lastTooltipSize = size;
|
|
695
|
+
const highlight = measurement ? highlightOf(measurement) : undefined;
|
|
696
|
+
setState({
|
|
697
|
+
phase: 'showing',
|
|
698
|
+
currentStep: index,
|
|
699
|
+
pendingStepIndex: undefined,
|
|
700
|
+
measure: undefined,
|
|
701
|
+
shown: {
|
|
702
|
+
transitionId: id,
|
|
703
|
+
stepIndex: index,
|
|
704
|
+
step,
|
|
705
|
+
highlight,
|
|
706
|
+
layout: computeLayout(step, highlight, size, undefined),
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
observeShown();
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function observeShown(): AbortSignal | undefined {
|
|
713
|
+
stepScope?.abort();
|
|
714
|
+
const {shown} = state;
|
|
715
|
+
if (!shown) {
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const scope = new AbortController();
|
|
720
|
+
stepScope = scope;
|
|
721
|
+
options.adapter.observe(
|
|
722
|
+
shown.step.target,
|
|
723
|
+
() => {
|
|
724
|
+
void remeasure(scope.signal);
|
|
725
|
+
},
|
|
726
|
+
scope.signal,
|
|
727
|
+
);
|
|
728
|
+
return scope.signal;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** `canKeepInView: false` only follows the target (after keepInView's own scroll: no second attempt). */
|
|
732
|
+
async function remeasure(signal: AbortSignal, {canKeepInView = true}: {canKeepInView?: boolean} = {}): Promise<void> {
|
|
733
|
+
const sequence = ++measureSequence;
|
|
734
|
+
const {shown} = state;
|
|
735
|
+
// While the next step is prepared, the step on screen still follows layout changes (rotation,
|
|
736
|
+
// resizing), but is neither scrolled back into view nor looked for again: it is about to go.
|
|
737
|
+
const isShowing = state.phase === 'showing';
|
|
738
|
+
if (!shown || !shownDetails || (!isShowing && state.phase !== 'preparing')) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const {target} = shown.step;
|
|
743
|
+
if (target === undefined) {
|
|
744
|
+
updateShown(undefined, shownDetails.size);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const measurement = await options.adapter.measure(target);
|
|
749
|
+
// The tooltip may have reported a new size meanwhile: use the latest.
|
|
750
|
+
const size = shownDetails?.size;
|
|
751
|
+
if (signal.aborted || sequence !== measureSequence || !size || state.shown !== shown) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (!measurement) {
|
|
756
|
+
if (state.phase === 'showing') {
|
|
757
|
+
// The target disappeared (re-mounted, for example): find it again without re-running onBeforeStep.
|
|
758
|
+
void prepare(shown.stepIndex, 1, {skipBeforeStepFor: shown.stepIndex});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
updateShown(measurement, size);
|
|
765
|
+
if (state.phase === 'showing' && canKeepInView) {
|
|
766
|
+
void keepInView({step: shown.step, target, measurement, size, signal});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
type KeepInViewInput = {
|
|
771
|
+
readonly step: TourStep;
|
|
772
|
+
readonly target: TourTarget;
|
|
773
|
+
readonly measurement: TargetMeasurement;
|
|
774
|
+
readonly size: Size;
|
|
775
|
+
readonly signal: AbortSignal;
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* When the app moves the shown target out of view (content inserted above it, a resize), scrolls
|
|
780
|
+
* it back. The step stays on screen and follows the target while it scrolls; nothing is re-run.
|
|
781
|
+
* Users cannot scroll the page themselves during a tour, so this never fights the user.
|
|
782
|
+
*/
|
|
783
|
+
async function keepInView({step, target, measurement, size, signal}: KeepInViewInput): Promise<void> {
|
|
784
|
+
if (isKeepingInView) {
|
|
785
|
+
isKeepInViewStale = true;
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const {adapter} = options;
|
|
790
|
+
const screen = unobstructed(adapter.getViewport(target));
|
|
791
|
+
// Only targets that can be seen whole are brought back (larger ones would be scrolled to and fro).
|
|
792
|
+
// Measured against the screen, not against what is left on screen of the lists around the
|
|
793
|
+
// target: after a rotation, a list can lie entirely off screen, and that is what scrolling fixes.
|
|
794
|
+
const fits = measurement.rect.width <= screen.width && measurement.rect.height <= screen.height;
|
|
795
|
+
if (!fits || visibilityOf(measurement, screen) === 'full') {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
isKeepingInView = true;
|
|
800
|
+
isKeepInViewStale = false;
|
|
801
|
+
let hasScrolled = false;
|
|
802
|
+
const scope = new AbortController();
|
|
803
|
+
keepInViewScope = scope;
|
|
804
|
+
const stop = (): void => {
|
|
805
|
+
scope.abort();
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
signal.addEventListener('abort', stop);
|
|
809
|
+
try {
|
|
810
|
+
const margins = buildRevealMargins({
|
|
811
|
+
...tourLayout,
|
|
812
|
+
target: highlightOf(measurement),
|
|
813
|
+
tooltip: size,
|
|
814
|
+
area: screen,
|
|
815
|
+
sides: sideOrder[step.placement ?? 'bottom'],
|
|
816
|
+
});
|
|
817
|
+
await withTimeout(
|
|
818
|
+
async (childSignal) => {
|
|
819
|
+
await adapter.reveal(target, {
|
|
820
|
+
margins,
|
|
821
|
+
behavior: 'smooth',
|
|
822
|
+
signal: childSignal,
|
|
823
|
+
onScrollStart() {
|
|
824
|
+
hasScrolled = true;
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
return true;
|
|
828
|
+
},
|
|
829
|
+
scope.signal,
|
|
830
|
+
revealTimeoutMs,
|
|
831
|
+
);
|
|
832
|
+
} finally {
|
|
833
|
+
signal.removeEventListener('abort', stop);
|
|
834
|
+
isKeepingInView = false;
|
|
835
|
+
if (keepInViewScope === scope) {
|
|
836
|
+
keepInViewScope = undefined;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (!scope.signal.aborted) {
|
|
840
|
+
if (isKeepInViewStale) {
|
|
841
|
+
// Pushed out again while scrolling (more content loaded): check once more.
|
|
842
|
+
isKeepInViewStale = false;
|
|
843
|
+
void remeasure(signal);
|
|
844
|
+
} else if (hasScrolled) {
|
|
845
|
+
// Where the target ended up: platforms whose scrolling reports no layout events (React
|
|
846
|
+
// Native) would otherwise keep the highlight where the target was.
|
|
847
|
+
void remeasure(signal, {canKeepInView: false});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function finish(reason: TourEndReason, extra: {stepIndex?: number; error?: unknown} = {}): void {
|
|
854
|
+
const {session} = state;
|
|
855
|
+
if (!session) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const stepIndex = extra.stepIndex ?? state.shown?.stepIndex ?? state.pendingStepIndex ?? state.currentStep;
|
|
860
|
+
const step = session.steps[stepIndex] ?? session.steps[0];
|
|
861
|
+
if (!step) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const info: TourEndInfo = {
|
|
866
|
+
reason,
|
|
867
|
+
stepIndex,
|
|
868
|
+
step,
|
|
869
|
+
totalSteps: session.steps.length,
|
|
870
|
+
...('error' in extra ? {error: extra.error} : {}),
|
|
871
|
+
};
|
|
872
|
+
transition?.abort();
|
|
873
|
+
transition = undefined;
|
|
874
|
+
stepScope?.abort();
|
|
875
|
+
stepScope = undefined;
|
|
876
|
+
shownDetails = undefined;
|
|
877
|
+
for (const resolve of sizeWaiters.values()) {
|
|
878
|
+
resolve(lastTooltipSize ?? fallbackTooltipSize);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
sizeWaiters.clear();
|
|
882
|
+
reportedSizes.clear();
|
|
883
|
+
// Reset before running user code, so a throwing callback can never leave the tour half-open.
|
|
884
|
+
setState({
|
|
885
|
+
phase: 'idle',
|
|
886
|
+
session: undefined,
|
|
887
|
+
currentStep: 0,
|
|
888
|
+
pendingStepIndex: undefined,
|
|
889
|
+
shown: undefined,
|
|
890
|
+
measure: undefined,
|
|
891
|
+
});
|
|
892
|
+
safeInvoke(() => session.onTourEnd?.(session.tourId, info));
|
|
893
|
+
safeInvoke(() => options.onTourEnd?.(session.tourId, info));
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// -------------------------------------------------------------------------------------------
|
|
897
|
+
// Actions
|
|
898
|
+
// -------------------------------------------------------------------------------------------
|
|
899
|
+
|
|
900
|
+
const actions: TourActions = {
|
|
901
|
+
startTour(steps: readonly TourStep[], tourId?: string, startOptions?: StartTourOptions): void {
|
|
902
|
+
if (lifecycle === 'suspended') {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (steps.length === 0) {
|
|
907
|
+
devWarn('startTour was called without steps; nothing to show.');
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const current = state.session;
|
|
912
|
+
if (current && (tourId === undefined ? steps === current.source : tourId === current.tourId)) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// End the running tour, and any tour its onTourEnd starts, before starting this one.
|
|
917
|
+
for (let ended = 0; state.session && ended < maxChainedEnds; ended++) {
|
|
918
|
+
finish('closed');
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (state.session) {
|
|
922
|
+
devWarn('onTourEnd keeps starting new tours; the last one is replaced without ending it.');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const session: Session = {
|
|
926
|
+
key: ++sessionCounter,
|
|
927
|
+
tourId,
|
|
928
|
+
steps: [...steps],
|
|
929
|
+
source: steps,
|
|
930
|
+
onTourEnd: startOptions?.onTourEnd,
|
|
931
|
+
};
|
|
932
|
+
lastTooltipSize = undefined;
|
|
933
|
+
setState({
|
|
934
|
+
session,
|
|
935
|
+
phase: 'preparing',
|
|
936
|
+
currentStep: 0,
|
|
937
|
+
pendingStepIndex: 0,
|
|
938
|
+
shown: undefined,
|
|
939
|
+
measure: undefined,
|
|
940
|
+
});
|
|
941
|
+
void prepare(0, 1);
|
|
942
|
+
},
|
|
943
|
+
|
|
944
|
+
endTour(): void {
|
|
945
|
+
finish('closed');
|
|
946
|
+
},
|
|
947
|
+
|
|
948
|
+
nextStep(): void {
|
|
949
|
+
if (lifecycle === 'suspended') {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const {session} = state;
|
|
954
|
+
if (!session || state.phase !== 'showing') {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (state.currentStep >= session.steps.length - 1) {
|
|
959
|
+
finish('completed');
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
void prepare(state.currentStep + 1, 1, {origin: state.currentStep});
|
|
964
|
+
},
|
|
965
|
+
|
|
966
|
+
prevStep(): void {
|
|
967
|
+
if (lifecycle === 'suspended') {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (!state.session || state.phase !== 'showing' || state.currentStep === 0) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
void prepare(state.currentStep - 1, -1, {origin: state.currentStep});
|
|
976
|
+
},
|
|
977
|
+
|
|
978
|
+
goToStep(index: number): void {
|
|
979
|
+
if (lifecycle === 'suspended') {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const {session} = state;
|
|
984
|
+
if (!session || !Number.isInteger(index) || index < 0 || index >= session.steps.length) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (state.phase === 'showing' ? index === state.currentStep : index === state.pendingStepIndex) {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const from = state.shown?.stepIndex ?? state.currentStep;
|
|
993
|
+
void prepare(index, index >= from ? 1 : -1, {origin: state.shown ? from : undefined});
|
|
994
|
+
},
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
// -------------------------------------------------------------------------------------------
|
|
998
|
+
// Lifecycle
|
|
999
|
+
// -------------------------------------------------------------------------------------------
|
|
1000
|
+
|
|
1001
|
+
function suspend(): void {
|
|
1002
|
+
lifecycle = 'suspended';
|
|
1003
|
+
transition?.abort();
|
|
1004
|
+
transition = undefined;
|
|
1005
|
+
stepScope?.abort();
|
|
1006
|
+
stepScope = undefined;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function resume(): void {
|
|
1010
|
+
lifecycle = 'live';
|
|
1011
|
+
if (state.phase === 'showing') {
|
|
1012
|
+
const signal = observeShown();
|
|
1013
|
+
if (signal) {
|
|
1014
|
+
void remeasure(signal);
|
|
1015
|
+
}
|
|
1016
|
+
} else if (state.phase !== 'idle') {
|
|
1017
|
+
void prepare(state.pendingStepIndex ?? state.currentStep, 1);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return {
|
|
1022
|
+
subscribe(listener) {
|
|
1023
|
+
listeners.add(listener);
|
|
1024
|
+
return () => {
|
|
1025
|
+
listeners.delete(listener);
|
|
1026
|
+
};
|
|
1027
|
+
},
|
|
1028
|
+
getPublicState: () => publicState,
|
|
1029
|
+
getViewState: () => viewState,
|
|
1030
|
+
actions,
|
|
1031
|
+
configure(next): void {
|
|
1032
|
+
options = next;
|
|
1033
|
+
},
|
|
1034
|
+
mount() {
|
|
1035
|
+
if (lifecycle === 'unmounting') {
|
|
1036
|
+
lifecycle = 'live';
|
|
1037
|
+
} else if (lifecycle === 'suspended') {
|
|
1038
|
+
resume();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
return () => {
|
|
1042
|
+
lifecycle = 'unmounting';
|
|
1043
|
+
void Promise.resolve().then(() => {
|
|
1044
|
+
if (lifecycle === 'unmounting') {
|
|
1045
|
+
suspend();
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
};
|
|
1049
|
+
},
|
|
1050
|
+
reportTooltipSize(id, rawSize): void {
|
|
1051
|
+
const size = sanitizeSize(rawSize);
|
|
1052
|
+
if (state.measure?.id === id) {
|
|
1053
|
+
reportedSizes.set(id, size);
|
|
1054
|
+
sizeWaiters.get(id)?.(size);
|
|
1055
|
+
sizeWaiters.delete(id);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (state.shown?.transitionId === id && shownDetails && !sizesEqual(shownDetails.size, size)) {
|
|
1060
|
+
lastTooltipSize = size;
|
|
1061
|
+
updateShown(shownDetails.measurement, size);
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
dismiss(reason): void {
|
|
1065
|
+
finish(reason);
|
|
1066
|
+
},
|
|
1067
|
+
};
|
|
1068
|
+
}
|