Ok working with no overlay and global gesture handler
This commit is contained in:
+8
-5
@@ -73,6 +73,7 @@ import {Splash} from '#/Splash'
|
|||||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
|
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
|
||||||
|
import {GlobalGestureEvents} from '#/state/shell/GlobalGestureEvents'
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync()
|
SplashScreen.preventAutoHideAsync()
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
@@ -154,11 +155,13 @@ function InnerApp() {
|
|||||||
<HideBottomBarBorderProvider>
|
<HideBottomBarBorderProvider>
|
||||||
<GestureHandlerRootView
|
<GestureHandlerRootView
|
||||||
style={s.h100pct}>
|
style={s.h100pct}>
|
||||||
<IntentDialogProvider>
|
<GlobalGestureEvents>
|
||||||
<TestCtrls />
|
<IntentDialogProvider>
|
||||||
<Shell />
|
<TestCtrls />
|
||||||
<NuxDialogs />
|
<Shell />
|
||||||
</IntentDialogProvider>
|
<NuxDialogs />
|
||||||
|
</IntentDialogProvider>
|
||||||
|
</GlobalGestureEvents>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
</HideBottomBarBorderProvider>
|
</HideBottomBarBorderProvider>
|
||||||
</ServiceAccountManager>
|
</ServiceAccountManager>
|
||||||
|
|||||||
+112
-118
@@ -1,17 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
createContext,
|
createContext,
|
||||||
useContext,
|
useContext,
|
||||||
useState,
|
useState,
|
||||||
isValidElement,
|
|
||||||
} from 'react'
|
} 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 {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import flattenReactChildren from 'react-keyed-flatten-children'
|
|
||||||
|
|
||||||
import {Portal} from '#/components/Portal'
|
import {Portal} from '#/components/Portal'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {useOnInteract} from '#/state/shell/GlobalGestureEvents'
|
||||||
|
|
||||||
type Context = {
|
type Context = {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
@@ -25,7 +25,6 @@ type Context = {
|
|||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
targetRef: React.RefObject<View>
|
targetRef: React.RefObject<View>
|
||||||
targetElement: any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TooltipContext = createContext<Context>({
|
const TooltipContext = createContext<Context>({
|
||||||
@@ -33,7 +32,6 @@ const TooltipContext = createContext<Context>({
|
|||||||
onVisibleChange: () => {},
|
onVisibleChange: () => {},
|
||||||
targetMeasurements: undefined,
|
targetMeasurements: undefined,
|
||||||
targetRef: {current: null},
|
targetRef: {current: null},
|
||||||
targetElement: null,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export function Outer({
|
export function Outer({
|
||||||
@@ -80,11 +78,6 @@ export function Outer({
|
|||||||
onVisibleChange,
|
onVisibleChange,
|
||||||
targetMeasurements: measurements,
|
targetMeasurements: measurements,
|
||||||
targetRef,
|
targetRef,
|
||||||
targetElement: flattenReactChildren(children).find(child => {
|
|
||||||
if (isValidElement(child) && child.type === Target) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
[visible, onVisibleChange, measurements, targetRef],
|
[visible, onVisibleChange, measurements, targetRef],
|
||||||
)
|
)
|
||||||
@@ -109,9 +102,35 @@ export function Target({
|
|||||||
const TIP_SIZE = 12
|
const TIP_SIZE = 12
|
||||||
|
|
||||||
export function Content({children}: {children: React.ReactNode}) {
|
export function Content({children}: {children: React.ReactNode}) {
|
||||||
const t = useTheme()
|
const {visible, onVisibleChange, targetMeasurements} =
|
||||||
const {visible, onVisibleChange, targetMeasurements, targetElement} =
|
|
||||||
useContext(TooltipContext)
|
useContext(TooltipContext)
|
||||||
|
const requestClose = useCallback(() => {
|
||||||
|
onVisibleChange(false)
|
||||||
|
}, [onVisibleChange])
|
||||||
|
|
||||||
|
if (!visible || !targetMeasurements) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Tooltip
|
||||||
|
targetMeasurements={targetMeasurements}
|
||||||
|
requestClose={requestClose}>
|
||||||
|
{children}
|
||||||
|
</Tooltip>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tooltip({
|
||||||
|
children,
|
||||||
|
requestClose,
|
||||||
|
targetMeasurements,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
requestClose: () => void
|
||||||
|
targetMeasurements: Context['targetMeasurements']
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
const [ready, setReady] = useState(false)
|
const [ready, setReady] = useState(false)
|
||||||
const [measurements, setMeasurements] = useState<
|
const [measurements, setMeasurements] = useState<
|
||||||
| {
|
| {
|
||||||
@@ -121,18 +140,13 @@ export function Content({children}: {children: React.ReactNode}) {
|
|||||||
| undefined
|
| undefined
|
||||||
>(undefined)
|
>(undefined)
|
||||||
const safe = useSafeAreaInsets()
|
const safe = useSafeAreaInsets()
|
||||||
|
const coords = useMemo(() => {
|
||||||
const requestClose = () => {
|
|
||||||
onVisibleChange(false)
|
|
||||||
setReady(false)
|
|
||||||
setMeasurements(undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {positionTop, contentLeft, tipTop, tipLeft} = useMemo(() => {
|
|
||||||
if (!targetMeasurements || !measurements)
|
if (!targetMeasurements || !measurements)
|
||||||
return {
|
return {
|
||||||
positionTop: 0,
|
top: 0,
|
||||||
contentLeft: 0,
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
tipTop: 0,
|
tipTop: 0,
|
||||||
tipLeft: 0,
|
tipLeft: 0,
|
||||||
}
|
}
|
||||||
@@ -145,118 +159,98 @@ export function Content({children}: {children: React.ReactNode}) {
|
|||||||
const minLeft = a.px_xl.paddingLeft
|
const minLeft = a.px_xl.paddingLeft
|
||||||
const maxLeft = ww - minLeft
|
const maxLeft = ww - minLeft
|
||||||
|
|
||||||
let positionTop = targetMeasurements.y + targetMeasurements.height
|
let top = targetMeasurements.y + targetMeasurements.height
|
||||||
let contentLeft = Math.max(
|
let left = Math.max(
|
||||||
minLeft,
|
minLeft,
|
||||||
targetMeasurements.x + targetMeasurements.width / 2 - cw / 2,
|
targetMeasurements.x + targetMeasurements.width / 2 - cw / 2,
|
||||||
)
|
)
|
||||||
let tipTop = (TIP_SIZE / 2) * -1
|
let tipTop = (TIP_SIZE / 2) * -1
|
||||||
let tipLeft =
|
|
||||||
targetMeasurements.x + targetMeasurements.width / 2 - TIP_SIZE / 2
|
|
||||||
|
|
||||||
if (contentLeft + cw > maxLeft) {
|
if (left + cw > maxLeft) {
|
||||||
contentLeft -= contentLeft + 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 {
|
return {
|
||||||
positionTop,
|
top,
|
||||||
contentLeft,
|
bottom: top + ch,
|
||||||
|
left,
|
||||||
|
right: left + cw,
|
||||||
tipTop,
|
tipTop,
|
||||||
tipLeft,
|
tipLeft,
|
||||||
}
|
}
|
||||||
}, [targetMeasurements, measurements, safe])
|
}, [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 (
|
return (
|
||||||
<Portal>
|
<View
|
||||||
<Modal
|
style={[
|
||||||
transparent
|
a.absolute,
|
||||||
onRequestClose={() => {}} // TODO
|
a.align_start,
|
||||||
style={[a.absolute, a.inset_0]}>
|
{
|
||||||
{/* Backdrop */}
|
width: 200,
|
||||||
<Pressable
|
opacity: ready ? 1 : 0,
|
||||||
style={[
|
top: coords.top,
|
||||||
a.absolute,
|
left: coords.left,
|
||||||
a.inset_0,
|
},
|
||||||
t.atoms.bg,
|
]}>
|
||||||
{
|
{/* The triangle "tip" */}
|
||||||
opacity: 0.4,
|
<View
|
||||||
},
|
style={[
|
||||||
]}
|
a.absolute,
|
||||||
onPress={requestClose}
|
a.top_0,
|
||||||
/>
|
a.z_10,
|
||||||
|
t.atoms.bg,
|
||||||
|
{
|
||||||
|
borderTopLeftRadius: a.rounded_xs.borderRadius,
|
||||||
|
width: TIP_SIZE,
|
||||||
|
height: TIP_SIZE,
|
||||||
|
transform: [{rotate: '45deg'}],
|
||||||
|
top: coords.tipTop,
|
||||||
|
left: coords.tipLeft,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Replica of the target element */}
|
{/* The content itself */}
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[a.px_md, a.py_sm, a.rounded_sm, t.atoms.bg, t.atoms.shadow_sm]}
|
||||||
a.absolute,
|
onLayout={e => {
|
||||||
{
|
setMeasurements({
|
||||||
top: targetMeasurements.y,
|
width: e.nativeEvent.layout.width,
|
||||||
left: targetMeasurements.x,
|
height: e.nativeEvent.layout.height,
|
||||||
height: targetMeasurements.height,
|
})
|
||||||
width: targetMeasurements.width,
|
setReady(true)
|
||||||
},
|
}}>
|
||||||
]}>
|
{children}
|
||||||
{targetElement}
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Outer content container */}
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
a.align_start,
|
|
||||||
{
|
|
||||||
opacity: ready ? 1 : 0,
|
|
||||||
top: positionTop,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
{/* The triangle "tip" */}
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
a.top_0,
|
|
||||||
a.z_10,
|
|
||||||
t.atoms.bg,
|
|
||||||
{
|
|
||||||
borderTopLeftRadius: a.rounded_xs.borderRadius,
|
|
||||||
width: TIP_SIZE,
|
|
||||||
height: TIP_SIZE,
|
|
||||||
transform: [{rotate: '45deg'}],
|
|
||||||
top: tipTop,
|
|
||||||
left: tipLeft,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* The content itself */}
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.px_md,
|
|
||||||
a.py_sm,
|
|
||||||
a.rounded_sm,
|
|
||||||
t.atoms.bg,
|
|
||||||
t.atoms.shadow_sm,
|
|
||||||
{
|
|
||||||
maxWidth: 200,
|
|
||||||
left: contentLeft,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onLayout={e => {
|
|
||||||
setReady(true)
|
|
||||||
setMeasurements({
|
|
||||||
width: e.nativeEvent.layout.width,
|
|
||||||
height: e.nativeEvent.layout.height,
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Modal>
|
|
||||||
</Portal>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<PanGestureHandlerEventPayload>
|
||||||
|
update: GestureUpdateEvent<PanGestureHandlerEventPayload>
|
||||||
|
end: GestureStateChangeEvent<PanGestureHandlerEventPayload>
|
||||||
|
finalize: GestureStateChangeEvent<PanGestureHandlerEventPayload>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Context.Provider value={ctx}>
|
||||||
|
<GestureDetector gesture={gesture}>{children}</GestureDetector>
|
||||||
|
</Context.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOnInteract(
|
||||||
|
onInteract: (
|
||||||
|
e: GestureStateChangeEvent<PanGestureHandlerEventPayload>,
|
||||||
|
) => void,
|
||||||
|
) {
|
||||||
|
const ctx = useContext(Context)
|
||||||
|
useEffect(() => {
|
||||||
|
ctx.register()
|
||||||
|
events.on('begin', onInteract)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ctx.unregister()
|
||||||
|
events.off('begin', onInteract)
|
||||||
|
}
|
||||||
|
}, [ctx, onInteract])
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user