diff --git a/package.json b/package.json index 72d265c1..168fac14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-window", - "version": "1.8.5", + "version": "1.9.0-alpha.1", "description": "React components for efficiently rendering large, scrollable lists and tabular data", "author": diff --git a/rollup.config.js b/rollup.config.js index b9421aa9..51bd44a7 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -19,6 +19,7 @@ const babelConfigEsModules = babel({ const umdGlobals = { react: 'React', + 'react-dom': 'ReactDOM', }; export default [ diff --git a/src/DynamicSizeList.js b/src/DynamicSizeList.js new file mode 100644 index 00000000..9eb2f9df --- /dev/null +++ b/src/DynamicSizeList.js @@ -0,0 +1,493 @@ +// @flow + +import { createElement } from 'react'; + +import createListComponent, { defaultItemKey } from './createListComponent'; +import ItemMeasurer from './ItemMeasurer'; + +import type { Props, ScrollToAlign } from './createListComponent'; + +const DEFAULT_ESTIMATED_ITEM_SIZE = 50; + +type DynanmicProps = {| + estimatedItemSize: number, + ...Props, +|}; + +export type HandleNewMeasurements = ( + index: number, + newSize: number, + isFirstMeasureAfterMounting: boolean +) => void; + +type ItemMetadata = {| + offset: number, + size: number, +|}; +type InstanceProps = {| + estimatedItemSize: number, + instance: any, + itemOffsetMap: { [index: number]: number }, + itemSizeMap: { [index: number]: number }, + lastMeasuredIndex: number, + lastPositionedIndex: number, + totalMeasuredSize: number, +|}; + +const getItemMetadata = ( + props: Props, + index: number, + instanceProps: InstanceProps +): ItemMetadata => { + const { + estimatedItemSize, + instance, + itemOffsetMap, + itemSizeMap, + lastMeasuredIndex, + lastPositionedIndex, + } = instanceProps; + + // If the specified item has not yet been measured, + // Just return an estimated size for now. + if (index > lastMeasuredIndex) { + return { + offset: 0, + size: estimatedItemSize, + }; + } + + // Lazily update positions if they are stale. + if (index > lastPositionedIndex) { + if (lastPositionedIndex < 0) { + itemOffsetMap[0] = 0; + } + + for (let i = Math.max(1, lastPositionedIndex + 1); i <= index; i++) { + const prevOffset = itemOffsetMap[i - 1]; + + // In some browsers (e.g. Firefox) fast scrolling may skip rows. + // In this case, our assumptions about last measured indices may be incorrect. + // Handle this edge case to prevent NaN values from breaking styles. + // Slow scrolling back over these skipped rows will adjust their sizes. + const prevSize = itemSizeMap[i - 1] || 0; + + itemOffsetMap[i] = prevOffset + prevSize; + + // Reset cached style to clear stale position. + delete instance._itemStyleCache[i]; + } + + instanceProps.lastPositionedIndex = index; + } + + let offset = itemOffsetMap[index]; + let size = itemSizeMap[index]; + + return { offset, size }; +}; + +const findNearestItemBinarySearch = ( + props: Props, + instanceProps: InstanceProps, + high: number, + low: number, + offset: number +): number => { + while (low <= high) { + const middle = low + Math.floor((high - low) / 2); + const currentOffset = getItemMetadata(props, middle, instanceProps).offset; + + if (currentOffset === offset) { + return middle; + } else if (currentOffset < offset) { + low = middle + 1; + } else if (currentOffset > offset) { + high = middle - 1; + } + } + + if (low > 0) { + return low - 1; + } else { + return 0; + } +}; + +const getEstimatedTotalSize = ( + { itemCount }: Props, + { + itemSizeMap, + estimatedItemSize, + lastMeasuredIndex, + totalMeasuredSize, + }: InstanceProps +) => + totalMeasuredSize + (itemCount - lastMeasuredIndex - 1) * estimatedItemSize; + +const DynamicSizeList = createListComponent({ + getItemOffset: ( + props: Props, + index: number, + instanceProps: InstanceProps + ): number => getItemMetadata(props, index, instanceProps).offset, + + getItemSize: ( + props: Props, + index: number, + instanceProps: InstanceProps + ): ?number => { + // Do not hard-code item dimensions. + // We don't know them initially. + // Even once we do, changes in item content or list size should reflow. + return undefined; + }, + + getEstimatedTotalSize, + + getOffsetForIndexAndAlignment: ( + props: Props, + index: number, + align: ScrollToAlign, + scrollOffset: number, + instanceProps: InstanceProps + ): number => { + const { direction, layout, height, width } = props; + + if (process.env.NODE_ENV !== 'production') { + const { lastMeasuredIndex } = instanceProps; + if (index > lastMeasuredIndex) { + console.warn( + `DynamicSizeList does not support scrolling to items that yave not yet measured. ` + + `scrollToItem() was called with index ${index} but the last measured item was ${lastMeasuredIndex}.` + ); + } + } + + const size = (((direction === 'horizontal' || layout === 'horizontal' + ? width + : height): any): number); + const itemMetadata = getItemMetadata(props, index, instanceProps); + + // Get estimated total size after ItemMetadata is computed, + // To ensure it reflects actual measurements instead of just estimates. + const estimatedTotalSize = getEstimatedTotalSize(props, instanceProps); + + const maxOffset = Math.min(estimatedTotalSize - size, itemMetadata.offset); + const minOffset = Math.max( + 0, + itemMetadata.offset - size + itemMetadata.size + ); + + switch (align) { + case 'start': + return maxOffset; + case 'end': + return minOffset; + case 'center': + return Math.round(minOffset + (maxOffset - minOffset) / 2); + case 'auto': + default: + if (scrollOffset >= minOffset && scrollOffset <= maxOffset) { + return scrollOffset; + } else if (scrollOffset - minOffset < maxOffset - scrollOffset) { + return minOffset; + } else { + return maxOffset; + } + } + }, + + getStartIndexForOffset: ( + props: Props, + offset: number, + instanceProps: InstanceProps + ): number => { + const { lastMeasuredIndex, totalMeasuredSize } = instanceProps; + + // If we've already positioned and measured past this point, + // Use a binary search to find the closets cell. + if (offset <= totalMeasuredSize) { + return findNearestItemBinarySearch( + props, + instanceProps, + lastMeasuredIndex, + 0, + offset + ); + } + + // Otherwise render a new batch of items starting from where we left off. + return lastMeasuredIndex + 1; + }, + + getStopIndexForStartIndex: ( + props: Props, + startIndex: number, + scrollOffset: number, + instanceProps: InstanceProps + ): number => { + const { direction, layout, height, itemCount, width } = props; + + const size = (((direction === 'horizontal' || layout === 'horizontal' + ? width + : height): any): number); + const itemMetadata = getItemMetadata(props, startIndex, instanceProps); + const maxOffset = scrollOffset + size; + + let offset = itemMetadata.offset + itemMetadata.size; + let stopIndex = startIndex; + + while (stopIndex < itemCount - 1 && offset < maxOffset) { + stopIndex++; + offset += getItemMetadata(props, stopIndex, instanceProps).size; + } + + return stopIndex; + }, + + initInstanceProps(props: Props, instance: any): InstanceProps { + const { estimatedItemSize } = ((props: any): DynanmicProps); + + const instanceProps = { + estimatedItemSize: estimatedItemSize || DEFAULT_ESTIMATED_ITEM_SIZE, + instance, + itemOffsetMap: {}, + itemSizeMap: {}, + lastMeasuredIndex: -1, + lastPositionedIndex: -1, + totalMeasuredSize: 0, + }; + + let debounceForceUpdateID = null; + const debounceForceUpdate = () => { + if (debounceForceUpdateID === null) { + debounceForceUpdateID = setTimeout(() => { + debounceForceUpdateID = null; + instance.forceUpdate(); + }, 1); + } + }; + + // This method is called before unmounting. + instance._unmountHook = () => { + if (debounceForceUpdateID !== null) { + clearTimeout(debounceForceUpdateID); + debounceForceUpdateID = null; + } + }; + + let hasNewMeasurements: boolean = false; + let sizeDeltaTotal = 0; + + // This method is called after mount and update. + instance._commitHook = () => { + if (hasNewMeasurements) { + hasNewMeasurements = false; + + // Edge case where cell sizes changed, but cancelled each other out. + // We still need to re-render in this case, + // Even though we don't need to adjust scroll offset. + if (sizeDeltaTotal === 0) { + instance.forceUpdate(); + return; + } + + let shouldForceUpdate; + + // In the setState commit hook, we'll decrement sizeDeltaTotal. + // In case the state update is processed synchronously, + // And triggers additional size updates itself, + // We should only drecement by the amount we updated state for originally. + const sizeDeltaForStateUpdate = sizeDeltaTotal; + + // If the user is scrolling up, we need to adjust the scroll offset, + // To prevent items from "jumping" as items before them have been resized. + instance.setState( + prevState => { + if ( + prevState.scrollDirection === 'backward' && + !prevState.scrollUpdateWasRequested + ) { + // TRICKY + // If item(s) have changed size since they were last displayed, content will appear to jump. + // To avoid this, we need to make small adjustments as a user scrolls to preserve apparent position. + // This also ensures that the first item eventually aligns with scroll offset 0. + return { + scrollOffset: prevState.scrollOffset + sizeDeltaForStateUpdate, + }; + } else { + // There's no state to update, + // But we still want to re-render in this case. + shouldForceUpdate = true; + + return null; + } + }, + () => { + if (shouldForceUpdate) { + instance.forceUpdate(); + } else { + const { scrollOffset } = instance.state; + const { direction, layout } = instance.props; + + // Adjusting scroll offset directly interrupts smooth scrolling for some browsers (e.g. Firefox). + // The relative scrollBy() method doesn't interrupt (or at least it won't as of Firefox v65). + // Other browsers (e.g. Chrome, Safari) seem to handle both adjustments equally well. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1502059 + const element = ((instance._outerRef: any): HTMLDivElement); + // $FlowFixMe Property scrollBy is missing in HTMLDivElement + if (typeof element.scrollBy === 'function') { + element.scrollBy( + direction === 'horizontal' || layout === 'horizontal' + ? sizeDeltaForStateUpdate + : 0, + direction === 'horizontal' || layout === 'horizontal' + ? 0 + : sizeDeltaForStateUpdate + ); + } else if ( + direction === 'horizontal' || + layout === 'horizontal' + ) { + element.scrollLeft = scrollOffset; + } else { + element.scrollTop = scrollOffset; + } + } + + sizeDeltaTotal -= sizeDeltaForStateUpdate; + } + ); + } + }; + + // This function may be called out of order! + // It is not safe to reposition items here. + // Be careful when comparing index and lastMeasuredIndex. + const handleNewMeasurements: HandleNewMeasurements = ( + index: number, + newSize: number, + isFirstMeasureAfterMounting: boolean + ) => { + const { + itemSizeMap, + lastMeasuredIndex, + lastPositionedIndex, + } = instanceProps; + + // In some browsers (e.g. Firefox) fast scrolling may skip rows. + // In this case, our assumptions about last measured indices may be incorrect. + // Handle this edge case to prevent NaN values from breaking styles. + // Slow scrolling back over these skipped rows will adjust their sizes. + const oldSize = itemSizeMap[index] || 0; + + // Mark offsets after this as stale so that getItemMetadata() will lazily recalculate it. + if (index < lastPositionedIndex) { + instanceProps.lastPositionedIndex = index; + } + + if (index <= lastMeasuredIndex) { + if (oldSize === newSize) { + return; + } + + // Adjust total size estimate by the delta in size. + instanceProps.totalMeasuredSize += newSize - oldSize; + + // Record the size delta here in case the user is scrolling up. + // In that event, we need to adjust the scroll offset by thie amount, + // To prevent items from "jumping" as items before them are resized. + // We only do this for items that are newly measured (after mounting). + // Ones that change size later do not need to affect scroll offset. + if (isFirstMeasureAfterMounting) { + sizeDeltaTotal += newSize - oldSize; + } + } else { + instanceProps.lastMeasuredIndex = index; + instanceProps.totalMeasuredSize += newSize; + } + + itemSizeMap[index] = newSize; + + // Even though the size has changed, we don't need to reset the cached style, + // Because dynamic list items don't have constrained sizes. + // This enables them to resize when their content (or container size) changes. + // It also lets us avoid an unnecessary render in this case. + + if (isFirstMeasureAfterMounting) { + hasNewMeasurements = true; + } else { + debounceForceUpdate(); + } + }; + instance._handleNewMeasurements = handleNewMeasurements; + + // Override the item-rendering process to wrap items with ItemMeasurer. + // This keep the external API simpler. + instance._renderItems = () => { + const { + children, + direction, + layout, + itemCount, + itemData, + itemKey = defaultItemKey, + useIsScrolling, + } = instance.props; + const { isScrolling } = instance.state; + + const [startIndex, stopIndex] = instance._getRangeToRender(); + + const items = []; + if (itemCount > 0) { + for (let index = startIndex; index <= stopIndex; index++) { + const { size } = getItemMetadata( + instance.props, + index, + instanceProps + ); + + // It's important to read style after fetching item metadata. + // getItemMetadata() will clear stale styles. + const style = instance._getItemStyle(index); + + const item = createElement(children, { + data: itemData, + index, + isScrolling: useIsScrolling ? isScrolling : undefined, + style, + }); + + // Always wrap children in a ItemMeasurer to detect changes in size. + items.push( + createElement(ItemMeasurer, { + direction, + layout, + handleNewMeasurements, + index, + item, + key: itemKey(index, itemData), + size, + }) + ); + } + } + return items; + }; + + return instanceProps; + }, + + shouldResetStyleCacheOnItemSizeChange: false, + + validateProps: ({ itemSize }: Props): void => { + if (process.env.NODE_ENV !== 'production') { + if (itemSize !== undefined) { + throw Error('An unexpected "itemSize" prop has been provided.'); + } + } + }, +}); + +export default DynamicSizeList; diff --git a/src/ItemMeasurer.js b/src/ItemMeasurer.js new file mode 100644 index 00000000..843fd6bb --- /dev/null +++ b/src/ItemMeasurer.js @@ -0,0 +1,159 @@ +// @flow + +import { cloneElement, Component } from 'react'; +import { findDOMNode } from 'react-dom'; + +import type { Direction, Layout } from './createListComponent'; +import type { HandleNewMeasurements } from './DynamicSizeList'; + +class DOMRectReadOnly { + +x: number; + +y: number; + +width: number; + +height: number; + +top: number; + +right: number; + +bottom: number; + +left: number; +} + +class ResizeObserverEntry { + +target: HTMLElement; + +contentRect: DOMRectReadOnly; +} + +type Entries = $ReadOnlyArray; + +type ResizeObserverCallback = { + // eslint-disable-next-line no-use-before-define + (entries: Entries, observer: ResizeObserver): void, +}; + +declare class ResizeObserver { + constructor(ResizeObserverCallback): ResizeObserver; + observe(target: HTMLElement): void; + unobserve(target: HTMLElement): void; + disconnect(): void; +} + +type ItemMeasurerProps = {| + direction: Direction, + layout: Layout, + handleNewMeasurements: HandleNewMeasurements, + index: number, + item: React$Element, + size: number, +|}; + +let findDOMNodeWarningsSet = ((null: any): Set); +if (process.env.NODE_ENV !== 'production') { + findDOMNodeWarningsSet = new Set(); +} + +export default class ItemMeasurer extends Component { + _didProvideValidRef: boolean = false; + _node: HTMLElement | null = null; + _resizeObserver: ResizeObserver | null = null; + + componentDidMount() { + if (process.env.NODE_ENV !== 'production') { + if (!this._didProvideValidRef) { + const { item } = this.props; + + const displayName = + item && item.type + ? item.type.displayName || item.type.name || '(unknown)' + : '(unknown)'; + + if (!findDOMNodeWarningsSet.has(displayName)) { + findDOMNodeWarningsSet.add(displayName); + + console.warn( + 'DynamicSizeList item renderers should attach a ref to the topmost HTMLElement they render. ' + + `The item renderer "${displayName}" did not attach a ref to a valid HTMLElement. ` + + 'findDOMNode() will be used as a fallback, but is slower and more error prone than using a ref.\n\n' + + 'Learn more about ref forwarding: ' + + 'https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-to-dom-components' + ); + } + } + } + + // Force sync measure for the initial mount. + // This is necessary to support the DynamicSizeList layout logic. + this._measureItem(true, true); + + if (typeof ResizeObserver !== 'undefined') { + // Watch for resizes due to changed content, + // Or changes in the size of the parent container. + this._resizeObserver = new ResizeObserver(this._onResize); + if (this._node !== null) { + this._resizeObserver.observe(this._node); + } + } + } + + componentWillUnmount() { + if (this._resizeObserver !== null) { + this._resizeObserver.disconnect(); + this._resizeObserver = null; + } + } + + render() { + return cloneElement(this.props.item, { + ref: this._refSetter, + }); + } + + _measureItem = (isCommitPhase: boolean, isMount: boolean) => { + const { + direction, + layout, + handleNewMeasurements, + index, + size: oldSize, + } = this.props; + + const node = this._node; + + if ( + node && + node.ownerDocument && + node.ownerDocument.defaultView && + node instanceof node.ownerDocument.defaultView.HTMLElement + ) { + const newSize = + direction === 'horizontal' || layout === 'horizontal' + ? Math.ceil(node.offsetWidth) + : Math.ceil(node.offsetHeight); + + if (oldSize !== newSize || isMount) { + handleNewMeasurements(index, newSize, isCommitPhase); + } + } + }; + + _refSetter = (ref: any) => { + if (this._resizeObserver !== null && this._node !== null) { + this._resizeObserver.unobserve(this._node); + } + + if (ref instanceof HTMLElement) { + this._didProvideValidRef = true; + this._node = ref; + } else if (ref) { + this._node = ((findDOMNode(ref): any): HTMLElement); + } else { + this._node = null; + } + + if (this._resizeObserver !== null && this._node !== null) { + this._resizeObserver.observe(this._node); + } + }; + + _onResize = () => { + this._measureItem(false, false); + }; +} diff --git a/src/__tests__/DynamicSizeList.js b/src/__tests__/DynamicSizeList.js new file mode 100644 index 00000000..98bc0bb1 --- /dev/null +++ b/src/__tests__/DynamicSizeList.js @@ -0,0 +1,142 @@ +import React, { + createRef, + forwardRef, + PureComponent, + unstable_Profiler as Profiler, +} from 'react'; +import { findDOMNode, render } from 'react-dom'; +import { DynamicSizeList } from '..'; + +describe('DynamicSizeList', () => { + let container, + defaultProps, + innerRef, + itemRenderer, + itemSizes, + onItemsRendered; + + // Use PureComponent to test memoization. + // Pass through to itemRenderer mock for easier test assertions. + class PureItemRenderer extends PureComponent { + componentDidMount() { + const { index } = this.props; + + const itemSize = itemSizes[index % itemSizes.length]; + + // Hack around the fact that JSDom doesn't support measurements. + const node = findDOMNode(this); + Object.defineProperty(node, 'offsetHeight', { + configurable: true, + get: () => itemSize, + }); + Object.defineProperty(node, 'offsetWidth', { + configurable: true, + get: () => itemSize, + }); + } + + render() { + return itemRenderer(this.props); + } + } + + const RefForwarder = forwardRef((props, ref) => ( + + )); + + beforeEach(() => { + jest.useFakeTimers(); + + container = document.createElement('div'); + + itemSizes = [20, 25, 30, 35, 40]; + itemRenderer = jest.fn(({ forwardedRef, style, ...rest }) => ( +
+ {JSON.stringify(rest, null, 2)} +
+ )); + onItemsRendered = jest.fn(); + innerRef = createRef(); + defaultProps = { + children: RefForwarder, + estimatedItemSize: 25, + height: 100, + innerRef, + itemCount: 20, + onItemsRendered, + overscanCount: 1, + width: 50, + }; + }); + + // Much of the shared List functionality is already tested by FixedSizeList tests. + // This test covers functionality that is unique to DynamicSizeList. + + it('should measure and position items after mounting', () => { + const onRender = jest.fn(() => { + switch (onRender.mock.calls.length) { + case 1: + // Initial render uses estimatedItemSize for scrollHeight. + expect(innerRef.current.style.height).toBe('500px'); + + // Given estimatedItemSize and overscanCount, we expect to render 5 items. + expect(innerRef.current.children).toHaveLength(5); + break; + case 2: + // Second render should adjust scrollHeight for newly measured items. + expect(innerRef.current.style.height).toBe('525px'); + + // Newly measured items should be positioned correctly. + Array.from(innerRef.current.children).forEach((node, index) => { + expect(node.style.top).toBe(`${index * 30}px`); + }); + break; + default: + throw Error('Unexpected render'); + } + }); + + itemSizes = [30]; + + render( + + + , + container + ); + + expect(onRender.mock.calls).toHaveLength(2); + }); + + describe('ref forwarding', () => { + it('should warn if ref is not forwarded', () => { + class ItemRenderer extends PureComponent { + render() { + const { index, style } = this.props; + return
{index}
; + } + } + + console.warn = jest.fn(); + render( + + {ItemRenderer} + , + container + ); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn.mock.calls[0][0]).toContain( + 'The item renderer "ItemRenderer" did not attach a ref' + ); + + // It should only warn once per item renderer type to avoid spamming the console. + render( + + {ItemRenderer} + , + container + ); + expect(console.warn).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/createGridComponent.js b/src/createGridComponent.js index f2895b60..e56e4612 100644 --- a/src/createGridComponent.js +++ b/src/createGridComponent.js @@ -753,6 +753,7 @@ export default function createGridComponent({ // So the simplest solution is to determine which browser behavior we're dealing with, and convert based on it. let calculatedScrollLeft = scrollLeft; if (direction === 'rtl') { + // eslint-disable-next-line default-case switch (getRTLOffsetType()) { case 'negative': calculatedScrollLeft = -scrollLeft; diff --git a/src/createListComponent.js b/src/createListComponent.js index ae7851e9..247ce4f8 100644 --- a/src/createListComponent.js +++ b/src/createListComponent.js @@ -11,10 +11,10 @@ export type ScrollToAlign = 'auto' | 'smart' | 'center' | 'start' | 'end'; type itemSize = number | ((index: number) => number); // TODO Deprecate directions "horizontal" and "vertical" -type Direction = 'ltr' | 'rtl' | 'horizontal' | 'vertical'; -type Layout = 'horizontal' | 'vertical'; +export type Direction = 'ltr' | 'rtl' | 'horizontal' | 'vertical'; +export type Layout = 'horizontal' | 'vertical'; -type RenderComponentProps = {| +export type RenderComponentProps = {| data: T, index: number, isScrolling?: boolean, @@ -97,7 +97,7 @@ type GetItemSize = ( props: Props, index: number, instanceProps: any -) => number; +) => ?number; type GetEstimatedTotalSize = (props: Props, instanceProps: any) => number; type GetOffsetForIndexAndAlignment = ( props: Props, @@ -122,7 +122,7 @@ type ValidateProps = (props: Props) => void; const IS_SCROLLING_DEBOUNCE_INTERVAL = 150; -const defaultItemKey = (index: number, data: any) => index; +export const defaultItemKey = (index: number, data: any) => index; // In DEV mode, this Set helps us only log a warning once per component instance. // This avoids spamming the console every time a render happens. @@ -243,6 +243,7 @@ export default function createListComponent({ } this._callPropsCallbacks(); + this._commitHook(); } componentDidUpdate() { @@ -279,31 +280,29 @@ export default function createListComponent({ } this._callPropsCallbacks(); + this._commitHook(); } componentWillUnmount() { if (this._resetIsScrollingTimeoutId !== null) { cancelTimeout(this._resetIsScrollingTimeoutId); } + + this._unmountHook(); } render() { const { - children, className, direction, height, innerRef, innerElementType, innerTagName, - itemCount, - itemData, - itemKey = defaultItemKey, layout, outerElementType, outerTagName, style, - useIsScrolling, width, } = this.props; const { isScrolling } = this.state; @@ -316,22 +315,7 @@ export default function createListComponent({ ? this._onScrollHorizontal : this._onScrollVertical; - const [startIndex, stopIndex] = this._getRangeToRender(); - - const items = []; - if (itemCount > 0) { - for (let index = startIndex; index <= stopIndex; index++) { - items.push( - createElement(children, { - data: itemData, - key: itemKey(index, itemData), - index, - isScrolling: useIsScrolling ? isScrolling : undefined, - style: this._getItemStyle(index), - }) - ); - } - } + const items = this._renderItems(); // Read this value AFTER items have been created, // So their actual sizes (if variable) are taken into consideration. @@ -347,10 +331,10 @@ export default function createListComponent({ onScroll, ref: this._outerRefSetter, style: { - position: 'relative', height, width, overflow: 'auto', + position: 'relative', WebkitOverflowScrolling: 'touch', willChange: 'transform', direction, @@ -441,6 +425,14 @@ export default function createListComponent({ } } + // This method is called after mount and update. + // List implementations can override this method to be notified. + _commitHook() {} + + // This method is called before unmounting. + // List implementations can override this method to be notified. + _unmountHook() {} + // Lazily create and cache item styles while scrolling, // So that pure component sCU will prevent re-renders. // We maintain this cache, and pass a style prop rather than index, @@ -478,8 +470,17 @@ export default function createListComponent({ return style; }; + _itemStyleCache: ItemStyleCache; + + // TODO This memoized getter doesn't make much sense. + // If all that's really needed is for the impl to be able to reset the cache, + // Then we could expose a better API for that. _getItemStyleCache: (_: any, __: any, ___: any) => ItemStyleCache; - _getItemStyleCache = memoizeOne((_: any, __: any, ___: any) => ({})); + _getItemStyleCache = memoizeOne((_: any, __: any, ___: any) => { + this._itemStyleCache = {}; + + return this._itemStyleCache; + }); _getRangeToRender(): [number, number, number, number] { const { itemCount, overscanCount } = this.props; @@ -520,6 +521,35 @@ export default function createListComponent({ ]; } + _renderItems() { + const { + children, + itemCount, + itemData, + itemKey = defaultItemKey, + useIsScrolling, + } = this.props; + const { isScrolling } = this.state; + + const [startIndex, stopIndex] = this._getRangeToRender(); + + const items = []; + if (itemCount > 0) { + for (let index = startIndex; index <= stopIndex; index++) { + items.push( + createElement(children, { + data: itemData, + key: itemKey(index, itemData), + index, + isScrolling: useIsScrolling ? isScrolling : undefined, + style: this._getItemStyle(index), + }) + ); + } + } + return items; + } + _onScrollHorizontal = (event: ScrollEvent): void => { const { clientWidth, scrollLeft, scrollWidth } = event.currentTarget; this.setState(prevState => { @@ -538,6 +568,7 @@ export default function createListComponent({ // This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left). // It's also easier for this component if we convert offsets to the same format as they would be in for ltr. // So the simplest solution is to determine which browser behavior we're dealing with, and convert based on it. + // eslint-disable-next-line default-case switch (getRTLOffsetType()) { case 'negative': scrollOffset = -scrollLeft; @@ -626,6 +657,10 @@ export default function createListComponent({ this._getItemStyleCache(-1, null); }); }; + + // Intentionally placed after all other instance properties have been initialized, + // So that DynamicSizeList can override the render behavior. + _instanceProps: any = initInstanceProps(this.props, this); }; } diff --git a/src/index.js b/src/index.js index dff39bf1..e06edcb2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,10 @@ // @flow -export { default as VariableSizeGrid } from './VariableSizeGrid'; -export { default as VariableSizeList } from './VariableSizeList'; +export { default as DynamicSizeList } from './DynamicSizeList'; export { default as FixedSizeGrid } from './FixedSizeGrid'; export { default as FixedSizeList } from './FixedSizeList'; +export { default as VariableSizeGrid } from './VariableSizeGrid'; +export { default as VariableSizeList } from './VariableSizeList'; export { default as areEqual } from './areEqual'; export { default as shouldComponentUpdate } from './shouldComponentUpdate'; diff --git a/website/now.json b/website/now.json index 45a83b12..9753a495 100644 --- a/website/now.json +++ b/website/now.json @@ -1,5 +1,5 @@ { - "name": "react-window", - "alias": "react-window", + "name": "react-window-next", + "alias": "react-window-next", "public": true } diff --git a/website/package.json b/website/package.json index edce0e75..a7ce5463 100644 --- a/website/package.json +++ b/website/package.json @@ -25,7 +25,7 @@ "babel-preset-react-app": "4.0.0-next.66cc7a90", "case-sensitive-paths-webpack-plugin": "2.1.1", "chalk": "1.1.3", - "classnames": "^2.2.5", + "classnames": "^2.2.6", "codemirror": "^5.37.0", "css-loader": "0.28.7", "dotenv": "4.0.0", @@ -42,12 +42,14 @@ "fs-extra": "3.0.1", "html-webpack-plugin": "2.29.0", "jest": "20.0.4", + "lorem-ipsum": "^1.0.6", "object-assign": "4.1.1", "postcss-flexbugs-fixes": "3.2.0", "postcss-loader": "2.0.10", "promise": "8.0.1", "prop-types": "^15.6.1", "raf": "3.4.0", + "random-words": "^1.1.0", "react": "^16.8.4", "react-codemirror2": "^5.0.1", "react-dev-utils": "^6.0.0-next.66cc7a90", @@ -58,6 +60,7 @@ "react-router-hash-link": "^1.2.0", "react-scripts": "^2.0.0-next.66cc7a90", "react-window": "link:..", + "resize-observer-polyfill": "^1.5.0", "resolve": "1.6.0", "style-loader": "0.19.0", "sw-precache-webpack-plugin": "0.11.4", diff --git a/website/sandboxes/dynamic-size-list-horizontal/index.js b/website/sandboxes/dynamic-size-list-horizontal/index.js new file mode 100644 index 00000000..3fe0620f --- /dev/null +++ b/website/sandboxes/dynamic-size-list-horizontal/index.js @@ -0,0 +1,45 @@ +import React, { Fragment } from 'react'; +import ReactDOM from 'react-dom'; +import { DynamicSizeList as List } from 'react-window'; +import loremIpsum from 'lorem-ipsum'; + +import './styles.css'; + +// Polyfill ResizeObserver for demo +if (typeof ResizeObserver === 'undefined') { + global.ResizeObserver = require('resize-observer-polyfill').default; +} + +// This example uses an array of random strings, +// But the list can also contain data that's async loaded, like images. +const items = new Array(500) + .fill(true) + .map(() => loremIpsum({ units: 'words', count: 3 })); + +const Column = ({ data, index, style }) => ( +
+ {data[index]} +
+); + +const Example = () => ( + + + {Column} + +

+ This component requires the{' '} + ResizeObserver API (or + polyfill). +

+
+); + +ReactDOM.render(, document.getElementById('root')); diff --git a/website/sandboxes/dynamic-size-list-horizontal/package.json b/website/sandboxes/dynamic-size-list-horizontal/package.json new file mode 100644 index 00000000..4575b0a6 --- /dev/null +++ b/website/sandboxes/dynamic-size-list-horizontal/package.json @@ -0,0 +1,11 @@ +{ + "description": "Demo of react-window horizontal VariableSizeList", + "main": "src/index.js", + "dependencies": { + "lorem-ipsum": "^1", + "react": "^16", + "react-dom": "^16", + "react-window": "^1", + "resize-observer-polyfill": "^1" + } +} diff --git a/website/sandboxes/dynamic-size-list-horizontal/styles.css b/website/sandboxes/dynamic-size-list-horizontal/styles.css new file mode 100644 index 00000000..9a5ae5cb --- /dev/null +++ b/website/sandboxes/dynamic-size-list-horizontal/styles.css @@ -0,0 +1,22 @@ +html { + font-family: sans-serif; + font-size: 12px; +} + +.List { + border: 1px solid #d9dddd; +} + +.ListItemEven, +.ListItemOdd { + display: flex; + align-items: center; + white-space: pre; + word-wrap: nowrap; + padding: 0.5rem; + box-sizing: border-box; +} + +.ListItemEven { + background-color: #f8f8f0; +} diff --git a/website/sandboxes/dynamic-size-list-vertical/index.js b/website/sandboxes/dynamic-size-list-vertical/index.js new file mode 100644 index 00000000..49915998 --- /dev/null +++ b/website/sandboxes/dynamic-size-list-vertical/index.js @@ -0,0 +1,44 @@ +import React, { Fragment } from 'react'; +import ReactDOM from 'react-dom'; +import { DynamicSizeList as List } from 'react-window'; +import loremIpsum from 'lorem-ipsum'; + +import './styles.css'; + +// Polyfill ResizeObserver for demo +if (typeof ResizeObserver === 'undefined') { + global.ResizeObserver = require('resize-observer-polyfill').default; +} + +// This example uses an array of random strings, +// But the list can also contain data that's async loaded, like images. +const items = new Array(500) + .fill(true) + .map(() => loremIpsum({ units: 'paragraphs' })); + +const Row = ({ data, index, style }) => ( +
+ {data[index]} +
+); + +const Example = () => ( + + + {Row} + +

+ This component requires the{' '} + ResizeObserver API (or + polyfill). +

+
+); + +ReactDOM.render(, document.getElementById('root')); diff --git a/website/sandboxes/dynamic-size-list-vertical/package.json b/website/sandboxes/dynamic-size-list-vertical/package.json new file mode 100644 index 00000000..e074cc1d --- /dev/null +++ b/website/sandboxes/dynamic-size-list-vertical/package.json @@ -0,0 +1,11 @@ +{ + "description": "Demo of react-window vertical VariableSizeList", + "main": "src/index.js", + "dependencies": { + "lorem-ipsum": "^1", + "react": "^16", + "react-dom": "^16", + "react-window": "^1", + "resize-observer-polyfill": "^1" + } +} diff --git a/website/sandboxes/dynamic-size-list-vertical/styles.css b/website/sandboxes/dynamic-size-list-vertical/styles.css new file mode 100644 index 00000000..6f4f5f0d --- /dev/null +++ b/website/sandboxes/dynamic-size-list-vertical/styles.css @@ -0,0 +1,18 @@ +html { + font-family: sans-serif; + font-size: 12px; +} + +.List { + border: 1px solid #d9dddd; +} + +.ListItemEven, +.ListItemOdd { + padding: 0.5rem; + box-sizing: border-box; +} + +.ListItemEven { + background-color: #f8f8f0; +} diff --git a/website/src/App.js b/website/src/App.js index 3a7ff7fb..170cf955 100644 --- a/website/src/App.js +++ b/website/src/App.js @@ -5,19 +5,21 @@ import { SubMenu } from './components/SubMenu'; // Routes import areEqualApi from './routes/api/areEqual'; -import shouldComponentUpdateApi from './routes/api/shouldComponentUpdate'; -import VariableSizeGridApi from './routes/api/VariableSizeGrid'; -import VariableSizeGridExample from './routes/examples/VariableSizeGrid'; -import VariableSizeListApi from './routes/api/VariableSizeList'; -import VariableSizeListExample from './routes/examples/VariableSizeList'; +import DynamicSizeListExample from './routes/examples/DynamicSizeList'; +import DynamicSizeListApi from './routes/api/DynamicSizeList'; import FixedSizeGridApi from './routes/api/FixedSizeGrid'; -import FixedSizeListApi from './routes/api/FixedSizeList'; import FixedSizeGridExample from './routes/examples/FixedSizeGrid'; +import FixedSizeListApi from './routes/api/FixedSizeList'; import FixedSizeListExample from './routes/examples/FixedSizeList'; import RTLLayoutExample from './routes/examples/RTLLayout'; import ListWithScrollingIndicatorExample from './routes/examples/ListWithScrollingIndicator'; -import ScrollToItemExample from './routes/examples/ScrollToItem'; import MemoizedListItemsExample from './routes/examples/MemoizedListItemsExample'; +import ScrollToItemExample from './routes/examples/ScrollToItem'; +import shouldComponentUpdateApi from './routes/api/shouldComponentUpdate'; +import VariableSizeGridApi from './routes/api/VariableSizeGrid'; +import VariableSizeGridExample from './routes/examples/VariableSizeGrid'; +import VariableSizeListApi from './routes/api/VariableSizeList'; +import VariableSizeListExample from './routes/examples/VariableSizeList'; import styles from './App.module.css'; @@ -71,6 +73,11 @@ const EXAMPLE_ROUTES = [ title: 'Variable Size List', component: VariableSizeListExample, }, + { + path: '/examples/list/dynamic-size', + title: 'Dynamic Size List', + component: DynamicSizeListExample, + }, { path: '/examples/grid/fixed-size', title: 'Fixed Size Grid', @@ -114,6 +121,11 @@ const COMPONENTS_ROUTES = [ title: 'VariableSizeList', component: VariableSizeListApi, }, + { + path: '/api/DynamicSizeList', + title: 'DynamicSizeList', + component: DynamicSizeListApi, + }, { path: '/api/FixedSizeGrid', title: 'FixedSizeGrid', diff --git a/website/src/code/DynamicSizeListHorizontal.js b/website/src/code/DynamicSizeListHorizontal.js new file mode 100644 index 00000000..a76b502e --- /dev/null +++ b/website/src/code/DynamicSizeListHorizontal.js @@ -0,0 +1,20 @@ +import { DynamicSizeList as List } from 'react-window'; + +// This example assumes 'items' is an array of strings. +// Your application may render a more complex item, +// Like the one shown in this demo. +const Column = ({ index, style }) => ( +
items[index]
+); + +// Note that no itemSize is required for dyanmic lists! +const Example = () => ( + + {Column} + +); diff --git a/website/src/code/DynamicSizeListVertical.js b/website/src/code/DynamicSizeListVertical.js new file mode 100644 index 00000000..801b1fa6 --- /dev/null +++ b/website/src/code/DynamicSizeListVertical.js @@ -0,0 +1,19 @@ +import { DynamicSizeList as List } from 'react-window'; + +// This example assumes 'items' is an array of strings. +// Your application may render a more complex item, +// Like the one shown in this demo. +const Row = ({ index, style }) => ( +
items[index]
+); + +// Note that no itemSize is required for dyanmic lists! +const Example = () => ( + + {Row} + +); \ No newline at end of file diff --git a/website/src/components/CodeSandboxLink.js b/website/src/components/CodeSandboxLink.js index 1ef25dd5..51e33a79 100644 --- a/website/src/components/CodeSandboxLink.js +++ b/website/src/components/CodeSandboxLink.js @@ -1,3 +1,4 @@ +import cn from 'classnames'; import React from 'react'; import styles from './CodeSandboxLink.module.css'; @@ -9,7 +10,7 @@ const CodeSandboxLink = ({ className, tree = 'master', sandbox }) => { return ( { {props.filter(prop => showAll || prop.isRequired).map(prop => (
+ {prop.showWarning ? ( + + + + ) : null} {prop.name}: {prop.type}{' '} {prop.defaultValue !== undefined ? ` = ${prop.defaultValue}` @@ -90,6 +95,11 @@ export default class ComponentApi extends Component { {methods.map(method => (
+ {method.showWarning ? ( + + + + ) : null} {method.signature}
diff --git a/website/src/components/ComponentApi.module.css b/website/src/components/ComponentApi.module.css index 42c38115..0f8cb8ae 100644 --- a/website/src/components/ComponentApi.module.css +++ b/website/src/components/ComponentApi.module.css @@ -37,6 +37,8 @@ } .ComponentApiPropType { + display: flex; + align-items: center; line-height: 2rem; } @@ -49,3 +51,10 @@ margin-left: 1rem; margin-bottom: 1.5rem; } + +.WarningIcon { + width: 2rem; + height: 2rem; + margin-right: 0.5rem; + fill: #ff5370; +} diff --git a/website/src/components/ProfiledExample.js b/website/src/components/ProfiledExample.js index 68025de6..54f405c2 100644 --- a/website/src/components/ProfiledExample.js +++ b/website/src/components/ProfiledExample.js @@ -10,6 +10,7 @@ import styles from './ProfiledExample.module.css'; type Props = {| className?: string, sandbox?: string, + style?: Object, |}; const isProfilingEnabled = window.location.hash.indexOf('profile=true') >= 0; @@ -21,11 +22,11 @@ export default class ProfiledExample extends PureComponent { _totalActualTime = 0; render() { - const { className, children, sandbox } = this.props; + const { className, children, sandbox, style } = this.props; if (isProfilingEnabled) { return ( -
+
{children} @@ -46,7 +47,7 @@ export default class ProfiledExample extends PureComponent { ); } else { return ( -
+
{children}
diff --git a/website/src/routes/api/DynamicSizeList.js b/website/src/routes/api/DynamicSizeList.js new file mode 100644 index 00000000..22e50d3c --- /dev/null +++ b/website/src/routes/api/DynamicSizeList.js @@ -0,0 +1,66 @@ +import React, { Fragment } from 'react'; +import { NavHashLink as Link } from 'react-router-hash-link'; +import ComponentApi from '../../components/ComponentApi'; + +export default () => ( + + This component has the same methods as{' '} + + FixedSizeList + , but with the following exceptions: +

+ } + name="DynamicSizeList" + props={PROPS} + propsIntro={ +

+ This component has the same props as{' '} + + FixedSizeList + , but with the following exceptions: +

+ } + /> +); + +const PROPS = [ + { + description: ( + +

+ Items within a dynamic list are automatically measured after + rendering. This means that no itemSize prop is required. +

+

+ The list will error if you accidentally supply one. +

+
+ ), + name: 'itemSize', + showWarning: true, + type: 'number', + }, +]; + +const METHODS = [ + { + description: ( + +

+ Dynamic lists do not support scrolling to items that have not yet been + rendered. +

+

+ + Attempting to scroll to such an item will log a console warning. + +

+
+ ), + signature: 'scrollToItem(index: number, align: string = "auto"): void', + showWarning: true, + }, +]; diff --git a/website/src/routes/examples/DynamicSizeList.js b/website/src/routes/examples/DynamicSizeList.js new file mode 100644 index 00000000..c5bd1114 --- /dev/null +++ b/website/src/routes/examples/DynamicSizeList.js @@ -0,0 +1,219 @@ +import React, { PureComponent } from 'react'; +import { DynamicSizeList as List } from 'react-window'; +import loremIpsum from 'lorem-ipsum'; +import CodeBlock from '../../components/CodeBlock'; +import ProfiledExample from '../../components/ProfiledExample'; + +import CODE_HORIZONTAL from '../../code/DynamicSizeListHorizontal.js'; +import CODE_VERTICAL from '../../code/DynamicSizeListVertical.js'; + +import styles from './shared.module.css'; + +// Polyfill ResizeObserver for demo +if (typeof ResizeObserver === 'undefined') { + global.ResizeObserver = require('resize-observer-polyfill').default; +} + +var colors = [ + ['#1E88E5', '#90CAF9'], + ['#6D4C41', '#D7CCC8'], + ['#212121', '#BDBDBD'], + ['#388E3C', '#A5D6A7'], + ['#E53935', '#EF9A9A'], + ['#F4511E', '#FFAB91'], + ['#8E24AA', '#E1BEE7'], + ['#FFD600', '#FFF59D'], +]; + +const items = new Array(500).fill(true).map(() => { + const text = loremIpsum({ units: 'paragraphs' }); + return { + colors: colors[Math.floor(Math.random() * colors.length)], + paragraph: text, + isColumnExpanded: true, + isRowExpanded: true, + sentence: text.substr(0, text.indexOf('.')) + '…', + word: loremIpsum({ units: 'words' }), + words: loremIpsum({ units: 'words', count: 3 }), + }; +}); + +class Row extends PureComponent { + toggleExpanded = () => { + const { index } = this.props; + // We mutate in place here rather than using setState, + // Because it persists the data after unmount an remount. + const item = items[index]; + item.isRowExpanded = !item.isRowExpanded; + this.forceUpdate(); + }; + + render() { + const { index, forwardedRef, style } = this.props; + const item = items[index]; + + return ( +
+
+ {index} +
+
+ {item.isRowExpanded ? item.paragraph : item.sentence} +
+
+ ); + } +} + +const RefForwardedColumn = React.forwardRef((props, ref) => ( + +)); +const RefForwardedRow = React.forwardRef((props, ref) => ( + +)); + +class Column extends PureComponent { + toggleExpanded = () => { + const { index } = this.props; + // We mutate in place here rather than using setState, + // Because it persists the data after unmount an remount. + const item = items[index]; + item.isColumnExpanded = !item.isColumnExpanded; + this.forceUpdate(); + }; + + render() { + const { data: showText, forwardedRef, index, style } = this.props; + const item = items[index]; + + return ( +
+
+ {index} +
+ {showText && ( +
+ {item.isColumnExpanded ? item.words : item.word} +
+ )} +
+ ); + } +} + +export default class DynamicSizeList extends PureComponent { + state = { + halfSize: false, + showText: true, + }; + + handleToggleResize = () => + this.setState(prevState => ({ + halfSize: !prevState.halfSize, + })); + + handleToggleText = () => + this.setState(prevState => ({ + showText: !prevState.showText, + })); + + render() { + const { halfSize, showText } = this.state; + + return ( +
+ ); + } +} diff --git a/website/src/routes/examples/shared.module.css b/website/src/routes/examples/shared.module.css index b1011d43..f49fde27 100644 --- a/website/src/routes/examples/shared.module.css +++ b/website/src/routes/examples/shared.module.css @@ -41,6 +41,31 @@ overflow: hidden; } +.Note { + display: inline-flex; + align-items: center; + padding: 1rem; + font-size: 1.15rem; + margin: 0 2rem 2rem 0; + border-radius: 0.25rem; + box-shadow: rgba(0, 0, 0, 0.5) 0px 0.5rem 1.5rem; + color: #fff; + background-color: #ff5370; +} + +.NoteIcon { + width: 2rem; + height: 2rem; + margin-right: 0.5rem; + fill: currentColor; +} + +.NoteLink { + font-family: monospace; + color: inherit; + text-decoration: none; +} + .TryItOutLink { margin-top: 1rem; } @@ -69,11 +94,56 @@ justify-content: center; } +.DynamicColumnEven, +.DynamicColumnOdd, +.DynamicRowEven, +.DynamicRowOdd { + box-sizing: border-box; + padding: 0.5rem; + display: flex; +} +.DynamicColumnEven, +.DynamicColumnOdd { + display: flex; + justify-content: flex-start; + align-items: center; +} + +.DynamicColumnEven, +.DynamicRowEven, .GridItemEven, .ListItemEven { background-color: #f8f8f0; } +.ListColumn { + white-space: pre; + word-wrap: nowrap; + align-items: center; +} + +.DynamicColumnAvatar, +.DynamicRowAvatar { + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + margin-right: 0.5rem; + flex: 0 0 auto; + line-height: 2.5rem; + text-align: center; +} + +.DynamicColumnText, +.DynamicRowText { + flex: 1; + display: flex; + align-items: center; + cursor: pointer; +} +.DynamicColumnText { + white-space: nowrap; +} + @media (max-width: 37.5rem) { .ExampleWrapper { padding: 0; @@ -98,4 +168,12 @@ .ExampleCode { padding: 1rem; } + + .Note { + border-top: 2rem solid #fff; + display: flex; + margin: 0; + border-radius: 0; + box-shadow: none; + } } diff --git a/website/yarn.lock b/website/yarn.lock index d92e4682..3e9d3f1c 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -2385,9 +2385,9 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" +classnames@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" clean-css@4.1.x: version "4.1.11" @@ -6077,6 +6077,12 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3 dependencies: js-tokens "^3.0.0" +lorem-ipsum@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/lorem-ipsum/-/lorem-ipsum-1.0.6.tgz#69e9ab02bbb0991915d71b5559fe016d526f013f" + dependencies: + minimist "~1.2.0" + loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -6295,7 +6301,7 @@ minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: +minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -7480,6 +7486,10 @@ raf@3.4.0: dependencies: performance-now "^2.1.0" +random-words@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/random-words/-/random-words-1.1.0.tgz#d42f9775d14ef5c58fd255968158303e1daa0a22" + randomatic@^1.1.3: version "1.1.7" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" @@ -7991,6 +8001,10 @@ requires-port@1.0.x, requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" +resize-observer-polyfill@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz#660ff1d9712a2382baa2cad450a4716209f9ca69" + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"