diff --git a/.flowconfig b/.flowconfig index 1254fc06..eaf19a7c 100644 --- a/.flowconfig +++ b/.flowconfig @@ -18,6 +18,7 @@ munge_underscores=true types_first=false well_formed_exports=false +esproposal.optional_chaining=enable suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FlowFixMeProps diff --git a/packages/react-native-web/src/exports/Picker/index.js b/packages/react-native-web/src/exports/Picker/index.js index 18242486..73e2e28e 100644 --- a/packages/react-native-web/src/exports/Picker/index.js +++ b/packages/react-native-web/src/exports/Picker/index.js @@ -55,7 +55,7 @@ const Picker = forwardRef((props, forwardedRef) => { } } - // $FlowFixMe ViewProps should be exact in the future + // $FlowFixMe const supportedProps: any = { children, disabled: enabled === false ? true : undefined, diff --git a/packages/react-native-web/src/exports/StyleSheet/compile.js b/packages/react-native-web/src/exports/StyleSheet/compile.js index 0db330d6..4928f401 100644 --- a/packages/react-native-web/src/exports/StyleSheet/compile.js +++ b/packages/react-native-web/src/exports/StyleSheet/compile.js @@ -99,7 +99,7 @@ export function classic(style: Style, name: string): CompilerOutput { * Compile simple style object to inline DOM styles. * No support for 'animationKeyframes', 'placeholderTextColor', 'scrollbarWidth', or 'pointerEvents'. */ -export function inline(style: Style): any { +export function inline(style: Style): Object { return prefixInlineStyles(createReactDOMStyle(style)); } diff --git a/packages/react-native-web/src/vendor/hash/index.js b/packages/react-native-web/src/vendor/hash/index.js index bfec7445..ab87d0d6 100644 --- a/packages/react-native-web/src/vendor/hash/index.js +++ b/packages/react-native-web/src/vendor/hash/index.js @@ -11,6 +11,8 @@ * @param {string} str ASCII only * @param {number} seed Positive integer only * @return {number} 32-bit positive integer hash + * + * @flow */ function murmurhash2_32_gc(str, seed) { @@ -53,6 +55,6 @@ function murmurhash2_32_gc(str, seed) { return h >>> 0; } -const hash = str => murmurhash2_32_gc(str, 1).toString(36); +const hash = (str: string): string => murmurhash2_32_gc(str, 1).toString(36); export default hash; diff --git a/packages/react-native-web/src/vendor/react-native/Animated/AnimatedEvent.js b/packages/react-native-web/src/vendor/react-native/Animated/AnimatedEvent.js index 7195bcde..d3819c01 100644 --- a/packages/react-native-web/src/vendor/react-native/Animated/AnimatedEvent.js +++ b/packages/react-native-web/src/vendor/react-native/Animated/AnimatedEvent.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @noflow + * @flow * @format */ 'use strict'; @@ -26,7 +26,7 @@ function attachNativeEvent( viewRef: any, eventName: string, argMapping: Array, -) { +): {|detach: () => void|} { // Find animated values in `argMapping` and create an array representing their // key path inside the `nativeEvent` object. Ex.: ['contentOffset', 'x']. const eventMappings = []; @@ -130,7 +130,7 @@ class AnimatedEvent { this._attachedEvent && this._attachedEvent.detach(); } - __getHandler() { + __getHandler(): ((...args: any) => void) { if (this.__isNative) { return this._callListeners; } diff --git a/packages/react-native-web/src/vendor/react-native/Animated/Easing.js b/packages/react-native-web/src/vendor/react-native/Animated/Easing.js index ed0e9c96..a42e62f6 100644 --- a/packages/react-native-web/src/vendor/react-native/Animated/Easing.js +++ b/packages/react-native-web/src/vendor/react-native/Animated/Easing.js @@ -63,14 +63,14 @@ class Easing { /** * A stepping function, returns 1 for any positive value of `n`. */ - static step0(n: number) { + static step0(n: number): number { return n > 0 ? 1 : 0; } /** * A stepping function, returns 1 if `n` is greater than or equal to 1. */ - static step1(n: number) { + static step1(n: number): number { return n >= 1 ? 1 : 0; } @@ -80,7 +80,7 @@ class Easing { * * http://cubic-bezier.com/#0,0,1,1 */ - static linear(t: number) { + static linear(t: number): number { return t; } @@ -103,7 +103,7 @@ class Easing { * * http://easings.net/#easeInQuad */ - static quad(t: number) { + static quad(t: number): number { return t * t; } @@ -113,7 +113,7 @@ class Easing { * * http://easings.net/#easeInCubic */ - static cubic(t: number) { + static cubic(t: number): number { return t * t * t; } @@ -123,7 +123,7 @@ class Easing { * n = 4: http://easings.net/#easeInQuart * n = 5: http://easings.net/#easeInQuint */ - static poly(n: number) { + static poly(n: number): ((t: number) => number) { return (t: number) => Math.pow(t, n); } @@ -132,7 +132,7 @@ class Easing { * * http://easings.net/#easeInSine */ - static sin(t: number) { + static sin(t: number): number { return 1 - Math.cos((t * Math.PI) / 2); } @@ -141,7 +141,7 @@ class Easing { * * http://easings.net/#easeInCirc */ - static circle(t: number) { + static circle(t: number): number { return 1 - Math.sqrt(1 - t * t); } @@ -150,7 +150,7 @@ class Easing { * * http://easings.net/#easeInExpo */ - static exp(t: number) { + static exp(t: number): number { return Math.pow(2, 10 * (t - 1)); } diff --git a/packages/react-native-web/src/vendor/react-native/Animated/NativeAnimatedHelper.js b/packages/react-native-web/src/vendor/react-native/Animated/NativeAnimatedHelper.js index dde1c112..7f597e2a 100644 --- a/packages/react-native-web/src/vendor/react-native/Animated/NativeAnimatedHelper.js +++ b/packages/react-native-web/src/vendor/react-native/Animated/NativeAnimatedHelper.js @@ -298,7 +298,7 @@ const NativeAnimatedHelper = { assertNativeAnimatedModule, shouldUseNativeDriver, transformDataType, - get nativeEventEmitter() { + get nativeEventEmitter(): NativeEventEmitter<*> { if (!nativeEventEmitter) { nativeEventEmitter = new NativeEventEmitter(NativeAnimatedModule); } diff --git a/packages/react-native-web/src/vendor/react-native/Animated/NativeAnimatedModule.js b/packages/react-native-web/src/vendor/react-native/Animated/NativeAnimatedModule.js index fdaa7f65..ad2ce12a 100644 --- a/packages/react-native-web/src/vendor/react-native/Animated/NativeAnimatedModule.js +++ b/packages/react-native-web/src/vendor/react-native/Animated/NativeAnimatedModule.js @@ -67,4 +67,4 @@ export interface Spec extends TurboModule { +removeListeners: (count: number) => void; } -export default TurboModuleRegistry.get('NativeAnimatedModule'); +export default (TurboModuleRegistry.get('NativeAnimatedModule'): ?Spec); diff --git a/packages/react-native-web/src/vendor/react-native/Animated/animations/DecayAnimation.js b/packages/react-native-web/src/vendor/react-native/Animated/animations/DecayAnimation.js index 05e5734a..b81405ce 100644 --- a/packages/react-native-web/src/vendor/react-native/Animated/animations/DecayAnimation.js +++ b/packages/react-native-web/src/vendor/react-native/Animated/animations/DecayAnimation.js @@ -45,7 +45,7 @@ class DecayAnimation extends Animation { this.__iterations = config.iterations ?? 1; } - __getNativeAnimationConfig() { + __getNativeAnimationConfig(): {|deceleration: number, iterations: number, type: string, velocity: number|} { return { type: 'decay', deceleration: this._deceleration, diff --git a/packages/react-native-web/src/vendor/react-native/Animated/animations/SpringAnimation.js b/packages/react-native-web/src/vendor/react-native/Animated/animations/SpringAnimation.js index 6371f0a6..9c2752ee 100644 --- a/packages/react-native-web/src/vendor/react-native/Animated/animations/SpringAnimation.js +++ b/packages/react-native-web/src/vendor/react-native/Animated/animations/SpringAnimation.js @@ -137,7 +137,18 @@ class SpringAnimation extends Animation { invariant(this._mass > 0, 'Mass value must be greater than 0'); } - __getNativeAnimationConfig() { + __getNativeAnimationConfig(): {| + damping: number, + initialVelocity: number, + iterations: number, + mass: number, + overshootClamping: boolean, + restDisplacementThreshold: number, + restSpeedThreshold: number, + stiffness: number, + toValue: any, + type: string, +|} { return { type: 'spring', overshootClamping: this._overshootClamping, diff --git a/packages/react-native-web/src/vendor/react-native/Animated/bezier.js b/packages/react-native-web/src/vendor/react-native/Animated/bezier.js index 68f9b438..fa3636c6 100644 --- a/packages/react-native-web/src/vendor/react-native/Animated/bezier.js +++ b/packages/react-native-web/src/vendor/react-native/Animated/bezier.js @@ -84,7 +84,7 @@ export default function bezier( mY1: number, mX2: number, mY2: number, -) { +): ((x: number) => number) { if (!(mX1 >= 0 && mX1 <= 1 && mX2 >= 0 && mX2 <= 1)) { throw new Error('bezier x values must be in [0, 1] range'); } diff --git a/packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedInterpolation.js b/packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedInterpolation.js index 217c8f28..5506555d 100644 --- a/packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedInterpolation.js +++ b/packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedInterpolation.js @@ -220,11 +220,11 @@ function createInterpolationFromStringOutputRange( }); }); - /* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to - * guard against this possibility. - */ const interpolations = outputRange[0] .match(stringShapeRegex) + /* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to + * guard against this possibility. + */ .map((value, i) => { return createInterpolation({ ...config, @@ -307,7 +307,7 @@ function checkInfiniteRange(name: string, arr: Array) { class AnimatedInterpolation extends AnimatedWithChildren { // Export for testing. - static __createInterpolation = createInterpolation; + static __createInterpolation: (InterpolationConfigType) => (input: number) => number | string = createInterpolation; _parent: AnimatedNode; _config: InterpolationConfigType; @@ -347,7 +347,7 @@ class AnimatedInterpolation extends AnimatedWithChildren { super.__detach(); } - __transformDataType(range: Array) { + __transformDataType(range: Array): Array { // $FlowFixMe return range.map(NativeAnimatedHelper.transformDataType); } diff --git a/packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedNode.js b/packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedNode.js index 8b903ad5..d58e1ae3 100644 --- a/packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedNode.js +++ b/packages/react-native-web/src/vendor/react-native/Animated/nodes/AnimatedNode.js @@ -120,6 +120,7 @@ class AnimatedNode { NativeAnimatedAPI.startListeningToAnimatedNodeValue(this.__getNativeTag()); this.__nativeAnimatedValueListener = NativeAnimatedHelper.nativeEventEmitter.addListener( + // $FlowFixMe (< 0.127.0) Maybe fixed in a later flow upgrade 'onAnimatedValueUpdate', data => { if (data.tag !== this.__getNativeTag()) { diff --git a/packages/react-native-web/src/vendor/react-native/FillRateHelper/index.js b/packages/react-native-web/src/vendor/react-native/FillRateHelper/index.js index f5901435..dcef451e 100644 --- a/packages/react-native-web/src/vendor/react-native/FillRateHelper/index.js +++ b/packages/react-native-web/src/vendor/react-native/FillRateHelper/index.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. @@ -10,25 +10,27 @@ 'use strict'; -import performanceNow from 'fbjs/lib/performanceNow'; -import warning from 'fbjs/lib/warning'; - export type FillRateInfo = Info; class Info { - any_blank_count = 0; - any_blank_ms = 0; - any_blank_speed_sum = 0; - mostly_blank_count = 0; - mostly_blank_ms = 0; - pixels_blank = 0; - pixels_sampled = 0; - pixels_scrolled = 0; - total_time_spent = 0; - sample_count = 0; + any_blank_count: number = 0; + any_blank_ms: number = 0; + any_blank_speed_sum: number = 0; + mostly_blank_count: number = 0; + mostly_blank_ms: number = 0; + pixels_blank: number = 0; + pixels_sampled: number = 0; + pixels_scrolled: number = 0; + total_time_spent: number = 0; + sample_count: number = 0; } -type FrameMetrics = {inLayout?: boolean, length: number, offset: number}; +type FrameMetrics = { + inLayout?: boolean, + length: number, + offset: number, + ... +}; const DEBUG = false; @@ -52,11 +54,12 @@ class FillRateHelper { _mostlyBlankStartTime = (null: ?number); _samplesStartTime = (null: ?number); - static addListener(callback: FillRateInfo => void): {remove: () => void} { - warning( - _sampleRate !== null, - 'Call `FillRateHelper.setSampleRate` before `addListener`.', - ); + static addListener( + callback: FillRateInfo => void, + ): {remove: () => void, ...} { + if (_sampleRate === null) { + console.warn('Call `FillRateHelper.setSampleRate` before `addListener`.'); + } _listeners.push(callback); return { remove: () => { @@ -82,7 +85,7 @@ class FillRateHelper { activate() { if (this._enabled && this._samplesStartTime == null) { DEBUG && console.debug('FillRateHelper: activate'); - this._samplesStartTime = performanceNow(); + this._samplesStartTime = global.performance.now(); } } @@ -101,7 +104,7 @@ class FillRateHelper { this._resetData(); return; } - const total_time_spent = performanceNow() - start; + const total_time_spent = global.performance.now() - start; const info: any = { ...this._info, total_time_spent, @@ -130,19 +133,22 @@ class FillRateHelper { computeBlankness( props: { - data: Array, - getItemCount: (data: Array) => number, + data: any, + getItemCount: (data: any) => number, initialNumToRender: number, + ... }, state: { first: number, last: number, + ... }, scrollMetrics: { dOffset: number, offset: number, velocity: number, visibleLength: number, + ... }, ): number { if ( @@ -162,7 +168,7 @@ class FillRateHelper { const scrollSpeed = Math.round(Math.abs(velocity) * 1000); // px / sec // Whether blank now or not, record the elapsed time blank if we were blank last time. - const now = performanceNow(); + const now = global.performance.now(); if (this._anyBlankStartTime != null) { this._info.any_blank_ms += now - this._anyBlankStartTime; } @@ -232,4 +238,4 @@ class FillRateHelper { } } -export default FillRateHelper; +export default FillRateHelper; \ No newline at end of file diff --git a/packages/react-native-web/src/vendor/react-native/FlatList/index.js b/packages/react-native-web/src/vendor/react-native/FlatList/index.js index 268b4f34..c9a4fa58 100644 --- a/packages/react-native-web/src/vendor/react-native/FlatList/index.js +++ b/packages/react-native-web/src/vendor/react-native/FlatList/index.js @@ -7,7 +7,6 @@ * @flow * @format */ -'use strict'; import type {ViewProps} from '../../../exports/View'; @@ -16,25 +15,25 @@ import type { ViewToken, ViewabilityConfigCallbackPair, } from '../ViewabilityHelper'; -import type {Props as VirtualizedListProps} from '../VirtualizedList'; import deepDiffer from '../deepDiffer'; import * as React from 'react'; import StyleSheet from '../../../exports/StyleSheet'; import View from '../../../exports/View'; -import VirtualizedList from '../VirtualizedList'; - -import invariant from 'fbjs/lib/invariant'; - -export type SeparatorsObj = { - highlight: () => void, - unhighlight: () => void, - updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, -}; +import ScrollView from '../../../exports/ScrollView'; +import VirtualizedList, { type RenderItemType } from '../VirtualizedList'; type ViewStyleProp = $PropertyType; +import invariant from 'fbjs/lib/invariant'; -type RequiredProps = { +type RequiredProps = {| + /** + * For simplicity, data is just a plain array. If you want to use something else, like an + * immutable list, use the underlying `VirtualizedList` directly. + */ + data: ?$ReadOnlyArray, +|}; +type OptionalProps = {| /** * Takes an item from `data` and renders it into the list. Example usage: * @@ -61,48 +60,8 @@ type RequiredProps = { * `highlight` and `unhighlight` (which set the `highlighted: boolean` prop) are insufficient for * your use-case. */ - renderItem: (info: { - item: ItemT, - index: number, - separators: SeparatorsObj, - }) => ?React.Node, - /** - * For simplicity, data is just a plain array. If you want to use something else, like an - * immutable list, use the underlying `VirtualizedList` directly. - */ - data: ?$ReadOnlyArray, -}; -type OptionalProps = { - /** - * Rendered in between each item, but not at the top or bottom. By default, `highlighted` and - * `leadingItem` props are provided. `renderItem` provides `separators.highlight`/`unhighlight` - * which will update the `highlighted` prop, but you can also add custom props with - * `separators.updateProps`. - */ - ItemSeparatorComponent?: ?React.ComponentType, - /** - * Rendered when the list is empty. Can be a React Component Class, a render function, or - * a rendered element. - */ - ListEmptyComponent?: ?(React.ComponentType | React.Element), - /** - * Rendered at the bottom of all the items. Can be a React Component Class, a render function, or - * a rendered element. - */ - ListFooterComponent?: ?(React.ComponentType | React.Element), - /** - * Styling for internal View for ListFooterComponent - */ - ListFooterComponentStyle?: ViewStyleProp, - /** - * Rendered at the top of all the items. Can be a React Component Class, a render function, or - * a rendered element. - */ - ListHeaderComponent?: ?(React.ComponentType | React.Element), - /** - * Styling for internal View for ListHeaderComponent - */ - ListHeaderComponentStyle?: ViewStyleProp, + renderItem?: ?RenderItemType, + /** * Optional custom style for multi-item rows generated when numColumns > 1. */ @@ -129,7 +88,12 @@ type OptionalProps = { getItemLayout?: ( data: ?Array, index: number, - ) => {length: number, offset: number, index: number}, + ) => { + length: number, + offset: number, + index: number, + ... + }, /** * If true, renders items next to each other horizontally instead of stacked vertically. */ @@ -163,71 +127,37 @@ type OptionalProps = { */ numColumns: number, /** - * Called once when the scroll position gets within `onEndReachedThreshold` of the rendered - * content. + * See `ScrollView` for flow type and further documentation. */ - onEndReached?: ?(info: {distanceFromEnd: number}) => void, - /** - * How far from the end (in units of visible length of the list) the bottom edge of the - * list must be from the end of the content to trigger the `onEndReached` callback. - * Thus a value of 0.5 will trigger `onEndReached` when the end of the content is - * within half the visible length of the list. - */ - onEndReachedThreshold?: ?number, - /** - * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make - * sure to also set the `refreshing` prop correctly. - */ - onRefresh?: ?() => void, - /** - * Called when the viewability of rows changes, as defined by the `viewabilityConfig` prop. - */ - onViewableItemsChanged?: ?(info: { - viewableItems: Array, - changed: Array, - }) => void, - /** - * Set this when offset is needed for the loading indicator to show correctly. - * @platform android - */ - progressViewOffset?: number, - /** - * The legacy implementation is no longer supported. - */ - legacyImplementation?: empty, - /** - * Set this true while waiting for new data from a refresh. - */ - refreshing?: ?boolean, - /** - * Note: may have bugs (missing content) in some circumstances - use at your own risk. - * - * This may improve scroll performance for large lists. - */ - removeClippedSubviews?: boolean, - /** - * See `ViewabilityHelper` for flow type and further documentation. - */ - viewabilityConfig?: ViewabilityConfig, - /** - * List of ViewabilityConfig/onViewableItemsChanged pairs. A specific onViewableItemsChanged - * will be called when its corresponding ViewabilityConfig's conditions are met. - */ - viewabilityConfigCallbackPairs?: Array, + fadingEdgeLength?: ?number, +|}; + +type FlatListProps = {| + ...RequiredProps, + ...OptionalProps, +|}; + +type VirtualizedListProps = React.ElementConfig; + +export type Props = { + ...$Diff< + VirtualizedListProps, + { + getItem: $PropertyType, + getItemCount: $PropertyType, + getItemLayout: $PropertyType, + renderItem: $PropertyType, + keyExtractor: $PropertyType, + ... + }, + >, + ...FlatListProps, + ... }; -export type Props = RequiredProps & - OptionalProps & - VirtualizedListProps; const defaultProps = { ...VirtualizedList.defaultProps, numColumns: 1, - /** - * Enabling this prop on Android greatly improves scrolling performance with no known issues. - * The alternative is that scrolling on Android is unusably bad. Enabling it on iOS has a few - * known issues. - */ - removeClippedSubviews: false, }; export type DefaultProps = typeof defaultProps; @@ -345,7 +275,7 @@ class FlatList extends React.PureComponent, void> { /** * Scrolls to the end of the content. May be janky without `getItemLayout` prop. */ - scrollToEnd(params?: ?{animated?: ?boolean}) { + scrollToEnd(params?: ?{animated?: ?boolean, ...}) { if (this._listRef) { this._listRef.scrollToEnd(params); } @@ -364,6 +294,7 @@ class FlatList extends React.PureComponent, void> { index: number, viewOffset?: number, viewPosition?: number, + ... }) { if (this._listRef) { this._listRef.scrollToIndex(params); @@ -380,6 +311,7 @@ class FlatList extends React.PureComponent, void> { animated?: ?boolean, item: ItemT, viewPosition?: number, + ... }) { if (this._listRef) { this._listRef.scrollToItem(params); @@ -391,7 +323,7 @@ class FlatList extends React.PureComponent, void> { * * Check out [scrollToOffset](docs/virtualizedlist.html#scrolltooffset) of VirtualizedList */ - scrollToOffset(params: {animated?: ?boolean, offset: number}) { + scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) { if (this._listRef) { this._listRef.scrollToOffset(params); } @@ -422,7 +354,7 @@ class FlatList extends React.PureComponent, void> { /** * Provides a handle to the underlying scroll responder. */ - getScrollResponder() { + getScrollResponder(): ?typeof ScrollView { if (this._listRef) { return this._listRef.getScrollResponder(); } @@ -431,19 +363,23 @@ class FlatList extends React.PureComponent, void> { /** * Provides a reference to the underlying host component */ - getNativeScrollRef() { + getNativeScrollRef(): + | ?React.ElementRef + | ?React.ElementRef { if (this._listRef) { + /* $FlowFixMe[incompatible-return] Suppresses errors found when fixing + * TextInput typing */ return this._listRef.getScrollRef(); } } - getScrollableNode() { + getScrollableNode(): any { if (this._listRef) { return this._listRef.getScrollableNode(); } } - setNativeProps(props: {[string]: mixed}) { + setNativeProps(props: {[string]: mixed, ...}) { if (this._listRef) { this._listRef.setNativeProps(props); } @@ -462,10 +398,10 @@ class FlatList extends React.PureComponent, void> { }), ); } else if (this.props.onViewableItemsChanged) { - /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.63 was deployed. To see the error delete this - * comment and run Flow. */ this._virtualizedListPairs.push({ + /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an + * error found when Flow v0.63 was deployed. To see the error delete + * this comment and run Flow. */ viewabilityConfig: this.props.viewabilityConfig, onViewableItemsChanged: this._createOnViewableItemsChanged( this.props.onViewableItemsChanged, @@ -500,14 +436,15 @@ class FlatList extends React.PureComponent, void> { _listRef: ?React.ElementRef; _virtualizedListPairs: Array = []; - // $FlowFixMe _captureRef = ref => { this._listRef = ref; }; _checkProps(props: Props) { const { + // $FlowFixMe this prop doesn't exist, is only used for an invariant getItem, + // $FlowFixMe this prop doesn't exist, is only used for an invariant getItemCount, horizontal, numColumns, @@ -551,7 +488,12 @@ class FlatList extends React.PureComponent, void> { }; _getItemCount = (data: ?Array): number => { - return data ? Math.ceil(data.length / this.props.numColumns) : 0; + if (data) { + const {numColumns} = this.props; + return numColumns > 1 ? Math.ceil(data.length / numColumns) : data.length; + } else { + return 0; + } }; _keyExtractor = (items: ItemT | Array, index: number) => { @@ -563,13 +505,14 @@ class FlatList extends React.PureComponent, void> { 'array with 1-%s columns; instead, received a single item.', numColumns, ); - return items - .map((it, kk) => keyExtractor(it, index * numColumns + kk)) - .join(':'); + return ( + items + // $FlowFixMe[incompatible-call] + .map((it, kk) => keyExtractor(it, index * numColumns + kk)) + .join(':') + ); } else { - /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.63 was deployed. To see the error delete this - * comment and run Flow. */ + // $FlowFixMe Can't call keyExtractor with an array return keyExtractor(items, index); } }; @@ -587,11 +530,13 @@ class FlatList extends React.PureComponent, void> { onViewableItemsChanged: ?(info: { viewableItems: Array, changed: Array, + ... }) => void, ) { return (info: { viewableItems: Array, changed: Array, + ... }) => { const {numColumns} = this.props; if (onViewableItemsChanged) { @@ -610,47 +555,74 @@ class FlatList extends React.PureComponent, void> { }; } - _renderItem = (info: Object): ?React.Node => { - const {renderItem, numColumns, columnWrapperStyle} = this.props; - if (numColumns > 1) { - const {item, index} = info; - invariant( - Array.isArray(item), - 'Expected array of items with numColumns > 1', - ); - return ( - - {item.map((it, kk) => { - const element = renderItem({ - item: it, - index: index * numColumns + kk, - separators: info.separators, - }); - return element != null ? ( - {element} - ) : null; - })} - - ); - } else { - return renderItem(info); - } + _renderer = () => { + const { + ListItemComponent, + renderItem, + numColumns, + columnWrapperStyle, + } = this.props; + + let virtualizedListRenderKey = ListItemComponent + ? 'ListItemComponent' + : 'renderItem'; + + const renderer = (props): React.Node => { + if (ListItemComponent) { + // $FlowFixMe Component isn't valid + return ; + } else if (renderItem) { + // $FlowFixMe[incompatible-call] + return renderItem(props); + } else { + return null; + } + }; + + return { + /* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment suppresses an + * error found when Flow v0.111 was deployed. To see the error, delete + * this comment and run Flow. */ + [virtualizedListRenderKey]: (info: RenderItemProps) => { + if (numColumns > 1) { + const {item, index} = info; + invariant( + Array.isArray(item), + 'Expected array of items with numColumns > 1', + ); + return ( + + {item.map((it, kk) => { + const element = renderer({ + item: it, + index: index * numColumns + kk, + separators: info.separators, + }); + return element != null ? ( + {element} + ) : null; + })} + + ); + } else { + return renderer(info); + } + }, + }; }; - render() { + render(): React.Node { + const {numColumns, columnWrapperStyle, ...restProps} = this.props; + return ( ); } @@ -660,4 +632,4 @@ const styles = StyleSheet.create({ row: {flexDirection: 'row'}, }); -export default FlatList; +export default FlatList; \ No newline at end of file diff --git a/packages/react-native-web/src/vendor/react-native/LayoutAnimation/index.js b/packages/react-native-web/src/vendor/react-native/LayoutAnimation/index.js index cfdf5a32..244256fd 100644 --- a/packages/react-native-web/src/vendor/react-native/LayoutAnimation/index.js +++ b/packages/react-native-web/src/vendor/react-native/LayoutAnimation/index.js @@ -68,8 +68,8 @@ function create( } const Presets = { - easeInEaseOut: create(300, 'easeInEaseOut', 'opacity'), - linear: create(500, 'linear', 'opacity'), + easeInEaseOut: (create(300, 'easeInEaseOut', 'opacity'): LayoutAnimationConfig), + linear: (create(500, 'linear', 'opacity'): LayoutAnimationConfig), spring: { duration: 700, create: { @@ -134,9 +134,9 @@ const LayoutAnimation = { console.error('LayoutAnimation.checkConfig(...) has been disabled.'); }, Presets, - easeInEaseOut: configureNext.bind(null, Presets.easeInEaseOut), - linear: configureNext.bind(null, Presets.linear), - spring: configureNext.bind(null, Presets.spring), + easeInEaseOut: (configureNext.bind(null, Presets.easeInEaseOut): (onAnimationDidEnd?: any) => void), + linear: (configureNext.bind(null, Presets.linear): (onAnimationDidEnd?: any) => void), + spring: (configureNext.bind(null, Presets.spring): (onAnimationDidEnd?: any) => void), }; export default LayoutAnimation; diff --git a/packages/react-native-web/src/vendor/react-native/NativeEventEmitter/RCTDeviceEventEmitter.js b/packages/react-native-web/src/vendor/react-native/NativeEventEmitter/RCTDeviceEventEmitter.js index 8e3d0156..c19ed35c 100644 --- a/packages/react-native-web/src/vendor/react-native/NativeEventEmitter/RCTDeviceEventEmitter.js +++ b/packages/react-native-web/src/vendor/react-native/NativeEventEmitter/RCTDeviceEventEmitter.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. @@ -11,9 +11,8 @@ 'use strict'; import EventEmitter from '../emitter/EventEmitter'; -import EventSubscriptionVendor from '../emitter/EventSubscriptionVendor'; - -import type EmitterSubscription from '../emitter/EmitterSubscription'; +import type EmitterSubscription from '../emitter/_EmitterSubscription'; +import EventSubscriptionVendor from '../emitter/_EventSubscriptionVendor'; const __DEV__ = process.env.NODE_ENV !== 'production'; @@ -47,34 +46,38 @@ function checkNativeEventModule(eventType: ?string) { * Deprecated - subclass NativeEventEmitter to create granular event modules instead of * adding all event listeners directly to RCTDeviceEventEmitter. */ -class RCTDeviceEventEmitter extends EventEmitter { - sharedSubscriber: EventSubscriptionVendor; +class RCTDeviceEventEmitter< + EventDefinitions: {...}, +> extends EventEmitter { + sharedSubscriber: EventSubscriptionVendor; constructor() { - const sharedSubscriber = new EventSubscriptionVendor(); + const sharedSubscriber = new EventSubscriptionVendor(); super(sharedSubscriber); this.sharedSubscriber = sharedSubscriber; } - addListener( - eventType: string, - listener: Function, - context: ?Object, - ): EmitterSubscription { + addListener>( + eventType: K, + listener: (...$ElementType) => mixed, + context: $FlowFixMe, + ): EmitterSubscription { if (__DEV__) { checkNativeEventModule(eventType); } return super.addListener(eventType, listener, context); } - removeAllListeners(eventType: ?string) { + removeAllListeners>(eventType: ?K): void { if (__DEV__) { checkNativeEventModule(eventType); } super.removeAllListeners(eventType); } - removeSubscription(subscription: EmitterSubscription) { + removeSubscription>( + subscription: EmitterSubscription, + ): void { if (subscription.emitter !== this) { subscription.emitter.removeSubscription(subscription); } else { @@ -83,4 +86,7 @@ class RCTDeviceEventEmitter extends EventEmitter { } } -export default new RCTDeviceEventEmitter(); +// FIXME: use typed events +type RCTDeviceEventDefinitions = $FlowFixMe; + +export default (new RCTDeviceEventEmitter(): RCTDeviceEventEmitter); diff --git a/packages/react-native-web/src/vendor/react-native/NativeEventEmitter/index.js b/packages/react-native-web/src/vendor/react-native/NativeEventEmitter/index.js index 0a5e89ad..4b21570b 100644 --- a/packages/react-native-web/src/vendor/react-native/NativeEventEmitter/index.js +++ b/packages/react-native-web/src/vendor/react-native/NativeEventEmitter/index.js @@ -11,54 +11,55 @@ 'use strict'; import EventEmitter from '../emitter/EventEmitter'; +import type EmitterSubscription from '../emitter/_EmitterSubscription'; import RCTDeviceEventEmitter from './RCTDeviceEventEmitter'; - import invariant from 'fbjs/lib/invariant'; -import type EmitterSubscription from '../emitter/EmitterSubscription'; - type NativeModule = { +addListener: (eventType: string) => void, +removeListeners: (count: number) => void, + ... }; /** * Abstract base class for implementing event-emitting modules. This implements * a subset of the standard EventEmitter node module API. */ -class NativeEventEmitter extends EventEmitter { +export default class NativeEventEmitter< + EventDefinitions: {...}, +> extends EventEmitter { _nativeModule: ?NativeModule; constructor(nativeModule: ?NativeModule) { super(RCTDeviceEventEmitter.sharedSubscriber); } - addListener( - eventType: string, - listener: Function, - context: ?Object, - ): EmitterSubscription { + addListener>( + eventType: K, + listener: (...$ElementType) => mixed, + context: $FlowFixMe, + ): EmitterSubscription { if (this._nativeModule != null) { this._nativeModule.addListener(eventType); } return super.addListener(eventType, listener, context); } - removeAllListeners(eventType: string) { + removeAllListeners>(eventType: ?K): void { invariant(eventType, 'eventType argument is required.'); - const count = this.listeners(eventType).length; + const count = this.listenerCount(eventType); if (this._nativeModule != null) { this._nativeModule.removeListeners(count); } super.removeAllListeners(eventType); } - removeSubscription(subscription: EmitterSubscription) { + removeSubscription>( + subscription: EmitterSubscription, + ): void { if (this._nativeModule != null) { this._nativeModule.removeListeners(1); } super.removeSubscription(subscription); } } - -export default NativeEventEmitter; diff --git a/packages/react-native-web/src/vendor/react-native/PanResponder/index.js b/packages/react-native-web/src/vendor/react-native/PanResponder/index.js index 8eb14221..d3806530 100644 --- a/packages/react-native-web/src/vendor/react-native/PanResponder/index.js +++ b/packages/react-native-web/src/vendor/react-native/PanResponder/index.js @@ -385,7 +385,24 @@ const PanResponder = { * accordingly. (numberActiveTouches) may not be totally accurate unless you * are the responder. */ - create(config: PanResponderConfig) { + create(config: PanResponderConfig): {| + getInteractionHandle: () => ?number, + panHandlers: {| + onClickCapture: (event: any) => void, + onMoveShouldSetResponder: (event: PressEvent) => boolean, + onMoveShouldSetResponderCapture: (event: PressEvent) => boolean, + onResponderEnd: (event: PressEvent) => void, + onResponderGrant: (event: PressEvent) => boolean, + onResponderMove: (event: PressEvent) => void, + onResponderReject: (event: PressEvent) => void, + onResponderRelease: (event: PressEvent) => void, + onResponderStart: (event: PressEvent) => void, + onResponderTerminate: (event: PressEvent) => void, + onResponderTerminationRequest: (event: PressEvent) => boolean, + onStartShouldSetResponder: (event: PressEvent) => boolean, + onStartShouldSetResponderCapture: (event: PressEvent) => boolean, + |}, +|} { const interactionState: InteractionState = { handle: null, shouldCancelClick: false, diff --git a/packages/react-native-web/src/vendor/react-native/SectionList/index.js b/packages/react-native-web/src/vendor/react-native/SectionList/index.js index 17cd2350..3f33756e 100644 --- a/packages/react-native-web/src/vendor/react-native/SectionList/index.js +++ b/packages/react-native-web/src/vendor/react-native/SectionList/index.js @@ -14,6 +14,7 @@ import * as React from 'react'; import ScrollView from '../../../exports/ScrollView'; import VirtualizedSectionList from '../VirtualizedSectionList'; +import type { ScrollToLocationParamsType } from '../VirtualizedSectionList'; import type {ViewToken} from '../ViewabilityHelper'; import type { SectionBase as _SectionBase, @@ -245,13 +246,7 @@ class SectionList> extends React.PureComponent< * Note: cannot scroll to locations outside the render window without specifying the * `getItemLayout` prop. */ - scrollToLocation(params: { - animated?: ?boolean, - itemIndex: number, - sectionIndex: number, - viewOffset?: number, - viewPosition?: number, - }) { + scrollToLocation(params: ScrollToLocationParamsType) { if (this._wrapperListRef != null) { this._wrapperListRef.scrollToLocation(params); } @@ -280,14 +275,14 @@ class SectionList> extends React.PureComponent< /** * Provides a handle to the underlying scroll responder. */ - getScrollResponder(): ?ScrollView { + getScrollResponder(): ?typeof ScrollView { const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); if (listRef) { return listRef.getScrollResponder(); } } - getScrollableNode() { + getScrollableNode(): any | void { const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); if (listRef) { return listRef.getScrollableNode(); @@ -301,7 +296,7 @@ class SectionList> extends React.PureComponent< } } - render() { + render(): React.Node { return ( /* $FlowFixMe(>=0.66.0 site=react_native_fb) This comment suppresses an * error found when Flow v0.66 was deployed. To see the error delete this @@ -317,7 +312,7 @@ class SectionList> extends React.PureComponent< _wrapperListRef: ?React.ElementRef; // $FlowFixMe - _captureRef = ref => { + _captureRef: ((ref: any) => void) = ref => { // $FlowFixMe this._wrapperListRef = ref; }; diff --git a/packages/react-native-web/src/vendor/react-native/StaticContainer/index.js b/packages/react-native-web/src/vendor/react-native/StaticContainer/index.js index 1dc0e59f..18bbb6aa 100644 --- a/packages/react-native-web/src/vendor/react-native/StaticContainer/index.js +++ b/packages/react-native-web/src/vendor/react-native/StaticContainer/index.js @@ -43,7 +43,7 @@ class StaticContainer extends React.Component { return !!nextProps.shouldUpdate; } - render() { + render(): null | React.Node { const child = this.props.children; return child === null || child === false ? null @@ -52,4 +52,3 @@ class StaticContainer extends React.Component { } export default StaticContainer; - diff --git a/packages/react-native-web/src/vendor/react-native/ViewabilityHelper/index.js b/packages/react-native-web/src/vendor/react-native/ViewabilityHelper/index.js index a2bfdcb0..9c88481b 100644 --- a/packages/react-native-web/src/vendor/react-native/ViewabilityHelper/index.js +++ b/packages/react-native-web/src/vendor/react-native/ViewabilityHelper/index.js @@ -71,10 +71,7 @@ export type ViewabilityConfig = {| class ViewabilityHelper { _config: ViewabilityConfig; _hasInteracted: boolean = false; - /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an error - * found when Flow v0.63 was deployed. To see the error delete this comment - * and run Flow. */ - _timers: Set = new Set(); + _timers: Set = new Set(); _viewableIndices: Array = []; _viewableItems: Map = new Map(); diff --git a/packages/react-native-web/src/vendor/react-native/VirtualizedList/VirtualizedListContext.js b/packages/react-native-web/src/vendor/react-native/VirtualizedList/VirtualizedListContext.js new file mode 100644 index 00000000..9bb56127 --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/VirtualizedList/VirtualizedListContext.js @@ -0,0 +1,155 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 VirtualizedList from './'; +import * as React from 'react'; +import {useMemo, useContext} from 'react'; + +const __DEV__ = process.env.NODE_ENV !== 'production'; + +type Frame = $ReadOnly<{ + offset: number, + length: number, + index: number, + inLayout: boolean, +}>; + +export type ChildListState = $ReadOnly<{ + first: number, + last: number, + frames: {[key: number]: Frame}, +}>; + +// Data propagated through nested lists (regardless of orientation) that is +// useful for producing diagnostics for usage errors involving nesting (e.g +// missing/duplicate keys). +export type ListDebugInfo = $ReadOnly<{ + cellKey: string, + listKey: string, + parent: ?ListDebugInfo, + // We include all ancestors regardless of orientation, so this is not always + // identical to the child's orientation. + horizontal: boolean, +}>; + +type Context = $ReadOnly<{ + cellKey: ?string, + getScrollMetrics: () => { + contentLength: number, + dOffset: number, + dt: number, + offset: number, + timestamp: number, + velocity: number, + visibleLength: number, + }, + horizontal: ?boolean, + getOutermostParentListRef: () => VirtualizedList, + getNestedChildState: string => ?ChildListState, + registerAsNestedChild: ({ + cellKey: string, + key: string, + ref: VirtualizedList, + parentDebugInfo: ListDebugInfo, + }) => ?ChildListState, + unregisterAsNestedChild: ({ + key: string, + state: ChildListState, + }) => void, + debugInfo: ListDebugInfo, +}>; + +export const VirtualizedListContext: React.Context = React.createContext( + null, +); +if (__DEV__) { + VirtualizedListContext.displayName = 'VirtualizedListContext'; +} + +/** + * Resets the context. Intended for use by portal-like components (e.g. Modal). + */ +export function VirtualizedListContextResetter({ + children, +}: { + children: React.Node, +}): React.Node { + return ( + + {children} + + ); +} + +/** + * Sets the context with memoization. Intended to be used by `VirtualizedList`. + */ +export function VirtualizedListContextProvider({ + children, + value, +}: { + children: React.Node, + value: Context, +}): React.Node { + // Avoid setting a newly created context object if the values are identical. + const context = useMemo( + () => ({ + cellKey: null, + getScrollMetrics: value.getScrollMetrics, + horizontal: value.horizontal, + getOutermostParentListRef: value.getOutermostParentListRef, + getNestedChildState: value.getNestedChildState, + registerAsNestedChild: value.registerAsNestedChild, + unregisterAsNestedChild: value.unregisterAsNestedChild, + debugInfo: { + cellKey: value.debugInfo.cellKey, + horizontal: value.debugInfo.horizontal, + listKey: value.debugInfo.listKey, + parent: value.debugInfo.parent, + }, + }), + [ + value.getScrollMetrics, + value.horizontal, + value.getOutermostParentListRef, + value.getNestedChildState, + value.registerAsNestedChild, + value.unregisterAsNestedChild, + value.debugInfo.cellKey, + value.debugInfo.horizontal, + value.debugInfo.listKey, + value.debugInfo.parent, + ], + ); + return ( + + {children} + + ); +} + +/** + * Sets the `cellKey`. Intended to be used by `VirtualizedList` for each cell. + */ +export function VirtualizedListCellContextProvider({ + cellKey, + children, +}: { + cellKey: string, + children: React.Node, +}): React.Node { + const context = useContext(VirtualizedListContext); + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/packages/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/packages/react-native-web/src/vendor/react-native/VirtualizedList/index.js index 8dc8fc97..23a9ccea 100644 --- a/packages/react-native-web/src/vendor/react-native/VirtualizedList/index.js +++ b/packages/react-native-web/src/vendor/react-native/VirtualizedList/index.js @@ -7,7 +7,6 @@ * @flow * @format */ -'use strict'; import type {ViewProps} from '../../../exports/View'; import type { @@ -33,6 +32,13 @@ import infoLog from '../infoLog'; import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; import { computeWindowedRenderLimits } from '../VirtualizeUtils'; +import { + VirtualizedListCellContextProvider, + VirtualizedListContext, + VirtualizedListContextProvider, + type ChildListState, + type ListDebugInfo, +} from './VirtualizedListContext.js'; type Item = any; type ViewStyleProp = $PropertyType; @@ -41,18 +47,35 @@ export type renderItemType = (info: any) => ?React.Element; const __DEV__ = process.env.NODE_ENV !== 'production'; +export type Separators = { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + ... +}; + +export type RenderItemProps = { + item: ItemT, + index: number, + separators: Separators, + ... +}; + +export type RenderItemType = ( + info: RenderItemProps, +) => React.Node; + type ViewabilityHelperCallbackTuple = { viewabilityHelper: ViewabilityHelper, onViewableItemsChanged: (info: { viewableItems: Array, changed: Array, + ... }) => void, + ... }; -type RequiredProps = { - // TODO: Conflicts with the optional `renderItem` in - // `VirtualizedSectionList`'s props. - renderItem: $FlowFixMe, +type RequiredProps = {| /** * The default accessor functions assume this is an Array<{key: string} | {id: string}> but you can override * getItem, getItemCount, and keyExtractor to handle any type of index-based data. @@ -66,8 +89,9 @@ type RequiredProps = { * Determines how many items are in the data blob. */ getItemCount: (data: any) => number, -}; -type OptionalProps = { +|}; +type OptionalProps = {| + renderItem?: ?RenderItemType, /** * `debug` will turn on extra logging and visual overlays to aid with debugging both usage and * implementation, but with a significant perf hit. @@ -85,10 +109,16 @@ type OptionalProps = { * `data` prop, stick it here and treat it immutably. */ extraData?: any, + // e.g. height, y getItemLayout?: ( data: any, index: number, - ) => {length: number, offset: number, index: number}, // e.g. height, y + ) => { + length: number, + offset: number, + index: number, + ... + }, horizontal?: ?boolean, /** * How many items to render in the initial batch. This should be enough to fill the screen but not @@ -113,6 +143,40 @@ type OptionalProps = { * or a render function. Defaults to using View. */ CellRendererComponent?: ?React.ComponentType, + /** + * Rendered in between each item, but not at the top or bottom. By default, `highlighted` and + * `leadingItem` props are provided. `renderItem` provides `separators.highlight`/`unhighlight` + * which will update the `highlighted` prop, but you can also add custom props with + * `separators.updateProps`. + */ + ItemSeparatorComponent?: ?React.ComponentType, + /** + * Takes an item from `data` and renders it into the list. Example usage: + * + * ( + * + * )} + * data={[{title: 'Title Text', key: 'item1'}]} + * ListItemComponent={({item, separators}) => ( + * this._onPress(item)} + * onShowUnderlay={separators.highlight} + * onHideUnderlay={separators.unhighlight}> + * + * {item.title} + * + * + * )} + * /> + * + * Provides additional metadata like `index` if you need it, as well as a more generic + * `separators.updateProps` function which let's you set whatever props you want to change the + * rendering of either the leading separator or trailing separator in case the more common + * `highlight` and `unhighlight` (which set the `highlighted: boolean` prop) are insufficient for + * your use-case. + */ + ListItemComponent?: ?(React.ComponentType | React.Element), /** * Rendered when the list is empty. Can be a React Component Class, a render function, or * a rendered element. @@ -144,18 +208,27 @@ type OptionalProps = { listKey?: string, /** * The maximum number of items to render in each incremental render batch. The more rendered at - * once, the better the fill rate, but responsiveness my suffer because rendering content may + * once, the better the fill rate, but responsiveness may suffer because rendering content may * interfere with responding to button taps or other interactions. */ maxToRenderPerBatch: number, - onEndReached?: ?(info: {distanceFromEnd: number}) => void, - onEndReachedThreshold?: ?number, // units of visible length - onLayout?: ?Function, + /** + * Called once when the scroll position gets within `onEndReachedThreshold` of the rendered + * content. + */ + onEndReached?: ?(info: {distanceFromEnd: number, ...}) => void, + /** + * How far from the end (in units of visible length of the list) the bottom edge of the + * list must be from the end of the content to trigger the `onEndReached` callback. + * Thus a value of 0.5 will trigger `onEndReached` when the end of the content is + * within half the visible length of the list. + */ + onEndReachedThreshold?: ?number, /** * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make * sure to also set the `refreshing` prop correctly. */ - onRefresh?: ?Function, + onRefresh?: ?() => void, /** * Used to handle failures when scrolling to an index that has not been measured yet. Recommended * action is to either compute your own offset and `scrollTo` it, or scroll as far as possible and @@ -165,6 +238,7 @@ type OptionalProps = { index: number, highestMeasuredFrameIndex: number, averageItemLength: number, + ... }) => void, /** * Called when the viewability of rows changes, as defined by the @@ -173,6 +247,7 @@ type OptionalProps = { onViewableItemsChanged?: ?(info: { viewableItems: Array, changed: Array, + ... }) => void, persistentScrollbar?: ?boolean, /** @@ -205,6 +280,9 @@ type OptionalProps = { * screen. Similar fill rate/responsiveness tradeoff as `maxToRenderPerBatch`. */ updateCellsBatchingPeriod: number, + /** + * See `ViewabilityHelper` for flow type and further documentation. + */ viewabilityConfig?: ViewabilityConfig, /** * List of ViewabilityConfig/onViewableItemsChanged pairs. A specific onViewableItemsChanged @@ -219,32 +297,42 @@ type OptionalProps = { * chance that fast scrolling may reveal momentary blank areas of unrendered content. */ windowSize: number, -}; -/* $FlowFixMe - this Props seems to be missing a bunch of stuff. Remove this - * comment to see the errors */ -export type Props = RequiredProps & OptionalProps; + /** + * The legacy implementation is no longer supported. + */ + legacyImplementation?: empty, +|}; + +type Props = {| + // $FlowFixMe: View should be changed to an exact type in the future + ...React.ElementConfig, + ...RequiredProps, + ...OptionalProps, +|}; + +type DefaultProps = {| + disableVirtualization: boolean, + horizontal: boolean, + initialNumToRender: number, + keyExtractor: (item: Item, index: number) => string, + maxToRenderPerBatch: number, + onEndReachedThreshold: number, + scrollEventThrottle: number, + updateCellsBatchingPeriod: number, + windowSize: number, +|}; let _usedIndexForKey = false; let _keylessItemComponentName: string = ''; -type Frame = { - offset: number, - length: number, - index: number, - inLayout: boolean, -}; - -type ChildListState = { +type State = { first: number, last: number, - frames: {[key: number]: Frame}, }; -type State = {first: number, last: number}; - /** - * Base implementation for the more convenient [``](/react-native/docs/flatlist.html) - * and [``](/react-native/docs/sectionlist.html) components, which are also better + * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist.html) + * and [``](https://reactnative.dev/docs/sectionlist.html) components, which are also better * documented. In general, this should only really be used if you need more flexibility than * `FlatList` provides, e.g. for use with immutable data instead of plain arrays. * @@ -271,10 +359,10 @@ type State = {first: number, last: number}; * */ class VirtualizedList extends React.PureComponent { - props: Props; + static contextType: typeof VirtualizedListContext = VirtualizedListContext; // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params?: ?{animated?: ?boolean}) { + scrollToEnd(params?: ?{animated?: ?boolean, ...}) { const animated = params ? params.animated : true; const veryLast = this.props.getItemCount(this.props.data) - 1; const frame = this._getFrameMetricsApprox(veryLast); @@ -285,9 +373,20 @@ class VirtualizedList extends React.PureComponent { this._footerLength - this._scrollMetrics.visibleLength, ); - /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment - * suppresses an error when upgrading Flow's support for React. To see the - * error delete this comment and run Flow. */ + + if (this._scrollRef == null) { + return; + } + + if (this._scrollRef.scrollTo == null) { + console.warn( + 'No scrollTo method provided. This may be because you have two nested ' + + 'VirtualizedLists with the same orientation, or because you are ' + + 'using a custom component that does not implement scrollTo.', + ); + return; + } + this._scrollRef.scrollTo( this.props.horizontal ? {x: offset, animated} : {y: offset, animated}, ); @@ -299,6 +398,7 @@ class VirtualizedList extends React.PureComponent { index: number, viewOffset?: number, viewPosition?: number, + ... }) { const { data, @@ -309,8 +409,20 @@ class VirtualizedList extends React.PureComponent { } = this.props; const {animated, index, viewOffset, viewPosition} = params; invariant( - index >= 0 && index < getItemCount(data), - `scrollToIndex out of range: ${index} vs ${getItemCount(data) - 1}`, + index >= 0, + `scrollToIndex out of range: requested index ${index} but minimum is 0`, + ); + invariant( + getItemCount(data) >= 1, + `scrollToIndex out of range: item length ${getItemCount( + data, + )} but minimum is 1`, + ); + invariant( + index < getItemCount(data), + `scrollToIndex out of range: requested index ${index} is out of 0 to ${getItemCount( + data, + ) - 1}`, ); if (!getItemLayout && index > this._highestMeasuredFrameIndex) { invariant( @@ -333,9 +445,20 @@ class VirtualizedList extends React.PureComponent { (viewPosition || 0) * (this._scrollMetrics.visibleLength - frame.length), ) - (viewOffset || 0); - /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment - * suppresses an error when upgrading Flow's support for React. To see the - * error delete this comment and run Flow. */ + + if (this._scrollRef == null) { + return; + } + + if (this._scrollRef.scrollTo == null) { + console.warn( + 'No scrollTo method provided. This may be because you have two nested ' + + 'VirtualizedLists with the same orientation, or because you are ' + + 'using a custom component that does not implement scrollTo.', + ); + return; + } + this._scrollRef.scrollTo( horizontal ? {x: offset, animated} : {y: offset, animated}, ); @@ -347,6 +470,7 @@ class VirtualizedList extends React.PureComponent { animated?: ?boolean, item: Item, viewPosition?: number, + ... }) { const {item} = params; const {data, getItem, getItemCount} = this.props; @@ -369,11 +493,22 @@ class VirtualizedList extends React.PureComponent { * Param `animated` (`true` by default) defines whether the list * should do an animation while scrolling. */ - scrollToOffset(params: {animated?: ?boolean, offset: number}) { + scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) { const {animated, offset} = params; - /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment - * suppresses an error when upgrading Flow's support for React. To see the - * error delete this comment and run Flow. */ + + if (this._scrollRef == null) { + return; + } + + if (this._scrollRef.scrollTo == null) { + console.warn( + 'No scrollTo method provided. This may be because you have two nested ' + + 'VirtualizedLists with the same orientation, or because you are ' + + 'using a custom component that does not implement scrollTo.', + ); + return; + } + this._scrollRef.scrollTo( this.props.horizontal ? {x: offset, animated} : {y: offset, animated}, ); @@ -390,9 +525,10 @@ class VirtualizedList extends React.PureComponent { } flashScrollIndicators() { - /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment - * suppresses an error when upgrading Flow's support for React. To see the - * error delete this comment and run Flow. */ + if (this._scrollRef == null) { + return; + } + this._scrollRef.flashScrollIndicators(); } @@ -401,13 +537,13 @@ class VirtualizedList extends React.PureComponent { * Note that `this._scrollRef` might not be a `ScrollView`, so we * need to check that it responds to `getScrollResponder` before calling it. */ - getScrollResponder() { + getScrollResponder(): ?typeof ScrollView { if (this._scrollRef && this._scrollRef.getScrollResponder) { return this._scrollRef.getScrollResponder(); } } - getScrollableNode() { + getScrollableNode(): ?number { if (this._scrollRef && this._scrollRef.getScrollableNode) { return this._scrollRef.getScrollableNode(); } else { @@ -415,7 +551,9 @@ class VirtualizedList extends React.PureComponent { } } - getScrollRef() { + getScrollRef(): + | ?React.ElementRef + | ?React.ElementRef { if (this._scrollRef && this._scrollRef.getScrollRef) { return this._scrollRef.getScrollRef(); } else { @@ -429,7 +567,7 @@ class VirtualizedList extends React.PureComponent { } } - static defaultProps = { + static defaultProps: DefaultProps = { disableVirtualization: false, horizontal: false, initialNumToRender: 10, @@ -453,49 +591,21 @@ class VirtualizedList extends React.PureComponent { windowSize: 21, // multiples of length }; - static contextTypes = { - virtualizedCell: PropTypes.shape({ - cellKey: PropTypes.string, - }), - virtualizedList: PropTypes.shape({ - getScrollMetrics: PropTypes.func, - horizontal: PropTypes.bool, - getOutermostParentListRef: PropTypes.func, - getNestedChildState: PropTypes.func, - registerAsNestedChild: PropTypes.func, - unregisterAsNestedChild: PropTypes.func, - }), - }; - - static childContextTypes = { - virtualizedList: PropTypes.shape({ - getScrollMetrics: PropTypes.func, - horizontal: PropTypes.bool, - getOutermostParentListRef: PropTypes.func, - getNestedChildState: PropTypes.func, - registerAsNestedChild: PropTypes.func, - unregisterAsNestedChild: PropTypes.func, - }), - }; - - getChildContext() { - return { - virtualizedList: { - getScrollMetrics: this._getScrollMetrics, - horizontal: this.props.horizontal, - getOutermostParentListRef: this._getOutermostParentListRef, - getNestedChildState: this._getNestedChildState, - registerAsNestedChild: this._registerAsNestedChild, - unregisterAsNestedChild: this._unregisterAsNestedChild, - }, - }; + _getCellKey(): string { + return this.context?.cellKey || 'rootList'; } - _getCellKey(): string { - return ( - (this.context.virtualizedCell && this.context.virtualizedCell.cellKey) || - 'rootList' - ); + _getListKey(): string { + return this.props.listKey || this._getCellKey(); + } + + _getDebugInfo(): ListDebugInfo { + return { + listKey: this._getListKey(), + cellKey: this._getCellKey(), + horizontal: !!this.props.horizontal, + parent: this.context?.debugInfo, + }; } _getScrollMetrics = () => { @@ -508,7 +618,7 @@ class VirtualizedList extends React.PureComponent { _getOutermostParentListRef = () => { if (this._isNestedWithSameOrientation()) { - return this.context.virtualizedList.getOutermostParentListRef(); + return this.context.getOutermostParentListRef(); } else { return this; } @@ -523,19 +633,26 @@ class VirtualizedList extends React.PureComponent { cellKey: string, key: string, ref: VirtualizedList, + parentDebugInfo: ListDebugInfo, + ... }): ?ChildListState => { // Register the mapping between this child key and the cellKey for its cell const childListsInCell = this._cellKeysToChildListKeys.get(childList.cellKey) || new Set(); childListsInCell.add(childList.key); this._cellKeysToChildListKeys.set(childList.cellKey, childListsInCell); - const existingChildData = this._nestedChildLists.get(childList.key); if (existingChildData && existingChildData.ref !== null) { console.error( 'A VirtualizedList contains a cell which itself contains ' + 'more than one VirtualizedList of the same orientation as the parent ' + - 'list. You must pass a unique listKey prop to each sibling list.', + 'list. You must pass a unique listKey prop to each sibling list.\n\n' + + describeNestedLists({ + ...childList, + // We're called from the child's componentDidMount, so it's safe to + // read the child's props here (albeit weird). + horizontal: !!childList.ref.props.horizontal, + }), ); } this._nestedChildLists.set(childList.key, { @@ -551,6 +668,7 @@ class VirtualizedList extends React.PureComponent { _unregisterAsNestedChild = (childList: { key: string, state: ChildListState, + ... }): void => { this._nestedChildLists.set(childList.key, { ref: null, @@ -560,9 +678,10 @@ class VirtualizedList extends React.PureComponent { state: State; - constructor(props: Props, context: Object) { - super(props, context); + constructor(props: Props) { + super(props); invariant( + // $FlowFixMe !props.onScroll || !props.onScroll.__isNative, 'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' + 'to support native onScroll events with useNativeDriver', @@ -587,9 +706,10 @@ class VirtualizedList extends React.PureComponent { }), ); } else if (this.props.onViewableItemsChanged) { + const onViewableItemsChanged = this.props.onViewableItemsChanged this._viewabilityTuples.push({ viewabilityHelper: new ViewabilityHelper(this.props.viewabilityConfig), - onViewableItemsChanged: this.props.onViewableItemsChanged, + onViewableItemsChanged, }); } @@ -603,9 +723,7 @@ class VirtualizedList extends React.PureComponent { }; if (this._isNestedWithSameOrientation()) { - const storedState = this.context.virtualizedList.getNestedChildState( - this.props.listKey || this._getCellKey(), - ); + const storedState = this.context.getNestedChildState(this._getListKey()); if (storedState) { initialState = storedState; this.state = storedState; @@ -618,18 +736,23 @@ class VirtualizedList extends React.PureComponent { componentDidMount() { if (this._isNestedWithSameOrientation()) { - this.context.virtualizedList.registerAsNestedChild({ + this.context.registerAsNestedChild({ cellKey: this._getCellKey(), - key: this.props.listKey || this._getCellKey(), + key: this._getListKey(), ref: this, + // NOTE: When the child mounts (here) it's not necessarily safe to read + // the parent's props. This is why we explicitly propagate debugInfo + // "down" via context and "up" again via this method call on the + // parent. + parentDebugInfo: this.context.debugInfo, }); } } componentWillUnmount() { if (this._isNestedWithSameOrientation()) { - this.context.virtualizedList.unregisterAsNestedChild({ - key: this.props.listKey || this._getCellKey(), + this.context.unregisterAsNestedChild({ + key: this._getListKey(), state: { first: this.state.first, last: this.state.last, @@ -645,7 +768,7 @@ class VirtualizedList extends React.PureComponent { this._fillRateHelper.deactivateAndFlush(); } - static getDerivedStateFromProps(newProps: Props, prevState: State) { + static getDerivedStateFromProps(newProps: Props, prevState: State): State { const {data, getItemCount, maxToRenderPerBatch} = newProps; // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make // sure we're rendering a reasonable range here. @@ -723,20 +846,21 @@ class VirtualizedList extends React.PureComponent { } _isNestedWithSameOrientation(): boolean { - const nestedContext = this.context.virtualizedList; + const nestedContext = this.context; return !!( nestedContext && !!nestedContext.horizontal === !!this.props.horizontal ); } - render() { + render(): React.Node { if (__DEV__) { const flatStyles = flattenStyle(this.props.contentContainerStyle); - warning( - flatStyles == null || flatStyles.flexWrap !== 'wrap', - '`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.' + - 'Consider using `numColumns` with `FlatList` instead.', - ); + if (flatStyles != null && flatStyles.flexWrap === 'wrap') { + console.warn( + '`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.' + + 'Consider using `numColumns` with `FlatList` instead.', + ); + } } const { ListEmptyComponent, @@ -764,7 +888,7 @@ class VirtualizedList extends React.PureComponent { ); cells.push( - { element } - , + , ); } const itemCount = this.props.getItemCount(data); @@ -813,6 +937,9 @@ class VirtualizedList extends React.PureComponent { initBlock.offset - (this.props.initialScrollIndex ? 0 : initBlock.length); cells.push( + /* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.111 was deployed. To + * see the error, delete this comment and run Flow. */ , ); this._pushCells( @@ -827,6 +954,9 @@ class VirtualizedList extends React.PureComponent { this._getFrameMetricsApprox(first).offset - (stickyBlock.offset + stickyBlock.length); cells.push( + /* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.111 was deployed. To + * see the error, delete this comment and run Flow. */ , ); insertedStickySpacer = true; @@ -840,6 +970,9 @@ class VirtualizedList extends React.PureComponent { this._getFrameMetricsApprox(first).offset - (initBlock.offset + initBlock.length); cells.push( + /* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.111 was deployed. To see + * the error, delete this comment and run Flow. */ , ); } @@ -874,6 +1007,9 @@ class VirtualizedList extends React.PureComponent { endFrame.length - (lastFrame.offset + lastFrame.length); cells.push( + /* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment suppresses + * an error found when Flow v0.111 was deployed. To see the error, + * delete this comment and run Flow. */ , ); } @@ -895,10 +1031,7 @@ class VirtualizedList extends React.PureComponent { element.props.onLayout(event); } }, - style: StyleSheet.compose( - inversionStyle, - element.props.style, - ), + style: StyleSheet.compose(inversionStyle, element.props.style), }), ); } @@ -910,8 +1043,8 @@ class VirtualizedList extends React.PureComponent { ); cells.push( - { element } - , + , ); } const scrollProps = { @@ -934,33 +1067,43 @@ class VirtualizedList extends React.PureComponent { onScroll: this._onScroll, onScrollBeginDrag: this._onScrollBeginDrag, onScrollEndDrag: this._onScrollEndDrag, + onMomentumScrollBegin: this._onMomentumScrollBegin, onMomentumScrollEnd: this._onMomentumScrollEnd, scrollEventThrottle: this.props.scrollEventThrottle, // TODO: Android support - invertStickyHeaders: - this.props.invertStickyHeaders !== undefined - ? this.props.invertStickyHeaders - : this.props.inverted, stickyHeaderIndices, + style: inversionStyle + ? [inversionStyle, this.props.style] + : this.props.style, }; - if (inversionStyle) { - /* $FlowFixMe(>=0.70.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.70 was deployed. To see the error delete - * this comment and run Flow. */ - scrollProps.style = [inversionStyle, this.props.style]; - } this._hasMore = this.state.last < this.props.getItemCount(this.props.data) - 1; - const ret = React.cloneElement( - (this.props.renderScrollComponent || this._defaultRenderScrollComponent)( - scrollProps, - ), - { - ref: this._captureScrollRef, - }, - cells, + const innerRet = ( + + {React.cloneElement( + ( + this.props.renderScrollComponent || + this._defaultRenderScrollComponent + )(scrollProps), + { + ref: this._captureScrollRef, + }, + cells, + )} + ); + let ret = innerRet; if (this.props.debug) { return ( @@ -976,8 +1119,6 @@ class VirtualizedList extends React.PureComponent { componentDidUpdate(prevProps: Props) { const {data, extraData} = this.props; if (data !== prevProps.data || extraData !== prevProps.extraData) { - this._hasDataChangedSinceEndReached = true; - // clear the viewableIndices cache to also trigger // the onViewableItemsChanged callback with the new data this._viewabilityTuples.forEach(tuple => { @@ -1006,7 +1147,6 @@ class VirtualizedList extends React.PureComponent { _fillRateHelper: FillRateHelper; _frames = {}; _footerLength = 0; - _hasDataChangedSinceEndReached = true; _hasDoneInitialScroll = false; _hasInteracted = false; _hasMore = false; @@ -1017,7 +1157,11 @@ class VirtualizedList extends React.PureComponent { _indicesToKeys: Map = new Map(); _nestedChildLists: Map< string, - {ref: ?VirtualizedList, state: ?ChildListState}, + { + ref: ?VirtualizedList, + state: ?ChildListState, + ... + }, > = new Map(); _offsetFromParentVirtualizedList: number = 0; _prevParentOffset: number = 0; @@ -1037,7 +1181,6 @@ class VirtualizedList extends React.PureComponent { _updateCellsToRenderBatcher: Batchinator; _viewabilityTuples: Array = []; - // $FlowFixMe _captureScrollRef = ref => { this._scrollRef = ref; }; @@ -1050,7 +1193,6 @@ class VirtualizedList extends React.PureComponent { ); } - // $FlowFixMe _defaultRenderScrollComponent = props => { const onRefresh = props.onRefresh; if (this._isNestedWithSameOrientation()) { @@ -1060,6 +1202,9 @@ class VirtualizedList extends React.PureComponent { invariant( typeof props.refreshing === 'boolean', '`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' + + /* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment suppresses + * an error found when Flow v0.111 was deployed. To see the error, + * delete this comment and run Flow. */ JSON.stringify(props.refreshing) + '`', ); @@ -1086,7 +1231,6 @@ class VirtualizedList extends React.PureComponent { } }; - // $FlowFixMe _onCellLayout(e, cellKey, index) { const layout = e.nativeEvent.layout; const next = { @@ -1116,15 +1260,7 @@ class VirtualizedList extends React.PureComponent { this._frames[cellKey].inLayout = true; } - const childListKeys = this._cellKeysToChildListKeys.get(cellKey); - if (childListKeys) { - for (let childKey of childListKeys) { - const childList = this._nestedChildLists.get(childKey); - childList && - childList.ref && - childList.ref.measureLayoutRelativeToContainingList(); - } - } + this._triggerRemeasureForChildListsInCell(cellKey); this._computeBlankness(); this._updateViewableItems(this.props.data); @@ -1137,6 +1273,18 @@ class VirtualizedList extends React.PureComponent { } }; + _triggerRemeasureForChildListsInCell(cellKey: string): void { + const childListKeys = this._cellKeysToChildListKeys.get(cellKey); + if (childListKeys) { + for (let childKey of childListKeys) { + const childList = this._nestedChildLists.get(childKey); + childList && + childList.ref && + childList.ref.measureLayoutRelativeToContainingList(); + } + } + } + measureLayoutRelativeToContainingList(): void { // TODO (T35574538): findNodeHandle sometimes crashes with "Unable to find // node on an unmounted component" during scrolling @@ -1144,13 +1292,10 @@ class VirtualizedList extends React.PureComponent { if (!this._scrollRef) { return; } - // We are asuming that getOutermostParentListRef().getScrollRef() + // We are assuming that getOutermostParentListRef().getScrollRef() // is a non-null reference to a ScrollView this._scrollRef.measureLayout( - this.context.virtualizedList - .getOutermostParentListRef() - .getScrollRef() - .getNativeScrollRef(), + this.context.getOutermostParentListRef().getScrollRef(), (x, y, width, height) => { this._offsetFromParentVirtualizedList = this._selectOffset({x, y}); this._scrollMetrics.contentLength = this._selectLength({ @@ -1158,7 +1303,7 @@ class VirtualizedList extends React.PureComponent { height, }); const scrollMetrics = this._convertParentScrollMetrics( - this.context.virtualizedList.getScrollMetrics(), + this.context.getScrollMetrics(), ); this._scrollMetrics.visibleLength = scrollMetrics.visibleLength; this._scrollMetrics.offset = scrollMetrics.offset; @@ -1193,17 +1338,19 @@ class VirtualizedList extends React.PureComponent { this._maybeCallOnEndReached(); }; - // $FlowFixMe _onLayoutEmpty = e => { this.props.onLayout && this.props.onLayout(e); }; - // $FlowFixMe + _getFooterCellKey(): string { + return this._getCellKey() + '-footer'; + } + _onLayoutFooter = e => { + this._triggerRemeasureForChildListsInCell(this._getFooterCellKey()); this._footerLength = this._selectLength(e.nativeEvent.layout); }; - // $FlowFixMe _onLayoutHeader = e => { this._headerLength = this._selectLength(e.nativeEvent.layout); }; @@ -1268,11 +1415,23 @@ class VirtualizedList extends React.PureComponent { ); } - _selectLength(metrics: $ReadOnly<{height: number, width: number}>): number { + _selectLength( + metrics: $ReadOnly<{ + height: number, + width: number, + ... + }>, + ): number { return !this.props.horizontal ? metrics.height : metrics.width; } - _selectOffset(metrics: $ReadOnly<{x: number, y: number}>): number { + _selectOffset( + metrics: $ReadOnly<{ + x: number, + y: number, + ... + }>, + ): number { return !this.props.horizontal ? metrics.y : metrics.x; } @@ -1285,20 +1444,22 @@ class VirtualizedList extends React.PureComponent { } = this.props; const {contentLength, visibleLength, offset} = this._scrollMetrics; const distanceFromEnd = contentLength - visibleLength - offset; + const threshold = onEndReachedThreshold + ? onEndReachedThreshold * visibleLength + : 2; if ( onEndReached && this.state.last === getItemCount(data) - 1 && - /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.63 was deployed. To see the error delete this - * comment and run Flow. */ - distanceFromEnd < onEndReachedThreshold * visibleLength && - (this._hasDataChangedSinceEndReached || - this._scrollMetrics.contentLength !== this._sentEndForContentLength) + distanceFromEnd < threshold && + this._scrollMetrics.contentLength !== this._sentEndForContentLength ) { - // Only call onEndReached once for a given dataset + content length. - this._hasDataChangedSinceEndReached = false; + // Only call onEndReached once for a given content length this._sentEndForContentLength = this._scrollMetrics.contentLength; onEndReached({distanceFromEnd}); + } else if (distanceFromEnd > threshold) { + // If the user scrolls away from the end and back again cause + // an onEndReached to be triggered again + this._sentEndForContentLength = 0; } } @@ -1310,10 +1471,6 @@ class VirtualizedList extends React.PureComponent { this.props.initialScrollIndex > 0 && !this._hasDoneInitialScroll ) { - this.scrollToIndex({ - animated: false, - index: this.props.initialScrollIndex, - }); this._hasDoneInitialScroll = true; } if (this.props.onContentSizeChange) { @@ -1330,6 +1487,7 @@ class VirtualizedList extends React.PureComponent { _convertParentScrollMetrics = (metrics: { visibleLength: number, offset: number, + ... }) => { // Offset of the top of the nested list relative to the top of its parent's viewport const offset = metrics.offset - this._offsetFromParentVirtualizedList; @@ -1466,7 +1624,6 @@ class VirtualizedList extends React.PureComponent { } } - // $FlowFixMe _onScrollBeginDrag = (e): void => { this._nestedChildLists.forEach(childList => { childList.ref && childList.ref._onScrollBeginDrag(e); @@ -1478,8 +1635,10 @@ class VirtualizedList extends React.PureComponent { this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); }; - // $FlowFixMe _onScrollEndDrag = (e): void => { + this._nestedChildLists.forEach(childList => { + childList.ref && childList.ref._onScrollEndDrag(e); + }); const {velocity} = e.nativeEvent; if (velocity) { this._scrollMetrics.velocity = this._selectOffset(velocity); @@ -1488,8 +1647,17 @@ class VirtualizedList extends React.PureComponent { this.props.onScrollEndDrag && this.props.onScrollEndDrag(e); }; - // $FlowFixMe + _onMomentumScrollBegin = (e): void => { + this._nestedChildLists.forEach(childList => { + childList.ref && childList.ref._onMomentumScrollBegin(e); + }); + this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e); + }; + _onMomentumScrollEnd = (e): void => { + this._nestedChildLists.forEach(childList => { + childList.ref && childList.ref._onMomentumScrollEnd(e); + }); this._scrollMetrics.velocity = 0; this._computeBlankness(); this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e); @@ -1504,12 +1672,13 @@ class VirtualizedList extends React.PureComponent { } this.setState(state => { let newState; + const {contentLength, offset, visibleLength} = this._scrollMetrics; if (!isVirtualizationDisabled) { // If we run this with bogus data, we'll force-render window {first: 0, last: 0}, // and wipe out the initialNumToRender rendered elements. // So let's wait until the scroll view metrics have been set up. And until then, // we will trust the initialNumToRender suggestion - if (this._scrollMetrics.visibleLength) { + if (visibleLength > 0 && contentLength > 0) { // If we have a non-zero initialScrollIndex and run this before we've scrolled, // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. // So let's wait until we've scrolled the view to the right place. And until then, @@ -1524,7 +1693,6 @@ class VirtualizedList extends React.PureComponent { } } } else { - const {contentLength, offset, visibleLength} = this._scrollMetrics; const distanceFromEnd = contentLength - visibleLength - offset; const renderAhead = /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses @@ -1562,12 +1730,19 @@ class VirtualizedList extends React.PureComponent { break; } } - if (someChildHasMore) { + if (someChildHasMore && newState) { newState.last = ii; break; } } } + if ( + newState != null && + newState.first === state.first && + newState.last === state.last + ) { + newState = null; + } return newState; }); }; @@ -1580,7 +1755,11 @@ class VirtualizedList extends React.PureComponent { _getFrameMetricsApprox = ( index: number, - ): {length: number, offset: number} => { + ): { + length: number, + offset: number, + ... + } => { const frame = this._getFrameMetrics(index); if (frame && frame.index === index) { // check for invalid frames due to row re-ordering @@ -1605,6 +1784,7 @@ class VirtualizedList extends React.PureComponent { offset: number, index: number, inLayout?: boolean, + ... } => { const { data, @@ -1622,19 +1802,6 @@ class VirtualizedList extends React.PureComponent { if (!frame || frame.index !== index) { if (getItemLayout) { frame = getItemLayout(data, index); - if (__DEV__) { - const frameType = PropTypes.shape({ - length: PropTypes.number.isRequired, - offset: PropTypes.number.isRequired, - index: PropTypes.number.isRequired, - }).isRequired; - PropTypes.checkPropTypes( - {frame: frameType}, - {frame}, - 'frame', - 'VirtualizedList.getItemLayout', - ); - } } } /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an @@ -1660,26 +1827,49 @@ class VirtualizedList extends React.PureComponent { } } -class CellRenderer extends React.Component< - { - CellRendererComponent?: ?React.ComponentType, - ItemSeparatorComponent: ?React.ComponentType<*>, - cellKey: string, - fillRateHelper: FillRateHelper, - horizontal: ?boolean, - index: number, - inversionStyle: ViewStyleProp, - item: Item, - onLayout: (event: Object) => void, // This is extracted by ScrollViewStickyHeader - onUnmount: (cellKey: string) => void, - onUpdateSeparators: (cellKeys: Array, props: Object) => void, - parentProps: { - getItemLayout?: ?Function, - renderItem: renderItemType, +type CellRendererProps = { + CellRendererComponent?: ?React.ComponentType, + ItemSeparatorComponent: ?React.ComponentType<*>, + cellKey: string, + fillRateHelper: FillRateHelper, + horizontal: ?boolean, + index: number, + inversionStyle: ViewStyleProp, + item: Item, + // This is extracted by ScrollViewStickyHeader + onLayout: (event: Object) => void, + onUnmount: (cellKey: string) => void, + onUpdateSeparators: (cellKeys: Array, props: Object) => void, + parentProps: { + // e.g. height, y, + getItemLayout?: ( + data: any, + index: number, + ) => { + length: number, + offset: number, + index: number, + ... }, - prevCellKey: ?string, + renderItem?: ?RenderItemType, + ListItemComponent?: ?(React.ComponentType | React.Element), + ... }, - $FlowFixMeState, + prevCellKey: ?string, + ... +}; + +type CellRendererState = { + separatorProps: $ReadOnly<{| + highlighted: boolean, + leadingItem: ?Item, + |}>, + ... +}; + +class CellRenderer extends React.Component< + CellRendererProps, + CellRendererState, > { state = { separatorProps: { @@ -1688,16 +1878,14 @@ class CellRenderer extends React.Component< }, }; - static childContextTypes = { - virtualizedCell: PropTypes.shape({ - cellKey: PropTypes.string, - }), - }; - - getChildContext() { + static getDerivedStateFromProps( + props: CellRendererProps, + prevState: CellRendererState, + ): ?CellRendererState { return { - virtualizedCell: { - cellKey: this.props.cellKey, + separatorProps: { + ...prevState.separatorProps, + leadingItem: props.item, }, }; } @@ -1736,6 +1924,39 @@ class CellRenderer extends React.Component< this.props.onUnmount(this.props.cellKey); } + _renderElement(renderItem, ListItemComponent, item, index) { + if (renderItem && ListItemComponent) { + console.warn( + 'VirtualizedList: Both ListItemComponent and renderItem props are present. ListItemComponent will take' + + ' precedence over renderItem.', + ); + } + + if (ListItemComponent) { + /* $FlowFixMe(>=0.108.0 site=react_native_fb) This comment suppresses an + * error found when Flow v0.108 was deployed. To see the error, delete + * this comment and run Flow. */ + return React.createElement(ListItemComponent, { + item, + index, + separators: this._separators, + }); + } + + if (renderItem) { + return renderItem({ + item, + index, + separators: this._separators, + }); + } + + invariant( + false, + 'VirtualizedList: Either ListItemComponent or renderItem props are required but none were found.', + ); + } + render() { const { CellRendererComponent, @@ -1747,13 +1968,14 @@ class CellRenderer extends React.Component< inversionStyle, parentProps, } = this.props; - const {renderItem, getItemLayout} = parentProps; - invariant(renderItem, 'no renderItem!'); - const element = renderItem({ + const {renderItem, getItemLayout, ListItemComponent} = parentProps; + const element = this._renderElement( + renderItem, + ListItemComponent, item, index, - separators: this._separators, - }); + ); + const onLayout = /* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an * error found when Flow v0.68 was deployed. To see the error delete this @@ -1773,18 +1995,15 @@ class CellRenderer extends React.Component< : horizontal ? [styles.row, inversionStyle] : inversionStyle; - if (!CellRendererComponent) { - return ( - /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.89 was deployed. To see the error, delete - * this comment and run Flow. */ - - {element} - {itemSeparator} - - ); - } - return ( + const result = !CellRendererComponent ? ( + /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an + * error found when Flow v0.89 was deployed. To see the error, delete + * this comment and run Flow. */ + + {element} + {itemSeparator} + + ) : ( ); + + return ( + + {result} + + ); } } -class VirtualizedCellWrapper extends React.Component<{ - cellKey: string, - children: React.Node, -}> { - static childContextTypes = { - virtualizedCell: PropTypes.shape({ - cellKey: PropTypes.string, - }), - }; +function describeNestedLists(childList: { + +cellKey: string, + +key: string, + +ref: VirtualizedList, + +parentDebugInfo: ListDebugInfo, + +horizontal: boolean, + ... +}) { + let trace = + 'VirtualizedList trace:\n' + + ` Child (${childList.horizontal ? 'horizontal' : 'vertical'}):\n` + + ` listKey: ${childList.key}\n` + + ` cellKey: ${childList.cellKey}`; - getChildContext() { - return { - virtualizedCell: { - cellKey: this.props.cellKey, - }, - }; - } - - render() { - return this.props.children; + let debugInfo = childList.parentDebugInfo; + while (debugInfo) { + trace += + `\n Parent (${debugInfo.horizontal ? 'horizontal' : 'vertical'}):\n` + + ` listKey: ${debugInfo.listKey}\n` + + ` cellKey: ${debugInfo.cellKey}`; + debugInfo = debugInfo.parent; } + return trace; } const styles = StyleSheet.create({ @@ -1827,13 +2054,13 @@ const styles = StyleSheet.create({ transform: [{scaleX: -1}], }, row: { - flexDirection: 'row' + flexDirection: 'row', }, rowReverse: { - flexDirection: 'row-reverse' + flexDirection: 'row-reverse', }, columnReverse: { - flexDirection: 'column-reverse' + flexDirection: 'column-reverse', }, debug: { flex: 1, @@ -1865,4 +2092,4 @@ const styles = StyleSheet.create({ }, }); -export default VirtualizedList; +export default VirtualizedList; \ No newline at end of file diff --git a/packages/react-native-web/src/vendor/react-native/VirtualizedSectionList/index.js b/packages/react-native-web/src/vendor/react-native/VirtualizedSectionList/index.js index e7d3cfe6..8d74f99c 100644 --- a/packages/react-native-web/src/vendor/react-native/VirtualizedSectionList/index.js +++ b/packages/react-native-web/src/vendor/react-native/VirtualizedSectionList/index.js @@ -7,6 +7,7 @@ * @flow * @format */ + 'use strict'; import * as React from 'react'; @@ -16,7 +17,6 @@ import VirtualizedList from '../VirtualizedList'; import invariant from 'fbjs/lib/invariant'; import type {ViewToken} from '../ViewabilityHelper'; -import type {Props as VirtualizedListProps} from '../VirtualizedList'; type Item = any; @@ -30,7 +30,6 @@ export type SectionBase = { * the array index will be used by default. */ key?: string, - // Optional props will override list-wide props just for this section. renderItem?: ?(info: { item: SectionItemT, @@ -40,25 +39,20 @@ export type SectionBase = { highlight: () => void, unhighlight: () => void, updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + ... }, - }) => ?React.Element, + ... + }) => null | React.Element, ItemSeparatorComponent?: ?React.ComponentType, keyExtractor?: (item: SectionItemT, index?: ?number) => string, + ... }; -type RequiredProps> = { +type RequiredProps> = {| sections: $ReadOnlyArray, -}; +|}; -type OptionalProps> = { - /** - * Rendered after the last item in the last section. - */ - ListFooterComponent?: ?(React.ComponentType | React.Element), - /** - * Rendered at the very beginning of the list. - */ - ListHeaderComponent?: ?(React.ComponentType | React.Element), +type OptionalProps> = {| /** * Default renderer for every item in every section. */ @@ -70,60 +64,65 @@ type OptionalProps> = { highlight: () => void, unhighlight: () => void, updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + ... }, - }) => ?React.Element, + ... + }) => null | React.Element, /** - * Rendered at the top of each section. + * Rendered at the top of each section. These stick to the top of the `ScrollView` by default on + * iOS. See `stickySectionHeadersEnabled`. */ - renderSectionHeader?: ?({section: SectionT}) => ?React.Element, + renderSectionHeader?: ?(info: { + section: SectionT, + ... + }) => null | React.Element, /** * Rendered at the bottom of each section. */ - renderSectionFooter?: ?({section: SectionT}) => ?React.Element, + renderSectionFooter?: ?(info: { + section: SectionT, + ... + }) => null | React.Element, /** - * Rendered at the bottom of every Section, except the very last one, in place of the normal - * ItemSeparatorComponent. + * Rendered at the top and bottom of each section (note this is different from + * `ItemSeparatorComponent` which is only rendered between items). These are intended to separate + * sections from the headers above and below and typically have the same highlight response as + * `ItemSeparatorComponent`. Also receives `highlighted`, `[leading/trailing][Item/Separator]`, + * and any custom props from `separators.updateProps`. */ SectionSeparatorComponent?: ?React.ComponentType, /** - * Rendered at the bottom of every Item except the very last one in the last section. + * Makes section headers stick to the top of the screen until the next one pushes it off. Only + * enabled by default on iOS because that is the platform standard there. */ - ItemSeparatorComponent?: ?React.ComponentType, - /** - * DEPRECATED: Virtualization provides significant performance and memory optimizations, but fully - * unmounts react instances that are outside of the render window. You should only need to disable - * this for debugging purposes. - */ - disableVirtualization?: ?boolean, - keyExtractor: (item: Item, index: number) => string, - onEndReached?: ?({distanceFromEnd: number}) => void, - /** - * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make - * sure to also set the `refreshing` prop correctly. - */ - onRefresh?: ?() => void, - /** - * Called when the viewability of rows changes, as defined by the - * `viewabilityConfig` prop. - */ - onViewableItemsChanged?: ?({ - viewableItems: Array, - changed: Array, - }) => void, - /** - * Set this true while waiting for new data from a refresh. - */ - refreshing?: ?boolean, -}; + stickySectionHeadersEnabled?: boolean, + onEndReached?: ?({distanceFromEnd: number, ...}) => void, +|}; -export type Props = RequiredProps & - OptionalProps & - VirtualizedListProps; +type VirtualizedListProps = React.ElementProps; -type DefaultProps = typeof VirtualizedList.defaultProps & { +export type Props = {| + ...RequiredProps, + ...OptionalProps, + ...$Diff< + VirtualizedListProps, + {renderItem: $PropertyType, ...}, + >, +|}; +export type ScrollToLocationParamsType = {| + animated?: ?boolean, + itemIndex: number, + sectionIndex: number, + viewOffset?: number, + viewPosition?: number, +|}; + +type DefaultProps = {| + ...typeof VirtualizedList.defaultProps, data: $ReadOnlyArray, -}; -type State = {childProps: VirtualizedListProps}; +|}; + +type State = {childProps: VirtualizedListProps, ...}; /** * Right now this just flattens everything into one list and uses VirtualizedList under the @@ -138,22 +137,21 @@ class VirtualizedSectionList< data: [], }; - scrollToLocation(params: { - animated?: ?boolean, - itemIndex: number, - sectionIndex: number, - viewPosition?: number, - }) { + scrollToLocation(params: ScrollToLocationParamsType) { let index = params.itemIndex; for (let i = 0; i < params.sectionIndex; i++) { index += this.props.getItemCount(this.props.sections[i].data) + 2; } - let viewOffset = 0; + let viewOffset = params.viewOffset || 0; + if (this._listRef == null) { + return; + } if (params.itemIndex > 0 && this.props.stickySectionHeadersEnabled) { + // $FlowFixMe[prop-missing] Cannot access private property const frame = this._listRef._getFrameMetricsApprox( index - params.itemIndex, ); - viewOffset = frame.length; + viewOffset += frame.length; } const toIndexParams = { ...params, @@ -163,55 +161,90 @@ class VirtualizedSectionList< this._listRef.scrollToIndex(toIndexParams); } - getListRef(): VirtualizedList { + getListRef(): ?React.ElementRef { return this._listRef; } - constructor(props: Props, context: Object) { - super(props, context); - this.state = this._computeState(props); - } + render(): React.Node { + const { + ItemSeparatorComponent, // don't pass through, rendered with renderItem + SectionSeparatorComponent, + renderItem: _renderItem, + renderSectionFooter, + renderSectionHeader, + sections: _sections, + stickySectionHeadersEnabled, + ...passThroughProps + } = this.props; - UNSAFE_componentWillReceiveProps(nextProps: Props) { - this.setState(this._computeState(nextProps)); - } + const listHeaderOffset = this.props.ListHeaderComponent ? 1 : 0; - _computeState(props: Props): State { - const offset = props.ListHeaderComponent ? 1 : 0; - const stickyHeaderIndices = []; - const itemCount = props.sections - ? props.sections.reduce((v, section) => { - stickyHeaderIndices.push(v + offset); - return v + props.getItemCount(section.data) + 2; // Add two for the section header and footer. - }, 0) - : 0; + const stickyHeaderIndices = this.props.stickySectionHeadersEnabled + ? [] + : undefined; - return { - childProps: { - ...props, - renderItem: this._renderItem, - ItemSeparatorComponent: undefined, // Rendered with renderItem - data: props.sections, - getItemCount: () => itemCount, - // $FlowFixMe - getItem: (sections, index) => getItem(props, sections, index), - keyExtractor: this._keyExtractor, - onViewableItemsChanged: props.onViewableItemsChanged - ? this._onViewableItemsChanged - : undefined, - stickyHeaderIndices: props.stickySectionHeadersEnabled - ? stickyHeaderIndices - : undefined, - }, - }; - } + let itemCount = 0; + for (const section of this.props.sections) { + // Track the section header indices + if (stickyHeaderIndices != null) { + stickyHeaderIndices.push(itemCount + listHeaderOffset); + } + + // Add two for the section header and footer. + itemCount += 2; + itemCount += this.props.getItemCount(section.data); + } + const renderItem = this._renderItem(itemCount); - render() { return ( - + + this._getItem(this.props, sections, index) + } + getItemCount={() => itemCount} + onViewableItemsChanged={ + this.props.onViewableItemsChanged + ? this._onViewableItemsChanged + : undefined + } + ref={this._captureRef} + /> ); } + _getItem = ( + props: Props, + sections: ?$ReadOnlyArray, + index: number, + ): ?Item => { + if (!sections) { + return null; + } + let itemIdx = index - 1; + for (let i = 0; i < sections.length; i++) { + const section = sections[i]; + const sectionData = section.data; + const itemCount = props.getItemCount(sectionData); + if (itemIdx === -1 || itemIdx === itemCount) { + // We intend for there to be overflow by one on both ends of the list. + // This will be for headers and footers. When returning a header or footer + // item the section itself is the item. + return section; + } else if (itemIdx < itemCount) { + // If we are in the bounds of the list's data then return the item. + return props.getItem(sectionData, itemIdx); + } else { + itemIdx -= itemCount + 2; // Add two for the header and footer + } + } + return null; + }; + _keyExtractor = (item: Item, index: number) => { const info = this._subExtractor(index); return (info && info.key) || String(index); @@ -221,13 +254,17 @@ class VirtualizedSectionList< index: number, ): ?{ section: SectionT, - key: string, // Key of the section or combined key for section + item - index: ?number, // Relative index within the section - header?: ?boolean, // True if this is the section header + // Key of the section or combined key for section + item + key: string, + // Relative index within the section + index: ?number, + // True if this is the section header + header?: ?boolean, leadingItem?: ?Item, leadingSection?: ?SectionT, trailingItem?: ?Item, trailingSection?: ?SectionT, + ... } { let itemIndex = index; const {getItem, getItemCount, keyExtractor, sections} = this.props; @@ -294,9 +331,11 @@ class VirtualizedSectionList< }: { viewableItems: Array, changed: Array, + ... }) => { - if (this.props.onViewableItemsChanged) { - this.props.onViewableItemsChanged({ + const onViewableItemsChanged = this.props.onViewableItemsChanged; + if (onViewableItemsChanged != null) { + onViewableItemsChanged({ viewableItems: viewableItems .map(this._convertViewable, this) .filter(Boolean), @@ -305,7 +344,14 @@ class VirtualizedSectionList< } }; - _renderItem = ({item, index}: {item: Item, index: number}) => { + _renderItem = (listItemCount: number) => ({ + item, + index, + }: { + item: Item, + index: number, + ... + }) => { const info = this._subExtractor(index); if (!info) { return null; @@ -322,7 +368,11 @@ class VirtualizedSectionList< } } else { const renderItem = info.section.renderItem || this.props.renderItem; - const SeparatorComponent = this._getSeparatorComponent(index, info); + const SeparatorComponent = this._getSeparatorComponent( + index, + info, + listItemCount, + ); invariant(renderItem, 'no renderItem!'); return ( ); } @@ -357,6 +408,7 @@ class VirtualizedSectionList< _getSeparatorComponent( index: number, info?: ?Object, + listItemCount: number, ): ?React.ComponentType { info = info || this._subExtractor(index); if (!info) { @@ -365,7 +417,7 @@ class VirtualizedSectionList< const ItemSeparatorComponent = info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent; const {SectionSeparatorComponent} = this.props; - const isLastItemInList = index === this.state.childProps.getItemCount() - 1; + const isLastItemInList = index === listItemCount - 1; const isLastItemInSection = info.index === this.props.getItemCount(info.section.data) - 1; if (SectionSeparatorComponent && isLastItemInSection) { @@ -378,12 +430,8 @@ class VirtualizedSectionList< } _cellRefs = {}; - _listRef: VirtualizedList; - // $FlowFixMe + _listRef: ?React.ElementRef; _captureRef = ref => { - /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment - * suppresses an error when upgrading Flow's support for React. To see the - * error delete this comment and run Flow. */ this._listRef = ref; }; } @@ -406,6 +454,7 @@ type ItemWithSeparatorProps = $ReadOnly<{| onUpdateSeparator: (cellKey: string, newProps: Object) => void, prevCellKey?: ?string, renderItem: Function, + inverted: boolean, |}>; type ItemWithSeparatorState = { @@ -417,6 +466,7 @@ type ItemWithSeparatorState = { highlighted: false, ...ItemWithSeparatorCommonProps, |}>, + ... }; class ItemWithSeparator extends React.Component< @@ -505,6 +555,7 @@ class ItemWithSeparator extends React.Component< item, index, section, + inverted, } = this.props; const element = this.props.renderItem({ item, @@ -512,20 +563,17 @@ class ItemWithSeparator extends React.Component< section, separators: this._separators, }); - const leadingSeparator = LeadingSeparatorComponent && ( + const leadingSeparator = LeadingSeparatorComponent != null && ( ); - const separator = SeparatorComponent && ( + const separator = SeparatorComponent != null && ( ); return leadingSeparator || separator ? ( - /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.89 was deployed. To see the error, delete - * this comment and run Flow. */ - {leadingSeparator} + {inverted === false ? leadingSeparator : separator} {element} - {separator} + {inverted === false ? separator : leadingSeparator} ) : ( element @@ -533,32 +581,4 @@ class ItemWithSeparator extends React.Component< } } -function getItem( - props: Props>, - sections: ?$ReadOnlyArray, - index: number, -): ?Item { - if (!sections) { - return null; - } - let itemIdx = index - 1; - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - const sectionData = section.data; - const itemCount = props.getItemCount(sectionData); - if (itemIdx === -1 || itemIdx === itemCount) { - // We intend for there to be overflow by one on both ends of the list. - // This will be for headers and footers. When returning a header or footer - // item the section itself is the item. - return section; - } else if (itemIdx < itemCount) { - // If we are in the bounds of the list's data then return the item. - return props.getItem(sectionData, itemIdx); - } else { - itemIdx -= itemCount + 2; // Add two for the header and footer - } - } - return null; -} - -export default VirtualizedSectionList; +export default VirtualizedSectionList; \ No newline at end of file diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EventEmitter.js b/packages/react-native-web/src/vendor/react-native/emitter/EventEmitter.js index 51e7e456..3a9cc78d 100644 --- a/packages/react-native-web/src/vendor/react-native/emitter/EventEmitter.js +++ b/packages/react-native-web/src/vendor/react-native/emitter/EventEmitter.js @@ -4,225 +4,50 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict * @format - * @noflow - * @typecheck */ 'use strict'; -import EmitterSubscription from './EmitterSubscription'; -import EventSubscriptionVendor from './EventSubscriptionVendor'; +import EventEmitter from './_EventEmitter'; -import invariant from 'fbjs/lib/invariant'; - -const sparseFilterPredicate = () => true; - -/** - * @class EventEmitter - * @description - * An EventEmitter is responsible for managing a set of listeners and publishing - * events to them when it is told that such events happened. In addition to the - * data for the given event it also sends a event control object which allows - * the listeners/handlers to prevent the default behavior of the given event. - * - * The emitter is designed to be generic enough to support all the different - * contexts in which one might want to emit events. It is a simple multicast - * mechanism on top of which extra functionality can be composed. For example, a - * more advanced emitter may use an EventHolder and EventFactory. - */ -class EventEmitter { - _subscriber: EventSubscriptionVendor; - _currentSubscription: ?EmitterSubscription; - - /** - * @constructor - * - * @param {EventSubscriptionVendor} subscriber - Optional subscriber instance - * to use. If omitted, a new subscriber will be created for the emitter. - */ - constructor(subscriber: ?EventSubscriptionVendor) { - this._subscriber = subscriber || new EventSubscriptionVendor(); - } - - /** - * Adds a listener to be invoked when events of the specified type are - * emitted. An optional calling context may be provided. The data arguments - * emitted will be passed to the listener function. - * - * TODO: Annotate the listener arg's type. This is tricky because listeners - * can be invoked with varargs. - * - * @param {string} eventType - Name of the event to listen to - * @param {function} listener - Function to invoke when the specified event is - * emitted - * @param {*} context - Optional context object to use when invoking the - * listener - */ - addListener( - eventType: string, - listener: Function, - context: ?Object, - ): EmitterSubscription { - return (this._subscriber.addSubscription( - eventType, - new EmitterSubscription(this, this._subscriber, listener, context), - ): any); - } - - /** - * Similar to addListener, except that the listener is removed after it is - * invoked once. - * - * @param {string} eventType - Name of the event to listen to - * @param {function} listener - Function to invoke only once when the - * specified event is emitted - * @param {*} context - Optional context object to use when invoking the - * listener - */ - once( - eventType: string, - listener: Function, - context: ?Object, - ): EmitterSubscription { - return this.addListener(eventType, (...args) => { - this.removeCurrentListener(); - listener.apply(context, args); - }); - } - - /** - * Removes all of the registered listeners, including those registered as - * listener maps. - * - * @param {?string} eventType - Optional name of the event whose registered - * listeners to remove - */ - removeAllListeners(eventType: ?string) { - this._subscriber.removeAllSubscriptions(eventType); - } - - /** - * Provides an API that can be called during an eventing cycle to remove the - * last listener that was invoked. This allows a developer to provide an event - * object that can remove the listener (or listener map) during the - * invocation. - * - * If it is called when not inside of an emitting cycle it will throw. - * - * @throws {Error} When called not during an eventing cycle - * - * @example - * var subscription = emitter.addListenerMap({ - * someEvent: function(data, event) { - * console.log(data); - * emitter.removeCurrentListener(); - * } - * }); - * - * emitter.emit('someEvent', 'abc'); // logs 'abc' - * emitter.emit('someEvent', 'def'); // does not log anything - */ - removeCurrentListener() { - invariant( - !!this._currentSubscription, - 'Not in an emitting cycle; there is no current subscription', - ); - this.removeSubscription(this._currentSubscription); - } - - /** - * Removes a specific subscription. Called by the `remove()` method of the - * subscription itself to ensure any necessary cleanup is performed. - */ - removeSubscription(subscription: EmitterSubscription) { - invariant( - subscription.emitter === this, - 'Subscription does not belong to this emitter.', - ); - this._subscriber.removeSubscription(subscription); - } - - /** - * Returns an array of listeners that are currently registered for the given - * event. - * - * @param {string} eventType - Name of the event to query - * @returns {array} - */ - listeners(eventType: string): [EmitterSubscription] { - const subscriptions = this._subscriber.getSubscriptionsForType(eventType); - return subscriptions - ? subscriptions - // We filter out missing entries because the array is sparse. - // "callbackfn is called only for elements of the array which actually - // exist; it is not called for missing elements of the array." - // https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.filter - .filter(sparseFilterPredicate) - .map(subscription => subscription.listener) - : []; - } - - /** - * Emits an event of the given type with the given data. All handlers of that - * particular type will be notified. - * - * @param {string} eventType - Name of the event to emit - * @param {...*} Arbitrary arguments to be passed to each registered listener - * - * @example - * emitter.addListener('someEvent', function(message) { - * console.log(message); - * }); - * - * emitter.emit('someEvent', 'abc'); // logs 'abc' - */ - emit(eventType: string) { - const subscriptions = this._subscriber.getSubscriptionsForType(eventType); - if (subscriptions) { - for (let i = 0, l = subscriptions.length; i < l; i++) { - const subscription = subscriptions[i]; - - // The subscription may have been removed during this event loop. - if (subscription && subscription.listener) { - this._currentSubscription = subscription; - subscription.listener.apply( - subscription.context, - Array.prototype.slice.call(arguments, 1), - ); - } - } - this._currentSubscription = null; - } - } - - /** - * Removes the given listener for event of specific type. - * - * @param {string} eventType - Name of the event to emit - * @param {function} listener - Function to invoke when the specified event is - * emitted - * - * @example - * emitter.removeListener('someEvent', function(message) { - * console.log(message); - * }); // removes the listener if already registered - * - */ - removeListener(eventType: String, listener) { - const subscriptions = this._subscriber.getSubscriptionsForType(eventType); - if (subscriptions) { - for (let i = 0, l = subscriptions.length; i < l; i++) { - const subscription = subscriptions[i]; - - // The subscription may have been removed during this event loop. - // its listener matches the listener in method parameters - if (subscription && subscription.listener === listener) { - subscription.remove(); - } - } - } - } -} +import type {EventSubscription} from './EventSubscription'; export default EventEmitter; + +export type {EventSubscription}; + +/** + * Essential interface for an EventEmitter. + */ +export interface IEventEmitter { + /** + * Registers a listener that is called when the supplied event is emitted. + * Returns a subscription that has a `remove` method to undo registration. + */ + addListener>( + eventType: TEvent, + listener: (...args: $ElementType) => mixed, + context?: mixed, + ): EventSubscription; + + /** + * Emits the supplied event. Additional arguments supplied to `emit` will be + * passed through to each of the registered listeners. + */ + emit>( + eventType: TEvent, + ...args: $ElementType + ): void; + + /** + * Removes all registered listeners. + */ + removeAllListeners>(eventType?: ?TEvent): void; + + /** + * Returns the number of registered listeners for the supplied event. + */ + listenerCount>(eventType: TEvent): number; +} diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EventEmitterWithHolding.js b/packages/react-native-web/src/vendor/react-native/emitter/EventEmitterWithHolding.js deleted file mode 100644 index 49247981..00000000 --- a/packages/react-native-web/src/vendor/react-native/emitter/EventEmitterWithHolding.js +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 - */ - -'use strict'; - -import type EmitterSubscription from './EmitterSubscription'; -import type EventEmitter from './EventEmitter'; -import type EventHolder from './EventHolder'; - -/** - * @class EventEmitterWithHolding - * @description - * An EventEmitterWithHolding decorates an event emitter and enables one to - * "hold" or cache events and then have a handler register later to actually - * handle them. - * - * This is separated into its own decorator so that only those who want to use - * the holding functionality have to and others can just use an emitter. Since - * it implements the emitter interface it can also be combined with anything - * that uses an emitter. - */ -class EventEmitterWithHolding { - _emitter: EventEmitter; - _eventHolder: EventHolder; - _currentEventToken: ?Object; - _emittingHeldEvents: boolean; - - /** - * @constructor - * @param {object} emitter - The object responsible for emitting the actual - * events. - * @param {object} holder - The event holder that is responsible for holding - * and then emitting held events. - */ - constructor(emitter: EventEmitter, holder: EventHolder) { - this._emitter = emitter; - this._eventHolder = holder; - this._currentEventToken = null; - this._emittingHeldEvents = false; - } - - /** - * @see EventEmitter#addListener - */ - addListener(eventType: string, listener: Function, context: ?Object) { - return this._emitter.addListener(eventType, listener, context); - } - - /** - * @see EventEmitter#once - */ - once(eventType: string, listener: Function, context: ?Object) { - return this._emitter.once(eventType, listener, context); - } - - /** - * Adds a listener to be invoked when events of the specified type are - * emitted. An optional calling context may be provided. The data arguments - * emitted will be passed to the listener function. In addition to subscribing - * to all subsequent events, this method will also handle any events that have - * already been emitted, held, and not released. - * - * @param {string} eventType - Name of the event to listen to - * @param {function} listener - Function to invoke when the specified event is - * emitted - * @param {*} context - Optional context object to use when invoking the - * listener - * - * @example - * emitter.emitAndHold('someEvent', 'abc'); - * - * emitter.addRetroactiveListener('someEvent', function(message) { - * console.log(message); - * }); // logs 'abc' - */ - addRetroactiveListener( - eventType: string, - listener: Function, - context: ?Object, - ): EmitterSubscription { - const subscription = this._emitter.addListener( - eventType, - listener, - context, - ); - - this._emittingHeldEvents = true; - this._eventHolder.emitToListener(eventType, listener, context); - this._emittingHeldEvents = false; - - return subscription; - } - - /** - * @see EventEmitter#removeAllListeners - */ - removeAllListeners(eventType: string) { - this._emitter.removeAllListeners(eventType); - } - - /** - * @see EventEmitter#removeCurrentListener - */ - removeCurrentListener() { - this._emitter.removeCurrentListener(); - } - - /** - * @see EventEmitter#listeners - */ - listeners(eventType: string) /* TODO: Annotate return type here */ { - return this._emitter.listeners(eventType); - } - - /** - * @see EventEmitter#emit - */ - emit(eventType: string, ...args: any) { - this._emitter.emit(eventType, ...args); - } - - /** - * Emits an event of the given type with the given data, and holds that event - * in order to be able to dispatch it to a later subscriber when they say they - * want to handle held events. - * - * @param {string} eventType - Name of the event to emit - * @param {...*} Arbitrary arguments to be passed to each registered listener - * - * @example - * emitter.emitAndHold('someEvent', 'abc'); - * - * emitter.addRetroactiveListener('someEvent', function(message) { - * console.log(message); - * }); // logs 'abc' - */ - emitAndHold(eventType: string, ...args: any) { - this._currentEventToken = this._eventHolder.holdEvent(eventType, ...args); - this._emitter.emit(eventType, ...args); - this._currentEventToken = null; - } - - /** - * @see EventHolder#releaseCurrentEvent - */ - releaseCurrentEvent() { - if (this._currentEventToken) { - this._eventHolder.releaseEvent(this._currentEventToken); - } else if (this._emittingHeldEvents) { - this._eventHolder.releaseCurrentEvent(); - } - } - - /** - * @see EventHolder#releaseEventType - * @param {string} eventType - */ - releaseHeldEventType(eventType: string) { - this._eventHolder.releaseEventType(eventType); - } -} - -export default EventEmitterWithHolding; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EventHolder.js b/packages/react-native-web/src/vendor/react-native/emitter/EventHolder.js deleted file mode 100644 index 044f49b6..00000000 --- a/packages/react-native-web/src/vendor/react-native/emitter/EventHolder.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 - */ - -'use strict'; - -import invariant from 'fbjs/lib/invariant'; - -class EventHolder { - _heldEvents: Object; - _currentEventKey: ?Object; - - constructor() { - this._heldEvents = {}; - this._currentEventKey = null; - } - - /** - * Holds a given event for processing later. - * - * TODO: Annotate return type better. The structural type of the return here - * is pretty obvious. - * - * @param {string} eventType - Name of the event to hold and later emit - * @param {...*} Arbitrary arguments to be passed to each registered listener - * @return {object} Token that can be used to release the held event - * - * @example - * - * holder.holdEvent({someEvent: 'abc'}); - * - * holder.emitToHandler({ - * someEvent: function(data, event) { - * console.log(data); - * } - * }); //logs 'abc' - * - */ - holdEvent(eventType: string, ...args: any) { - this._heldEvents[eventType] = this._heldEvents[eventType] || []; - const eventsOfType = this._heldEvents[eventType]; - const key = { - eventType: eventType, - index: eventsOfType.length, - }; - eventsOfType.push(args); - return key; - } - - /** - * Emits the held events of the specified type to the given listener. - * - * @param {?string} eventType - Optional name of the events to replay - * @param {function} listener - The listener to which to dispatch the event - * @param {?object} context - Optional context object to use when invoking - * the listener - */ - emitToListener(eventType: ?string, listener: Function, context: ?Object) { - const eventsOfType = this._heldEvents[eventType]; - if (!eventsOfType) { - return; - } - const origEventKey = this._currentEventKey; - eventsOfType.forEach((/*?array*/ eventHeld, /*number*/ index) => { - if (!eventHeld) { - return; - } - this._currentEventKey = { - eventType: eventType, - index: index, - }; - listener.apply(context, eventHeld); - }); - this._currentEventKey = origEventKey; - } - - /** - * Provides an API that can be called during an eventing cycle to release - * the last event that was invoked, so that it is no longer "held". - * - * If it is called when not inside of an emitting cycle it will throw. - * - * @throws {Error} When called not during an eventing cycle - */ - releaseCurrentEvent() { - invariant( - this._currentEventKey !== null, - 'Not in an emitting cycle; there is no current event', - ); - this._currentEventKey && this.releaseEvent(this._currentEventKey); - } - - /** - * Releases the event corresponding to the handle that was returned when the - * event was first held. - * - * @param {object} token - The token returned from holdEvent - */ - releaseEvent(token: Object) { - delete this._heldEvents[token.eventType][token.index]; - } - - /** - * Releases all events of a certain type. - * - * @param {string} type - */ - releaseEventType(type: string) { - this._heldEvents[type] = []; - } -} - -export default EventHolder; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EventSubscription.js b/packages/react-native-web/src/vendor/react-native/emitter/EventSubscription.js index ca0a8c91..c6e95015 100644 --- a/packages/react-native-web/src/vendor/react-native/emitter/EventSubscription.js +++ b/packages/react-native-web/src/vendor/react-native/emitter/EventSubscription.js @@ -4,37 +4,16 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict * @format - * @flow strict-local */ 'use strict'; -import type EventSubscriptionVendor from './EventSubscriptionVendor'; +// This exists as a separate file only to avoid circular dependencies from +// using this in `_EmitterSubscription`. Combine this back into `EventEmitter` +// after migration and cleanup is done. -/** - * EventSubscription represents a subscription to a particular event. It can - * remove its own subscription. - */ -class EventSubscription { - eventType: string; - key: number; - subscriber: EventSubscriptionVendor; - - /** - * @param {EventSubscriptionVendor} subscriber the subscriber that controls - * this subscription. - */ - constructor(subscriber: EventSubscriptionVendor) { - this.subscriber = subscriber; - } - - /** - * Removes this subscription from the subscriber that controls it. - */ - remove() { - this.subscriber.removeSubscription(this); - } +export interface EventSubscription { + remove(): void; } - -export default EventSubscription; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EventValidator.js b/packages/react-native-web/src/vendor/react-native/emitter/EventValidator.js deleted file mode 100644 index 4e389a75..00000000 --- a/packages/react-native-web/src/vendor/react-native/emitter/EventValidator.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 - */ - -'use strict'; - -const __DEV__ = process.env.NODE_ENV !== 'production'; - -/** - * EventValidator is designed to validate event types to make it easier to catch - * common mistakes. It accepts a map of all of the different types of events - * that the emitter can emit. Then, if a user attempts to emit an event that is - * not one of those specified types the emitter will throw an error. Also, it - * provides a relatively simple matcher so that if it thinks that you likely - * mistyped the event name it will suggest what you might have meant to type in - * the error message. - */ -const EventValidator = { - /** - * @param {Object} emitter - The object responsible for emitting the actual - * events - * @param {Object} types - The collection of valid types that will be used to - * check for errors - * @return {Object} A new emitter with event type validation - * @example - * const types = {someEvent: true, anotherEvent: true}; - * const emitter = EventValidator.addValidation(emitter, types); - */ - addValidation: function(emitter: Object, types: Object) { - const eventTypes = Object.keys(types); - const emitterWithValidation = Object.create(emitter); - - Object.assign(emitterWithValidation, { - emit: function emit(type, a, b, c, d, e, _) { - assertAllowsEventType(type, eventTypes); - return emitter.emit.call(this, type, a, b, c, d, e, _); - }, - }); - - return emitterWithValidation; - }, -}; - -function assertAllowsEventType(type, allowedTypes) { - if (allowedTypes.indexOf(type) === -1) { - throw new TypeError(errorMessageFor(type, allowedTypes)); - } -} - -function errorMessageFor(type, allowedTypes) { - let message = 'Unknown event type "' + type + '". '; - if (__DEV__) { - message += recommendationFor(type, allowedTypes); - } - message += 'Known event types: ' + allowedTypes.join(', ') + '.'; - return message; -} - -// Allow for good error messages -if (__DEV__) { - var recommendationFor = function(type, allowedTypes) { - const closestTypeRecommendation = closestTypeFor(type, allowedTypes); - if (isCloseEnough(closestTypeRecommendation, type)) { - return 'Did you mean "' + closestTypeRecommendation.type + '"? '; - } else { - return ''; - } - }; - - const closestTypeFor = function(type, allowedTypes) { - const typeRecommendations = allowedTypes.map( - typeRecommendationFor.bind(this, type), - ); - return typeRecommendations.sort(recommendationSort)[0]; - }; - - const typeRecommendationFor = function(type, recommendedType) { - return { - type: recommendedType, - distance: damerauLevenshteinDistance(type, recommendedType), - }; - }; - - const recommendationSort = function(recommendationA, recommendationB) { - if (recommendationA.distance < recommendationB.distance) { - return -1; - } else if (recommendationA.distance > recommendationB.distance) { - return 1; - } else { - return 0; - } - }; - - const isCloseEnough = function(closestType, actualType) { - return closestType.distance / actualType.length < 0.334; - }; - - const damerauLevenshteinDistance = function(a, b) { - let i, j; - const d = []; - - for (i = 0; i <= a.length; i++) { - d[i] = [i]; - } - - for (j = 1; j <= b.length; j++) { - d[0][j] = j; - } - - for (i = 1; i <= a.length; i++) { - for (j = 1; j <= b.length; j++) { - const cost = a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1; - - d[i][j] = Math.min( - d[i - 1][j] + 1, - d[i][j - 1] + 1, - d[i - 1][j - 1] + cost, - ); - - if ( - i > 1 && - j > 1 && - a.charAt(i - 1) === b.charAt(j - 2) && - a.charAt(i - 2) === b.charAt(j - 1) - ) { - d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost); - } - } - } - - return d[a.length][b.length]; - }; -} - -export default EventValidator; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EmitterSubscription.js b/packages/react-native-web/src/vendor/react-native/emitter/_EmitterSubscription.js similarity index 60% rename from packages/react-native-web/src/vendor/react-native/emitter/EmitterSubscription.js rename to packages/react-native-web/src/vendor/react-native/emitter/_EmitterSubscription.js index e3b13d02..17cfbce4 100644 --- a/packages/react-native-web/src/vendor/react-native/emitter/EmitterSubscription.js +++ b/packages/react-native-web/src/vendor/react-native/emitter/_EmitterSubscription.js @@ -5,24 +5,25 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow + * @flow strict */ 'use strict'; -import EventSubscription from './EventSubscription'; - import type EventEmitter from './EventEmitter'; -import type EventSubscriptionVendor from './EventSubscriptionVendor'; +import _EventSubscription from './_EventSubscription'; +import type EventSubscriptionVendor from './_EventSubscriptionVendor'; +import {type EventSubscription} from './EventSubscription'; /** * EmitterSubscription represents a subscription with listener and context data. */ -class EmitterSubscription extends EventSubscription { - - emitter: EventEmitter; - listener: Function; - context: ?Object; +class EmitterSubscription> + extends _EventSubscription + implements EventSubscription { + emitter: EventEmitter; + listener: ?(...$ElementType) => mixed; + context: ?$FlowFixMe; /** * @param {EventEmitter} emitter - The event emitter that registered this @@ -35,10 +36,10 @@ class EmitterSubscription extends EventSubscription { * listener */ constructor( - emitter: EventEmitter, - subscriber: EventSubscriptionVendor, - listener: Function, - context: ?Object + emitter: EventEmitter, + subscriber: EventSubscriptionVendor, + listener: (...$ElementType) => mixed, + context: ?$FlowFixMe, ) { super(subscriber); this.emitter = emitter; @@ -48,11 +49,11 @@ class EmitterSubscription extends EventSubscription { /** * Removes this subscription from the emitter that registered it. - * Note: we're overriding the `remove()` method of EventSubscription here + * Note: we're overriding the `remove()` method of _EventSubscription here * but deliberately not calling `super.remove()` as the responsibility * for removing the subscription lies with the EventEmitter. */ - remove() { + remove(): void { this.emitter.removeSubscription(this); } } diff --git a/packages/react-native-web/src/vendor/react-native/emitter/_EventEmitter.js b/packages/react-native-web/src/vendor/react-native/emitter/_EventEmitter.js new file mode 100644 index 00000000..f04c643d --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/emitter/_EventEmitter.js @@ -0,0 +1,189 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 + * @typecheck + */ + +import invariant from 'fbjs/lib/invariant'; + +import EmitterSubscription from './_EmitterSubscription'; +import EventSubscriptionVendor from './_EventSubscriptionVendor'; + +const sparseFilterPredicate = () => true; + +export interface IEventEmitter { + addListener>( + eventType: K, + listener: (...$ElementType) => mixed, + context: $FlowFixMe, + ): EmitterSubscription; + + removeAllListeners>(eventType: ?K): void; + + emit>( + eventType: K, + ...args: $ElementType + ): void; +} + +/** + * @class EventEmitter + * @description + * An EventEmitter is responsible for managing a set of listeners and publishing + * events to them when it is told that such events happened. In addition to the + * data for the given event it also sends a event control object which allows + * the listeners/handlers to prevent the default behavior of the given event. + * + * The emitter is designed to be generic enough to support all the different + * contexts in which one might want to emit events. It is a simple multicast + * mechanism on top of which extra functionality can be composed. For example, a + * more advanced emitter may use an EventHolder and EventFactory. + */ +class EventEmitter + implements IEventEmitter { + _subscriber: EventSubscriptionVendor; + + /** + * @constructor + * + * @param {EventSubscriptionVendor} subscriber - Optional subscriber instance + * to use. If omitted, a new subscriber will be created for the emitter. + */ + constructor(subscriber: ?EventSubscriptionVendor) { + this._subscriber = + subscriber || new EventSubscriptionVendor(); + } + + /** + * Adds a listener to be invoked when events of the specified type are + * emitted. An optional calling context may be provided. The data arguments + * emitted will be passed to the listener function. + * + * TODO: Annotate the listener arg's type. This is tricky because listeners + * can be invoked with varargs. + * + * @param {string} eventType - Name of the event to listen to + * @param {function} listener - Function to invoke when the specified event is + * emitted + * @param {*} context - Optional context object to use when invoking the + * listener + */ + addListener>( + eventType: K, + // FIXME: listeners should return void instead of mixed to prevent issues + listener: (...$ElementType) => mixed, + context: $FlowFixMe, + ): EmitterSubscription { + return (this._subscriber.addSubscription( + eventType, + new EmitterSubscription(this, this._subscriber, listener, context), + ): $FlowFixMe); + } + + /** + * Removes all of the registered listeners, including those registered as + * listener maps. + * + * @param {?string} eventType - Optional name of the event whose registered + * listeners to remove + */ + removeAllListeners>(eventType: ?K): void { + this._subscriber.removeAllSubscriptions(eventType); + } + + /** + * @deprecated Use `remove` on the EventSubscription from `addListener`. + */ + removeSubscription>( + subscription: EmitterSubscription, + ): void { + invariant( + subscription.emitter === this, + 'Subscription does not belong to this emitter.', + ); + this._subscriber.removeSubscription(subscription); + } + + /** + * Returns the number of listeners that are currently registered for the given + * event. + * + * @param {string} eventType - Name of the event to query + * @returns {number} + */ + listenerCount>(eventType: K): number { + const subscriptions = this._subscriber.getSubscriptionsForType(eventType); + return subscriptions + ? // We filter out missing entries because the array is sparse. + // "callbackfn is called only for elements of the array which actually + // exist; it is not called for missing elements of the array." + // https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.filter + subscriptions.filter(sparseFilterPredicate).length + : 0; + } + + /** + * Emits an event of the given type with the given data. All handlers of that + * particular type will be notified. + * + * @param {string} eventType - Name of the event to emit + * @param {...*} Arbitrary arguments to be passed to each registered listener + * + * @example + * emitter.addListener('someEvent', function(message) { + * console.log(message); + * }); + * + * emitter.emit('someEvent', 'abc'); // logs 'abc' + */ + emit>( + eventType: K, + ...args: $ElementType + ): void { + const subscriptions = this._subscriber.getSubscriptionsForType(eventType); + if (subscriptions) { + for (let i = 0, l = subscriptions.length; i < l; i++) { + const subscription = subscriptions[i]; + + // The subscription may have been removed during this event loop. + if (subscription && subscription.listener) { + subscription.listener.apply(subscription.context, args); + } + } + } + } + + /** + * @deprecated Use `remove` on the EventSubscription from `addListener`. + */ + removeListener>( + eventType: K, + // FIXME: listeners should return void instead of mixed to prevent issues + listener: (...$ElementType) => mixed, + ): void { + console.error( + `EventEmitter.removeListener('${eventType}', ...): Method has been ` + + 'deprecated. Please instead use `remove()` on the subscription ' + + 'returned by `EventEmitter.addListener`.', + ); + const subscriptions = this._subscriber.getSubscriptionsForType(eventType); + if (subscriptions) { + for (let i = 0, l = subscriptions.length; i < l; i++) { + const subscription = subscriptions[i]; + + // The subscription may have been removed during this event loop. + // its listener matches the listener in method parameters + if (subscription && subscription.listener === listener) { + subscription.remove(); + } + } + } + } +} + +export default EventEmitter; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/_EventSubscription.js b/packages/react-native-web/src/vendor/react-native/emitter/_EventSubscription.js new file mode 100644 index 00000000..fd7b0b78 --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/emitter/_EventSubscription.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 + */ + +'use strict'; + +import {type EventSubscription} from './EventSubscription'; +import type EventSubscriptionVendor from './_EventSubscriptionVendor'; + +/** + * EventSubscription represents a subscription to a particular event. It can + * remove its own subscription. + */ +class _EventSubscription> + implements EventSubscription { + eventType: K; + key: number; + subscriber: EventSubscriptionVendor; + listener: ?(...$ElementType) => mixed; + context: ?$FlowFixMe; + + /** + * @param {EventSubscriptionVendor} subscriber the subscriber that controls + * this subscription. + */ + constructor(subscriber: EventSubscriptionVendor) { + this.subscriber = subscriber; + } + + /** + * Removes this subscription from the subscriber that controls it. + */ + remove(): void { + this.subscriber.removeSubscription(this); + } +} + +export default _EventSubscription; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EventSubscriptionVendor.js b/packages/react-native-web/src/vendor/react-native/emitter/_EventSubscriptionVendor.js similarity index 73% rename from packages/react-native-web/src/vendor/react-native/emitter/EventSubscriptionVendor.js rename to packages/react-native-web/src/vendor/react-native/emitter/_EventSubscriptionVendor.js index 22a0817f..467646b3 100644 --- a/packages/react-native-web/src/vendor/react-native/emitter/EventSubscriptionVendor.js +++ b/packages/react-native-web/src/vendor/react-native/emitter/_EventSubscriptionVendor.js @@ -5,26 +5,29 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow + * @flow strict */ 'use strict'; import invariant from 'fbjs/lib/invariant'; -import type EventSubscription from './EventSubscription'; +import type EventSubscription from './_EventSubscription'; /** * EventSubscriptionVendor stores a set of EventSubscriptions that are * subscribed to a particular event type. */ -class EventSubscriptionVendor { - _subscriptionsForType: Object; - _currentSubscription: ?EventSubscription; +class EventSubscriptionVendor { + _subscriptionsForType: { + [type: $Keys]: Array< + EventSubscription, + >, + ..., + }; constructor() { this._subscriptionsForType = {}; - this._currentSubscription = null; } /** @@ -33,10 +36,10 @@ class EventSubscriptionVendor { * @param {string} eventType * @param {EventSubscription} subscription */ - addSubscription( - eventType: string, - subscription: EventSubscription, - ): EventSubscription { + addSubscription>( + eventType: K, + subscription: EventSubscription, + ): EventSubscription { invariant( subscription.subscriber === this, 'The subscriber of the subscription is incorrectly set.', @@ -57,8 +60,8 @@ class EventSubscriptionVendor { * @param {?string} eventType - Optional name of the event type whose * registered supscriptions to remove, if null remove all subscriptions. */ - removeAllSubscriptions(eventType: ?string) { - if (eventType === undefined) { + removeAllSubscriptions>(eventType: ?K): void { + if (eventType == null) { this._subscriptionsForType = {}; } else { delete this._subscriptionsForType[eventType]; @@ -71,7 +74,9 @@ class EventSubscriptionVendor { * * @param {object} subscription */ - removeSubscription(subscription: Object) { + removeSubscription>( + subscription: EventSubscription, + ): void { const eventType = subscription.eventType; const key = subscription.key; @@ -93,7 +98,9 @@ class EventSubscriptionVendor { * @param {string} eventType * @returns {?array} */ - getSubscriptionsForType(eventType: string): ?[EventSubscription] { + getSubscriptionsForType>( + eventType: K, + ): ?Array> { return this._subscriptionsForType[eventType]; } } diff --git a/packages/react-native-web/src/vendor/react-native/emitter/mixInEventEmitter.js b/packages/react-native-web/src/vendor/react-native/emitter/mixInEventEmitter.js deleted file mode 100644 index 2492a66d..00000000 --- a/packages/react-native-web/src/vendor/react-native/emitter/mixInEventEmitter.js +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 - */ - -'use strict'; - -import EventEmitter from './EventEmitter'; -import EventEmitterWithHolding from './EventEmitterWithHolding'; -import EventHolder from './EventHolder'; -import EventValidator from './EventValidator'; - -import invariant from 'fbjs/lib/invariant'; -import keyOf from 'fbjs/lib/keyOf'; - -import type EmitterSubscription from './EmitterSubscription'; - -const __DEV__ = process.env.NODE_ENV !== 'production'; -const TYPES_KEY = keyOf({__types: true}); - -/** - * API to setup an object or constructor to be able to emit data events. - * - * @example - * function Dog() { ...dog stuff... } - * mixInEventEmitter(Dog, {bark: true}); - * - * var puppy = new Dog(); - * puppy.addListener('bark', function (volume) { - * console.log('Puppy', this, 'barked at volume:', volume); - * }); - * puppy.emit('bark', 'quiet'); - * // Puppy barked at volume: quiet - * - * - * // A "singleton" object may also be commissioned: - * - * var Singleton = {}; - * mixInEventEmitter(Singleton, {lonely: true}); - * Singleton.emit('lonely', true); - */ -function mixInEventEmitter(cls: Function | Object, types: Object) { - invariant(types, 'Must supply set of valid event types'); - - // If this is a constructor, write to the prototype, otherwise write to the - // singleton object. - const target = cls.prototype || cls; - - invariant(!target.__eventEmitter, 'An active emitter is already mixed in'); - - const ctor = cls.constructor; - if (ctor) { - invariant( - ctor === Object || ctor === Function, - 'Mix EventEmitter into a class, not an instance', - ); - } - - // Keep track of the provided types, union the types if they already exist, - // which allows for prototype subclasses to provide more types. - if (target.hasOwnProperty(TYPES_KEY)) { - Object.assign(target.__types, types); - } else if (target.__types) { - target.__types = Object.assign({}, target.__types, types); - } else { - target.__types = types; - } - Object.assign(target, EventEmitterMixin); -} - -const EventEmitterMixin = { - emit: function(eventType, a, b, c, d, e, _) { - return this.__getEventEmitter().emit(eventType, a, b, c, d, e, _); - }, - - emitAndHold: function(eventType, a, b, c, d, e, _) { - return this.__getEventEmitter().emitAndHold(eventType, a, b, c, d, e, _); - }, - - addListener: function(eventType, listener, context): EmitterSubscription { - return this.__getEventEmitter().addListener(eventType, listener, context); - }, - - once: function(eventType, listener, context) { - return this.__getEventEmitter().once(eventType, listener, context); - }, - - addRetroactiveListener: function(eventType, listener, context) { - return this.__getEventEmitter().addRetroactiveListener( - eventType, - listener, - context, - ); - }, - - addListenerMap: function(listenerMap, context) { - return this.__getEventEmitter().addListenerMap(listenerMap, context); - }, - - addRetroactiveListenerMap: function(listenerMap, context) { - return this.__getEventEmitter().addListenerMap(listenerMap, context); - }, - - removeAllListeners: function() { - this.__getEventEmitter().removeAllListeners(); - }, - - removeCurrentListener: function() { - this.__getEventEmitter().removeCurrentListener(); - }, - - releaseHeldEventType: function(eventType) { - this.__getEventEmitter().releaseHeldEventType(eventType); - }, - - __getEventEmitter: function() { - if (!this.__eventEmitter) { - let emitter = new EventEmitter(); - if (__DEV__) { - emitter = EventValidator.addValidation(emitter, this.__types); - } - - const holder = new EventHolder(); - this.__eventEmitter = new EventEmitterWithHolding(emitter, holder); - } - return this.__eventEmitter; - }, -}; - -export default mixInEventEmitter;