@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,185 @@
|
|
|
1
|
+
import type {Box, Insets, Point, Size} from './geometry.type.js';
|
|
2
|
+
|
|
3
|
+
export const zeroInsets: Insets = {top: 0, right: 0, bottom: 0, left: 0};
|
|
4
|
+
|
|
5
|
+
/** Returns `value` if it is a finite number, otherwise `fallback`. */
|
|
6
|
+
export function finiteOr(value: number, fallback: number): number {
|
|
7
|
+
return Number.isFinite(value) ? value : fallback;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Finite and non-negative, otherwise 0. */
|
|
11
|
+
export function nonNegative(value: number): number {
|
|
12
|
+
return Math.max(0, finiteOr(value, 0));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
16
|
+
return Math.min(Math.max(value, min), max);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function right(box: Box): number {
|
|
20
|
+
return box.x + box.width;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function bottom(box: Box): number {
|
|
24
|
+
return box.y + box.height;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function centerX(box: Box): number {
|
|
28
|
+
return box.x + box.width / 2;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function centerY(box: Box): number {
|
|
32
|
+
return box.y + box.height / 2;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Coordinates beyond this magnitude are clamped so that sums of coordinates and sizes can never
|
|
37
|
+
* overflow to `Infinity`. No real screen comes anywhere near it.
|
|
38
|
+
*/
|
|
39
|
+
const coordinateLimit = 1e9;
|
|
40
|
+
|
|
41
|
+
function coordinate(value: number): number {
|
|
42
|
+
return clamp(finiteOr(value, 0), -coordinateLimit, coordinateLimit);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sanitizeLength(value: number): number {
|
|
46
|
+
return Math.min(nonNegative(value), coordinateLimit);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Replaces non-finite coordinates with 0, negative sizes with 0 and clamps absurd magnitudes. */
|
|
50
|
+
export function sanitizeBox(box: Box): Box {
|
|
51
|
+
return {
|
|
52
|
+
x: coordinate(box.x),
|
|
53
|
+
y: coordinate(box.y),
|
|
54
|
+
width: sanitizeLength(box.width),
|
|
55
|
+
height: sanitizeLength(box.height),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function sanitizeSize(size: Size): Size {
|
|
60
|
+
return {width: sanitizeLength(size.width), height: sanitizeLength(size.height)};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function sanitizeInsets(insets: Insets): Insets {
|
|
64
|
+
return {
|
|
65
|
+
top: sanitizeLength(insets.top),
|
|
66
|
+
right: sanitizeLength(insets.right),
|
|
67
|
+
bottom: sanitizeLength(insets.bottom),
|
|
68
|
+
left: sanitizeLength(insets.left),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isFiniteBox(box: Box): boolean {
|
|
73
|
+
return Number.isFinite(box.x) && Number.isFinite(box.y) && Number.isFinite(box.width) && Number.isFinite(box.height);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function inflate(box: Box, amount: number): Box {
|
|
77
|
+
return {x: box.x - amount, y: box.y - amount, width: box.width + amount * 2, height: box.height + amount * 2};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function deflate(box: Box, insets: Insets): Box {
|
|
81
|
+
return {
|
|
82
|
+
x: box.x + insets.left,
|
|
83
|
+
y: box.y + insets.top,
|
|
84
|
+
width: Math.max(0, box.width - insets.left - insets.right),
|
|
85
|
+
height: Math.max(0, box.height - insets.top - insets.bottom),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Intersection of two boxes, or `undefined` when they do not overlap. Touching edges count as an
|
|
91
|
+
* empty (zero-area) intersection and also return `undefined`, except when one of the boxes is
|
|
92
|
+
* itself degenerate (a zero-size target is "visible" when it lies inside the other box).
|
|
93
|
+
*/
|
|
94
|
+
export function intersect(a: Box, b: Box): Box | undefined {
|
|
95
|
+
const x1 = Math.max(a.x, b.x);
|
|
96
|
+
const y1 = Math.max(a.y, b.y);
|
|
97
|
+
const x2 = Math.min(right(a), right(b));
|
|
98
|
+
const y2 = Math.min(bottom(a), bottom(b));
|
|
99
|
+
if (x2 < x1 || y2 < y1) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const degenerate = a.width === 0 || a.height === 0 || b.width === 0 || b.height === 0;
|
|
104
|
+
if (!degenerate && (x2 === x1 || y2 === y1)) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {x: x1, y: y1, width: x2 - x1, height: y2 - y1};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Collapses `box` onto the nearest edge of `area` when the two do not overlap (a zero-size box on that edge). */
|
|
112
|
+
export function projectOnto(box: Box, area: Box): Box {
|
|
113
|
+
const x1 = clamp(box.x, area.x, right(area));
|
|
114
|
+
const x2 = clamp(right(box), area.x, right(area));
|
|
115
|
+
const y1 = clamp(box.y, area.y, bottom(area));
|
|
116
|
+
const y2 = clamp(bottom(box), area.y, bottom(area));
|
|
117
|
+
return {x: x1, y: y1, width: x2 - x1, height: y2 - y1};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Narrows a clip area by another clipping box. When the two do not overlap, the result is an empty
|
|
122
|
+
* box on the edge of `box` nearest to `clip`, so it still tells in which direction the content is.
|
|
123
|
+
*/
|
|
124
|
+
export function narrowClip(clip: Box | undefined, box: Box): Box {
|
|
125
|
+
if (!clip) {
|
|
126
|
+
return box;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return intersect(clip, box) ?? projectOnto(clip, box);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function containsPoint(box: Box, point: Point): boolean {
|
|
133
|
+
return point.x >= box.x && point.x <= right(box) && point.y >= box.y && point.y <= bottom(box);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function containsBox(outer: Box, inner: Box, epsilon = 0): boolean {
|
|
137
|
+
return (
|
|
138
|
+
inner.x >= outer.x - epsilon &&
|
|
139
|
+
inner.y >= outer.y - epsilon &&
|
|
140
|
+
right(inner) <= right(outer) + epsilon &&
|
|
141
|
+
bottom(inner) <= bottom(outer) + epsilon
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function boxesEqual(a: Box | undefined, b: Box | undefined, epsilon = 0.01): boolean {
|
|
146
|
+
if (a === b) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!a || !b) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
Math.abs(a.x - b.x) <= epsilon &&
|
|
156
|
+
Math.abs(a.y - b.y) <= epsilon &&
|
|
157
|
+
Math.abs(a.width - b.width) <= epsilon &&
|
|
158
|
+
Math.abs(a.height - b.height) <= epsilon
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function sizesEqual(a: Size | undefined, b: Size | undefined, epsilon = 0.01): boolean {
|
|
163
|
+
if (a === b) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!a || !b) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return Math.abs(a.width - b.width) <= epsilon && Math.abs(a.height - b.height) <= epsilon;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function insetsEqual(a: Insets, b: Insets, epsilon = 0.01): boolean {
|
|
175
|
+
return (
|
|
176
|
+
Math.abs(a.top - b.top) <= epsilon &&
|
|
177
|
+
Math.abs(a.right - b.right) <= epsilon &&
|
|
178
|
+
Math.abs(a.bottom - b.bottom) <= epsilon &&
|
|
179
|
+
Math.abs(a.left - b.left) <= epsilon
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function pointsEqual(a: Point, b: Point, epsilon = 0.01): boolean {
|
|
184
|
+
return Math.abs(a.x - b.x) <= epsilon && Math.abs(a.y - b.y) <= epsilon;
|
|
185
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bottom,
|
|
3
|
+
centerX,
|
|
4
|
+
centerY,
|
|
5
|
+
clamp,
|
|
6
|
+
deflate,
|
|
7
|
+
finiteOr,
|
|
8
|
+
inflate,
|
|
9
|
+
intersect,
|
|
10
|
+
projectOnto,
|
|
11
|
+
right,
|
|
12
|
+
sanitizeBox,
|
|
13
|
+
sanitizeInsets,
|
|
14
|
+
sanitizeLength,
|
|
15
|
+
sanitizeSize,
|
|
16
|
+
} from './box.util.js';
|
|
17
|
+
import type {Box, Side, Size, Viewport} from './geometry.type.js';
|
|
18
|
+
|
|
19
|
+
export type LayoutPlacement = Side | 'center';
|
|
20
|
+
|
|
21
|
+
export type TooltipLayoutInput = {
|
|
22
|
+
/** The target in layer coordinates, or `undefined` for a centred step. */
|
|
23
|
+
readonly target: Box | undefined;
|
|
24
|
+
/** The tooltip's measured size (already rendered with `maxWidth`/`maxHeight` applied). */
|
|
25
|
+
readonly tooltip: Size;
|
|
26
|
+
readonly viewport: Viewport;
|
|
27
|
+
/** Preferred side. */
|
|
28
|
+
readonly placement: Side;
|
|
29
|
+
/** Side used for the same step before; kept while it still fits so the tooltip does not flip-flop. */
|
|
30
|
+
readonly previousPlacement?: LayoutPlacement | undefined;
|
|
31
|
+
/** Distance between the highlighted area and the tooltip. Arrows must not be longer than this. */
|
|
32
|
+
readonly gap: number;
|
|
33
|
+
/** Space between the target and the edge of the highlighted area. */
|
|
34
|
+
readonly highlightPadding: number;
|
|
35
|
+
/** Minimum distance between the tooltip and the edge of the safe area. */
|
|
36
|
+
readonly viewportPadding: number;
|
|
37
|
+
/** Minimum distance between the arrow and the tooltip's corners. */
|
|
38
|
+
readonly arrowPadding: number;
|
|
39
|
+
/** When set, the position is snapped to device pixels without leaving the safe area. */
|
|
40
|
+
readonly pixelRatio?: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type TooltipLayout = {
|
|
44
|
+
readonly x: number;
|
|
45
|
+
readonly y: number;
|
|
46
|
+
/** The size the tooltip occupies; the box `x, y, width, height` always lies inside the safe area. */
|
|
47
|
+
readonly width: number;
|
|
48
|
+
readonly height: number;
|
|
49
|
+
/** Side the tooltip was placed on, or `'center'` for steps without a target. */
|
|
50
|
+
readonly placement: LayoutPlacement;
|
|
51
|
+
/** Size limits the tooltip must be rendered with. Independent of the tooltip's size, so measuring again with them is stable. */
|
|
52
|
+
readonly maxWidth: number;
|
|
53
|
+
readonly maxHeight: number;
|
|
54
|
+
/** Offset of the arrow along the tooltip edge facing the target, or `undefined` when no arrow should be drawn. */
|
|
55
|
+
readonly arrowOffset: number | undefined;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const sides: readonly Side[] = ['top', 'right', 'bottom', 'left'];
|
|
59
|
+
const knownSides = new Set<string>(sides);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sub-pixel slack when deciding whether a side has room. Without it, floating-point noise (for
|
|
63
|
+
* example from scrolling) could flip the tooltip between sides for differences of 1e-15 px.
|
|
64
|
+
*/
|
|
65
|
+
export const fitTolerance = 1e-7;
|
|
66
|
+
|
|
67
|
+
const opposite: Record<Side, Side> = {top: 'bottom', bottom: 'top', left: 'right', right: 'left'};
|
|
68
|
+
|
|
69
|
+
const perpendicular: Record<Side, readonly [Side, Side]> = {
|
|
70
|
+
top: ['right', 'left'],
|
|
71
|
+
bottom: ['right', 'left'],
|
|
72
|
+
left: ['top', 'bottom'],
|
|
73
|
+
right: ['top', 'bottom'],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function isVertical(side: Side): boolean {
|
|
77
|
+
return side === 'top' || side === 'bottom';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The area the tooltip may occupy: the viewport minus obstructed insets minus padding. On an axis
|
|
82
|
+
* where that would leave less than one pixel, the padding is dropped first and then the insets, so
|
|
83
|
+
* the result is never empty while the viewport itself has room. The result is always inside the
|
|
84
|
+
* viewport rect.
|
|
85
|
+
*/
|
|
86
|
+
export function getSafeArea(viewport: Viewport, viewportPadding: number): Box {
|
|
87
|
+
const rect = sanitizeBox(viewport.rect);
|
|
88
|
+
const insets = sanitizeInsets(viewport.insets);
|
|
89
|
+
const padding = sanitizeLength(viewportPadding);
|
|
90
|
+
|
|
91
|
+
const axis = (start: number, size: number, insetStart: number, insetEnd: number): [number, number] => {
|
|
92
|
+
const candidates: Array<[number, number]> = [
|
|
93
|
+
[start + insetStart + padding, start + size - insetEnd - padding],
|
|
94
|
+
[start + insetStart, start + size - insetEnd],
|
|
95
|
+
];
|
|
96
|
+
for (const [from, to] of candidates) {
|
|
97
|
+
if (to - from >= 1) {
|
|
98
|
+
return [from, to];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return [start, start + size];
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const [x1, x2] = axis(rect.x, rect.width, insets.left, insets.right);
|
|
106
|
+
const [y1, y2] = axis(rect.y, rect.height, insets.top, insets.bottom);
|
|
107
|
+
return {x: x1, y: y1, width: x2 - x1, height: y2 - y1};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Where to put the tooltip so it never leaves the visible, unobstructed part of the screen.
|
|
112
|
+
*
|
|
113
|
+
* Guarantees, for any input (including non-finite numbers):
|
|
114
|
+
* - the returned box lies inside the safe area (see {@link getSafeArea}), and therefore inside the viewport;
|
|
115
|
+
* - `maxWidth`/`maxHeight` depend only on the viewport, so re-measuring with them reaches a fixed point;
|
|
116
|
+
* - when any side has room, the tooltip does not overlap the highlighted area;
|
|
117
|
+
* - the preferred side wins whenever it fits, then the opposite side, then the roomier perpendicular side.
|
|
118
|
+
*/
|
|
119
|
+
export function computeTooltipLayout(input: TooltipLayoutInput): TooltipLayout {
|
|
120
|
+
const safe = getSafeArea(input.viewport, input.viewportPadding);
|
|
121
|
+
const gap = sanitizeLength(input.gap);
|
|
122
|
+
const tooltip = sanitizeSize(input.tooltip);
|
|
123
|
+
const width = Math.min(tooltip.width, safe.width);
|
|
124
|
+
const height = Math.min(tooltip.height, safe.height);
|
|
125
|
+
const base = {width, height, maxWidth: safe.width, maxHeight: safe.height};
|
|
126
|
+
|
|
127
|
+
const finish = (x: number, y: number, placement: LayoutPlacement, arrowOffset: number | undefined): TooltipLayout => {
|
|
128
|
+
const snapped = snap(x, y, input.pixelRatio);
|
|
129
|
+
return {
|
|
130
|
+
...base,
|
|
131
|
+
x: clamp(snapped.x, safe.x, right(safe) - width),
|
|
132
|
+
y: clamp(snapped.y, safe.y, bottom(safe) - height),
|
|
133
|
+
placement,
|
|
134
|
+
arrowOffset,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const rawTarget = input.target ? sanitizeBox(input.target) : undefined;
|
|
139
|
+
if (!rawTarget) {
|
|
140
|
+
return finish(safe.x + (safe.width - width) / 2, safe.y + (safe.height - height) / 2, 'center', undefined);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const anchor = inflate(rawTarget, sanitizeLength(input.highlightPadding));
|
|
144
|
+
const visible = intersect(anchor, safe);
|
|
145
|
+
const effective = visible ?? projectOnto(anchor, safe);
|
|
146
|
+
|
|
147
|
+
const space: Record<Side, number> = {
|
|
148
|
+
top: effective.y - safe.y - gap,
|
|
149
|
+
bottom: bottom(safe) - bottom(effective) - gap,
|
|
150
|
+
left: effective.x - safe.x - gap,
|
|
151
|
+
right: right(safe) - right(effective) - gap,
|
|
152
|
+
};
|
|
153
|
+
const room = (side: Side): number => space[side] - (isVertical(side) ? height : width);
|
|
154
|
+
const fits = (side: Side): boolean => room(side) >= -fitTolerance;
|
|
155
|
+
|
|
156
|
+
const preferred: Side = knownSides.has(input.placement) ? input.placement : 'bottom';
|
|
157
|
+
const order: Side[] = [preferred, opposite[preferred], ...roomiestFirst(perpendicular[preferred], room)];
|
|
158
|
+
const previous = input.previousPlacement;
|
|
159
|
+
if (previous && previous !== 'center' && previous !== preferred) {
|
|
160
|
+
order.unshift(previous);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const chosen = order.find((side) => fits(side));
|
|
164
|
+
const side = chosen ?? roomiestFirst(order, room)[0] ?? preferred;
|
|
165
|
+
const position = placeOnSide({side, anchor: effective, size: {width, height}, gap, safe});
|
|
166
|
+
|
|
167
|
+
// The arrow points at the centre of the part of the actual target the user can see.
|
|
168
|
+
const visibleTarget = intersect(
|
|
169
|
+
rawTarget,
|
|
170
|
+
deflate(sanitizeBox(input.viewport.rect), sanitizeInsets(input.viewport.insets)),
|
|
171
|
+
);
|
|
172
|
+
let arrowOffset: number | undefined;
|
|
173
|
+
if (chosen && visibleTarget) {
|
|
174
|
+
const edge = isVertical(side) ? width : height;
|
|
175
|
+
const inset = Math.min(sanitizeLength(input.arrowPadding), edge / 2);
|
|
176
|
+
const target = isVertical(side) ? centerX(visibleTarget) - position.x : centerY(visibleTarget) - position.y;
|
|
177
|
+
arrowOffset = clamp(target, inset, edge - inset);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return finish(position.x, position.y, side, arrowOffset);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Sorts sides by available room, largest first. Sides whose room differs by less than
|
|
185
|
+
* {@link fitTolerance} keep their given order, so the result does not depend on rounding noise.
|
|
186
|
+
*/
|
|
187
|
+
function roomiestFirst(candidates: readonly Side[], room: (side: Side) => number): Side[] {
|
|
188
|
+
const result: Side[] = [];
|
|
189
|
+
for (const side of candidates) {
|
|
190
|
+
const index = result.findIndex((other) => room(side) > room(other) + fitTolerance);
|
|
191
|
+
if (index === -1) {
|
|
192
|
+
result.push(side);
|
|
193
|
+
} else {
|
|
194
|
+
result.splice(index, 0, side);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
type PlaceOnSideInput = {
|
|
202
|
+
readonly side: Side;
|
|
203
|
+
readonly anchor: Box;
|
|
204
|
+
readonly size: Size;
|
|
205
|
+
readonly gap: number;
|
|
206
|
+
readonly safe: Box;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function placeOnSide({side, anchor, size, gap, safe}: PlaceOnSideInput): {x: number; y: number} {
|
|
210
|
+
const crossX = clamp(centerX(anchor) - size.width / 2, safe.x, right(safe) - size.width);
|
|
211
|
+
const crossY = clamp(centerY(anchor) - size.height / 2, safe.y, bottom(safe) - size.height);
|
|
212
|
+
switch (side) {
|
|
213
|
+
case 'top': {
|
|
214
|
+
return {x: crossX, y: anchor.y - gap - size.height};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case 'bottom': {
|
|
218
|
+
return {x: crossX, y: bottom(anchor) + gap};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
case 'left': {
|
|
222
|
+
return {x: anchor.x - gap - size.width, y: crossY};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case 'right': {
|
|
226
|
+
return {x: right(anchor) + gap, y: crossY};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function snap(x: number, y: number, pixelRatio: number | undefined): {x: number; y: number} {
|
|
232
|
+
const ratio = finiteOr(pixelRatio ?? 0, 0);
|
|
233
|
+
if (ratio <= 0) {
|
|
234
|
+
return {x, y};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {x: Math.round(x * ratio) / ratio, y: Math.round(y * ratio) / ratio};
|
|
238
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Axis-aligned box in the tour layer's coordinate space (CSS pixels on web, density-independent
|
|
3
|
+
* pixels on React Native). `x`/`y` are the top-left corner.
|
|
4
|
+
*/
|
|
5
|
+
export type Box = {
|
|
6
|
+
readonly x: number;
|
|
7
|
+
readonly y: number;
|
|
8
|
+
readonly width: number;
|
|
9
|
+
readonly height: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type Size = {
|
|
13
|
+
readonly width: number;
|
|
14
|
+
readonly height: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type Point = {
|
|
18
|
+
readonly x: number;
|
|
19
|
+
readonly y: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type Insets = {
|
|
23
|
+
readonly top: number;
|
|
24
|
+
readonly right: number;
|
|
25
|
+
readonly bottom: number;
|
|
26
|
+
readonly left: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The region the user can actually see, plus the parts of it that are obstructed
|
|
31
|
+
* (notch, home indicator, on-screen keyboard, system bars).
|
|
32
|
+
*/
|
|
33
|
+
export type Viewport = {
|
|
34
|
+
readonly rect: Box;
|
|
35
|
+
readonly insets: Insets;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type Side = 'top' | 'bottom' | 'left' | 'right';
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {bottom, right, zeroInsets} from './box.util.js';
|
|
2
|
+
import type {Box, Insets, Point, Side, Size} from './geometry.type.js';
|
|
3
|
+
|
|
4
|
+
const fitSlack = 1e-6;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How far a container has to scroll (positive: further down/right) so that the target plus a
|
|
8
|
+
* margin is inside `area` and clear of `obstacles` (elements that stay put while the content
|
|
9
|
+
* scrolls: sticky headers, floating buttons, cookie banners, any number of them).
|
|
10
|
+
*
|
|
11
|
+
* Per axis, the obstacles in the target's column (row) cut the area into free gaps. The target is
|
|
12
|
+
* moved into the gap that needs the least scrolling, so scrolling up and down behave the same. The
|
|
13
|
+
* first margin that fits is used (preferring one that fits on both axes). A target that only fits
|
|
14
|
+
* without margin is centred in the nearest gap that can hold it; a target that fits in no gap is
|
|
15
|
+
* aligned to the start of the largest one, keeping the smallest margin.
|
|
16
|
+
*/
|
|
17
|
+
export function computeRevealDelta(
|
|
18
|
+
target: Box,
|
|
19
|
+
area: Box,
|
|
20
|
+
margins: readonly Insets[],
|
|
21
|
+
obstacles: readonly Box[] = [],
|
|
22
|
+
): Point {
|
|
23
|
+
const candidates = margins.length > 0 ? margins : [zeroInsets];
|
|
24
|
+
const smallest = candidates.at(-1) ?? zeroInsets;
|
|
25
|
+
// Pages scroll vertically: the vertical gaps depend on the obstacles in the target's column, the
|
|
26
|
+
// horizontal ones on the obstacles in the row the target ends up in.
|
|
27
|
+
const ySegments = freeSegments(
|
|
28
|
+
area.y,
|
|
29
|
+
bottom(area),
|
|
30
|
+
obstacles.filter((o) => o.x < right(target) && right(o) > target.x).map((o) => [o.y, bottom(o)] as const),
|
|
31
|
+
);
|
|
32
|
+
const xSegmentsAfter = (dy: number): Segment[] =>
|
|
33
|
+
freeSegments(
|
|
34
|
+
area.x,
|
|
35
|
+
right(area),
|
|
36
|
+
obstacles
|
|
37
|
+
.filter((o) => o.y < bottom(target) - dy && bottom(o) > target.y - dy)
|
|
38
|
+
.map((o) => [o.x, right(o)] as const),
|
|
39
|
+
);
|
|
40
|
+
const yAxis = {start: target.y, size: target.height};
|
|
41
|
+
const xAxis = {start: target.x, size: target.width};
|
|
42
|
+
|
|
43
|
+
// The first margin that fits on both axes (it may leave room for the tooltip on one side).
|
|
44
|
+
for (const margin of candidates) {
|
|
45
|
+
const y = fitDelta(yAxis, ySegments, [margin.top, margin.bottom]);
|
|
46
|
+
const x = y === undefined ? undefined : fitDelta(xAxis, xSegmentsAfter(y), [margin.left, margin.right]);
|
|
47
|
+
if (y !== undefined && x !== undefined) {
|
|
48
|
+
return {x, y};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Otherwise each axis on its own.
|
|
53
|
+
const y =
|
|
54
|
+
firstDefined(candidates, (m) => fitDelta(yAxis, ySegments, [m.top, m.bottom])) ??
|
|
55
|
+
fallbackDelta(yAxis, ySegments, smallest.top);
|
|
56
|
+
const xSegments = xSegmentsAfter(y);
|
|
57
|
+
const x =
|
|
58
|
+
firstDefined(candidates, (m) => fitDelta(xAxis, xSegments, [m.left, m.right])) ??
|
|
59
|
+
fallbackDelta(xAxis, xSegments, smallest.left);
|
|
60
|
+
return {x, y};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function firstDefined<T>(items: readonly T[], map: (item: T) => number | undefined): number | undefined {
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
const value = map(item);
|
|
66
|
+
if (value !== undefined) {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type Segment = {readonly from: number; readonly to: number};
|
|
75
|
+
|
|
76
|
+
/** The parts of `[from, to]` not covered by `blocked`. Fully blocked: the whole range (nothing can be avoided). */
|
|
77
|
+
export function freeSegments(from: number, to: number, blocked: ReadonlyArray<readonly [number, number]>): Segment[] {
|
|
78
|
+
const sorted = [...blocked]
|
|
79
|
+
.map(([a, b]) => [Math.max(a, from), Math.min(b, to)] as const)
|
|
80
|
+
.filter(([a, b]) => b > a)
|
|
81
|
+
// eslint-disable-next-line unicorn/no-array-sort -- sorting a fresh copy (ES2023 `toSorted` is not available on every supported React Native version)
|
|
82
|
+
.sort((a, b) => a[0] - b[0]);
|
|
83
|
+
const segments: Segment[] = [];
|
|
84
|
+
let cursor = from;
|
|
85
|
+
for (const [a, b] of sorted) {
|
|
86
|
+
if (a > cursor) {
|
|
87
|
+
segments.push({from: cursor, to: a});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
cursor = Math.max(cursor, b);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (to > cursor) {
|
|
94
|
+
segments.push({from: cursor, to});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return segments.length > 0 ? segments : [{from, to}];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type Axis = {readonly start: number; readonly size: number};
|
|
101
|
+
|
|
102
|
+
/** Smallest movement that puts `[from, to]` inside `segment`. */
|
|
103
|
+
function deltaInto(from: number, to: number, segment: Segment): number {
|
|
104
|
+
if (from < segment.from) {
|
|
105
|
+
return from - segment.from;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return to > segment.to ? to - segment.to : 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function smallestBy<T>(items: readonly T[], score: (item: T) => number): T | undefined {
|
|
112
|
+
let best: T | undefined;
|
|
113
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
114
|
+
for (const item of items) {
|
|
115
|
+
const value = score(item);
|
|
116
|
+
if (value < bestScore) {
|
|
117
|
+
best = item;
|
|
118
|
+
bestScore = value;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return best;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The smallest movement that puts the target plus margin into a gap that can hold it, if any. */
|
|
126
|
+
function fitDelta(
|
|
127
|
+
{start, size}: Axis,
|
|
128
|
+
segments: readonly Segment[],
|
|
129
|
+
[before, after]: readonly [number, number],
|
|
130
|
+
): number | undefined {
|
|
131
|
+
const from = start - before;
|
|
132
|
+
const to = start + size + after;
|
|
133
|
+
// Sub-pixel slack: gap widths computed from coordinates lose their last bits of precision.
|
|
134
|
+
const fitting = segments.filter((segment) => segment.to - segment.from >= to - from - fitSlack);
|
|
135
|
+
const best = smallestBy(fitting, (segment) => Math.abs(deltaInto(from, to, segment)));
|
|
136
|
+
return best ? deltaInto(from, to, best) : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* When no margin fits: centre the target in the nearest gap that can hold it, or align it to the
|
|
141
|
+
* start of the largest gap (keeping `leadingMargin`) when it fits nowhere.
|
|
142
|
+
*/
|
|
143
|
+
function fallbackDelta({start, size}: Axis, segments: readonly Segment[], leadingMargin: number): number {
|
|
144
|
+
const centreDelta = (segment: Segment): number => start + size / 2 - (segment.from + segment.to) / 2;
|
|
145
|
+
const holding = segments.filter((segment) => segment.to - segment.from >= size);
|
|
146
|
+
const nearest = smallestBy(holding, (segment) => Math.abs(centreDelta(segment)));
|
|
147
|
+
if (nearest) {
|
|
148
|
+
return centreDelta(nearest);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const largest = smallestBy(segments, (segment) => -(segment.to - segment.from)) ?? {from: start, to: start};
|
|
152
|
+
return start - Math.min(leadingMargin, (largest.to - largest.from) / 2) - largest.from;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type RevealMarginInput = {
|
|
156
|
+
readonly target: Box;
|
|
157
|
+
readonly tooltip: Size;
|
|
158
|
+
/** The visible, unobstructed part of the screen. */
|
|
159
|
+
readonly area: Box;
|
|
160
|
+
/** Sides in the order the tooltip would try them. */
|
|
161
|
+
readonly sides: readonly Side[];
|
|
162
|
+
readonly gap: number;
|
|
163
|
+
readonly highlightPadding: number;
|
|
164
|
+
readonly viewportPadding: number;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Margins to keep around the target while scrolling it into view. The first entry also leaves
|
|
169
|
+
* room for the tooltip on the first side (in the given order) where target and tooltip fit on
|
|
170
|
+
* screen together. The last entry only keeps the highlight padding and viewport padding.
|
|
171
|
+
*/
|
|
172
|
+
export function buildRevealMargins(input: RevealMarginInput): Insets[] {
|
|
173
|
+
const base = input.highlightPadding + input.viewportPadding;
|
|
174
|
+
const baseMargin: Insets = {top: base, right: base, bottom: base, left: base};
|
|
175
|
+
for (const side of input.sides) {
|
|
176
|
+
const vertical = side === 'top' || side === 'bottom';
|
|
177
|
+
const room = input.gap + (vertical ? input.tooltip.height : input.tooltip.width);
|
|
178
|
+
const mainFits = vertical
|
|
179
|
+
? input.target.height + base * 2 + room <= input.area.height
|
|
180
|
+
: input.target.width + base * 2 + room <= input.area.width;
|
|
181
|
+
const crossFits = vertical
|
|
182
|
+
? input.tooltip.width + input.viewportPadding * 2 <= input.area.width
|
|
183
|
+
: input.tooltip.height + input.viewportPadding * 2 <= input.area.height;
|
|
184
|
+
if (mainFits && crossFits) {
|
|
185
|
+
return [{...baseMargin, [side]: base + room}, baseMargin];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return [baseMargin];
|
|
190
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type {Box} from './geometry.type.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How much of a target the user can see:
|
|
5
|
+
* - `full`: the whole target is visible, or, for targets larger than the area, it covers the area.
|
|
6
|
+
* - `partial`: at least half of what could be visible is visible on both axes.
|
|
7
|
+
* - `none`: anything less.
|
|
8
|
+
*/
|
|
9
|
+
export type TargetVisibility = 'full' | 'partial' | 'none';
|
|
10
|
+
|
|
11
|
+
function axisVisibility(start: number, size: number, areaStart: number, areaSize: number): TargetVisibility {
|
|
12
|
+
const end = start + size;
|
|
13
|
+
const areaEnd = areaStart + areaSize;
|
|
14
|
+
if (areaSize <= 0) {
|
|
15
|
+
return 'none';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (size === 0) {
|
|
19
|
+
return start >= areaStart && start <= areaEnd ? 'full' : 'none';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fullyInside = start >= areaStart && end <= areaEnd;
|
|
23
|
+
const covers = start <= areaStart && end >= areaEnd;
|
|
24
|
+
if (size <= areaSize ? fullyInside : covers) {
|
|
25
|
+
return 'full';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const visible = Math.max(0, Math.min(end, areaEnd) - Math.max(start, areaStart));
|
|
29
|
+
if (visible === 0 || visible < Math.min(size, areaSize) / 2) {
|
|
30
|
+
return 'none';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return 'partial';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getTargetVisibility(target: Box, area: Box): TargetVisibility {
|
|
37
|
+
const horizontal = axisVisibility(target.x, target.width, area.x, area.width);
|
|
38
|
+
const vertical = axisVisibility(target.y, target.height, area.y, area.height);
|
|
39
|
+
if (horizontal === 'none' || vertical === 'none') {
|
|
40
|
+
return 'none';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return horizontal === 'full' && vertical === 'full' ? 'full' : 'partial';
|
|
44
|
+
}
|