-
-
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, using measureInWindow
should be equivalent. However, what is necessary to replicate this example is synchronous useLayoutEffect
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.
Thanks for the help @lunaleaps - great info!
I'm excited and curious to test this out and learn about how it works but measureInWindow
wasn't properly working here either because it returns measurements in an async callback, which prevented me from getting updated data as part of the synchronous useLayoutEffect
commit. Am I missing something?
Thanks for the help @lunaleaps - great info! I'm excited and curious to test this out and learn about how it works but
measureInWindow
wasn't properly working here either because it returns measurements in an async callback, which prevented me from getting updated data as part of the synchronoususeLayoutEffect
commit. Am I missing something?
The callback isn't really async in this case. You can do something like this:
React.useLayoutEffect(() => {
let measurements;
targetRef.current?.measureInWindow((x, y, width, height) => {
measurements = {x, y, width, height};
});
// measurements is {x, y, width, height} here
}, []);
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 sure
I moved to a discussion here: reactwg/react-native-new-architecture#164
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
What version of React Native is being used?
I'm seeing
ref.current?.getBoundingClientRect is not a function (it is undefined)
on RN 0.73.6 with New Arch enabled