Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 95 additions & 3 deletions packages/react-native/flow/bom.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ declare class PerformanceEntry {
entryType: string;
name: string;
startTime: DOMHighResTimeStamp;
toJSON(): string;
toJSON(): {[string]: unknown};
}

// https://w3c.github.io/user-timing/#performancemark
Expand All @@ -127,7 +127,7 @@ declare class PerformanceServerTiming {
description: string;
duration: DOMHighResTimeStamp;
name: string;
toJSON(): string;
toJSON(): {[string]: unknown};
}

// https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming
Expand Down Expand Up @@ -224,7 +224,7 @@ declare class Performance {
endMark?: string,
): PerformanceMeasure;
now(): DOMHighResTimeStamp;
toJSON(): string;
toJSON(): {[string]: unknown};
}

declare var performance: Performance;
Expand Down Expand Up @@ -327,6 +327,98 @@ declare class DOMRectList {
[index: number]: DOMRect;
}

declare class MutationRecord {
// Always an empty NodeList for `attributes` and `characterData` mutations.
// React Native currently only supports `childList`, so this contains the
// added nodes for that mutation type.
+addedNodes: NodeList<Node>;
// Always `null` in React Native (only `childList` mutations are supported).
+attributeName: null;
// Always `null` in React Native (only `childList` mutations are supported).
+nextSibling: null;
// Always `null` in React Native (only `childList` mutations are supported,
// and `attributeOldValue`/`characterDataOldValue` are not supported).
+oldValue: null;
// Always `null` in React Native (only `childList` mutations are supported).
+previousSibling: null;
// Always an empty NodeList for `attributes` and `characterData` mutations.
// React Native currently only supports `childList`, so this contains the
// removed nodes for that mutation type.
+removedNodes: NodeList<Node>;
+target: Node;
// React Native currently only supports `childList` mutations.
+type: 'childList';
}

// React Native currently only supports `childList` mutations, so `childList`
// is required and must be `true`. The `attributes`/`attributeFilter`/
// `attributeOldValue`/`characterData`/`characterDataOldValue` options are not
// supported and will throw if provided.
declare type MutationObserverInit = {
+childList: true,
+subtree?: boolean,
...
};

declare class MutationObserver {
constructor(
callback: (
arr: Array<MutationRecord>,
observer: MutationObserver,
) => unknown,
): void;
disconnect(): void;
observe(target: Node, options: MutationObserverInit): void;
}

declare type IntersectionObserverEntry = {
+boundingClientRect: DOMRectReadOnly,
+intersectionRatio: number,
+intersectionRect: DOMRectReadOnly,
+isIntersecting: boolean,
// Always non-null in React Native.
+rootBounds: DOMRectReadOnly,
// React Native-specific extension. Equivalent to `intersectionRatio` but
// computed against the `rnRootThreshold` root-relative thresholds.
+rnRootIntersectionRatio: number,
+target: Element,
+time: DOMHighResTimeStamp,
...
};

declare type IntersectionObserverCallback = (
entries: Array<IntersectionObserverEntry>,
observer: IntersectionObserver,
) => unknown;

declare type IntersectionObserverOptions = {
root?: Node | null,
rootMargin?: string,
threshold?: number | Array<number>,
// React Native-specific extension. Thresholds expressed as a fraction of the
// root's size (instead of the target's size).
rnRootThreshold?: number | Array<number>,
...
};

// The `delay`, `scrollMargin` and `trackVisibility` options are not supported
// in React Native and will throw if provided.
declare class IntersectionObserver {
constructor(
callback: IntersectionObserverCallback,
options?: IntersectionObserverOptions,
): void;
disconnect(): void;
observe(target: Element): void;
+root: Element | null;
+rootMargin: string;
// React Native-specific extension. The thresholds expressed as a fraction of
// the root's size (set via the `rnRootThreshold` option).
+rnRootThresholds: ReadonlyArray<number> | null;
+thresholds: ReadonlyArray<number>;
unobserve(target: Element): void;
}

declare class CloseEvent extends Event {
code: number;
reason: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
* @format
*/

import type IntersectionObserverType from 'react-native/src/private/webapis/intersectionobserver/IntersectionObserver';

import {RNTesterThemeContext} from '../../components/RNTesterTheme';
import * as React from 'react';
import {
Expand All @@ -21,8 +19,6 @@ import {
} from 'react';
import {Button, ScrollView, StyleSheet, Text, View} from 'react-native';

declare var IntersectionObserver: Class<IntersectionObserverType>;

export const name = 'IntersectionObserver MDN Example';
export const title = name;
export const description =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/**
* 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.
*
* @flow strict-local
* @format
*/

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';

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];
}

component MutationObserverExample() {
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-type]
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<Node>): string {
return [...nodeList]
.map(node => (node instanceof Element && 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,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* 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.
*
* @flow strict-local
* @format
*/

import type {RNTesterModuleExample} from '../../types/RNTesterTypes';

import * as MutationObserverExample from './MutationObserverExample';
import * as VisualCompletionExample from './VisualCompletionExample/VisualCompletionExample';

export const framework = 'React';
export const title = 'MutationObserver';
export const category = 'UI';
export const documentationURL =
'https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver';
export const description = 'API to detect mutations in React Native nodes.';
export const showIndividualExamples = true;
export const examples: Array<RNTesterModuleExample> = [MutationObserverExample];

if (typeof IntersectionObserver !== 'undefined') {
examples.push(VisualCompletionExample);
}
Loading
Loading