Ok pretty good on native
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
export const TIP_SIZE = 12
|
||||||
@@ -12,10 +12,14 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|||||||
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'
|
import {useOnInteract} from '#/state/shell/GlobalGestureEvents'
|
||||||
|
import {TIP_SIZE} from '#/components/Tooltip/const'
|
||||||
|
|
||||||
type Context = {
|
type TooltipContextType = {
|
||||||
visible: boolean
|
ready: boolean
|
||||||
onVisibleChange: (visible: boolean) => void
|
onVisibleChange: (visible: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type TargetContextType = {
|
||||||
targetMeasurements:
|
targetMeasurements:
|
||||||
| {
|
| {
|
||||||
x: number
|
x: number
|
||||||
@@ -27,9 +31,12 @@ type Context = {
|
|||||||
targetRef: React.RefObject<View>
|
targetRef: React.RefObject<View>
|
||||||
}
|
}
|
||||||
|
|
||||||
const TooltipContext = createContext<Context>({
|
const TooltipContext = createContext<TooltipContextType>({
|
||||||
visible: false,
|
ready: false,
|
||||||
onVisibleChange: () => {},
|
onVisibleChange: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const TargetContext = createContext<TargetContextType>({
|
||||||
targetMeasurements: undefined,
|
targetMeasurements: undefined,
|
||||||
targetRef: {current: null},
|
targetRef: {current: null},
|
||||||
})
|
})
|
||||||
@@ -43,12 +50,22 @@ export function Outer({
|
|||||||
visible: boolean
|
visible: boolean
|
||||||
onVisibleChange: (visible: boolean) => void
|
onVisibleChange: (visible: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
const targetRef = useRef<View>(null)
|
/**
|
||||||
|
* Whether we have measured the target and are ready to show the tooltip.
|
||||||
|
*/
|
||||||
|
const [ready, setReady] = useState(false)
|
||||||
|
/**
|
||||||
|
* Lagging state to track the externally-controlled visibility of the
|
||||||
|
* tooltip.
|
||||||
|
*/
|
||||||
const [prevRequestVisible, setPrevRequestVisible] = useState<
|
const [prevRequestVisible, setPrevRequestVisible] = useState<
|
||||||
boolean | undefined
|
boolean | undefined
|
||||||
>()
|
>()
|
||||||
const [visible, setVisible] = useState(false)
|
/**
|
||||||
const [measurements, setMeasurements] = useState<
|
* Needs to reference the element this Tooltip is attached to.
|
||||||
|
*/
|
||||||
|
const targetRef = useRef<View>(null)
|
||||||
|
const [targetMeasurements, setTargetMeasurements] = useState<
|
||||||
| {
|
| {
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
@@ -59,89 +76,100 @@ export function Outer({
|
|||||||
>(undefined)
|
>(undefined)
|
||||||
|
|
||||||
if (requestVisible && !prevRequestVisible) {
|
if (requestVisible && !prevRequestVisible) {
|
||||||
setPrevRequestVisible(requestVisible)
|
setPrevRequestVisible(true)
|
||||||
|
|
||||||
if (targetRef.current) {
|
if (targetRef.current) {
|
||||||
|
/*
|
||||||
|
* Once opened, measure the dimensions and position of the target
|
||||||
|
*/
|
||||||
targetRef.current.measure((_x, _y, width, height, pageX, pageY) => {
|
targetRef.current.measure((_x, _y, width, height, pageX, pageY) => {
|
||||||
setMeasurements({x: pageX, y: pageY, width, height})
|
setTargetMeasurements({x: pageX, y: pageY, width, height})
|
||||||
setVisible(true)
|
setReady(true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (!requestVisible && prevRequestVisible) {
|
} else if (!requestVisible && prevRequestVisible) {
|
||||||
setPrevRequestVisible(requestVisible)
|
setPrevRequestVisible(false)
|
||||||
setMeasurements(undefined)
|
setTargetMeasurements(undefined)
|
||||||
setVisible(false)
|
setReady(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = useMemo(
|
const ctx = useMemo(
|
||||||
() => ({
|
() => ({ready, onVisibleChange}),
|
||||||
visible,
|
[ready, onVisibleChange],
|
||||||
onVisibleChange,
|
)
|
||||||
targetMeasurements: measurements,
|
const targetCtx = useMemo(
|
||||||
targetRef,
|
() => ({targetMeasurements, targetRef}),
|
||||||
}),
|
[targetMeasurements, targetRef],
|
||||||
[visible, onVisibleChange, measurements, targetRef],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipContext.Provider value={ctx}>{children}</TooltipContext.Provider>
|
<TooltipContext.Provider value={ctx}>
|
||||||
|
<TargetContext.Provider value={targetCtx}>
|
||||||
|
{children}
|
||||||
|
</TargetContext.Provider>
|
||||||
|
</TooltipContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Target({
|
export function Target({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: (props: {ref: Context['targetRef']}) => React.ReactNode
|
children: (props: {ref: TargetContextType['targetRef']}) => React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const {targetRef} = useContext(TooltipContext)
|
const {targetRef} = useContext(TargetContext)
|
||||||
|
|
||||||
return children({
|
return children({
|
||||||
ref: targetRef,
|
ref: targetRef,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIP_SIZE = 12
|
|
||||||
|
|
||||||
export function Content({children}: {children: React.ReactNode}) {
|
export function Content({children}: {children: React.ReactNode}) {
|
||||||
const {visible, onVisibleChange, targetMeasurements} =
|
const {ready, onVisibleChange} = useContext(TooltipContext)
|
||||||
useContext(TooltipContext)
|
const {targetMeasurements} = useContext(TargetContext)
|
||||||
const requestClose = useCallback(() => {
|
const requestClose = useCallback(() => {
|
||||||
onVisibleChange(false)
|
onVisibleChange(false)
|
||||||
}, [onVisibleChange])
|
}, [onVisibleChange])
|
||||||
|
|
||||||
if (!visible || !targetMeasurements) return null
|
if (!ready || !targetMeasurements) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<Tooltip
|
<Bubble
|
||||||
|
/*
|
||||||
|
* Gotta pass these in here. Inside the Bubble, we're Potal-ed outside
|
||||||
|
* the context providers.
|
||||||
|
*/
|
||||||
targetMeasurements={targetMeasurements}
|
targetMeasurements={targetMeasurements}
|
||||||
requestClose={requestClose}>
|
requestClose={requestClose}>
|
||||||
{children}
|
{children}
|
||||||
</Tooltip>
|
</Bubble>
|
||||||
</Portal>
|
</Portal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tooltip({
|
function Bubble({
|
||||||
children,
|
children,
|
||||||
requestClose,
|
requestClose,
|
||||||
targetMeasurements,
|
targetMeasurements,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
requestClose: () => void
|
requestClose: () => void
|
||||||
targetMeasurements: Context['targetMeasurements']
|
targetMeasurements: Exclude<
|
||||||
|
TargetContextType['targetMeasurements'],
|
||||||
|
undefined
|
||||||
|
>
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [ready, setReady] = useState(false)
|
const insets = useSafeAreaInsets()
|
||||||
const [measurements, setMeasurements] = useState<
|
const [bubbleMeasurements, setBubbleMeasurements] = useState<
|
||||||
| {
|
| {
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
>(undefined)
|
>(undefined)
|
||||||
const safe = useSafeAreaInsets()
|
|
||||||
const coords = useMemo(() => {
|
const coords = useMemo(() => {
|
||||||
if (!targetMeasurements || !measurements)
|
if (!bubbleMeasurements)
|
||||||
return {
|
return {
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
@@ -153,9 +181,9 @@ function Tooltip({
|
|||||||
|
|
||||||
const win = Dimensions.get('window')
|
const win = Dimensions.get('window')
|
||||||
const {width: ww, height: wh} = win
|
const {width: ww, height: wh} = win
|
||||||
const maxTop = safe.top
|
const maxTop = insets.top
|
||||||
const maxBottom = wh - safe.bottom
|
const maxBottom = wh - insets.bottom
|
||||||
const {width: cw, height: ch} = measurements
|
const {width: cw, height: ch} = bubbleMeasurements
|
||||||
const minLeft = a.px_xl.paddingLeft
|
const minLeft = a.px_xl.paddingLeft
|
||||||
const maxLeft = ww - minLeft
|
const maxLeft = ww - minLeft
|
||||||
|
|
||||||
@@ -183,11 +211,10 @@ function Tooltip({
|
|||||||
tipTop,
|
tipTop,
|
||||||
tipLeft,
|
tipLeft,
|
||||||
}
|
}
|
||||||
}, [targetMeasurements, measurements, safe])
|
}, [targetMeasurements, bubbleMeasurements, insets])
|
||||||
|
|
||||||
const requestCloseWrapped = useCallback(() => {
|
const requestCloseWrapped = useCallback(() => {
|
||||||
setReady(false)
|
setBubbleMeasurements(undefined)
|
||||||
setMeasurements(undefined)
|
|
||||||
requestClose()
|
requestClose()
|
||||||
}, [requestClose])
|
}, [requestClose])
|
||||||
|
|
||||||
@@ -216,12 +243,11 @@ function Tooltip({
|
|||||||
a.align_start,
|
a.align_start,
|
||||||
{
|
{
|
||||||
width: 200,
|
width: 200,
|
||||||
opacity: ready ? 1 : 0,
|
opacity: !!bubbleMeasurements ? 1 : 0,
|
||||||
top: coords.top,
|
top: coords.top,
|
||||||
left: coords.left,
|
left: coords.left,
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
{/* The triangle "tip" */}
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.absolute,
|
a.absolute,
|
||||||
@@ -238,16 +264,13 @@ function Tooltip({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* The content itself */}
|
|
||||||
<View
|
<View
|
||||||
style={[a.px_md, a.py_sm, a.rounded_sm, t.atoms.bg, t.atoms.shadow_sm]}
|
style={[a.px_md, a.py_sm, a.rounded_sm, t.atoms.bg, t.atoms.shadow_sm]}
|
||||||
onLayout={e => {
|
onLayout={e => {
|
||||||
setMeasurements({
|
setBubbleMeasurements({
|
||||||
width: e.nativeEvent.layout.width,
|
width: e.nativeEvent.layout.width,
|
||||||
height: e.nativeEvent.layout.height,
|
height: e.nativeEvent.layout.height,
|
||||||
})
|
})
|
||||||
setReady(true)
|
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Reference in New Issue
Block a user