diff --git a/src/App.native.tsx b/src/App.native.tsx
index 81d4a870e9..dc8d29fee2 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -73,6 +73,7 @@ import {Splash} from '#/Splash'
import {BottomSheetProvider} from '../modules/bottom-sheet'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
+import {GlobalGestureEvents} from '#/state/shell/GlobalGestureEvents'
SplashScreen.preventAutoHideAsync()
if (isIOS) {
@@ -154,11 +155,13 @@ function InnerApp() {
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx
index 3e36b5af46..b5ee915cdb 100644
--- a/src/components/Tooltip/index.tsx
+++ b/src/components/Tooltip/index.tsx
@@ -1,17 +1,17 @@
import {
+ useCallback,
useMemo,
useRef,
createContext,
useContext,
useState,
- isValidElement,
} from 'react'
-import {View, Modal, Dimensions, Pressable} from 'react-native'
+import {View, Dimensions} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import flattenReactChildren from 'react-keyed-flatten-children'
import {Portal} from '#/components/Portal'
import {atoms as a, useTheme} from '#/alf'
+import {useOnInteract} from '#/state/shell/GlobalGestureEvents'
type Context = {
visible: boolean
@@ -25,7 +25,6 @@ type Context = {
}
| undefined
targetRef: React.RefObject
- targetElement: any
}
const TooltipContext = createContext({
@@ -33,7 +32,6 @@ const TooltipContext = createContext({
onVisibleChange: () => {},
targetMeasurements: undefined,
targetRef: {current: null},
- targetElement: null,
})
export function Outer({
@@ -80,11 +78,6 @@ export function Outer({
onVisibleChange,
targetMeasurements: measurements,
targetRef,
- targetElement: flattenReactChildren(children).find(child => {
- if (isValidElement(child) && child.type === Target) {
- return true
- }
- }),
}),
[visible, onVisibleChange, measurements, targetRef],
)
@@ -109,9 +102,35 @@ export function Target({
const TIP_SIZE = 12
export function Content({children}: {children: React.ReactNode}) {
- const t = useTheme()
- const {visible, onVisibleChange, targetMeasurements, targetElement} =
+ const {visible, onVisibleChange, targetMeasurements} =
useContext(TooltipContext)
+ const requestClose = useCallback(() => {
+ onVisibleChange(false)
+ }, [onVisibleChange])
+
+ if (!visible || !targetMeasurements) return null
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+function Tooltip({
+ children,
+ requestClose,
+ targetMeasurements,
+}: {
+ children: React.ReactNode
+ requestClose: () => void
+ targetMeasurements: Context['targetMeasurements']
+}) {
+ const t = useTheme()
const [ready, setReady] = useState(false)
const [measurements, setMeasurements] = useState<
| {
@@ -121,18 +140,13 @@ export function Content({children}: {children: React.ReactNode}) {
| undefined
>(undefined)
const safe = useSafeAreaInsets()
-
- const requestClose = () => {
- onVisibleChange(false)
- setReady(false)
- setMeasurements(undefined)
- }
-
- const {positionTop, contentLeft, tipTop, tipLeft} = useMemo(() => {
+ const coords = useMemo(() => {
if (!targetMeasurements || !measurements)
return {
- positionTop: 0,
- contentLeft: 0,
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
tipTop: 0,
tipLeft: 0,
}
@@ -145,118 +159,98 @@ export function Content({children}: {children: React.ReactNode}) {
const minLeft = a.px_xl.paddingLeft
const maxLeft = ww - minLeft
- let positionTop = targetMeasurements.y + targetMeasurements.height
- let contentLeft = Math.max(
+ let top = targetMeasurements.y + targetMeasurements.height
+ let left = Math.max(
minLeft,
targetMeasurements.x + targetMeasurements.width / 2 - cw / 2,
)
let tipTop = (TIP_SIZE / 2) * -1
- let tipLeft =
- targetMeasurements.x + targetMeasurements.width / 2 - TIP_SIZE / 2
- if (contentLeft + cw > maxLeft) {
- contentLeft -= contentLeft + cw - maxLeft
+ if (left + cw > maxLeft) {
+ left -= left + cw - maxLeft
}
- positionTop += TIP_SIZE / 3
+ let tipLeft =
+ targetMeasurements.x - left + targetMeasurements.width / 2 - TIP_SIZE / 2
+
+ top += TIP_SIZE / 3
return {
- positionTop,
- contentLeft,
+ top,
+ bottom: top + ch,
+ left,
+ right: left + cw,
tipTop,
tipLeft,
}
}, [targetMeasurements, measurements, safe])
- if (!visible || !targetMeasurements) return null
+ const requestCloseWrapped = useCallback(() => {
+ setReady(false)
+ setMeasurements(undefined)
+ requestClose()
+ }, [requestClose])
+
+ useOnInteract(
+ useCallback(
+ e => {
+ const {x, y} = e
+ const isInside =
+ x > coords.left &&
+ x < coords.right &&
+ y > coords.top &&
+ y < coords.bottom
+
+ if (!isInside) {
+ requestCloseWrapped()
+ }
+ },
+ [coords, requestCloseWrapped],
+ ),
+ )
return (
-
- {}} // TODO
- style={[a.absolute, a.inset_0]}>
- {/* Backdrop */}
-
+
+ {/* The triangle "tip" */}
+
- {/* Replica of the target element */}
-
- {targetElement}
-
-
- {/* Outer content container */}
-
- {/* The triangle "tip" */}
-
-
- {/* The content itself */}
- {
- setReady(true)
- setMeasurements({
- width: e.nativeEvent.layout.width,
- height: e.nativeEvent.layout.height,
- })
- }}>
- {children}
-
-
-
-
+ {/* The content itself */}
+ {
+ setMeasurements({
+ width: e.nativeEvent.layout.width,
+ height: e.nativeEvent.layout.height,
+ })
+ setReady(true)
+ }}>
+ {children}
+
+
)
}
diff --git a/src/state/shell/GlobalGestureEvents.tsx b/src/state/shell/GlobalGestureEvents.tsx
new file mode 100644
index 0000000000..8c3c4eb0c7
--- /dev/null
+++ b/src/state/shell/GlobalGestureEvents.tsx
@@ -0,0 +1,86 @@
+import {createContext, useContext, useMemo, useEffect, useRef, useState} from 'react'
+import EventEmitter from 'eventemitter3'
+import {
+ GestureDetector,
+ Gesture,
+ GestureStateChangeEvent,
+ GestureUpdateEvent,
+ PanGestureHandlerEventPayload,
+} from 'react-native-gesture-handler'
+
+const events = new EventEmitter<{
+ begin: GestureStateChangeEvent
+ update: GestureUpdateEvent
+ end: GestureStateChangeEvent
+ finalize: GestureStateChangeEvent
+}>()
+
+const Context = createContext<{
+ register: () => void
+ unregister: () => void
+}>({
+ register: () => {},
+ unregister: () => {},
+})
+
+export function GlobalGestureEvents({
+ children,
+}: {
+ children: React.ReactNode
+}): React.ReactElement {
+ const refCount = useRef(0)
+ const [enabled, setEnabled] = useState(false)
+ const ctx = useMemo(() => ({
+ register() {
+ refCount.current += 1
+ if (refCount.current === 1) {
+ setEnabled(true)
+ }
+ },
+ unregister() {
+ refCount.current -= 1
+ if (refCount.current === 0) {
+ setEnabled(false)
+ }
+ },
+ }), [setEnabled])
+ const gesture = Gesture.Pan()
+ .runOnJS(true)
+ .enabled(enabled)
+ .simultaneousWithExternalGesture()
+ .onBegin(e => {
+ events.emit('begin', e)
+ })
+ .onUpdate(e => {
+ events.emit('update', e)
+ })
+ .onEnd(e => {
+ events.emit('end', e)
+ })
+ .onFinalize(e => {
+ events.emit('finalize', e)
+ })
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useOnInteract(
+ onInteract: (
+ e: GestureStateChangeEvent,
+ ) => void,
+) {
+ const ctx = useContext(Context)
+ useEffect(() => {
+ ctx.register()
+ events.on('begin', onInteract)
+
+ return () => {
+ ctx.unregister()
+ events.off('begin', onInteract)
+ }
+ }, [ctx, onInteract])
+}