import { action, computed, makeObservable } from 'mobx'; import { GridState } from './GridState'; import { VariableSizeGrid } from 'react-window'; import { CSSProperties } from 'react'; export interface ReactWindowGridRef extends VariableSizeGrid { readonly state: { readonly scrollTop: number; }; _getVerticalRangeToRender(): number[]; _getHorizontalRangeToRender(): number[]; _getItemStyle(rowIndex: number, columnIndex: number): CSSProperties; _getItemStyleCache(...args: any[]): unknown; } export interface VirtualGridRepeaterStateParams { gridState: GridState; } export class VirtualGridRepeaterState { readonly gridState; gridRef?: ReactWindowGridRef | null; scrollHeight?: number | string | null; innerElement?: HTMLElement | null = null; constructor(params: VirtualGridRepeaterStateParams) { this.gridState = params.gridState; makeObservable(this, { calcGridProps: computed, rowCount: computed, height: computed, width: computed, itemKeyRecycler: computed, }); } get height() { return this.gridState.toolbar.itemsContentHeight; } get width() { return this.gridState.itemsContentWidth; } get calcGridProps() { return this.gridState.calcGridProps; } get rowCount() { const { gridState: { itemsToRenderCount }, calcGridProps: { columnCount }, } = this; return Math.ceil(itemsToRenderCount / columnCount); } _itemKeyRecycler = (index: number) => { const { gridState: { itemKeyRecyclerOverride }, } = this; const forceKey = itemKeyRecyclerOverride?.(index); if (forceKey != null) { return forceKey; } // https://github.com/wix-private/cairo/pull/1142 // const [verticalStartIndex, verticalEndIndex] = this.gridRef?._getVerticalRangeToRender() ?? []; // const verticalCount = verticalEndIndex - verticalStartIndex + 1; // return index % (verticalCount * columnCount); return index; }; get itemKeyRecycler() { const { gridState: { itemKeyRecyclerOverride }, calcGridProps: { columnCount }, } = this; void itemKeyRecyclerOverride; return ({ rowIndex, columnIndex, }: { columnIndex: number; rowIndex: number; }) => { const index = rowIndex * columnCount + columnIndex; return this._itemKeyRecycler(index); }; } init({ container: scrollableContainer }: { container?: HTMLElement | null }) { let animationFrameId: number; if (!scrollableContainer) { return; } const onScroll = (ev: Event) => { if (process.env.NODE_ENV === 'test') { if (this.gridRef == null) { return; } const { _testScroll } = (ev ?? {}) as { _testScroll?: 'top' | 'up' }; let scrollTopValue = this.gridRef.state.scrollTop + this.gridState.toolbar.itemsContentHeight; if (_testScroll === 'top') { scrollTopValue = 0; } if (_testScroll === 'up') { scrollTopValue = -this.gridState.toolbar.itemsContentHeight; } this.gridRef.scrollTo({ scrollTop: scrollTopValue, }); return; } animationFrameId = window.requestAnimationFrame( action(() => { if (this.gridRef == null || this.innerElement == null) { return; } const { innerElement, gridState } = this; const { top: innerElementTop } = innerElement.getBoundingClientRect(); const { containerTop } = gridState.toolbar; if (containerTop == null) { return; } const offsetTop = innerElementTop - containerTop; this.gridRef.scrollTo({ scrollTop: Math.max(-offsetTop, 0), }); }), ); }; scrollableContainer.addEventListener('scroll', onScroll); return () => { scrollableContainer.removeEventListener('scroll', onScroll); window.cancelAnimationFrame(animationFrameId); }; } }