11import * as React from 'react' ;
22import {
33 Dimensions ,
4- View ,
5- LayoutChangeEvent ,
64 StyleSheet ,
75 Platform ,
86 Pressable ,
97 ViewStyle ,
108} from 'react-native' ;
119
12- import Animated , {
13- Easing ,
14- ReduceMotion ,
15- useAnimatedStyle ,
16- useSharedValue ,
17- withTiming ,
18- } from 'react-native-reanimated' ;
10+ import Animated from 'react-native-reanimated' ;
1911
12+ import { useTooltipFade } from './hooks' ;
2013import { Tokens } from './tokens' ;
2114import { getTooltipPosition , Measurement , TooltipChildProps } from './utils' ;
2215import { useInternalTheme } from '../../core/theming' ;
23- import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext' ;
2416import type { ThemeProp } from '../../types' ;
2517import { addEventListener } from '../../utils/addEventListener' ;
2618import Portal from '../Portal/Portal' ;
@@ -86,53 +78,17 @@ const Tooltip = ({
8678 const isWeb = Platform . OS === 'web' ;
8779
8880 const theme = useInternalTheme ( themeOverrides ) ;
89- const reduceMotion = useReduceMotion ( ) ;
90- // `visible` is the show/hide intent; `rendered` keeps the tooltip mounted
91- // through the exit fade so it can animate out before unmounting.
81+ // `visible` is the show/hide intent; the fade hook keeps the tooltip mounted
82+ // through the exit animation and owns the measurement + opacity.
9283 const [ visible , setVisible ] = React . useState ( false ) ;
93- const [ rendered , setRendered ] = React . useState ( false ) ;
84+ const { rendered, measurement, animatedStyle, onLayout, childrenWrapperRef } =
85+ useTooltipFade ( theme , visible ) ;
9486
95- const [ measurement , setMeasurement ] = React . useState ( {
96- children : { } ,
97- tooltip : { } ,
98- measured : false ,
99- } ) ;
10087 const showTooltipTimer = React . useRef < NodeJS . Timeout [ ] > ( [ ] ) ;
10188 const hideTooltipTimer = React . useRef < NodeJS . Timeout [ ] > ( [ ] ) ;
10289
103- const childrenWrapperRef = React . useRef < View > ( null ) ;
10490 const touched = React . useRef ( false ) ;
10591
106- const opacity = useSharedValue ( 0 ) ;
107- const reanimatedReduceMotion = reduceMotion
108- ? ReduceMotion . Always
109- : ReduceMotion . Never ;
110-
111- const enterConfig = React . useMemo (
112- ( ) => ( {
113- duration : theme . motion . duration [ Tokens . motion . enter . duration ] ,
114- easing : Easing . bezier ( ...theme . motion . easing [ Tokens . motion . enter . easing ] ) ,
115- reduceMotion : reanimatedReduceMotion ,
116- } ) ,
117- [ theme . motion , reanimatedReduceMotion ]
118- ) ;
119- const exitConfig = React . useMemo (
120- ( ) => ( {
121- duration : theme . motion . duration [ Tokens . motion . exit . duration ] ,
122- easing : Easing . bezier ( ...theme . motion . easing [ Tokens . motion . exit . easing ] ) ,
123- reduceMotion : reanimatedReduceMotion ,
124- } ) ,
125- [ theme . motion , reanimatedReduceMotion ]
126- ) ;
127- // The visual fade-out is handled by Reanimated; the actual unmount is
128- // deferred by this same duration so the fade can play. Reduce-motion skips
129- // the wait entirely.
130- const exitDurationMs = reduceMotion
131- ? 0
132- : theme . motion . duration [ Tokens . motion . exit . duration ] ;
133-
134- const animatedStyle = useAnimatedStyle ( ( ) => ( { opacity : opacity . value } ) ) ;
135-
13692 const isValidChild = React . useMemo (
13793 ( ) => React . isValidElement < TooltipChildProps > ( children ) ,
13894 [ children ]
@@ -152,43 +108,6 @@ const Tooltip = ({
152108 } ;
153109 } , [ ] ) ;
154110
155- // Mount as soon as the tooltip is requested.
156- React . useEffect ( ( ) => {
157- if ( visible ) {
158- setRendered ( true ) ;
159- }
160- } , [ visible ] ) ;
161-
162- // Drive the fade and defer unmount until the exit animation has played.
163- React . useEffect ( ( ) => {
164- if ( ! rendered ) {
165- return ;
166- }
167-
168- if ( visible ) {
169- // Hold at 0 until measured so the tooltip never flashes at the wrong
170- // position, then fade in.
171- opacity . value = measurement . measured ? withTiming ( 1 , enterConfig ) : 0 ;
172- return ;
173- }
174-
175- opacity . value = withTiming ( 0 , exitConfig ) ;
176- const id = setTimeout ( ( ) => {
177- setRendered ( false ) ;
178- setMeasurement ( { children : { } , tooltip : { } , measured : false } ) ;
179- } , exitDurationMs ) as unknown as NodeJS . Timeout ;
180-
181- return ( ) => clearTimeout ( id ) ;
182- } , [
183- visible ,
184- rendered ,
185- measurement . measured ,
186- opacity ,
187- enterConfig ,
188- exitConfig ,
189- exitDurationMs ,
190- ] ) ;
191-
192111 React . useEffect ( ( ) => {
193112 const subscription = addEventListener ( Dimensions , 'change' , ( ) =>
194113 setVisible ( false )
@@ -252,18 +171,6 @@ const Tooltip = ({
252171 }
253172 } , [ children . props , handleTouchEnd , isValidChild ] ) ;
254173
255- const handleOnLayout = ( { nativeEvent : { layout } } : LayoutChangeEvent ) => {
256- childrenWrapperRef . current ?. measure (
257- ( _x , _y , width , height , pageX , pageY ) => {
258- setMeasurement ( {
259- children : { pageX, pageY, height, width } ,
260- tooltip : { ...layout } ,
261- measured : true ,
262- } ) ;
263- }
264- ) ;
265- } ;
266-
267174 const mobilePressProps = {
268175 onPress : handlePress ,
269176 onLongPress : ( ) => handleTouchStart ( ) ,
@@ -281,7 +188,7 @@ const Tooltip = ({
281188 { rendered && (
282189 < Portal >
283190 < Animated . View
284- onLayout = { handleOnLayout }
191+ onLayout = { onLayout }
285192 style = { [
286193 styles . tooltip ,
287194 {
0 commit comments