-
-
Save lunaleaps/148756563999c83220887757f2e549a3 to your computer and use it in GitHub Desktop.
import * as React from 'react'; | |
import {Pressable, Text, View} from 'react-native'; | |
function calculateX(toolTip, target, rootView) { | |
// Try and center the tooltip | |
let toolTipX = target.x + target.width / 2 - toolTip.width / 2; | |
// If the tooltip is outside of root, we need to move it | |
if (toolTipX < rootView.x) { | |
toolTipX = target.x; | |
} | |
if (toolTipX + toolTip.width > rootView.x + rootView.width) { | |
toolTipX = rootView.x + rootView.width - toolTip.width; | |
} | |
return toolTipX - rootView.x; | |
} | |
function calculateY(toolTip, target, rootView) { | |
// Try and place tooltip above | |
let toolTipY = target.y - toolTip.height; | |
// If the tooltip is outside of root, we need to move it below | |
if (toolTipY < rootView.y) { | |
toolTipY = target.y + target.height; | |
} | |
return toolTipY - rootView.y; | |
} | |
function wait(ms) { | |
const end = Date.now() + ms; | |
while (Date.now() < end); | |
} | |
function ToolTip({position, targetRect, rootRect, children}) { | |
const ref = React.useRef(null); | |
const [rect, setRect] = React.useState(null); | |
React.useLayoutEffect(() => { | |
// Place an artificial delay to illustrate synchronous update | |
wait(200); | |
setRect(ref.current?.getBoundingClientRect()); | |
}, [setRect, position]); | |
let left = 0; | |
let top = 0; | |
if (rect != null && targetRect != null && rootRect != null) { | |
left = calculateX(rect, targetRect, rootRect); | |
top = calculateY(rect, targetRect, rootRect); | |
} | |
return ( | |
<View | |
ref={ref} | |
style={{ | |
position: 'absolute', | |
borderColor: 'green', | |
borderRadius: 8, | |
borderWidth: 2, | |
padding: 4, | |
top, | |
left, | |
}}> | |
{children} | |
</View> | |
); | |
} | |
function Target({toolTipText, targetText, position, rootRect}) { | |
const targetRef = React.useRef(null); | |
const [rect, setRect] = React.useState(null); | |
React.useLayoutEffect(() => { | |
// Place an artificial delay to illustrate synchronous update | |
wait(200); | |
setRect(targetRef.current?.getBoundingClientRect()); | |
}, [setRect, position]); | |
return ( | |
<> | |
<View | |
ref={targetRef} | |
style={{ | |
borderColor: 'red', | |
borderWidth: 2, | |
padding: 10, | |
}}> | |
<Text>{targetText}</Text> | |
</View> | |
<ToolTip position={position} rootRect={rootRect} targetRect={rect}> | |
<Text>{toolTipText}</Text> | |
</ToolTip> | |
</> | |
); | |
} | |
const positions = [ | |
'top-left', | |
'top-right', | |
'center-center', | |
'bottom-left', | |
'bottom-right', | |
]; | |
function Example() { | |
const toolTipText = 'This is the tooltip'; | |
const targetText = 'This is the target'; | |
const ref = React.useRef(null); | |
const [index, setIndex] = React.useState(0); | |
const [rect, setRect] = React.useState(null); | |
React.useEffect(() => { | |
const setPosition = setInterval(() => { | |
setIndex((index + 1) % positions.length); | |
}, 1000); | |
return () => { | |
clearInterval(setPosition); | |
}; | |
}); | |
const position = positions[index]; | |
const style = getStyle(position); | |
React.useLayoutEffect(() => { | |
// Place an artificial delay to illustrate synchronous update | |
wait(200); | |
setRect(ref.current?.getBoundingClientRect()); | |
}, [setRect, position]); | |
return ( | |
<> | |
<Text style={{margin: 20}}>Position: {position}</Text> | |
<View | |
style={{...style, flex: 1, borderWidth: 1}} | |
ref={ref}> | |
<Target | |
toolTipText={toolTipText} | |
targetText={targetText} | |
rootRect={rect} | |
position={position} | |
/> | |
</View> | |
</> | |
); | |
} | |
function getStyle(position) { | |
switch (position) { | |
case 'top-left': | |
return { | |
justifyContent: 'flex-start', | |
alignItems: 'flex-start', | |
}; | |
case 'top-center': | |
return { | |
justifyContent: 'flex-start', | |
alignItems: 'center', | |
}; | |
case 'top-right': | |
return { | |
justifyContent: 'flex-start', | |
alignItems: 'flex-end', | |
}; | |
case 'center-center': | |
return { | |
justifyContent: 'center', | |
alignItems: 'center', | |
}; | |
case 'bottom-center': | |
return { | |
justifyContent: 'flex-end', | |
alignItems: 'center', | |
}; | |
case 'bottom-left': | |
return { | |
justifyContent: 'flex-end', | |
alignItems: 'flex-start', | |
}; | |
case 'bottom-right': | |
return { | |
alignItems: 'flex-end', | |
justifyContent: 'flex-end', | |
}; | |
} | |
} | |
export default Example; |
This was off of main, so
getBoundingClientRect
may be available only in 0.74 but from a search it seems the API was landed before the 0.73 cut.It may be behind a flag currently as @rubennorte is rolling out https://github.com/react-native-community/discussions-and-proposals/blob/main/proposals/0607-dom-traversal-and-layout-apis.md
To clarify,
getBoundingClientRect
shouldn't be relevant from what the example is trying to illustrate, usingmeasureInWindow
should be equivalent. However, what is necessary to replicate this example is synchronoususeLayoutEffect
commits which will require Bridgeless mode. Beyond Bridgeless mode, it may be behind feature flags. I would follow up either on the Bridgeless announcement or generally in New Architecture WG on timelines for shipping.
on React Native 0.74 also getting same error "ref.current?.getBoundingClientRect is not a function (it is undefined)"
I can confirm this works in RN 75 (and the nightly) when replacing getBoundingClientRect
with measureInWindow
and the new architecture is enabled :)
i think is not stable yet, since it is available under name unstable_getBoundingClientRect
You're right, thanks @rubennorte - looks like I'm getting the measurements synchronously but
useLayoutEffect
is not blocking like expected. I must be missing some config or flag to properly enable bridgeless or synchronous effects, I'm not sureI moved to a discussion here: reactwg/react-native-new-architecture#164