Fix visibility state of native tooltip (#8581)

* Fix visibility state of native tooltip

* rm onLayout

---------

Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Samuel Newman
2025-06-27 20:06:10 +03:00
committed by GitHub
parent e71664ee18
commit 53d5680d19
+46 -48
View File
@@ -3,6 +3,7 @@ import {
createContext, createContext,
useCallback, useCallback,
useContext, useContext,
useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@@ -30,31 +31,33 @@ const BUBBLE_SHADOW_OFFSET = ARROW_SIZE / 3 // vibes-based, provide more shadow
type TooltipContextType = { type TooltipContextType = {
position: 'top' | 'bottom' position: 'top' | 'bottom'
ready: boolean visible: boolean
onVisibleChange: (visible: boolean) => void onVisibleChange: (visible: boolean) => void
} }
type TargetMeasurements = {
x: number
y: number
width: number
height: number
}
type TargetContextType = { type TargetContextType = {
targetMeasurements: targetMeasurements: TargetMeasurements | undefined
| { setTargetMeasurements: (measurements: TargetMeasurements) => void
x: number shouldMeasure: boolean
y: number
width: number
height: number
}
| undefined
targetRef: React.RefObject<View>
} }
const TooltipContext = createContext<TooltipContextType>({ const TooltipContext = createContext<TooltipContextType>({
position: 'bottom', position: 'bottom',
ready: false, visible: false,
onVisibleChange: () => {}, onVisibleChange: () => {},
}) })
const TargetContext = createContext<TargetContextType>({ const TargetContext = createContext<TargetContextType>({
targetMeasurements: undefined, targetMeasurements: undefined,
targetRef: {current: null}, setTargetMeasurements: () => {},
shouldMeasure: false,
}) })
export function Outer({ export function Outer({
@@ -68,21 +71,12 @@ export function Outer({
visible: boolean visible: boolean
onVisibleChange: (visible: boolean) => void onVisibleChange: (visible: boolean) => void
}) { }) {
/**
* 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 * Lagging state to track the externally-controlled visibility of the
* tooltip. * tooltip, which needs to wait for the target to be measured before
* actually being shown.
*/ */
const [prevRequestVisible, setPrevRequestVisible] = useState< const [visible, setVisible] = useState<boolean>(false)
boolean | undefined
>()
/**
* Needs to reference the element this Tooltip is attached to.
*/
const targetRef = useRef<View>(null)
const [targetMeasurements, setTargetMeasurements] = useState< const [targetMeasurements, setTargetMeasurements] = useState<
| { | {
x: number x: number
@@ -93,33 +87,24 @@ export function Outer({
| undefined | undefined
>(undefined) >(undefined)
if (requestVisible && !prevRequestVisible) { if (requestVisible && !visible && targetMeasurements) {
setPrevRequestVisible(true) setVisible(true)
} else if (!requestVisible && visible) {
if (targetRef.current) { setVisible(false)
/*
* Once opened, measure the dimensions and position of the target
*/
targetRef.current.measure((_x, _y, width, height, pageX, pageY) => {
if (pageX !== undefined && pageY !== undefined && width && height) {
setTargetMeasurements({x: pageX, y: pageY, width, height})
setReady(true)
}
})
}
} else if (!requestVisible && prevRequestVisible) {
setPrevRequestVisible(false)
setTargetMeasurements(undefined) setTargetMeasurements(undefined)
setReady(false)
} }
const ctx = useMemo( const ctx = useMemo(
() => ({position, ready, onVisibleChange}), () => ({position, visible, onVisibleChange}),
[position, ready, onVisibleChange], [position, visible, onVisibleChange],
) )
const targetCtx = useMemo( const targetCtx = useMemo(
() => ({targetMeasurements, targetRef}), () => ({
[targetMeasurements, targetRef], targetMeasurements,
setTargetMeasurements,
shouldMeasure: requestVisible,
}),
[requestVisible, targetMeasurements, setTargetMeasurements],
) )
return ( return (
@@ -132,7 +117,20 @@ export function Outer({
} }
export function Target({children}: {children: React.ReactNode}) { export function Target({children}: {children: React.ReactNode}) {
const {targetRef} = useContext(TargetContext) const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext)
const targetRef = useRef<View>(null)
useEffect(() => {
if (!shouldMeasure) return
/*
* Once opened, measure the dimensions and position of the target
*/
targetRef.current?.measure((_x, _y, width, height, pageX, pageY) => {
if (pageX !== undefined && pageY !== undefined && width && height) {
setTargetMeasurements({x: pageX, y: pageY, width, height})
}
})
}, [shouldMeasure, setTargetMeasurements])
return ( return (
<View collapsable={false} ref={targetRef}> <View collapsable={false} ref={targetRef}>
@@ -148,13 +146,13 @@ export function Content({
children: React.ReactNode children: React.ReactNode
label: string label: string
}) { }) {
const {position, ready, onVisibleChange} = useContext(TooltipContext) const {position, visible, onVisibleChange} = useContext(TooltipContext)
const {targetMeasurements} = useContext(TargetContext) const {targetMeasurements} = useContext(TargetContext)
const requestClose = useCallback(() => { const requestClose = useCallback(() => {
onVisibleChange(false) onVisibleChange(false)
}, [onVisibleChange]) }, [onVisibleChange])
if (!ready || !targetMeasurements) return null if (!visible || !targetMeasurements) return null
return ( return (
<Portal> <Portal>