@robertlinde/react-tour-kit 0.8.9 → 1.0.1
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 +48 -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 -10
- package/dist/react-native.d.ts.map +1 -0
- package/dist/react-native.js +7 -7
- 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 -10
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +5 -7
- 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 +67 -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 +101 -0
- package/dist/shared/engine/tour-engine.d.ts.map +1 -0
- package/dist/shared/engine/tour-engine.js +835 -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 +145 -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 +73 -0
- package/src/shared/engine/tour-engine.ts +1147 -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 +169 -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 -7
- package/dist/shared/index.js +0 -6
- 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,105 @@
|
|
|
1
|
+
import {type ComponentRef, type ReactNode, useContext, useRef, useState, useSyncExternalStore} from 'react';
|
|
2
|
+
import {Dimensions, type LayoutChangeEvent, StyleSheet, View} from 'react-native';
|
|
3
|
+
import {devWarn, isDevelopment} from '../../shared/engine/report.util.js';
|
|
4
|
+
import {useIsomorphicLayoutEffect} from '../../shared/react/use-isomorphic-layout-effect.hook.js';
|
|
5
|
+
import {NativeTourContext} from '../native-context.js';
|
|
6
|
+
import {TourOverlay} from '../../shared/renderer/tour-overlay.js';
|
|
7
|
+
import {LayerSizeContext} from './native-renderer.js';
|
|
8
|
+
|
|
9
|
+
let hostCounter = 0;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Where the tour is drawn. `TourProvider` already renders one around your app; add another
|
|
13
|
+
* `<TourHost />` as the last child of a screen presented as a native modal (react-native-screens
|
|
14
|
+
* `presentation: 'modal'`, or inside your own `<Modal>`), so tours can highlight elements there.
|
|
15
|
+
* The most recently mounted host is used.
|
|
16
|
+
*/
|
|
17
|
+
export function TourHost(): ReactNode {
|
|
18
|
+
return <HostView isRoot={false} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @internal Also used by `TourProvider` for its root host. */
|
|
22
|
+
export function HostView({isRoot}: {readonly isRoot: boolean}): ReactNode {
|
|
23
|
+
const context = useContext(NativeTourContext);
|
|
24
|
+
const viewRef = useRef<ComponentRef<typeof View> | null>(null);
|
|
25
|
+
const [key] = useState(() => ++hostCounter);
|
|
26
|
+
const [size, setSize] = useState({width: 0, height: 0});
|
|
27
|
+
const hosts = context?.hosts;
|
|
28
|
+
const activeKey = useSyncExternalStore(
|
|
29
|
+
hosts?.subscribe ?? subscribeNothing,
|
|
30
|
+
() => hosts?.activeKey(),
|
|
31
|
+
() => hosts?.activeKey(),
|
|
32
|
+
);
|
|
33
|
+
const engine = context?.overlay.engine;
|
|
34
|
+
const isTourActive = useSyncExternalStore(
|
|
35
|
+
engine?.subscribe ?? subscribeNothing,
|
|
36
|
+
() => engine?.getPublicState().isActive ?? false,
|
|
37
|
+
() => engine?.getPublicState().isActive ?? false,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
useIsomorphicLayoutEffect(() => hosts?.register(key, () => viewRef.current, isRoot), [hosts, key, isRoot]);
|
|
41
|
+
|
|
42
|
+
if (!context) {
|
|
43
|
+
if (!isRoot) {
|
|
44
|
+
devWarn('<TourHost /> must be rendered inside <TourProvider>.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const onLayout = (event: LayoutChangeEvent): void => {
|
|
51
|
+
const {width, height} = event.nativeEvent.layout;
|
|
52
|
+
const next = {width, height};
|
|
53
|
+
setSize(next);
|
|
54
|
+
context.hosts.setSize(key, next);
|
|
55
|
+
if (isRoot && isDevelopment) {
|
|
56
|
+
warnIfNotFullWindow(viewRef.current);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const isActive = activeKey === key;
|
|
61
|
+
const isModal = !isRoot && isActive && isTourActive;
|
|
62
|
+
return (
|
|
63
|
+
<View
|
|
64
|
+
ref={viewRef}
|
|
65
|
+
collapsable={false}
|
|
66
|
+
pointerEvents="box-none"
|
|
67
|
+
style={StyleSheet.absoluteFill}
|
|
68
|
+
// A TourHost drawing the tour hides the screen it is in from VoiceOver (its siblings).
|
|
69
|
+
accessibilityViewIsModal={isModal}
|
|
70
|
+
onLayout={onLayout}
|
|
71
|
+
>
|
|
72
|
+
{/* Drawn in the active host only, once it has been laid out. */}
|
|
73
|
+
{isActive && size.width > 0 && size.height > 0 ? (
|
|
74
|
+
<LayerSizeContext.Provider value={size}>
|
|
75
|
+
<TourOverlay {...context.overlay} />
|
|
76
|
+
</LayerSizeContext.Provider>
|
|
77
|
+
) : null}
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function subscribeNothing(): () => void {
|
|
83
|
+
return () => {
|
|
84
|
+
// Nothing to unsubscribe.
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let warnedNotFullWindow = false;
|
|
89
|
+
|
|
90
|
+
function warnIfNotFullWindow(view: ComponentRef<typeof View> | null): void {
|
|
91
|
+
if (!view || warnedNotFullWindow) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
view.measureInWindow((x: number, y: number, width: number, height: number) => {
|
|
96
|
+
const window = Dimensions.get('window');
|
|
97
|
+
if (x > 1 || y > 1 || width + 1 < window.width || height + 1 < window.height) {
|
|
98
|
+
warnedNotFullWindow = true;
|
|
99
|
+
devWarn(
|
|
100
|
+
'TourProvider does not fill the window, so the overlay will not cover the whole screen. ' +
|
|
101
|
+
'Place it at the root of your app, outside SafeAreaView or padded containers, and pass `insets` instead.',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type {Box} from '../shared/geometry/geometry.type.js';
|
|
2
|
+
|
|
3
|
+
/** React Native's `measure` callback signature. */
|
|
4
|
+
// eslint-disable-next-line max-params -- signature defined by React Native
|
|
5
|
+
type MeasureCallback = (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void;
|
|
6
|
+
|
|
7
|
+
type MeasurableInstance = {
|
|
8
|
+
getBoundingClientRect?: () => {x: number; y: number; width: number; height: number};
|
|
9
|
+
measure?: (callback: MeasureCallback) => void;
|
|
10
|
+
measureLayout?: (
|
|
11
|
+
relativeTo: unknown,
|
|
12
|
+
onSuccess: (left: number, top: number, width: number, height: number) => void,
|
|
13
|
+
onFail?: () => void,
|
|
14
|
+
) => void;
|
|
15
|
+
contains?: (other: unknown) => boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function isMeasurable(value: unknown): value is MeasurableInstance {
|
|
19
|
+
return typeof value === 'object' && value !== null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Measures a native view relative to the root view. Uses the synchronous `getBoundingClientRect`
|
|
24
|
+
* (New Architecture, includes transforms) and falls back to `measure`. Resolves `undefined` for
|
|
25
|
+
* views that are not rendered (unmounted, zero size, or flattened away on the old architecture).
|
|
26
|
+
*
|
|
27
|
+
* Positions are only compared with other positions from this function, so whichever origin the
|
|
28
|
+
* platform uses (status bar, edge-to-edge) cancels out.
|
|
29
|
+
*/
|
|
30
|
+
export async function measureView(instance: unknown): Promise<Box | undefined> {
|
|
31
|
+
if (!isMeasurable(instance)) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof instance.getBoundingClientRect === 'function') {
|
|
36
|
+
const rect = instance.getBoundingClientRect();
|
|
37
|
+
return valid({x: rect.x, y: rect.y, width: rect.width, height: rect.height});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const {measure} = instance;
|
|
41
|
+
if (typeof measure !== 'function') {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
try {
|
|
47
|
+
// eslint-disable-next-line max-params -- signature defined by React Native
|
|
48
|
+
measure.call(instance, (_x, _y, width, height, pageX, pageY) => {
|
|
49
|
+
resolve(valid({x: pageX, y: pageY, width, height}));
|
|
50
|
+
});
|
|
51
|
+
} catch {
|
|
52
|
+
resolve(undefined);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function valid(box: Box): Box | undefined {
|
|
58
|
+
const finite = [box.x, box.y, box.width, box.height].every(
|
|
59
|
+
(value) => typeof value === 'number' && Number.isFinite(value),
|
|
60
|
+
);
|
|
61
|
+
if (!finite || (box.width <= 0 && box.height <= 0)) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return box;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function relativeTo(box: Box, origin: Box): Box {
|
|
69
|
+
return {x: box.x - origin.x, y: box.y - origin.y, width: box.width, height: box.height};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Longest wait for `measureLayout` (on the Old Architecture a round trip to native). */
|
|
73
|
+
const measureLayoutTimeoutMs = 500;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Where `instance` is inside `ancestor` (from the layout, so also for views that cannot be
|
|
77
|
+
* measured on screen; possibly zero-sized), `false` when it is not inside it, or `undefined` when
|
|
78
|
+
* React Native cannot tell.
|
|
79
|
+
*/
|
|
80
|
+
export async function measureLayoutIn(instance: unknown, ancestor: unknown): Promise<Box | false | undefined> {
|
|
81
|
+
if (!isMeasurable(instance) || !isMeasurable(ancestor) || typeof instance.measureLayout !== 'function') {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const {measureLayout} = instance;
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
resolve(undefined);
|
|
89
|
+
}, measureLayoutTimeoutMs);
|
|
90
|
+
const settle = (result: Box | false | undefined): void => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
resolve(result);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
measureLayout.call(
|
|
97
|
+
instance,
|
|
98
|
+
ancestor,
|
|
99
|
+
(left, top, width, height) => {
|
|
100
|
+
const box = {x: left, y: top, width, height};
|
|
101
|
+
settle(Object.values(box).every((value) => Number.isFinite(value)) ? box : undefined);
|
|
102
|
+
},
|
|
103
|
+
() => {
|
|
104
|
+
settle(false);
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
} catch {
|
|
108
|
+
settle(undefined);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Ancestry does not change for a mounted view (React Native never moves views to another parent). */
|
|
114
|
+
const ancestry = new WeakMap<object, WeakMap<object, boolean>>();
|
|
115
|
+
|
|
116
|
+
/** Whether `instance` is a descendant of `ancestor`, or `undefined` when React Native cannot tell. */
|
|
117
|
+
export async function isDescendant(instance: unknown, ancestor: unknown): Promise<boolean | undefined> {
|
|
118
|
+
if (!isMeasurable(instance) || !isMeasurable(ancestor)) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const known = ancestry.get(instance)?.get(ancestor);
|
|
123
|
+
if (known !== undefined) {
|
|
124
|
+
return known;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result =
|
|
128
|
+
typeof ancestor.contains === 'function'
|
|
129
|
+
? ancestor.contains(instance)
|
|
130
|
+
: await measureLayoutIn(instance, ancestor).then((position) =>
|
|
131
|
+
position === undefined ? undefined : position !== false,
|
|
132
|
+
);
|
|
133
|
+
if (result !== undefined) {
|
|
134
|
+
const forInstance = ancestry.get(instance) ?? new WeakMap<object, boolean>();
|
|
135
|
+
forInstance.set(ancestor, result);
|
|
136
|
+
ancestry.set(instance, forInstance);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {createContext} from 'react';
|
|
2
|
+
import type {TourOverlayProps} from '../shared/renderer/tour-overlay.js';
|
|
3
|
+
import type {HostStore} from './host-store.js';
|
|
4
|
+
import type {TourRegistry} from './registry.js';
|
|
5
|
+
|
|
6
|
+
/** Everything the native hooks and hosts need from the provider. Internal. */
|
|
7
|
+
export type NativeTourContextValue = {
|
|
8
|
+
readonly registry: TourRegistry;
|
|
9
|
+
readonly hosts: HostStore;
|
|
10
|
+
/** Rendered by the active host. */
|
|
11
|
+
readonly overlay: TourOverlayProps;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const NativeTourContext = createContext<NativeTourContextValue | undefined>(undefined);
|
|
15
|
+
NativeTourContext.displayName = 'NativeTourContext';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {Insets, Size} from '../shared/geometry/geometry.type.js';
|
|
2
|
+
import {resolveInsets, type TourInsets} from './insets.util.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Values the native platform reads at measuring time (outside React rendering): safe-area insets
|
|
6
|
+
* and the on-screen keyboard. Updated by the provider in effects.
|
|
7
|
+
*/
|
|
8
|
+
export type NativeEnvironment = {
|
|
9
|
+
readonly setInsets: (insets: TourInsets | undefined) => void;
|
|
10
|
+
/** How much of the layer the keyboard covers. Returns whether that changed. */
|
|
11
|
+
readonly setKeyboardOverlap: (overlap: number) => boolean;
|
|
12
|
+
readonly getInsets: (layer: Size) => Insets;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createNativeEnvironment(): NativeEnvironment {
|
|
16
|
+
let insets: TourInsets | undefined;
|
|
17
|
+
let keyboardOverlap = 0;
|
|
18
|
+
return {
|
|
19
|
+
setInsets(value): void {
|
|
20
|
+
insets = value;
|
|
21
|
+
},
|
|
22
|
+
setKeyboardOverlap(overlap): boolean {
|
|
23
|
+
const next = Number.isFinite(overlap) ? Math.max(0, overlap) : 0;
|
|
24
|
+
if (Math.abs(next - keyboardOverlap) < 0.5) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
keyboardOverlap = next;
|
|
29
|
+
return true;
|
|
30
|
+
},
|
|
31
|
+
getInsets: (layer) => resolveInsets(insets, layer, keyboardOverlap),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import {nextFrame} from '../shared/engine/abort.util.js';
|
|
2
|
+
import type {TargetMeasurement} from '../shared/engine/tour-adapter.type.js';
|
|
3
|
+
import {containsBox, intersect, narrowClip} from '../shared/geometry/box.util.js';
|
|
4
|
+
import type {Box, Insets, Size, Viewport} from '../shared/geometry/geometry.type.js';
|
|
5
|
+
import type {
|
|
6
|
+
ExternalValue,
|
|
7
|
+
PlatformAdapter,
|
|
8
|
+
ScrollContainer,
|
|
9
|
+
ScrollContainerMetrics,
|
|
10
|
+
} from '../shared/platform/platform.type.js';
|
|
11
|
+
import type {TourTarget} from '../shared/types/tour-step.type.js';
|
|
12
|
+
import {isDescendant, measureLayoutIn, measureView, relativeTo} from './measure.util.js';
|
|
13
|
+
import type {TourRegistry} from './registry.js';
|
|
14
|
+
|
|
15
|
+
export type NativePlatformOptions = {
|
|
16
|
+
readonly registry: TourRegistry;
|
|
17
|
+
/** The view the tour is drawn in (the provider's root layer, or the newest `TourHost`). */
|
|
18
|
+
readonly getHost: () => {readonly instance: unknown; readonly size: Size} | undefined;
|
|
19
|
+
readonly getInsets: () => Insets;
|
|
20
|
+
/** Subscribes to anything that can move targets: layer size, keyboard, safe-area changes. */
|
|
21
|
+
readonly subscribeLayout: (listener: () => void) => () => void;
|
|
22
|
+
readonly reducedMotion: ExternalValue<boolean>;
|
|
23
|
+
readonly getPixelRatio: () => number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** What the tour uses of a `ScrollView` instance. */
|
|
27
|
+
type ScrollViewInstance = {
|
|
28
|
+
getNativeScrollRef?: () => unknown;
|
|
29
|
+
getInnerViewRef?: () => unknown;
|
|
30
|
+
getScrollResponder?: () => unknown;
|
|
31
|
+
scrollTo?: (params: {x?: number; y?: number; animated?: boolean}) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type ScrollViewGeometry = {
|
|
35
|
+
readonly scrollView: ScrollViewInstance;
|
|
36
|
+
/** The visible area of the scroll view, in layer coordinates. */
|
|
37
|
+
readonly viewport: Box;
|
|
38
|
+
/** The scrollable content, in layer coordinates. */
|
|
39
|
+
readonly content: Box;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** A target found on screen: the view, and where it is in layer coordinates. */
|
|
43
|
+
type LocatedTarget = {
|
|
44
|
+
readonly instance: unknown;
|
|
45
|
+
readonly rect: Box;
|
|
46
|
+
/** The registered scroll views, when they were already measured to find the target. */
|
|
47
|
+
readonly geometries: readonly ScrollViewGeometry[] | undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const area = (box: Box): number => box.width * box.height;
|
|
51
|
+
|
|
52
|
+
function isObject(value: unknown): value is ScrollViewInstance {
|
|
53
|
+
return typeof value === 'object' && value !== null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The `ScrollView` behind a registered ref. Lists (`FlatList`, `SectionList`, `VirtualizedList`)
|
|
58
|
+
* expose theirs as the scroll responder; a `ScrollView` is its own.
|
|
59
|
+
*/
|
|
60
|
+
function scrollViewOf(registered: unknown): ScrollViewInstance | undefined {
|
|
61
|
+
if (!isObject(registered)) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof registered.scrollTo === 'function') {
|
|
66
|
+
return registered;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const responder = registered.getScrollResponder?.();
|
|
70
|
+
return isObject(responder) && typeof responder.scrollTo === 'function' ? responder : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Scroll offsets are rounded to physical pixels: up to one point off still counts as arrived. */
|
|
74
|
+
const arrivalTolerance = 1;
|
|
75
|
+
/** Frames without movement after which a scroll that moved but stopped short counts as finished. */
|
|
76
|
+
const stillFramesAfterMoving = 3;
|
|
77
|
+
|
|
78
|
+
/** Which axes a scroll view can scroll: those where its content is bigger than the view. */
|
|
79
|
+
function scrollAxes(geometry: ScrollViewGeometry): {readonly x: boolean; readonly y: boolean} {
|
|
80
|
+
return {
|
|
81
|
+
x: geometry.content.width > geometry.viewport.width + arrivalTolerance,
|
|
82
|
+
y: geometry.content.height > geometry.viewport.height + arrivalTolerance,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Orders scroll views that all contain the target innermost first: a scroll view inside another
|
|
88
|
+
* one lies within that one's content.
|
|
89
|
+
*/
|
|
90
|
+
function innermostFirst(geometries: readonly ScrollViewGeometry[]): ScrollViewGeometry[] {
|
|
91
|
+
const depth = (geometry: ScrollViewGeometry): number =>
|
|
92
|
+
geometries.filter((other) => other !== geometry && containsBox(other.content, geometry.viewport, 1)).length;
|
|
93
|
+
// Not `toSorted`: ES2023 is not available on every supported React Native version.
|
|
94
|
+
// eslint-disable-next-line unicorn/no-array-sort
|
|
95
|
+
return [...geometries].sort((a, b) => depth(b) - depth(a) || area(a.content) - area(b.content));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** React Native's raw facts for the tour: measuring views, registered targets and scroll views, the host layer. */
|
|
99
|
+
export function createNativePlatform(env: NativePlatformOptions): PlatformAdapter {
|
|
100
|
+
async function hostOrigin(): Promise<Box | undefined> {
|
|
101
|
+
const host = env.getHost();
|
|
102
|
+
return host ? measureView(host.instance) : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function candidates(target: TourTarget): unknown[] {
|
|
106
|
+
if (typeof target === 'string') {
|
|
107
|
+
return env.registry.targets(target).map((ref) => ref.current);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [target.current];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function registeredScrollViews(origin: Box): Promise<ScrollViewGeometry[]> {
|
|
114
|
+
const geometries = await Promise.all(
|
|
115
|
+
env.registry.scrollViews().map(async (ref) => scrollViewGeometry(ref.current, origin)),
|
|
116
|
+
);
|
|
117
|
+
return geometries.filter((geometry): geometry is ScrollViewGeometry => geometry !== undefined);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function scrollViewGeometry(registered: unknown, origin: Box): Promise<ScrollViewGeometry | undefined> {
|
|
121
|
+
const scrollView = scrollViewOf(registered);
|
|
122
|
+
if (!scrollView) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const [viewport, content] = await Promise.all([
|
|
127
|
+
measureView(scrollView.getNativeScrollRef?.() ?? scrollView),
|
|
128
|
+
measureView(scrollView.getInnerViewRef?.()),
|
|
129
|
+
]);
|
|
130
|
+
if (!viewport || !content) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {scrollView, viewport: relativeTo(viewport, origin), content: relativeTo(content, origin)};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The scroll views around the target, innermost first. A scroll view counts when the target is
|
|
139
|
+
* one of its descendants, where React Native can tell: a header button above a page that has
|
|
140
|
+
* been scrolled is not inside the page, even though the page's content reaches behind it.
|
|
141
|
+
* Otherwise it counts when its content contains the target, or another scroll view around it (a
|
|
142
|
+
* target at the end of a horizontal list below the fold is inside the list, the list inside the page).
|
|
143
|
+
*/
|
|
144
|
+
async function containingScrollViews(
|
|
145
|
+
located: LocatedTarget,
|
|
146
|
+
geometries: readonly ScrollViewGeometry[],
|
|
147
|
+
): Promise<ScrollViewGeometry[]> {
|
|
148
|
+
const ancestry = await Promise.all(
|
|
149
|
+
geometries.map(async (geometry) => isDescendant(located.instance, geometry.scrollView.getInnerViewRef?.())),
|
|
150
|
+
);
|
|
151
|
+
const around = new Set(geometries.filter((_geometry, index) => ancestry[index] === true));
|
|
152
|
+
const unknown = geometries.filter((_geometry, index) => ancestry[index] === undefined);
|
|
153
|
+
let isGrowing = true;
|
|
154
|
+
while (isGrowing) {
|
|
155
|
+
isGrowing = false;
|
|
156
|
+
for (const geometry of unknown) {
|
|
157
|
+
const contains =
|
|
158
|
+
!around.has(geometry) &&
|
|
159
|
+
(containsBox(geometry.content, located.rect, 1) ||
|
|
160
|
+
[...around].some((inner) => containsBox(geometry.content, inner.viewport, 1)));
|
|
161
|
+
if (contains) {
|
|
162
|
+
around.add(geometry);
|
|
163
|
+
isGrowing = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return innermostFirst([...around]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The part of `box` not covered by insets (system bars, the keyboard); all of it when nothing of it is uncovered. */
|
|
172
|
+
function unobstructedPart(box: Box): Box {
|
|
173
|
+
const size = env.getHost()?.size ?? {width: 0, height: 0};
|
|
174
|
+
const insets = env.getInsets();
|
|
175
|
+
const screen = {
|
|
176
|
+
x: insets.left,
|
|
177
|
+
y: insets.top,
|
|
178
|
+
width: size.width - insets.left - insets.right,
|
|
179
|
+
height: size.height - insets.top - insets.bottom,
|
|
180
|
+
};
|
|
181
|
+
const visible = intersect(box, screen);
|
|
182
|
+
return visible && visible.width > 0 && visible.height > 0 ? visible : box;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function scrollViewMetrics(scrollView: ScrollViewInstance): Promise<ScrollContainerMetrics | undefined> {
|
|
186
|
+
const origin = await hostOrigin();
|
|
187
|
+
const current = origin ? await scrollViewGeometry(scrollView, origin) : undefined;
|
|
188
|
+
if (!current) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const offset = {x: current.viewport.x - current.content.x, y: current.viewport.y - current.content.y};
|
|
193
|
+
const axes = scrollAxes(current);
|
|
194
|
+
const end = {
|
|
195
|
+
x: current.content.width - current.viewport.width,
|
|
196
|
+
y: current.content.height - current.viewport.height,
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
// The part the user can see: scroll views of edge-to-edge apps reach behind the keyboard and
|
|
200
|
+
// the system bars, and a target scrolled there would still be hidden.
|
|
201
|
+
area: unobstructedPart(current.viewport),
|
|
202
|
+
offset,
|
|
203
|
+
// Content can rest away from offset 0 (content insets under a translucent header, margins,
|
|
204
|
+
// content centred on a tablet), so the offset can be negative. An axis the scroll view does
|
|
205
|
+
// not scroll stays where it is.
|
|
206
|
+
minOffset: {x: axes.x ? Math.min(0, offset.x) : offset.x, y: axes.y ? Math.min(0, offset.y) : offset.y},
|
|
207
|
+
maxOffset: {x: axes.x ? Math.max(offset.x, end.x) : offset.x, y: axes.y ? Math.max(offset.y, end.y) : offset.y},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function scrollContainer(scrollView: ScrollViewInstance): ScrollContainer {
|
|
212
|
+
return {
|
|
213
|
+
measure: async () => scrollViewMetrics(scrollView),
|
|
214
|
+
async scrollTo(offset, {animated, signal}): Promise<void> {
|
|
215
|
+
const start = await scrollViewMetrics(scrollView);
|
|
216
|
+
scrollView.scrollTo?.({x: offset.x, y: offset.y, animated});
|
|
217
|
+
if (!start) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Scroll views report no "scroll end" for programmatic scrolls, and measurements cannot be
|
|
222
|
+
// trusted to move either: on the New Architecture they come from the shadow tree, which iOS
|
|
223
|
+
// only updates when a scroll animation has finished. So wait until the scroll view measures
|
|
224
|
+
// at the offset it was sent to, on the axes it was asked to move. Content that rests away
|
|
225
|
+
// from offset 0 can stop a little short: then it counts as done once it moved and stopped.
|
|
226
|
+
const moves = {
|
|
227
|
+
x: Math.abs(offset.x - start.offset.x) > arrivalTolerance,
|
|
228
|
+
y: Math.abs(offset.y - start.offset.y) > arrivalTolerance,
|
|
229
|
+
};
|
|
230
|
+
let previous = start.offset;
|
|
231
|
+
let stillFrames = 0;
|
|
232
|
+
let hasMoved = false;
|
|
233
|
+
while (!signal.aborted) {
|
|
234
|
+
// eslint-disable-next-line no-await-in-loop -- frame by frame
|
|
235
|
+
await nextFrame(signal);
|
|
236
|
+
// eslint-disable-next-line no-await-in-loop -- see above
|
|
237
|
+
const current = await scrollViewMetrics(scrollView);
|
|
238
|
+
if (signal.aborted || !current) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const hasArrived =
|
|
243
|
+
(!moves.x || Math.abs(current.offset.x - offset.x) <= arrivalTolerance) &&
|
|
244
|
+
(!moves.y || Math.abs(current.offset.y - offset.y) <= arrivalTolerance);
|
|
245
|
+
const isStill =
|
|
246
|
+
Math.abs(current.offset.x - previous.x) < 0.5 && Math.abs(current.offset.y - previous.y) < 0.5;
|
|
247
|
+
hasMoved ||= !isStill;
|
|
248
|
+
stillFrames = isStill ? stillFrames + 1 : 0;
|
|
249
|
+
previous = current.offset;
|
|
250
|
+
if (hasArrived || (hasMoved && stillFrames >= stillFramesAfterMoving)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Measures a target that cannot be measured directly through a scroll view's content: on the
|
|
260
|
+
* Old Architecture, views the layout flattened away and list items scrolled out of the visible
|
|
261
|
+
* window cannot be measured on screen, but their position in the content is known.
|
|
262
|
+
*/
|
|
263
|
+
async function measureThroughScrollViews(
|
|
264
|
+
instance: unknown,
|
|
265
|
+
geometries: readonly ScrollViewGeometry[],
|
|
266
|
+
): Promise<Box | undefined> {
|
|
267
|
+
for (const geometry of geometries) {
|
|
268
|
+
// eslint-disable-next-line no-await-in-loop -- the first scroll view that knows the target is enough
|
|
269
|
+
const position = await measureLayoutIn(instance, geometry.scrollView.getInnerViewRef?.());
|
|
270
|
+
if (position && (position.width > 0 || position.height > 0)) {
|
|
271
|
+
return {...position, x: geometry.content.x + position.x, y: geometry.content.y + position.y};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function locateTarget(target: TourTarget, origin: Box): Promise<LocatedTarget | undefined> {
|
|
279
|
+
const screen = {x: 0, y: 0, width: origin.width, height: origin.height};
|
|
280
|
+
let geometries: ScrollViewGeometry[] | undefined;
|
|
281
|
+
// Several components can register the same id (tabs that stay mounted, two screens during a
|
|
282
|
+
// transition): prefer the newest one that is actually on screen.
|
|
283
|
+
let fallback: LocatedTarget | undefined;
|
|
284
|
+
for (const instance of candidates(target)) {
|
|
285
|
+
// eslint-disable-next-line no-await-in-loop -- candidates are checked in priority order
|
|
286
|
+
const measured = await measureView(instance);
|
|
287
|
+
let rect = measured ? relativeTo(measured, origin) : undefined;
|
|
288
|
+
if (!rect) {
|
|
289
|
+
// eslint-disable-next-line no-await-in-loop -- only when measuring directly failed
|
|
290
|
+
geometries ??= await registeredScrollViews(origin);
|
|
291
|
+
// eslint-disable-next-line no-await-in-loop -- see above
|
|
292
|
+
rect = await measureThroughScrollViews(instance, geometries);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!rect) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (intersect(rect, screen)) {
|
|
300
|
+
return {instance, rect, geometries};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fallback ??= {instance, rect, geometries};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return fallback;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function measureTarget(target: TourTarget): Promise<TargetMeasurement | undefined> {
|
|
310
|
+
const origin = await hostOrigin();
|
|
311
|
+
const located = origin ? await locateTarget(target, origin) : undefined;
|
|
312
|
+
if (!origin || !located) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const containers = await containingScrollViews(
|
|
317
|
+
located,
|
|
318
|
+
located.geometries ?? (await registeredScrollViews(origin)),
|
|
319
|
+
);
|
|
320
|
+
let clip: Box | undefined;
|
|
321
|
+
for (const container of containers) {
|
|
322
|
+
clip = narrowClip(clip, container.viewport);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {rect: located.rect, clip};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
measure: measureTarget,
|
|
330
|
+
|
|
331
|
+
subscribe(_target, onChange): () => void {
|
|
332
|
+
// Views report no layout events to the outside; registrations, the host layer, the keyboard
|
|
333
|
+
// and safe-area changes do.
|
|
334
|
+
const unsubscribeLayout = env.subscribeLayout(onChange);
|
|
335
|
+
const unsubscribeRegistry = env.registry.subscribe(onChange);
|
|
336
|
+
return () => {
|
|
337
|
+
unsubscribeLayout();
|
|
338
|
+
unsubscribeRegistry();
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
// A registered view (or a ref) that cannot be measured yet has not been laid out: nothing
|
|
343
|
+
// announces when it is, so the tour checks every frame.
|
|
344
|
+
isAwaitingLayout: (target) => typeof target !== 'string' || env.registry.targets(target).length > 0,
|
|
345
|
+
|
|
346
|
+
async scrollContainers(target): Promise<readonly ScrollContainer[]> {
|
|
347
|
+
const origin = await hostOrigin();
|
|
348
|
+
const located = origin ? await locateTarget(target, origin) : undefined;
|
|
349
|
+
if (!origin || !located) {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const chain = await containingScrollViews(located, located.geometries ?? (await registeredScrollViews(origin)));
|
|
354
|
+
return chain.map(({scrollView}) => scrollContainer(scrollView));
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
getViewport(): Viewport {
|
|
358
|
+
const size = env.getHost()?.size ?? {width: 0, height: 0};
|
|
359
|
+
return {rect: {x: 0, y: 0, width: size.width, height: size.height}, insets: env.getInsets()};
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
reducedMotion: env.reducedMotion,
|
|
363
|
+
|
|
364
|
+
measuresWhileScrolling: false,
|
|
365
|
+
|
|
366
|
+
getPixelRatio: env.getPixelRatio,
|
|
367
|
+
};
|
|
368
|
+
}
|