mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
a77d8d9d50
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/47908 Changelog: [General][Added] - Add support for `rn_rootThreshold` in Intersection Observer `rn_rootThreshold` is a custom IntersectionObserver option and not part of the IntersectionObserver spec. We are adding it because it covers a specific use-case for measuring viewability that is robust for `target`s that are larger than the viewport or specified `root`. The threshold ratio is of the intersection area (of `root` and `target`) to the total area of the `root`. {F1960832959} Source - EX314979 `rn_rootThreshold` is an optional threshold and can be combined with the `thresholds` option. An intersection will fire if any specified thresholds is met. Note: If you use specify a `rn_rootThreshold`, the default `threshold` is no longer applied The main use case of `rn_rootThreshold` is being able to specify a level of viewability independent of `target` size. For example, a `target` that is larger than the `root` (commonly the viewport) will not trigger the IntersectionObserver for a `threshold` of `1`. Setting `rn_rootThreshold` of `1`, will trigger once the item takes full size of the `root`.'; Reviewed By: yungsters Differential Revision: D66031119 fbshipit-source-id: 7bdc871dc5b4e6c0edc7d6e17a0a0cfd51c4fe81
222 lines
6.2 KiB
JavaScript
222 lines
6.2 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
* @flow strict-local
|
|
*/
|
|
|
|
import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet';
|
|
|
|
import {RNTesterThemeContext} from '../../components/RNTesterTheme';
|
|
import * as React from 'react';
|
|
import {
|
|
type ElementRef,
|
|
useContext,
|
|
useLayoutEffect,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import {Button, ScrollView, StyleSheet, Text, View} from 'react-native';
|
|
|
|
export const name = 'IntersectionObserver Root Threshold';
|
|
export const title = name;
|
|
export const description =
|
|
'Examples of setting threshold and rn_rootThreshold. Views will change background color if they meet their threshold.';
|
|
|
|
export function render(): React.Node {
|
|
return <IntersectionObserverRootThreshold />;
|
|
}
|
|
|
|
/**
|
|
* Similar to the example in MDN: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
|
*/
|
|
function IntersectionObserverRootThreshold(): React.Node {
|
|
const theme = useContext(RNTesterThemeContext);
|
|
const [showMargin, setShowMargin] = useState(true);
|
|
|
|
return (
|
|
<ScrollView>
|
|
<Button
|
|
title={`Click to ${showMargin ? 'remove' : 'add'} margin`}
|
|
onPress={() => {
|
|
setShowMargin(show => !show);
|
|
}}
|
|
/>
|
|
<Text style={[styles.scrollDownText, {color: theme.LabelColor}]}>
|
|
↓↓ Scroll down ↓↓
|
|
</Text>
|
|
{showMargin ? <View style={styles.margin} /> : null}
|
|
|
|
<ListItem
|
|
position={1}
|
|
rootThreshold={0.5}
|
|
threshold={0.5}
|
|
description="Should intersect when item half visible and when item takes up half the viewport"
|
|
/>
|
|
<ListItem
|
|
position={2}
|
|
rootThreshold={0.5}
|
|
threshold={1}
|
|
description="Should intersect when view takes up half of viewport and when item is fully visible"
|
|
/>
|
|
<ListItem
|
|
position={3}
|
|
threshold={1}
|
|
rootThreshold={0}
|
|
description="This should intersect when any part is visible, even though `thresholds` is 1"
|
|
/>
|
|
<ListItem
|
|
position={4}
|
|
rootThreshold={1}
|
|
threshold={1}
|
|
description="Since this item is smaller than viewport, should only intersect when view is fully visible"
|
|
/>
|
|
<ListItem
|
|
position={1}
|
|
rootThreshold={1}
|
|
threshold={1}
|
|
style={{height: 1000}}
|
|
description="This list item is larger than viewport and should intersect when it takes up all of viewport. However, this is impossible because of clipping of the scrollview. However, if we set `root` to the scrollview, we can."
|
|
/>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
function ListItem(props: {
|
|
position: number,
|
|
rootThreshold: number,
|
|
threshold: number,
|
|
initialValue?: number,
|
|
description: string,
|
|
style?: ?ViewStyleProp,
|
|
}): React.Node {
|
|
const itemRef = useRef<?ElementRef<typeof View>>(null);
|
|
const [intersectionRatio, setIntersectionRatio] = useState(
|
|
props.initialValue ?? 0,
|
|
);
|
|
const [intersectionRootRatio, setIntersectionRootRatio] = useState(
|
|
props.initialValue ?? 0,
|
|
);
|
|
|
|
useLayoutEffect(() => {
|
|
const itemNode = itemRef.current;
|
|
if (itemNode == null) {
|
|
return;
|
|
}
|
|
|
|
const intersectionObserver = new IntersectionObserver(
|
|
entries => {
|
|
entries.forEach(entry => {
|
|
setIntersectionRatio(entry.intersectionRatio);
|
|
// $FlowFixMe[prop-missing] - React Native specific entry property
|
|
setIntersectionRootRatio(entry.rn_intersectionRootRatio);
|
|
});
|
|
},
|
|
{threshold: props.threshold, rn_rootThreshold: props.rootThreshold},
|
|
);
|
|
|
|
// $FlowFixMe[incompatible-call]
|
|
intersectionObserver.observe(itemNode);
|
|
|
|
return () => {
|
|
intersectionObserver.disconnect();
|
|
};
|
|
}, [props.position, props.threshold, props.rootThreshold]);
|
|
|
|
return (
|
|
<View
|
|
style={[
|
|
styles.item,
|
|
intersectionRatio >= props.threshold ? styles.intersecting : null,
|
|
intersectionRootRatio >= props.rootThreshold
|
|
? styles.rootIntersecting
|
|
: null,
|
|
props.style,
|
|
]}
|
|
ref={itemRef}>
|
|
<Text style={styles.description}>{props.description}</Text>
|
|
<Text>rn_rootThreshold: {props.rootThreshold}</Text>
|
|
<Text>threshold: {props.threshold}</Text>
|
|
|
|
<IntersectionRatioIndicator
|
|
intersectionRatio={intersectionRatio}
|
|
intersectionRootRatio={intersectionRootRatio}
|
|
style={{left: 0, top: 0}}
|
|
/>
|
|
<IntersectionRatioIndicator
|
|
intersectionRatio={intersectionRatio}
|
|
intersectionRootRatio={intersectionRootRatio}
|
|
style={{right: 0, top: 0}}
|
|
/>
|
|
<IntersectionRatioIndicator
|
|
intersectionRatio={intersectionRatio}
|
|
intersectionRootRatio={intersectionRootRatio}
|
|
style={{left: 0, bottom: 0}}
|
|
/>
|
|
<IntersectionRatioIndicator
|
|
intersectionRatio={intersectionRatio}
|
|
intersectionRootRatio={intersectionRootRatio}
|
|
style={{right: 0, bottom: 0}}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function IntersectionRatioIndicator(props: {
|
|
intersectionRatio: number,
|
|
intersectionRootRatio: number,
|
|
style: {top?: number, bottom?: number, left?: number, right?: number},
|
|
}): React.Node {
|
|
return (
|
|
<View style={[styles.intersectionRatioIndicator, props.style]}>
|
|
<Text>
|
|
target ratio: {`${Math.floor(props.intersectionRatio * 100)}%`}
|
|
</Text>
|
|
<Text>
|
|
root ratio: {`${Math.floor(props.intersectionRootRatio * 100)}%`}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
scrollDownText: {
|
|
textAlign: 'center',
|
|
fontSize: 20,
|
|
marginTop: 20,
|
|
},
|
|
intersecting: {
|
|
backgroundColor: 'rgb(226, 237, 166)',
|
|
},
|
|
rootIntersecting: {
|
|
backgroundColor: 'rgb(237, 90, 45)',
|
|
},
|
|
margin: {
|
|
marginBottom: 700,
|
|
},
|
|
item: {
|
|
backgroundColor: 'rgb(186, 186, 186)',
|
|
borderColor: 'rgb(201, 126, 17)',
|
|
borderWidth: 2,
|
|
height: 500,
|
|
margin: 6,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
intersectionRatioIndicator: {
|
|
position: 'absolute',
|
|
padding: 5,
|
|
backgroundColor: 'white',
|
|
opacity: 0.7,
|
|
borderWidth: 1,
|
|
borderColor: 'black',
|
|
},
|
|
description: {
|
|
margin: 20,
|
|
fontSize: 16,
|
|
},
|
|
});
|