Skip to content

Instantly share code, notes, and snippets.

@lunaleaps
Created February 23, 2024 07:18
Show Gist options
  • Save lunaleaps/148756563999c83220887757f2e549a3 to your computer and use it in GitHub Desktop.
Save lunaleaps/148756563999c83220887757f2e549a3 to your computer and use it in GitHub Desktop.
Synchronous measurement and layout updates
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;
@DigitalZebra
Copy link

I can confirm this works in RN 75 (and the nightly) when replacing getBoundingClientRect with measureInWindow and the new architecture is enabled :)

@gorhom
Copy link

gorhom commented Oct 26, 2024

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