Skip to content

Commit 7cdb7f7

Browse files
rubennortemeta-codesync[bot]
authored andcommitted
Create example for MutationObserver in RNTester (disabled by feature flag) (#56622)
Summary: Pull Request resolved: #56622 Changelog: [internal] Adds examples for MutationObserver if the API is available (which isn't normally as it's gated). Differential Revision: D102593725
1 parent 358fd0d commit 7cdb7f7

7 files changed

Lines changed: 622 additions & 0 deletions

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import {RNTesterThemeContext} from '../../components/RNTesterTheme';
12+
import * as React from 'react';
13+
import {type ElementRef, useContext, useEffect, useRef, useState} from 'react';
14+
import {Pressable, ScrollView, StyleSheet, Text, View} from 'react-native';
15+
16+
export const name = 'MutationObserver Example';
17+
export const title = name;
18+
export const description =
19+
'- Tap on elements to append a child.\n- Long tap on elements to remove them.';
20+
21+
export function render(): React.Node {
22+
return <MutationObserverExample />;
23+
}
24+
25+
const nextIdByPrefix: Map<string, number> = new Map();
26+
function generateId(prefix: string): string {
27+
let nextId = nextIdByPrefix.get(prefix);
28+
if (nextId == null) {
29+
nextId = 1;
30+
}
31+
nextIdByPrefix.set(prefix, nextId + 1);
32+
return prefix + nextId;
33+
}
34+
35+
const rootId = generateId('example-item-');
36+
37+
function useTemporaryValue<T>(duration: number = 2000): [?T, (?T) => void] {
38+
const [value, setValue] = useState<?T>(null);
39+
40+
useEffect(() => {
41+
const timeoutId = setTimeout(() => {
42+
setValue(null);
43+
}, duration);
44+
return () => clearTimeout(timeoutId);
45+
// we need to set the timer every time the value changes
46+
}, [duration, value]);
47+
48+
return [value, setValue];
49+
}
50+
51+
component MutationObserverExample() {
52+
const parentViewRef = useRef<?ElementRef<typeof View>>(null);
53+
const [showExample, setShowExample] = useState(true);
54+
const theme = useContext(RNTesterThemeContext);
55+
const [message, setMessage] = useTemporaryValue<string>();
56+
57+
useEffect(() => {
58+
const parentNode = parentViewRef.current;
59+
if (!parentNode) {
60+
return;
61+
}
62+
63+
const mutationObserver = new MutationObserver(records => {
64+
const messages = [];
65+
records.forEach(record => {
66+
if (record.addedNodes.length > 0) {
67+
console.log(
68+
'MutationObserverExample: added nodes',
69+
nodeListToString(record.addedNodes),
70+
);
71+
messages.push(`Added nodes: ${nodeListToString(record.addedNodes)}`);
72+
}
73+
if (record.removedNodes.length > 0) {
74+
console.log(
75+
'MutationObserverExample: removed nodes',
76+
nodeListToString(record.removedNodes),
77+
);
78+
messages.push(
79+
`Removed nodes: ${nodeListToString(record.removedNodes)}`,
80+
);
81+
}
82+
});
83+
setMessage(messages.join(',\n'));
84+
});
85+
86+
// $FlowExpectedError[incompatible-type]
87+
mutationObserver.observe(parentNode, {
88+
subtree: true,
89+
childList: true,
90+
});
91+
92+
return () => {
93+
console.log('MutationObserverExample: disconnecting mutation observer');
94+
mutationObserver.disconnect();
95+
nextIdByPrefix.clear();
96+
};
97+
}, [setMessage]);
98+
99+
const exampleId = showExample ? rootId : '';
100+
101+
return (
102+
<>
103+
<ScrollView id="scroll-view">
104+
<View style={styles.parent} ref={parentViewRef} id="parent">
105+
{showExample ? (
106+
<ExampleItem
107+
label={exampleId}
108+
id={exampleId}
109+
onRemove={() => setShowExample(false)}
110+
/>
111+
) : null}
112+
</View>
113+
</ScrollView>
114+
<Text id="message" style={[styles.message, {color: theme.LabelColor}]}>
115+
{message}
116+
</Text>
117+
</>
118+
);
119+
}
120+
121+
function ExampleItem(props: {
122+
id: string,
123+
label: string,
124+
onRemove?: () => void,
125+
}): React.Node {
126+
const theme = useContext(RNTesterThemeContext);
127+
const [children, setChildren] = useState<ReadonlyArray<[string, React.Node]>>(
128+
[],
129+
);
130+
131+
return (
132+
<View id={props.id}>
133+
<Pressable
134+
testID={'pressable-' + props.id}
135+
style={[styles.item]}
136+
onLongPress={() => {
137+
props.onRemove?.();
138+
}}
139+
onPress={() => {
140+
const id = generateId(props.label + '-');
141+
setChildren(prevChildren => [
142+
...prevChildren,
143+
[
144+
id,
145+
<ExampleItem
146+
id={id}
147+
key={id}
148+
label={id}
149+
onRemove={() => {
150+
setChildren(prevChildren2 =>
151+
prevChildren2.filter(pair => pair[0] !== id),
152+
);
153+
}}
154+
/>,
155+
],
156+
]);
157+
}}>
158+
{props.label != null ? (
159+
<Text
160+
id={'text-' + props.id}
161+
style={[styles.label, {color: theme.LabelColor}]}>
162+
{props.label}
163+
</Text>
164+
) : null}
165+
{children.map(([id, child]) => child)}
166+
</Pressable>
167+
</View>
168+
);
169+
}
170+
171+
function nodeListToString(nodeList: NodeList<Node>): string {
172+
return [...nodeList]
173+
.map(node => (node instanceof Element && node.id) || '<unknown-node>')
174+
.join(', ');
175+
}
176+
177+
const styles = StyleSheet.create({
178+
parent: {
179+
flex: 1,
180+
backgroundColor: 'white',
181+
},
182+
item: {
183+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
184+
flex: 1,
185+
gap: 16,
186+
minHeight: 50,
187+
padding: 40,
188+
},
189+
label: {
190+
position: 'absolute',
191+
top: 0,
192+
right: 0,
193+
fontSize: 10,
194+
},
195+
message: {
196+
padding: 10,
197+
},
198+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
12+
13+
import * as MutationObserverExample from './MutationObserverExample';
14+
import * as VisualCompletionExample from './VisualCompletionExample/VisualCompletionExample';
15+
16+
export const framework = 'React';
17+
export const title = 'MutationObserver';
18+
export const category = 'UI';
19+
export const documentationURL =
20+
'https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver';
21+
export const description = 'API to detect mutations in React Native nodes.';
22+
export const showIndividualExamples = true;
23+
export const examples: Array<RNTesterModuleExample> = [MutationObserverExample];
24+
25+
if (typeof IntersectionObserver !== 'undefined') {
26+
examples.push(VisualCompletionExample);
27+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import type VCTracker, {VisualElement} from './VCTrackerExample';
12+
13+
import * as React from 'react';
14+
import {useEffect, useState} from 'react';
15+
import {Dimensions, StyleSheet, View} from 'react-native';
16+
17+
const OVERLAY_SCALE = 0.25;
18+
19+
export default function VCOverlayExample(props: {
20+
vcTracker: VCTracker,
21+
}): React.Node {
22+
const [visualElements, setVisualElements] = useState<
23+
ReadonlyArray<VisualElement>,
24+
>([]);
25+
26+
useEffect(() => {
27+
setVisualElements(props.vcTracker.getVisualElements());
28+
props.vcTracker.onUpdateVisualElements(elements => {
29+
setVisualElements(elements);
30+
});
31+
}, [props.vcTracker]);
32+
33+
return (
34+
<View style={styles.overlay}>
35+
{visualElements.map((visualElement, index) => (
36+
<View
37+
key={index}
38+
style={[
39+
styles.overlayElement,
40+
{
41+
top: visualElement.rect.top * OVERLAY_SCALE,
42+
left: visualElement.rect.left * OVERLAY_SCALE,
43+
width: visualElement.rect.width * OVERLAY_SCALE,
44+
height: visualElement.rect.height * OVERLAY_SCALE,
45+
},
46+
]}
47+
/>
48+
))}
49+
</View>
50+
);
51+
}
52+
53+
const styles = StyleSheet.create({
54+
overlay: {
55+
position: 'absolute',
56+
bottom: 60,
57+
right: 10,
58+
width: OVERLAY_SCALE * Dimensions.get('window').width,
59+
height: OVERLAY_SCALE * Dimensions.get('window').height,
60+
backgroundColor: 'gray',
61+
opacity: 0.9,
62+
},
63+
overlayElement: {
64+
position: 'absolute',
65+
borderWidth: 1,
66+
borderColor: 'black',
67+
},
68+
});

0 commit comments

Comments
 (0)