Files
react-native/packages/rn-tester/js/examples/MutationObserver/MutationObserverExample.js
Rubén Norte 8a4ddab2c2 Move MutationObserver to react-native/src/private (#45791)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/45791

Changelog: [internal]

These files are safe to move because they haven't been enabled in OSS and people shouldn't be importing them directly.

Reviewed By: rshest

Differential Revision: D60377869

fbshipit-source-id: 02bc0335385859c0589a494de9b59b0c2ebc06f6
2024-07-30 10:08:22 -07:00

206 lines
5.5 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 ReadOnlyNode from 'react-native/src/private/webapis/dom/nodes/ReadOnlyNode';
import type NodeList from 'react-native/src/private/webapis/dom/oldstylecollections/NodeList';
import {RNTesterThemeContext} from '../../components/RNTesterTheme';
import * as React from 'react';
import {type ElementRef, useContext, useEffect, useRef, useState} from 'react';
import {Pressable, ScrollView, StyleSheet, Text, View} from 'react-native';
import ReadOnlyElement from 'react-native/src/private/webapis/dom/nodes/ReadOnlyElement';
import MutationObserver from 'react-native/src/private/webapis/mutationobserver/MutationObserver';
export const name = 'MutationObserver Example';
export const title = name;
export const description =
'- Tap on elements to append a child.\n- Long tap on elements to remove them.';
export function render(): React.Node {
return <MutationObserverExample />;
}
const nextIdByPrefix: Map<string, number> = new Map();
function generateId(prefix: string): string {
let nextId = nextIdByPrefix.get(prefix);
if (nextId == null) {
nextId = 1;
}
nextIdByPrefix.set(prefix, nextId + 1);
return prefix + nextId;
}
const rootId = generateId('example-item-');
function useTemporaryValue<T>(duration: number = 2000): [?T, (?T) => void] {
const [value, setValue] = useState<?T>(null);
useEffect(() => {
const timeoutId = setTimeout(() => {
setValue(null);
}, duration);
return () => clearTimeout(timeoutId);
// we need to set the timer every time the value changes
}, [duration, value]);
return [value, setValue];
}
function MutationObserverExample(): React.Node {
const parentViewRef = useRef<?ElementRef<typeof View>>(null);
const [showExample, setShowExample] = useState(true);
const theme = useContext(RNTesterThemeContext);
const [message, setMessage] = useTemporaryValue<string>();
useEffect(() => {
const parentNode = parentViewRef.current;
if (!parentNode) {
return;
}
const mutationObserver = new MutationObserver(records => {
const messages = [];
records.forEach(record => {
if (record.addedNodes.length > 0) {
console.log(
'MutationObserverExample: added nodes',
nodeListToString(record.addedNodes),
);
messages.push(`Added nodes: ${nodeListToString(record.addedNodes)}`);
}
if (record.removedNodes.length > 0) {
console.log(
'MutationObserverExample: removed nodes',
nodeListToString(record.removedNodes),
);
messages.push(
`Removed nodes: ${nodeListToString(record.removedNodes)}`,
);
}
});
setMessage(messages.join(',\n'));
});
// $FlowExpectedError[incompatible-call]
mutationObserver.observe(parentNode, {
subtree: true,
childList: true,
});
return () => {
console.log('MutationObserverExample: disconnecting mutation observer');
mutationObserver.disconnect();
nextIdByPrefix.clear();
};
}, [setMessage]);
const exampleId = showExample ? rootId : '';
return (
<>
<ScrollView id="scroll-view">
<View style={styles.parent} ref={parentViewRef} id="parent">
{showExample ? (
<ExampleItem
label={exampleId}
id={exampleId}
onRemove={() => setShowExample(false)}
/>
) : null}
</View>
</ScrollView>
<Text id="message" style={[styles.message, {color: theme.LabelColor}]}>
{message}
</Text>
</>
);
}
function ExampleItem(props: {
id: string,
label: string,
onRemove?: () => void,
}): React.Node {
const theme = useContext(RNTesterThemeContext);
const [children, setChildren] = useState<
$ReadOnlyArray<[string, React.Node]>,
>([]);
return (
<View id={props.id}>
<Pressable
testID={'pressable-' + props.id}
style={[styles.item]}
onLongPress={() => {
props.onRemove?.();
}}
onPress={() => {
const id = generateId(props.label + '-');
setChildren(prevChildren => [
...prevChildren,
[
id,
<ExampleItem
id={id}
key={id}
label={id}
onRemove={() => {
setChildren(prevChildren2 =>
prevChildren2.filter(pair => pair[0] !== id),
);
}}
/>,
],
]);
}}>
{props.label != null ? (
<Text
id={'text-' + props.id}
style={[styles.label, {color: theme.LabelColor}]}>
{props.label}
</Text>
) : null}
{children.map(([id, child]) => child)}
</Pressable>
</View>
);
}
function nodeListToString(nodeList: NodeList<ReadOnlyNode>): string {
return [...nodeList]
.map(
node => (node instanceof ReadOnlyElement && node.id) || '<unknown-node>',
)
.join(', ');
}
const styles = StyleSheet.create({
parent: {
flex: 1,
backgroundColor: 'white',
},
item: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
flex: 1,
gap: 16,
minHeight: 50,
padding: 40,
},
label: {
position: 'absolute',
top: 0,
right: 0,
fontSize: 10,
},
message: {
padding: 10,
},
});