Created
February 23, 2024 07:18
-
-
Save lunaleaps/148756563999c83220887757f2e549a3 to your computer and use it in GitHub Desktop.
Synchronous measurement and layout updates
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
i think is not stable yet, since it is available under name unstable_getBoundingClientRect
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I can confirm this works in RN 75 (and the nightly) when replacing
getBoundingClientRect
withmeasureInWindow
and the new architecture is enabled :)