diff --git a/packages/react-native-web/src/vendor/Batchinator/SHA b/packages/react-native-web/src/vendor/Batchinator/SHA new file mode 100644 index 00000000..c0129096 --- /dev/null +++ b/packages/react-native-web/src/vendor/Batchinator/SHA @@ -0,0 +1 @@ +facebook/react-native@19a4a7d3cb6d00780ccbbbd7b0062896f64ab24d diff --git a/packages/react-native-web/src/vendor/Batchinator/index.js b/packages/react-native-web/src/vendor/Batchinator/index.js new file mode 100644 index 00000000..6444b2a2 --- /dev/null +++ b/packages/react-native-web/src/vendor/Batchinator/index.js @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule Batchinator + * @flow + */ +'use strict'; + +const InteractionManager = require('InteractionManager'); + +/** + * A simple class for batching up invocations of a low-pri callback. A timeout is set to run the + * callback once after a delay, no matter how many times it's scheduled. Once the delay is reached, + * InteractionManager.runAfterInteractions is used to invoke the callback after any hi-pri + * interactions are done running. + * + * Make sure to cleanup with dispose(). Example: + * + * class Widget extends React.Component { + * _batchedSave: new Batchinator(() => this._saveState, 1000); + * _saveSate() { + * // save this.state to disk + * } + * componentDidUpdate() { + * this._batchedSave.schedule(); + * } + * componentWillUnmount() { + * this._batchedSave.dispose(); + * } + * ... + * } + */ +class Batchinator { + _callback: () => void; + _delay: number; + _taskHandle: ?{ cancel: () => void }; + constructor(callback: () => void, delayMS: number) { + this._delay = delayMS; + this._callback = callback; + } + /* + * Cleanup any pending tasks. + * + * By default, if there is a pending task the callback is run immediately. Set the option abort to + * true to not call the callback if it was pending. + */ + dispose(options: { abort: boolean } = { abort: false }) { + if (this._taskHandle) { + this._taskHandle.cancel(); + if (!options.abort) { + this._callback(); + } + this._taskHandle = null; + } + } + schedule() { + if (this._taskHandle) { + return; + } + const timeoutHandle = setTimeout(() => { + this._taskHandle = InteractionManager.runAfterInteractions(() => { + // Note that we clear the handle before invoking the callback so that if the callback calls + // schedule again, it will actually schedule another task. + this._taskHandle = null; + this._callback(); + }); + }, this._delay); + this._taskHandle = { cancel: () => clearTimeout(timeoutHandle) }; + } +} + +module.exports = Batchinator; diff --git a/packages/react-native-web/src/vendor/FillRateHelper/SHA b/packages/react-native-web/src/vendor/FillRateHelper/SHA new file mode 100644 index 00000000..c0129096 --- /dev/null +++ b/packages/react-native-web/src/vendor/FillRateHelper/SHA @@ -0,0 +1 @@ +facebook/react-native@19a4a7d3cb6d00780ccbbbd7b0062896f64ab24d diff --git a/packages/react-native-web/src/vendor/FillRateHelper/index.js b/packages/react-native-web/src/vendor/FillRateHelper/index.js new file mode 100644 index 00000000..41f2b6d0 --- /dev/null +++ b/packages/react-native-web/src/vendor/FillRateHelper/index.js @@ -0,0 +1,225 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule FillRateHelper + * @flow + * @format + */ + +'use strict'; + +/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error + * found when Flow v0.54 was deployed. To see the error delete this comment and + * run Flow. */ +const performanceNow = require('fbjs/lib/performanceNow'); +/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error + * found when Flow v0.54 was deployed. To see the error delete this comment and + * run Flow. */ +const warning = require('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; +} + +type FrameMetrics = { inLayout?: boolean, length: number, offset: number }; + +const DEBUG = false; + +let _listeners: Array<(Info) => void> = []; +let _minSampleCount = 10; +let _sampleRate = DEBUG ? 1 : null; + +/** + * A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded. + * By default the sampling rate is set to zero and this will do nothing. If you want to collect + * samples (e.g. to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`. + * + * Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with + * `SceneTracker.getActiveScene` to determine the context of the events. + */ +class FillRateHelper { + _anyBlankStartTime = (null: ?number); + _enabled = false; + _getFrameMetrics: (index: number) => ?FrameMetrics; + _info = new Info(); + _mostlyBlankStartTime = (null: ?number); + _samplesStartTime = (null: ?number); + + static addListener(callback: FillRateInfo => void): { remove: () => void } { + warning(_sampleRate !== null, 'Call `FillRateHelper.setSampleRate` before `addListener`.'); + _listeners.push(callback); + return { + remove: () => { + _listeners = _listeners.filter(listener => callback !== listener); + } + }; + } + + static setSampleRate(sampleRate: number) { + _sampleRate = sampleRate; + } + + static setMinSampleCount(minSampleCount: number) { + _minSampleCount = minSampleCount; + } + + constructor(getFrameMetrics: (index: number) => ?FrameMetrics) { + this._getFrameMetrics = getFrameMetrics; + this._enabled = (_sampleRate || 0) > Math.random(); + this._resetData(); + } + + activate() { + if (this._enabled && this._samplesStartTime == null) { + DEBUG && console.debug('FillRateHelper: activate'); + this._samplesStartTime = performanceNow(); + } + } + + deactivateAndFlush() { + if (!this._enabled) { + return; + } + const start = this._samplesStartTime; // const for flow + if (start == null) { + DEBUG && console.debug('FillRateHelper: bail on deactivate with no start time'); + return; + } + if (this._info.sample_count < _minSampleCount) { + // Don't bother with under-sampled events. + this._resetData(); + return; + } + const total_time_spent = performanceNow() - start; + const info: any = { + ...this._info, + total_time_spent + }; + if (DEBUG) { + const derived = { + avg_blankness: this._info.pixels_blank / this._info.pixels_sampled, + avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000), + avg_speed_when_any_blank: this._info.any_blank_speed_sum / this._info.any_blank_count, + any_blank_per_min: this._info.any_blank_count / (total_time_spent / 1000 / 60), + any_blank_time_frac: this._info.any_blank_ms / total_time_spent, + mostly_blank_per_min: this._info.mostly_blank_count / (total_time_spent / 1000 / 60), + mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent + }; + for (const key in derived) { + derived[key] = Math.round(1000 * derived[key]) / 1000; + } + console.debug('FillRateHelper deactivateAndFlush: ', { derived, info }); + } + _listeners.forEach(listener => listener(info)); + this._resetData(); + } + + computeBlankness( + props: { + data: Array, + getItemCount: (data: Array) => number, + initialNumToRender: number + }, + state: { + first: number, + last: number + }, + scrollMetrics: { + dOffset: number, + offset: number, + velocity: number, + visibleLength: number + } + ): number { + if (!this._enabled || props.getItemCount(props.data) === 0 || this._samplesStartTime == null) { + return 0; + } + const { dOffset, offset, velocity, visibleLength } = scrollMetrics; + + // Denominator metrics that we track for all events - most of the time there is no blankness and + // we want to capture that. + this._info.sample_count++; + this._info.pixels_sampled += Math.round(visibleLength); + this._info.pixels_scrolled += Math.round(Math.abs(dOffset)); + 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(); + if (this._anyBlankStartTime != null) { + this._info.any_blank_ms += now - this._anyBlankStartTime; + } + this._anyBlankStartTime = null; + if (this._mostlyBlankStartTime != null) { + this._info.mostly_blank_ms += now - this._mostlyBlankStartTime; + } + this._mostlyBlankStartTime = null; + + let blankTop = 0; + let first = state.first; + let firstFrame = this._getFrameMetrics(first); + while (first <= state.last && (!firstFrame || !firstFrame.inLayout)) { + firstFrame = this._getFrameMetrics(first); + first++; + } + // Only count blankTop if we aren't rendering the first item, otherwise we will count the header + // as blank. + if (firstFrame && first > 0) { + blankTop = Math.min(visibleLength, Math.max(0, firstFrame.offset - offset)); + } + let blankBottom = 0; + let last = state.last; + let lastFrame = this._getFrameMetrics(last); + while (last >= state.first && (!lastFrame || !lastFrame.inLayout)) { + lastFrame = this._getFrameMetrics(last); + last--; + } + // Only count blankBottom if we aren't rendering the last item, otherwise we will count the + // footer as blank. + if (lastFrame && last < props.getItemCount(props.data) - 1) { + const bottomEdge = lastFrame.offset + lastFrame.length; + blankBottom = Math.min(visibleLength, Math.max(0, offset + visibleLength - bottomEdge)); + } + const pixels_blank = Math.round(blankTop + blankBottom); + const blankness = pixels_blank / visibleLength; + if (blankness > 0) { + this._anyBlankStartTime = now; + this._info.any_blank_speed_sum += scrollSpeed; + this._info.any_blank_count++; + this._info.pixels_blank += pixels_blank; + if (blankness > 0.5) { + this._mostlyBlankStartTime = now; + this._info.mostly_blank_count++; + } + } else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) { + this.deactivateAndFlush(); + } + return blankness; + } + + enabled(): boolean { + return this._enabled; + } + + _resetData() { + this._anyBlankStartTime = null; + this._info = new Info(); + this._mostlyBlankStartTime = null; + this._samplesStartTime = null; + } +} + +module.exports = FillRateHelper; diff --git a/packages/react-native-web/src/vendor/ViewabilityHelper/SHA b/packages/react-native-web/src/vendor/ViewabilityHelper/SHA new file mode 100644 index 00000000..c0129096 --- /dev/null +++ b/packages/react-native-web/src/vendor/ViewabilityHelper/SHA @@ -0,0 +1 @@ +facebook/react-native@19a4a7d3cb6d00780ccbbbd7b0062896f64ab24d diff --git a/packages/react-native-web/src/vendor/ViewabilityHelper/index.js b/packages/react-native-web/src/vendor/ViewabilityHelper/index.js new file mode 100644 index 00000000..281bc011 --- /dev/null +++ b/packages/react-native-web/src/vendor/ViewabilityHelper/index.js @@ -0,0 +1,279 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule ViewabilityHelper + * @flow + * @format + */ +'use strict'; + +const invariant = require('fbjs/lib/invariant'); + +export type ViewToken = { + item: any, + key: string, + index: ?number, + isViewable: boolean, + section?: any +}; + +export type ViewabilityConfigCallbackPair = { + viewabilityConfig: ViewabilityConfig, + onViewableItemsChanged: (info: { + viewableItems: Array, + changed: Array + }) => void +}; + +export type ViewabilityConfig = {| + /** + * Minimum amount of time (in milliseconds) that an item must be physically viewable before the + * viewability callback will be fired. A high number means that scrolling through content without + * stopping will not mark the content as viewable. + */ + minimumViewTime?: number, + + /** + * Percent of viewport that must be covered for a partially occluded item to count as + * "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means + * that a single pixel in the viewport makes the item viewable, and a value of 100 means that + * an item must be either entirely visible or cover the entire viewport to count as viewable. + */ + viewAreaCoveragePercentThreshold?: number, + + /** + * Similar to `viewAreaPercentThreshold`, but considers the percent of the item that is visible, + * rather than the fraction of the viewable area it covers. + */ + itemVisiblePercentThreshold?: number, + + /** + * Nothing is considered viewable until the user scrolls or `recordInteraction` is called after + * render. + */ + waitForInteraction?: boolean +|}; + +/** + * A Utility class for calculating viewable items based on current metrics like scroll position and + * layout. + * + * An item is said to be in a "viewable" state when any of the following + * is true for longer than `minimumViewTime` milliseconds (after an interaction if `waitForInteraction` + * is true): + * + * - Occupying >= `viewAreaCoveragePercentThreshold` of the view area XOR fraction of the item + * visible in the view area >= `itemVisiblePercentThreshold`. + * - Entirely visible on screen + */ +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(); + _viewableIndices: Array = []; + _viewableItems: Map = new Map(); + + constructor(config: ViewabilityConfig = { viewAreaCoveragePercentThreshold: 0 }) { + this._config = config; + } + + /** + * Cleanup, e.g. on unmount. Clears any pending timers. + */ + dispose() { + this._timers.forEach(clearTimeout); + } + + /** + * Determines which items are viewable based on the current metrics and config. + */ + computeViewableItems( + itemCount: number, + scrollOffset: number, + viewportHeight: number, + getFrameMetrics: (index: number) => ?{ length: number, offset: number }, + renderRange?: { first: number, last: number } // Optional optimization to reduce the scan size + ): Array { + const { itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold } = this._config; + const viewAreaMode = viewAreaCoveragePercentThreshold != null; + const viewablePercentThreshold = viewAreaMode + ? viewAreaCoveragePercentThreshold + : itemVisiblePercentThreshold; + invariant( + viewablePercentThreshold != null && + (itemVisiblePercentThreshold != null) !== (viewAreaCoveragePercentThreshold != null), + 'Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold' + ); + const viewableIndices = []; + if (itemCount === 0) { + return viewableIndices; + } + let firstVisible = -1; + const { first, last } = renderRange || { first: 0, last: itemCount - 1 }; + invariant( + last < itemCount, + 'Invalid render range ' + JSON.stringify({ renderRange, itemCount }) + ); + for (let idx = first; idx <= last; idx++) { + const metrics = getFrameMetrics(idx); + if (!metrics) { + continue; + } + const top = metrics.offset - scrollOffset; + const bottom = top + metrics.length; + if (top < viewportHeight && bottom > 0) { + firstVisible = idx; + if ( + _isViewable( + viewAreaMode, + viewablePercentThreshold, + top, + bottom, + viewportHeight, + metrics.length + ) + ) { + viewableIndices.push(idx); + } + } else if (firstVisible >= 0) { + break; + } + } + return viewableIndices; + } + + /** + * Figures out which items are viewable and how that has changed from before and calls + * `onViewableItemsChanged` as appropriate. + */ + onUpdate( + itemCount: number, + scrollOffset: number, + viewportHeight: number, + getFrameMetrics: (index: number) => ?{ length: number, offset: number }, + createViewToken: (index: number, isViewable: boolean) => ViewToken, + onViewableItemsChanged: ({ + viewableItems: Array, + changed: Array + }) => void, + renderRange?: { first: number, last: number } // Optional optimization to reduce the scan size + ): void { + if ( + (this._config.waitForInteraction && !this._hasInteracted) || + itemCount === 0 || + !getFrameMetrics(0) + ) { + return; + } + let viewableIndices = []; + if (itemCount) { + viewableIndices = this.computeViewableItems( + itemCount, + scrollOffset, + viewportHeight, + getFrameMetrics, + renderRange + ); + } + if ( + this._viewableIndices.length === viewableIndices.length && + this._viewableIndices.every((v, ii) => v === viewableIndices[ii]) + ) { + // We might get a lot of scroll events where visibility doesn't change and we don't want to do + // extra work in those cases. + return; + } + this._viewableIndices = viewableIndices; + if (this._config.minimumViewTime) { + const handle = setTimeout(() => { + this._timers.delete(handle); + this._onUpdateSync(viewableIndices, onViewableItemsChanged, createViewToken); + }, this._config.minimumViewTime); + this._timers.add(handle); + } else { + this._onUpdateSync(viewableIndices, onViewableItemsChanged, createViewToken); + } + } + + /** + * clean-up cached _viewableIndices to evaluate changed items on next update + */ + resetViewableIndices() { + this._viewableIndices = []; + } + + /** + * Records that an interaction has happened even if there has been no scroll. + */ + recordInteraction() { + this._hasInteracted = true; + } + + _onUpdateSync(viewableIndicesToCheck, onViewableItemsChanged, createViewToken) { + // Filter out indices that have gone out of view since this call was scheduled. + viewableIndicesToCheck = viewableIndicesToCheck.filter(ii => + this._viewableIndices.includes(ii) + ); + const prevItems = this._viewableItems; + const nextItems = new Map( + viewableIndicesToCheck.map(ii => { + const viewable = createViewToken(ii, true); + return [viewable.key, viewable]; + }) + ); + + const changed = []; + for (const [key, viewable] of nextItems) { + if (!prevItems.has(key)) { + changed.push(viewable); + } + } + for (const [key, viewable] of prevItems) { + if (!nextItems.has(key)) { + changed.push({ ...viewable, isViewable: false }); + } + } + if (changed.length > 0) { + this._viewableItems = nextItems; + onViewableItemsChanged({ + viewableItems: Array.from(nextItems.values()), + changed, + viewabilityConfig: this._config + }); + } + } +} + +function _isViewable( + viewAreaMode: boolean, + viewablePercentThreshold: number, + top: number, + bottom: number, + viewportHeight: number, + itemLength: number +): boolean { + if (_isEntirelyVisible(top, bottom, viewportHeight)) { + return true; + } else { + const pixels = _getPixelsVisible(top, bottom, viewportHeight); + const percent = 100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength); + return percent >= viewablePercentThreshold; + } +} + +function _getPixelsVisible(top: number, bottom: number, viewportHeight: number): number { + const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0); + return Math.max(0, visibleHeight); +} + +function _isEntirelyVisible(top: number, bottom: number, viewportHeight: number): boolean { + return top >= 0 && bottom <= viewportHeight && bottom > top; +} + +module.exports = ViewabilityHelper; diff --git a/packages/react-native-web/src/vendor/VirtualizeUtils/SHA b/packages/react-native-web/src/vendor/VirtualizeUtils/SHA new file mode 100644 index 00000000..c0129096 --- /dev/null +++ b/packages/react-native-web/src/vendor/VirtualizeUtils/SHA @@ -0,0 +1 @@ +facebook/react-native@19a4a7d3cb6d00780ccbbbd7b0062896f64ab24d diff --git a/packages/react-native-web/src/vendor/VirtualizeUtils/index.js b/packages/react-native-web/src/vendor/VirtualizeUtils/index.js new file mode 100644 index 00000000..5d91dce1 --- /dev/null +++ b/packages/react-native-web/src/vendor/VirtualizeUtils/index.js @@ -0,0 +1,205 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule VirtualizeUtils + * @flow + * @format + */ +'use strict'; + +const invariant = require('fbjs/lib/invariant'); + +/** + * Used to find the indices of the frames that overlap the given offsets. Useful for finding the + * items that bound different windows of content, such as the visible area or the buffered overscan + * area. + */ +function elementsThatOverlapOffsets( + offsets: Array, + itemCount: number, + getFrameMetrics: (index: number) => { length: number, offset: number } +): Array { + const out = []; + let outLength = 0; + for (let ii = 0; ii < itemCount; ii++) { + const frame = getFrameMetrics(ii); + const trailingOffset = frame.offset + frame.length; + for (let kk = 0; kk < offsets.length; kk++) { + if (out[kk] == null && trailingOffset >= offsets[kk]) { + out[kk] = ii; + outLength++; + if (kk === offsets.length - 1) { + invariant( + outLength === offsets.length, + 'bad offsets input, should be in increasing order: %s', + JSON.stringify(offsets) + ); + return out; + } + } + } + } + return out; +} + +/** + * Computes the number of elements in the `next` range that are new compared to the `prev` range. + * Handy for calculating how many new items will be rendered when the render window changes so we + * can restrict the number of new items render at once so that content can appear on the screen + * faster. + */ +function newRangeCount( + prev: { first: number, last: number }, + next: { first: number, last: number } +): number { + return ( + next.last - + next.first + + 1 - + Math.max(0, 1 + Math.min(next.last, prev.last) - Math.max(next.first, prev.first)) + ); +} + +/** + * Custom logic for determining which items should be rendered given the current frame and scroll + * metrics, as well as the previous render state. The algorithm may evolve over time, but generally + * prioritizes the visible area first, then expands that with overscan regions ahead and behind, + * biased in the direction of scroll. + */ +function computeWindowedRenderLimits( + props: { + data: any, + getItemCount: (data: any) => number, + maxToRenderPerBatch: number, + windowSize: number + }, + prev: { first: number, last: number }, + getFrameMetricsApprox: (index: number) => { length: number, offset: number }, + scrollMetrics: { + dt: number, + offset: number, + velocity: number, + visibleLength: number + } +): { first: number, last: number } { + const { data, getItemCount, maxToRenderPerBatch, windowSize } = props; + const itemCount = getItemCount(data); + if (itemCount === 0) { + return prev; + } + const { offset, velocity, visibleLength } = scrollMetrics; + + // Start with visible area, then compute maximum overscan region by expanding from there, biased + // in the direction of scroll. Total overscan area is capped, which should cap memory consumption + // too. + const visibleBegin = Math.max(0, offset); + const visibleEnd = visibleBegin + visibleLength; + const overscanLength = (windowSize - 1) * visibleLength; + + // Considering velocity seems to introduce more churn than it's worth. + const leadFactor = 0.5; // Math.max(0, Math.min(1, velocity / 25 + 0.5)); + + const fillPreference = velocity > 1 ? 'after' : velocity < -1 ? 'before' : 'none'; + + const overscanBegin = Math.max(0, visibleBegin - (1 - leadFactor) * overscanLength); + const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength); + + const lastItemOffset = getFrameMetricsApprox(itemCount - 1).offset; + if (lastItemOffset < overscanBegin) { + // Entire list is before our overscan window + return { + first: Math.max(0, itemCount - 1 - maxToRenderPerBatch), + last: itemCount - 1 + }; + } + + // Find the indices that correspond to the items at the render boundaries we're targeting. + let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets( + [overscanBegin, visibleBegin, visibleEnd, overscanEnd], + props.getItemCount(props.data), + getFrameMetricsApprox + ); + overscanFirst = overscanFirst == null ? 0 : overscanFirst; + first = first == null ? Math.max(0, overscanFirst) : first; + overscanLast = overscanLast == null ? itemCount - 1 : overscanLast; + last = last == null ? Math.min(overscanLast, first + maxToRenderPerBatch - 1) : last; + const visible = { first, last }; + + // We want to limit the number of new cells we're rendering per batch so that we can fill the + // content on the screen quickly. If we rendered the entire overscan window at once, the user + // could be staring at white space for a long time waiting for a bunch of offscreen content to + // render. + let newCellCount = newRangeCount(prev, visible); + + while (true) { + if (first <= overscanFirst && last >= overscanLast) { + // If we fill the entire overscan range, we're done. + break; + } + const maxNewCells = newCellCount >= maxToRenderPerBatch; + const firstWillAddMore = first <= prev.first || first > prev.last; + const firstShouldIncrement = first > overscanFirst && (!maxNewCells || !firstWillAddMore); + const lastWillAddMore = last >= prev.last || last < prev.first; + const lastShouldIncrement = last < overscanLast && (!maxNewCells || !lastWillAddMore); + if (maxNewCells && !firstShouldIncrement && !lastShouldIncrement) { + // We only want to stop if we've hit maxNewCells AND we cannot increment first or last + // without rendering new items. This let's us preserve as many already rendered items as + // possible, reducing render churn and keeping the rendered overscan range as large as + // possible. + break; + } + if ( + firstShouldIncrement && + !(fillPreference === 'after' && lastShouldIncrement && lastWillAddMore) + ) { + if (firstWillAddMore) { + newCellCount++; + } + first--; + } + if ( + lastShouldIncrement && + !(fillPreference === 'before' && firstShouldIncrement && firstWillAddMore) + ) { + if (lastWillAddMore) { + newCellCount++; + } + last++; + } + } + if ( + !( + last >= first && + first >= 0 && + last < itemCount && + first >= overscanFirst && + last <= overscanLast && + first <= visible.first && + last >= visible.last + ) + ) { + throw new Error( + 'Bad window calculation ' + + JSON.stringify({ + first, + last, + itemCount, + overscanFirst, + overscanLast, + visible + }) + ); + } + return { first, last }; +} + +const VirtualizeUtils = { + computeWindowedRenderLimits, + elementsThatOverlapOffsets, + newRangeCount +}; + +module.exports = VirtualizeUtils; diff --git a/packages/react-native-web/src/vendor/VirtualizedList/SHA b/packages/react-native-web/src/vendor/VirtualizedList/SHA new file mode 100644 index 00000000..c0129096 --- /dev/null +++ b/packages/react-native-web/src/vendor/VirtualizedList/SHA @@ -0,0 +1 @@ +facebook/react-native@19a4a7d3cb6d00780ccbbbd7b0062896f64ab24d diff --git a/packages/react-native-web/src/vendor/VirtualizedList/index.js b/packages/react-native-web/src/vendor/VirtualizedList/index.js new file mode 100644 index 00000000..658b032c --- /dev/null +++ b/packages/react-native-web/src/vendor/VirtualizedList/index.js @@ -0,0 +1,1593 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule VirtualizedList + * @flow + * @format + */ +'use strict'; + +const Batchinator = require('Batchinator'); +const FillRateHelper = require('FillRateHelper'); +const PropTypes = require('prop-types'); +const React = require('React'); +const ReactNative = require('ReactNative'); +const RefreshControl = require('RefreshControl'); +const ScrollView = require('ScrollView'); +const StyleSheet = require('StyleSheet'); +const UIManager = require('UIManager'); +const View = require('View'); +const ViewabilityHelper = require('ViewabilityHelper'); + +const flattenStyle = require('flattenStyle'); +const infoLog = require('infoLog'); +const invariant = require('fbjs/lib/invariant'); +/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error + * found when Flow v0.54 was deployed. To see the error delete this comment and + * run Flow. */ +const warning = require('fbjs/lib/warning'); + +const { computeWindowedRenderLimits } = require('VirtualizeUtils'); + +import type { StyleObj } from 'StyleSheetTypes'; +import type { + ViewabilityConfig, + ViewToken, + ViewabilityConfigCallbackPair +} from 'ViewabilityHelper'; + +type Item = any; + +export type renderItemType = (info: any) => ?React.Element; + +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, + /** + * The default accessor functions assume this is an Array<{key: string}> but you can override + * getItem, getItemCount, and keyExtractor to handle any type of index-based data. + */ + data?: any, + /** + * A generic accessor for extracting an item from any sort of data blob. + */ + getItem: (data: any, index: number) => ?Item, + /** + * Determines how many items are in the data blob. + */ + getItemCount: (data: any) => number +}; +type OptionalProps = { + /** + * `debug` will turn on extra logging and visual overlays to aid with debugging both usage and + * implementation, but with a significant perf hit. + */ + debug?: ?boolean, + /** + * 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, + /** + * A marker property for telling the list to re-render (since it implements `PureComponent`). If + * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the + * `data` prop, stick it here and treat it immutably. + */ + extraData?: any, + getItemLayout?: (data: any, index: number) => { length: number, offset: number, index: number }, // e.g. height, y + horizontal?: ?boolean, + /** + * How many items to render in the initial batch. This should be enough to fill the screen but not + * much more. Note these items will never be unmounted as part of the windowed rendering in order + * to improve perceived performance of scroll-to-top actions. + */ + initialNumToRender: number, + /** + * Instead of starting at the top with the first item, start at `initialScrollIndex`. This + * disables the "scroll to top" optimization that keeps the first `initialNumToRender` items + * always rendered and immediately renders the items starting at this initial index. Requires + * `getItemLayout` to be implemented. + */ + initialScrollIndex?: ?number, + /** + * Reverses the direction of scroll. Uses scale transforms of -1. + */ + inverted?: ?boolean, + keyExtractor: (item: Item, index: number) => string, + /** + * Each cell is rendered using this element. Can be a React Component Class, + * or a render function. Defaults to using View. + */ + CellRendererComponent?: ?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), + /** + * 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), + /** + * A unique identifier for this list. If there are multiple VirtualizedLists at the same level of + * nesting within another VirtualizedList, this key is necessary for virtualization to + * work properly. + */ + 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 + * interfere with responding to button taps or other interactions. + */ + maxToRenderPerBatch: number, + onEndReached?: ?(info: { distanceFromEnd: number }) => void, + onEndReachedThreshold?: ?number, // units of visible length + onLayout?: ?Function, + /** + * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make + * sure to also set the `refreshing` prop correctly. + */ + onRefresh?: ?Function, + /** + * 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 + * then try again after more items have been rendered. + */ + onScrollToIndexFailed?: ?(info: { + index: number, + highestMeasuredFrameIndex: number, + averageItemLength: number + }) => 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, + /** + * 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, + /** + * Render a custom scroll component, e.g. with a differently styled `RefreshControl`. + */ + renderScrollComponent?: (props: Object) => React.Element, + /** + * Amount of time between low-pri item render batches, e.g. for rendering items quite a ways off + * screen. Similar fill rate/responsiveness tradeoff as `maxToRenderPerBatch`. + */ + updateCellsBatchingPeriod: number, + viewabilityConfig?: ViewabilityConfig, + /** + * List of ViewabilityConfig/onViewableItemsChanged pairs. A specific onViewableItemsChanged + * will be called when its corresponding ViewabilityConfig's conditions are met. + */ + viewabilityConfigCallbackPairs?: Array, + /** + * Determines the maximum number of items rendered outside of the visible area, in units of + * visible lengths. So if your list fills the screen, then `windowSize={21}` (the default) will + * render the visible screen area plus up to 10 screens above and 10 below the viewport. Reducing + * this number will reduce memory consumption and may improve performance, but will increase the + * 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; + +let _usedIndexForKey = false; + +type Frame = { + offset: number, + length: number, + index: number, + inLayout: boolean +}; + +type ChildListState = { + 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 + * 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. + * + * Virtualization massively improves memory consumption and performance of large lists by + * maintaining a finite render window of active items and replacing all items outside of the render + * window with appropriately sized blank space. The window adapts to scrolling behavior, and items + * are rendered incrementally with low-pri (after any running interactions) if they are far from the + * visible area, or with hi-pri otherwise to minimize the potential of seeing blank space. + * + * Some caveats: + * + * - Internal state is not preserved when content scrolls out of the render window. Make sure all + * your data is captured in the item data or external stores like Flux, Redux, or Relay. + * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- + * equal. Make sure that everything your `renderItem` function depends on is passed as a prop + * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on + * changes. This includes the `data` prop and parent component state. + * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously + * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see + * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, + * and we are working on improving it behind the scenes. + * - By default, the list looks for a `key` prop on each item and uses that for the React key. + * Alternatively, you can provide a custom `keyExtractor` prop. + * + */ +class VirtualizedList extends React.PureComponent { + props: Props; + + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params?: ?{ animated?: ?boolean }) { + const animated = params ? params.animated : true; + const veryLast = this.props.getItemCount(this.props.data) - 1; + const frame = this._getFrameMetricsApprox(veryLast); + const offset = Math.max( + 0, + frame.offset + frame.length + 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. */ + this._scrollRef.scrollTo( + /* $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.props.horizontal ? { x: offset, animated } : { y: offset, animated } + ); + } + + // scrollToIndex may be janky without getItemLayout prop + scrollToIndex(params: { + animated?: ?boolean, + index: number, + viewOffset?: number, + viewPosition?: number + }) { + const { data, horizontal, getItemCount, getItemLayout, onScrollToIndexFailed } = this.props; + const { animated, index, viewOffset, viewPosition } = params; + invariant( + index >= 0 && index < getItemCount(data), + `scrollToIndex out of range: ${index} vs ${getItemCount(data) - 1}` + ); + if (!getItemLayout && index > this._highestMeasuredFrameIndex) { + invariant( + !!onScrollToIndexFailed, + 'scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, ' + + 'otherwise there is no way to know the location of offscreen indices or handle failures.' + ); + onScrollToIndexFailed({ + averageItemLength: this._averageCellLength, + highestMeasuredFrameIndex: this._highestMeasuredFrameIndex, + index + }); + return; + } + const frame = this._getFrameMetricsApprox(index); + const offset = + Math.max( + 0, + frame.offset - (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. */ + this._scrollRef.scrollTo( + /* $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. */ + horizontal ? { x: offset, animated } : { y: offset, animated } + ); + } + + // scrollToItem may be janky without getItemLayout prop. Required linear scan through items - + // use scrollToIndex instead if possible. + scrollToItem(params: { animated?: ?boolean, item: Item, viewPosition?: number }) { + const { item } = params; + const { data, getItem, getItemCount } = this.props; + const itemCount = getItemCount(data); + for (let index = 0; index < itemCount; index++) { + if (getItem(data, index) === item) { + this.scrollToIndex({ ...params, index }); + break; + } + } + } + + /** + * Scroll to a specific content pixel offset in the list. + * + * Param `offset` expects the offset to scroll to. + * In case of `horizontal` is true, the offset is the x-value, + * in any other case the offset is the y-value. + * + * Param `animated` (`true` by default) defines whether the list + * should do an animation while scrolling. + */ + 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. */ + this._scrollRef.scrollTo( + /* $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.props.horizontal ? { x: offset, animated } : { y: offset, animated } + ); + } + + recordInteraction() { + this._nestedChildLists.forEach(childList => { + childList.ref && childList.ref.recordInteraction(); + }); + this._viewabilityTuples.forEach(t => { + t.viewabilityHelper.recordInteraction(); + }); + this._updateViewableItems(this.props.data); + } + + 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. */ + this._scrollRef.flashScrollIndicators(); + } + + /** + * Provides a handle to the underlying scroll responder. + * Note that `this._scrollRef` might not be a `ScrollView`, so we + * need to check that it responds to `getScrollResponder` before calling it. + */ + getScrollResponder() { + if (this._scrollRef && this._scrollRef.getScrollResponder) { + return this._scrollRef.getScrollResponder(); + } + } + + getScrollableNode() { + if (this._scrollRef && this._scrollRef.getScrollableNode) { + return this._scrollRef.getScrollableNode(); + } else { + return ReactNative.findNodeHandle(this._scrollRef); + } + } + + setNativeProps(props: Object) { + if (this._scrollRef) { + this._scrollRef.setNativeProps(props); + } + } + + static defaultProps = { + disableVirtualization: false, + horizontal: false, + initialNumToRender: 10, + keyExtractor: (item: Item, index: number) => { + if (item.key != null) { + return item.key; + } + _usedIndexForKey = true; + return String(index); + }, + maxToRenderPerBatch: 10, + onEndReachedThreshold: 2, // multiples of length + scrollEventThrottle: 50, + updateCellsBatchingPeriod: 50, + 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.virtualizedCell && this.context.virtualizedCell.cellKey) || 'rootList'; + } + + _getScrollMetrics = () => { + return this._scrollMetrics; + }; + + hasMore(): boolean { + return this._hasMore; + } + + _getOutermostParentListRef = () => { + if (this._isNestedWithSameOrientation()) { + return this.context.virtualizedList.getOutermostParentListRef(); + } else { + return this; + } + }; + + _getNestedChildState = (key: string): ?ChildListState => { + const existingChildData = this._nestedChildLists.get(key); + return existingChildData && existingChildData.state; + }; + + _registerAsNestedChild = (childList: { + cellKey: string, + key: string, + ref: VirtualizedList + }): ?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); + invariant( + !(existingChildData && existingChildData.ref !== null), + '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.' + ); + this._nestedChildLists.set(childList.key, { + ref: childList.ref, + state: null + }); + + if (this._hasInteracted) { + childList.ref.recordInteraction(); + } + }; + + _unregisterAsNestedChild = (childList: { key: string, state: ChildListState }): void => { + this._nestedChildLists.set(childList.key, { + ref: null, + state: childList.state + }); + }; + + state: State; + + constructor(props: Props, context: Object) { + super(props, context); + invariant( + !props.onScroll || !props.onScroll.__isNative, + 'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' + + 'to support native onScroll events with useNativeDriver' + ); + + invariant( + props.windowSize > 0, + 'VirtualizedList: The windowSize prop must be present and set to a value greater than 0.' + ); + + this._fillRateHelper = new FillRateHelper(this._getFrameMetrics); + this._updateCellsToRenderBatcher = new Batchinator( + this._updateCellsToRender, + this.props.updateCellsBatchingPeriod + ); + + if (this.props.viewabilityConfigCallbackPairs) { + this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map(pair => ({ + viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig), + onViewableItemsChanged: pair.onViewableItemsChanged + })); + } else if (this.props.onViewableItemsChanged) { + this._viewabilityTuples.push({ + viewabilityHelper: new ViewabilityHelper(this.props.viewabilityConfig), + onViewableItemsChanged: this.props.onViewableItemsChanged + }); + } + + let initialState = { + first: this.props.initialScrollIndex || 0, + last: + Math.min( + this.props.getItemCount(this.props.data), + (this.props.initialScrollIndex || 0) + this.props.initialNumToRender + ) - 1 + }; + + if (this._isNestedWithSameOrientation()) { + const storedState = this.context.virtualizedList.getNestedChildState( + this.props.listKey || this._getCellKey() + ); + if (storedState) { + initialState = storedState; + this.state = storedState; + this._frames = storedState.frames; + } + } + + this.state = initialState; + } + + componentDidMount() { + if (this._isNestedWithSameOrientation()) { + this.context.virtualizedList.registerAsNestedChild({ + cellKey: this._getCellKey(), + key: this.props.listKey || this._getCellKey(), + ref: this + }); + } + } + + componentWillUnmount() { + if (this._isNestedWithSameOrientation()) { + this.context.virtualizedList.unregisterAsNestedChild({ + key: this.props.listKey || this._getCellKey(), + state: { + first: this.state.first, + last: this.state.last, + frames: this._frames + } + }); + } + this._updateViewableItems(null); + this._updateCellsToRenderBatcher.dispose({ abort: true }); + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.dispose(); + }); + this._fillRateHelper.deactivateAndFlush(); + } + + static getDerivedStateFromProps(newProps: Props, prevState: State) { + const { data, extraData, 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. + return { + first: Math.max(0, Math.min(prevState.first, getItemCount(data) - 1 - maxToRenderPerBatch)), + last: Math.max(0, Math.min(prevState.last, getItemCount(data) - 1)) + }; + } + + _pushCells( + cells: Array, + stickyHeaderIndices: Array, + stickyIndicesFromProps: Set, + first: number, + last: number, + inversionStyle: ?StyleObj + ) { + const { + CellRendererComponent, + ItemSeparatorComponent, + data, + getItem, + getItemCount, + horizontal, + keyExtractor + } = this.props; + const stickyOffset = this.props.ListHeaderComponent ? 1 : 0; + const end = getItemCount(data) - 1; + let prevCellKey; + last = Math.min(end, last); + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); + const key = keyExtractor(item, ii); + this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { + stickyHeaderIndices.push(cells.length); + } + cells.push( + this._onCellLayout(e, key, ii)} + onUnmount={this._onCellUnmount} + parentProps={this.props} + ref={ref => { + this._cellRefs[key] = ref; + }} + /> + ); + prevCellKey = key; + } + } + + _onUpdateSeparators = (keys: Array, newProps: Object) => { + keys.forEach(key => { + const ref = key != null && this._cellRefs[key]; + ref && ref.updateSeparatorProps(newProps); + }); + }; + + _isVirtualizationDisabled(): boolean { + return this.props.disableVirtualization; + } + + _isNestedWithSameOrientation(): boolean { + const nestedContext = this.context.virtualizedList; + return !!(nestedContext && !!nestedContext.horizontal === !!this.props.horizontal); + } + + render() { + 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.' + ); + } + const { ListEmptyComponent, ListFooterComponent, ListHeaderComponent } = this.props; + const { data, horizontal } = this.props; + const isVirtualizationDisabled = this._isVirtualizationDisabled(); + const inversionStyle = this.props.inverted + ? this.props.horizontal ? styles.horizontallyInverted : styles.verticallyInverted + : null; + const cells = []; + const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices); + const stickyHeaderIndices = []; + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { + stickyHeaderIndices.push(0); + } + const element = React.isValidElement(ListHeaderComponent) ? ( + ListHeaderComponent + ) : ( + // $FlowFixMe + + ); + cells.push( + + + {element} + + + ); + } + const itemCount = this.props.getItemCount(data); + if (itemCount > 0) { + _usedIndexForKey = false; + const spacerKey = !horizontal ? 'height' : 'width'; + const lastInitialIndex = this.props.initialScrollIndex + ? -1 + : this.props.initialNumToRender - 1; + const { first, last } = this.state; + this._pushCells( + cells, + stickyHeaderIndices, + stickyIndicesFromProps, + 0, + lastInitialIndex, + inversionStyle + ); + const firstAfterInitial = Math.max(lastInitialIndex + 1, first); + if (!isVirtualizationDisabled && first > lastInitialIndex + 1) { + let insertedStickySpacer = false; + if (stickyIndicesFromProps.size > 0) { + const stickyOffset = ListHeaderComponent ? 1 : 0; + // See if there are any sticky headers in the virtualized space that we need to render. + for (let ii = firstAfterInitial - 1; ii > lastInitialIndex; ii--) { + if (stickyIndicesFromProps.has(ii + stickyOffset)) { + const initBlock = this._getFrameMetricsApprox(lastInitialIndex); + const stickyBlock = this._getFrameMetricsApprox(ii); + const leadSpace = stickyBlock.offset - (initBlock.offset + initBlock.length); + cells.push(); + this._pushCells( + cells, + stickyHeaderIndices, + stickyIndicesFromProps, + ii, + ii, + inversionStyle + ); + const trailSpace = + this._getFrameMetricsApprox(first).offset - + (stickyBlock.offset + stickyBlock.length); + cells.push(); + insertedStickySpacer = true; + break; + } + } + } + if (!insertedStickySpacer) { + const initBlock = this._getFrameMetricsApprox(lastInitialIndex); + const firstSpace = + this._getFrameMetricsApprox(first).offset - (initBlock.offset + initBlock.length); + cells.push(); + } + } + this._pushCells( + cells, + stickyHeaderIndices, + stickyIndicesFromProps, + firstAfterInitial, + last, + inversionStyle + ); + if (!this._hasWarned.keys && _usedIndexForKey) { + console.warn( + 'VirtualizedList: missing keys for items, make sure to specify a key property on each ' + + 'item or provide a custom keyExtractor.' + ); + this._hasWarned.keys = true; + } + if (!isVirtualizationDisabled && last < itemCount - 1) { + const lastFrame = this._getFrameMetricsApprox(last); + // Without getItemLayout, we limit our tail spacer to the _highestMeasuredFrameIndex to + // prevent the user for hyperscrolling into un-measured area because otherwise content will + // likely jump around as it renders in above the viewport. + const end = this.props.getItemLayout + ? itemCount - 1 + : Math.min(itemCount - 1, this._highestMeasuredFrameIndex); + const endFrame = this._getFrameMetricsApprox(end); + const tailSpacerLength = + endFrame.offset + endFrame.length - (lastFrame.offset + lastFrame.length); + cells.push(); + } + } else if (ListEmptyComponent) { + const element = React.isValidElement(ListEmptyComponent) ? ( + ListEmptyComponent + ) : ( + // $FlowFixMe + + ); + cells.push( + + {element} + + ); + } + if (ListFooterComponent) { + const element = React.isValidElement(ListFooterComponent) ? ( + ListFooterComponent + ) : ( + // $FlowFixMe + + ); + cells.push( + + + {element} + + + ); + } + const scrollProps = { + ...this.props, + onContentSizeChange: this._onContentSizeChange, + onLayout: this._onLayout, + onScroll: this._onScroll, + onScrollBeginDrag: this._onScrollBeginDrag, + onScrollEndDrag: this._onScrollEndDrag, + onMomentumScrollEnd: this._onMomentumScrollEnd, + scrollEventThrottle: this.props.scrollEventThrottle, // TODO: Android support + invertStickyHeaders: this.props.inverted, + stickyHeaderIndices + }; + if (inversionStyle) { + 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 + ); + if (this.props.debug) { + return ( + + {ret} + {this._renderDebugOverlay()} + + ); + } else { + return ret; + } + } + + 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 => { + tuple.viewabilityHelper.resetViewableIndices(); + }); + } + this._scheduleCellsToRenderUpdate(); + } + + _averageCellLength = 0; + // Maps a cell key to the set of keys for all outermost child lists within that cell + _cellKeysToChildListKeys: Map> = new Map(); + _cellRefs = {}; + _fillRateHelper: FillRateHelper; + _frames = {}; + _footerLength = 0; + _hasDataChangedSinceEndReached = true; + _hasInteracted = false; + _hasMore = false; + _hasWarned = {}; + _highestMeasuredFrameIndex = 0; + _headerLength = 0; + _indicesToKeys: Map = new Map(); + _hasDoneInitialScroll = false; + _nestedChildLists: Map = new Map(); + _offsetFromParentVirtualizedList: number = 0; + _prevParentOffset: number = 0; + _scrollMetrics = { + contentLength: 0, + dOffset: 0, + dt: 10, + offset: 0, + timestamp: 0, + velocity: 0, + visibleLength: 0 + }; + _scrollRef = (null: any); + _sentEndForContentLength = 0; + _totalCellLength = 0; + _totalCellsMeasured = 0; + _updateCellsToRenderBatcher: Batchinator; + _viewabilityTuples: Array = []; + + _captureScrollRef = ref => { + this._scrollRef = ref; + }; + + _computeBlankness() { + this._fillRateHelper.computeBlankness(this.props, this.state, this._scrollMetrics); + } + + _defaultRenderScrollComponent = props => { + if (this._isNestedWithSameOrientation()) { + return ; + } else if (props.onRefresh) { + invariant( + typeof props.refreshing === 'boolean', + '`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' + + JSON.stringify(props.refreshing) + + '`' + ); + return ( + =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. */ + + } + /> + ); + } else { + return ; + } + }; + + _onCellLayout(e, cellKey, index) { + const layout = e.nativeEvent.layout; + const next = { + offset: this._selectOffset(layout), + length: this._selectLength(layout), + index, + inLayout: true + }; + const curr = this._frames[cellKey]; + if ( + !curr || + next.offset !== curr.offset || + next.length !== curr.length || + index !== curr.index + ) { + this._totalCellLength += next.length - (curr ? curr.length : 0); + this._totalCellsMeasured += curr ? 0 : 1; + this._averageCellLength = this._totalCellLength / this._totalCellsMeasured; + this._frames[cellKey] = next; + this._highestMeasuredFrameIndex = Math.max(this._highestMeasuredFrameIndex, index); + this._scheduleCellsToRenderUpdate(); + } else { + this._frames[cellKey].inLayout = true; + } + this._computeBlankness(); + } + + _onCellUnmount = (cellKey: string) => { + const curr = this._frames[cellKey]; + if (curr) { + this._frames[cellKey] = { ...curr, inLayout: false }; + } + }; + + _measureLayoutRelativeToContainingList(): void { + UIManager.measureLayout( + ReactNative.findNodeHandle(this), + ReactNative.findNodeHandle(this.context.virtualizedList.getOutermostParentListRef()), + error => { + console.warn( + "VirtualizedList: Encountered an error while measuring a list's" + + ' offset from its containing VirtualizedList.' + ); + }, + (x, y, width, height) => { + this._offsetFromParentVirtualizedList = this._selectOffset({ x, y }); + this._scrollMetrics.contentLength = this._selectLength({ width, height }); + + const scrollMetrics = this._convertParentScrollMetrics( + this.context.virtualizedList.getScrollMetrics() + ); + this._scrollMetrics.visibleLength = scrollMetrics.visibleLength; + this._scrollMetrics.offset = scrollMetrics.offset; + } + ); + } + + _onLayout = (e: Object) => { + if (this._isNestedWithSameOrientation()) { + // Need to adjust our scroll metrics to be relative to our containing + // VirtualizedList before we can make claims about list item viewability + this._measureLayoutRelativeToContainingList(); + } else { + this._scrollMetrics.visibleLength = this._selectLength(e.nativeEvent.layout); + } + this.props.onLayout && this.props.onLayout(e); + this._scheduleCellsToRenderUpdate(); + this._maybeCallOnEndReached(); + }; + + _onLayoutEmpty = e => { + this.props.onLayout && this.props.onLayout(e); + }; + + _onLayoutFooter = e => { + this._footerLength = this._selectLength(e.nativeEvent.layout); + }; + + _onLayoutHeader = e => { + this._headerLength = this._selectLength(e.nativeEvent.layout); + }; + + _renderDebugOverlay() { + const normalize = this._scrollMetrics.visibleLength / this._scrollMetrics.contentLength; + const framesInLayout = []; + const itemCount = this.props.getItemCount(this.props.data); + for (let ii = 0; ii < itemCount; ii++) { + const frame = this._getFrameMetricsApprox(ii); + if (frame.inLayout) { + framesInLayout.push(frame); + } + } + const windowTop = this._getFrameMetricsApprox(this.state.first).offset; + const frameLast = this._getFrameMetricsApprox(this.state.last); + const windowLen = frameLast.offset + frameLast.length - windowTop; + const visTop = this._scrollMetrics.offset; + const visLen = this._scrollMetrics.visibleLength; + const baseStyle = { position: 'absolute', top: 0, right: 0 }; + return ( + + {framesInLayout.map((f, ii) => ( + + ))} + + + + ); + } + + _selectLength(metrics: { height: number, width: number }): number { + return !this.props.horizontal ? metrics.height : metrics.width; + } + + _selectOffset(metrics: { x: number, y: number }): number { + return !this.props.horizontal ? metrics.y : metrics.x; + } + + _maybeCallOnEndReached() { + const { data, getItemCount, onEndReached, onEndReachedThreshold } = this.props; + const { contentLength, visibleLength, offset } = this._scrollMetrics; + const distanceFromEnd = contentLength - visibleLength - offset; + 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) + ) { + // Only call onEndReached once for a given dataset + content length. + this._hasDataChangedSinceEndReached = false; + this._sentEndForContentLength = this._scrollMetrics.contentLength; + onEndReached({ distanceFromEnd }); + } + } + + _onContentSizeChange = (width: number, height: number) => { + if ( + width > 0 && + height > 0 && + this.props.initialScrollIndex != null && + this.props.initialScrollIndex > 0 && + !this._hasDoneInitialScroll + ) { + this.scrollToIndex({ + animated: false, + index: this.props.initialScrollIndex + }); + this._hasDoneInitialScroll = true; + } + if (this.props.onContentSizeChange) { + this.props.onContentSizeChange(width, height); + } + this._scrollMetrics.contentLength = this._selectLength({ height, width }); + this._scheduleCellsToRenderUpdate(); + this._maybeCallOnEndReached(); + }; + + /* Translates metrics from a scroll event in a parent VirtualizedList into + * coordinates relative to the child list. + */ + _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; + // Child's visible length is the same as its parent's + const visibleLength = metrics.visibleLength; + const dOffset = offset - this._scrollMetrics.offset; + const contentLength = this._scrollMetrics.contentLength; + + return { + visibleLength, + contentLength, + offset, + dOffset + }; + }; + + _onScroll = (e: Object) => { + this._nestedChildLists.forEach(childList => { + childList.ref && childList.ref._onScroll(e); + }); + if (this.props.onScroll) { + this.props.onScroll(e); + } + const timestamp = e.timeStamp; + let visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement); + let contentLength = this._selectLength(e.nativeEvent.contentSize); + let offset = this._selectOffset(e.nativeEvent.contentOffset); + let dOffset = offset - this._scrollMetrics.offset; + + if (this._isNestedWithSameOrientation()) { + if (this._scrollMetrics.contentLength === 0) { + // Ignore scroll events until onLayout has been called and we + // know our offset from our offset from our parent + return; + } + ({ visibleLength, contentLength, offset, dOffset } = this._convertParentScrollMetrics({ + visibleLength, + offset + })); + } + + const dt = this._scrollMetrics.timestamp + ? Math.max(1, timestamp - this._scrollMetrics.timestamp) + : 1; + const velocity = dOffset / dt; + + if ( + dt > 500 && + this._scrollMetrics.dt > 500 && + contentLength > 5 * visibleLength && + !this._hasWarned.perf + ) { + infoLog( + 'VirtualizedList: You have a large list that is slow to update - make sure your ' + + 'renderItem function renders components that follow React performance best practices ' + + 'like PureComponent, shouldComponentUpdate, etc.', + { dt, prevDt: this._scrollMetrics.dt, contentLength } + ); + this._hasWarned.perf = true; + } + this._scrollMetrics = { + contentLength, + dt, + dOffset, + offset, + timestamp, + velocity, + visibleLength + }; + this._updateViewableItems(this.props.data); + if (!this.props) { + return; + } + this._maybeCallOnEndReached(); + if (velocity !== 0) { + this._fillRateHelper.activate(); + } + this._computeBlankness(); + this._scheduleCellsToRenderUpdate(); + }; + + _scheduleCellsToRenderUpdate() { + const { first, last } = this.state; + const { offset, visibleLength, velocity } = this._scrollMetrics; + const itemCount = this.props.getItemCount(this.props.data); + let hiPri = false; + if (first > 0 || last < itemCount - 1) { + const distTop = offset - this._getFrameMetricsApprox(first).offset; + const distBottom = this._getFrameMetricsApprox(last).offset - (offset + visibleLength); + const scrollingThreshold = + /* $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.props.onEndReachedThreshold * visibleLength / 2; + hiPri = + Math.min(distTop, distBottom) < 0 || + (velocity < -2 && distTop < scrollingThreshold) || + (velocity > 2 && distBottom < scrollingThreshold); + } + // Only trigger high-priority updates if we've actually rendered cells, + // and with that size estimate, accurately compute how many cells we should render. + // Otherwise, it would just render as many cells as it can (of zero dimension), + // each time through attempting to render more (limited by maxToRenderPerBatch), + // starving the renderer from actually laying out the objects and computing _averageCellLength. + if (hiPri && this._averageCellLength) { + // Don't worry about interactions when scrolling quickly; focus on filling content as fast + // as possible. + this._updateCellsToRenderBatcher.dispose({ abort: true }); + this._updateCellsToRender(); + return; + } else { + this._updateCellsToRenderBatcher.schedule(); + } + } + + _onScrollBeginDrag = (e): void => { + this._nestedChildLists.forEach(childList => { + childList.ref && childList.ref._onScrollBeginDrag(e); + }); + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.recordInteraction(); + }); + this._hasInteracted = true; + this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); + }; + + _onScrollEndDrag = (e): void => { + const { velocity } = e.nativeEvent; + if (velocity) { + this._scrollMetrics.velocity = this._selectOffset(velocity); + } + this._computeBlankness(); + this.props.onScrollEndDrag && this.props.onScrollEndDrag(e); + }; + + _onMomentumScrollEnd = (e): void => { + this._scrollMetrics.velocity = 0; + this._computeBlankness(); + this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e); + }; + + _updateCellsToRender = () => { + const { data, getItemCount, onEndReachedThreshold } = this.props; + const isVirtualizationDisabled = this._isVirtualizationDisabled(); + this._updateViewableItems(data); + if (!data) { + return; + } + this.setState(state => { + let newState; + 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 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, + // we will trust the initialScrollIndex suggestion. + if (!this.props.initialScrollIndex || this._scrollMetrics.offset) { + newState = computeWindowedRenderLimits( + this.props, + state, + this._getFrameMetricsApprox, + this._scrollMetrics + ); + } + } + } 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 + * an error found when Flow v0.63 was deployed. To see the error + * delete this comment and run Flow. */ + distanceFromEnd < onEndReachedThreshold * visibleLength + ? this.props.maxToRenderPerBatch + : 0; + newState = { + first: 0, + last: Math.min(state.last + renderAhead, getItemCount(data) - 1) + }; + } + if (newState && this._nestedChildLists.size > 0) { + const newFirst = newState.first; + const newLast = newState.last; + // If some cell in the new state has a child list in it, we should only render + // up through that item, so that we give that list a chance to render. + // Otherwise there's churn from multiple child lists mounting and un-mounting + // their items. + for (let ii = newFirst; ii <= newLast; ii++) { + const cellKeyForIndex = this._indicesToKeys.get(ii); + const childListKeys = + cellKeyForIndex && this._cellKeysToChildListKeys.get(cellKeyForIndex); + if (!childListKeys) { + continue; + } + let someChildHasMore = false; + // For each cell, need to check whether any child list in it has more elements to render + for (let childKey of childListKeys) { + const childList = this._nestedChildLists.get(childKey); + if (childList && childList.ref && childList.ref.hasMore()) { + someChildHasMore = true; + break; + } + } + if (someChildHasMore) { + newState.last = ii; + break; + } + } + } + return newState; + }); + }; + + _createViewToken = (index: number, isViewable: boolean) => { + const { data, getItem, keyExtractor } = this.props; + const item = getItem(data, index); + return { index, item, key: keyExtractor(item, index), isViewable }; + }; + + _getFrameMetricsApprox = (index: number): { length: number, offset: number } => { + const frame = this._getFrameMetrics(index); + if (frame && frame.index === index) { + // check for invalid frames due to row re-ordering + return frame; + } else { + const { getItemLayout } = this.props; + invariant( + !getItemLayout, + 'Should not have to estimate frames when a measurement metrics function is provided' + ); + return { + length: this._averageCellLength, + offset: this._averageCellLength * index + }; + } + }; + + _getFrameMetrics = ( + index: number + ): ?{ + length: number, + offset: number, + index: number, + inLayout?: boolean + } => { + const { data, getItem, getItemCount, getItemLayout, keyExtractor } = this.props; + invariant(getItemCount(data) > index, 'Tried to get frame for out of range index ' + index); + const item = getItem(data, index); + let frame = item && this._frames[keyExtractor(item, index)]; + 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 + * error found when Flow v0.63 was deployed. To see the error delete this + * comment and run Flow. */ + return frame; + }; + + _updateViewableItems(data: any) { + const { getItemCount } = this.props; + + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + getItemCount(data), + this._scrollMetrics.offset, + this._scrollMetrics.visibleLength, + this._getFrameMetrics, + this._createViewToken, + tuple.onViewableItemsChanged, + this.state + ); + }); + } +} + +class CellRenderer extends React.Component< + { + CellRendererComponent?: ?React.ComponentType, + ItemSeparatorComponent: ?React.ComponentType<*>, + cellKey: string, + fillRateHelper: FillRateHelper, + horizontal: ?boolean, + index: number, + inversionStyle: ?StyleObj, + 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 + }, + prevCellKey: ?string + }, + $FlowFixMeState +> { + state = { + separatorProps: { + highlighted: false, + leadingItem: this.props.item + } + }; + + static childContextTypes = { + virtualizedCell: PropTypes.shape({ + cellKey: PropTypes.string + }) + }; + + getChildContext() { + return { + virtualizedCell: { + cellKey: this.props.cellKey + } + }; + } + + // TODO: consider factoring separator stuff out of VirtualizedList into FlatList since it's not + // reused by SectionList and we can keep VirtualizedList simpler. + _separators = { + highlight: () => { + const { cellKey, prevCellKey } = this.props; + this.props.onUpdateSeparators([cellKey, prevCellKey], { + highlighted: true + }); + }, + unhighlight: () => { + const { cellKey, prevCellKey } = this.props; + this.props.onUpdateSeparators([cellKey, prevCellKey], { + highlighted: false + }); + }, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => { + const { cellKey, prevCellKey } = this.props; + this.props.onUpdateSeparators([select === 'leading' ? prevCellKey : cellKey], newProps); + } + }; + + updateSeparatorProps(newProps: Object) { + this.setState(state => ({ + separatorProps: { ...state.separatorProps, ...newProps } + })); + } + + componentWillUnmount() { + this.props.onUnmount(this.props.cellKey); + } + + render() { + const { + CellRendererComponent, + ItemSeparatorComponent, + fillRateHelper, + horizontal, + item, + index, + inversionStyle, + parentProps + } = this.props; + const { renderItem, getItemLayout } = parentProps; + invariant(renderItem, 'no renderItem!'); + const element = renderItem({ + item, + index, + separators: this._separators + }); + const onLayout = + getItemLayout && !parentProps.debug && !fillRateHelper.enabled() + ? undefined + : this.props.onLayout; + // NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and + // called explicitly by `ScrollViewStickyHeader`. + const itemSeparator = ItemSeparatorComponent && ( + + ); + const cellStyle = inversionStyle + ? horizontal + ? [{ flexDirection: 'row-reverse' }, inversionStyle] + : [{ flexDirection: 'column-reverse' }, inversionStyle] + : horizontal ? [{ flexDirection: 'row' }, inversionStyle] : inversionStyle; + if (!CellRendererComponent) { + return ( + + {element} + {itemSeparator} + + ); + } + return ( + + {element} + {itemSeparator} + + ); + } +} + +class VirtualizedCellWrapper extends React.Component<{ + cellKey: string, + children: React.Node +}> { + static childContextTypes = { + virtualizedCell: PropTypes.shape({ + cellKey: PropTypes.string + }) + }; + + getChildContext() { + return { + virtualizedCell: { + cellKey: this.props.cellKey + } + }; + } + + render() { + return this.props.children; + } +} + +const styles = StyleSheet.create({ + verticallyInverted: { + transform: [{ scaleY: -1 }] + }, + horizontallyInverted: { + transform: [{ scaleX: -1 }] + } +}); + +module.exports = VirtualizedList; diff --git a/packages/react-native-web/src/vendor/infoLog/index.js b/packages/react-native-web/src/vendor/infoLog/index.js new file mode 100644 index 00000000..66278f52 --- /dev/null +++ b/packages/react-native-web/src/vendor/infoLog/index.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule infoLog + */ +'use strict'; + +/** + * Intentional info-level logging for clear separation from ad-hoc console debug logging. + */ +function infoLog(...args) { + return console.log(...args); +} + +module.exports = infoLog;