first commit

This commit is contained in:
Stefan Hacker
2026-04-03 09:38:48 +02:00
commit 37ad745546
47450 changed files with 3120798 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
import * as React from 'react';
import { ScrollToMode } from './List.types';
import type { IList, IListProps, IPage } from './List.types';
import type { JSXElement } from '@fluentui/utilities';
export interface IListState<T = any> {
pages?: IPage<T>[];
/** The last versionstamp for */
measureVersion?: number;
isScrolling?: boolean;
getDerivedStateFromProps(nextProps: IListProps<T>, previousState: IListState<T>): IListState<T>;
pagesVersion?: {};
hasMounted: boolean;
}
/**
* The List renders virtualized pages of items. Each page's item count is determined by the getItemCountForPage callback
* if provided by the caller, or 10 as default. Each page's height is determined by the getPageHeight callback if
* provided by the caller, or by cached measurements if available, or by a running average, or a default fallback.
*
* The algorithm for rendering pages works like this:
*
* 1. Predict visible pages based on "current measure data" (page heights, surface position, visible window)
* 2. If changes are necessary, apply changes (add/remove pages)
* 3. For pages that are added, measure the page heights if we need to using getBoundingClientRect
* 4. If measurements don't match predictions, update measure data and goto step 1 asynchronously
*
* Measuring too frequently can pull performance down significantly. To compensate, we cache measured values so that
* we can avoid re-measuring during operations that should not alter heights, like scrolling.
*
* To optimize glass rendering performance, onShouldVirtualize can be set. When onShouldVirtualize return false,
* List will run in fast mode (not virtualized) to render all items without any measurements to improve page load time.
* And we start doing measurements and rendering in virtualized mode when items grows larger than this threshold.
*
* However, certain operations can make measure data stale. For example, resizing the list, or passing in new props,
* or forcing an update change cause pages to shrink/grow. When these operations occur, we increment a measureVersion
* number, which we associate with cached measurements and use to determine if a remeasure should occur.
*/
export declare class List<T = any> extends React.Component<IListProps<T>, IListState<T>> implements IList {
static defaultProps: {
startIndex: number;
onRenderCell: (item: any, index: number, containsFocus: boolean) => JSXElement;
onRenderCellConditional: undefined;
renderedWindowsAhead: number;
renderedWindowsBehind: number;
};
static contextType: React.Context<import("@fluentui/react-window-provider").WindowProviderProps>;
context: any;
private _root;
private _surface;
private _pageRefs;
private _async;
private _events;
private _onAsyncScrollDebounced;
private _onAsyncIdleDebounced;
private _onScrollingDoneDebounced;
private _onAsyncResizeDebounced;
private _estimatedPageHeight;
private _totalEstimates;
private _cachedPageHeights;
private _focusedIndex;
private _scrollElement?;
private _hasCompletedFirstRender;
private _surfaceRect;
private _requiredRect;
private _allowedRect;
private _visibleRect;
private _materializedRect;
private _requiredWindowsAhead;
private _requiredWindowsBehind;
private _measureVersion;
private _scrollHeight?;
private _scrollTop;
private _pageCache;
static getDerivedStateFromProps<U = any>(nextProps: IListProps<U>, previousState: IListState<U>): IListState<U>;
constructor(props: IListProps<T>);
get pageRefs(): Readonly<Record<string, unknown>>;
/**
* Scroll to the given index. By default will bring the page the specified item is on into the view. If a callback
* to measure the height of an individual item is specified, will only scroll to bring the specific item into view.
*
* Note: with items of variable height and no passed in `getPageHeight` method, the list might jump after scrolling
* when windows before/ahead are being rendered, and the estimated height is replaced using actual elements.
*
* @param index - Index of item to scroll to
* @param measureItem - Optional callback to measure the height of an individual item
* @param scrollToMode - Optional defines where in the window the item should be positioned to when scrolling
*/
scrollToIndex(index: number, measureItem?: (itemIndex: number) => number, scrollToMode?: ScrollToMode): void;
getStartItemIndexInView(measureItem?: (itemIndex: number) => number): number;
componentDidMount(): void;
componentDidUpdate(previousProps: IListProps, previousState: IListState<T>): void;
componentWillUnmount(): void;
shouldComponentUpdate(newProps: IListProps<T>, newState: IListState<T>): boolean;
forceUpdate(): void;
/**
* Get the current height the list and it's pages.
*/
getTotalListHeight(): number;
render(): JSXElement | null;
private _getDerivedStateFromProps;
private _shouldVirtualize;
/**
* when props.items change or forceUpdate called, throw away cached pages
*/
private _invalidatePageCache;
private _renderPage;
private _onRenderRoot;
private _onRenderSurface;
/** Generate the style object for the page. */
private _getPageStyle;
private _onRenderPage;
/** Track the last item index focused so that we ensure we keep it rendered. */
private _onFocus;
/**
* Called synchronously to reset the required render range to 0 on scrolling. After async scroll has executed,
* we will call onAsyncIdle which will reset it back to it's correct value.
*/
private _onScroll;
private _resetRequiredWindows;
/**
* Debounced method to asynchronously update the visible region on a scroll event.
*/
private _onAsyncScroll;
/**
* This is an async debounced method that will try and increment the windows we render. If we can increment
* either, we increase the amount we render and re-evaluate.
*/
private _onAsyncIdle;
/**
* Function to call when the list is done scrolling.
* This function is debounced.
*/
private _onScrollingDone;
private _onAsyncResize;
private _updatePages;
/**
* Notify consumers that the rendered pages have changed
* @param oldPages - The old pages
* @param newPages - The new pages
* @param props - The props to use
*/
private _notifyPageChanges;
private _updatePageMeasurements;
/**
* Given a page, measure its dimensions, update cache.
* @returns True if the height has changed.
*/
private _measurePage;
/** Called when a page has been added to the DOM. */
private _onPageAdded;
/** Called when a page has been removed from the DOM. */
private _onPageRemoved;
/** Build up the pages that should be rendered. */
private _buildPages;
private _getPageSpecification;
/**
* Get the pixel height of a give page. Will use the props getPageHeight first, and if not provided, fallback to
* cached height, or estimated page height, or default page height.
*/
private _getPageHeight;
private _getItemCountForPage;
private _createPage;
private _getRenderCount;
/** Calculate the visible rect within the list where top: 0 and left: 0 is the top/left of the list. */
private _updateRenderRects;
}
+904
View File
@@ -0,0 +1,904 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.List = void 0;
var tslib_1 = require("tslib");
var React = require("react");
var Utilities_1 = require("../../Utilities");
var List_types_1 = require("./List.types");
var Utilities_2 = require("../../Utilities");
var scroll_1 = require("./utils/scroll");
var react_window_provider_1 = require("@fluentui/react-window-provider");
var dom_1 = require("../../utilities/dom");
// import { ListDebugRenderer } from './utils/ListDebugRenderer';
var RESIZE_DELAY = 16;
var MIN_SCROLL_UPDATE_DELAY = 100;
var MAX_SCROLL_UPDATE_DELAY = 500;
var IDLE_DEBOUNCE_DELAY = 200;
// The amount of time to wait before declaring that the list isn't scrolling
var DONE_SCROLLING_WAIT = 500;
var DEFAULT_ITEMS_PER_PAGE = 10;
var DEFAULT_PAGE_HEIGHT = 30;
var DEFAULT_RENDERED_WINDOWS_BEHIND = 2;
var DEFAULT_RENDERED_WINDOWS_AHEAD = 2;
var PAGE_KEY_PREFIX = 'page-';
var SPACER_KEY_PREFIX = 'spacer-';
// Fraction of a page to have been scrolled before re-running expensive calculations
var SCROLL_RATIO = 1 / 3;
var EMPTY_RECT = {
top: -1,
bottom: -1,
left: -1,
right: -1,
width: 0,
height: 0,
};
// Naming expensive measures so that they're named in profiles.
var _measurePageRect = function (element) { return element.getBoundingClientRect(); };
var _measureSurfaceRect = _measurePageRect;
var _measureScrollRect = _measurePageRect;
/**
* The List renders virtualized pages of items. Each page's item count is determined by the getItemCountForPage callback
* if provided by the caller, or 10 as default. Each page's height is determined by the getPageHeight callback if
* provided by the caller, or by cached measurements if available, or by a running average, or a default fallback.
*
* The algorithm for rendering pages works like this:
*
* 1. Predict visible pages based on "current measure data" (page heights, surface position, visible window)
* 2. If changes are necessary, apply changes (add/remove pages)
* 3. For pages that are added, measure the page heights if we need to using getBoundingClientRect
* 4. If measurements don't match predictions, update measure data and goto step 1 asynchronously
*
* Measuring too frequently can pull performance down significantly. To compensate, we cache measured values so that
* we can avoid re-measuring during operations that should not alter heights, like scrolling.
*
* To optimize glass rendering performance, onShouldVirtualize can be set. When onShouldVirtualize return false,
* List will run in fast mode (not virtualized) to render all items without any measurements to improve page load time.
* And we start doing measurements and rendering in virtualized mode when items grows larger than this threshold.
*
* However, certain operations can make measure data stale. For example, resizing the list, or passing in new props,
* or forcing an update change cause pages to shrink/grow. When these operations occur, we increment a measureVersion
* number, which we associate with cached measurements and use to determine if a remeasure should occur.
*/
var List = /** @class */ (function (_super) {
tslib_1.__extends(List, _super);
function List(props) {
var _this = _super.call(this, props) || this;
_this._root = React.createRef();
_this._surface = React.createRef();
_this._pageRefs = {};
_this._getDerivedStateFromProps = function (nextProps, previousState) {
if (nextProps.items !== _this.props.items ||
nextProps.renderCount !== _this.props.renderCount ||
nextProps.startIndex !== _this.props.startIndex ||
nextProps.version !== _this.props.version ||
(!previousState.hasMounted && _this.props.renderEarly && (0, Utilities_1.canUseDOM)())) {
// We have received new items so we want to make sure that initially we only render a single window to
// fill the currently visible rect, and then later render additional windows.
_this._resetRequiredWindows();
_this._requiredRect = null;
_this._measureVersion++;
_this._invalidatePageCache();
return _this._updatePages(nextProps, previousState);
}
return previousState;
};
_this._onRenderRoot = function (props) {
var rootRef = props.rootRef, surfaceElement = props.surfaceElement, divProps = props.divProps;
return (React.createElement("div", tslib_1.__assign({ ref: rootRef }, divProps), surfaceElement));
};
_this._onRenderSurface = function (props) {
var surfaceRef = props.surfaceRef, pageElements = props.pageElements, divProps = props.divProps;
return (React.createElement("div", tslib_1.__assign({ ref: surfaceRef }, divProps), pageElements));
};
_this._onRenderPage = function (pageProps, defaultRender) {
var _a;
var _b = _this.props, onRenderCell = _b.onRenderCell, onRenderCellConditional = _b.onRenderCellConditional, role = _b.role;
var _c = pageProps.page, _d = _c.items, items = _d === void 0 ? [] : _d, startIndex = _c.startIndex, divProps = tslib_1.__rest(pageProps, ["page"]);
// only assign list item role if no role is assigned
var cellRole = role === undefined ? 'listitem' : 'presentation';
var cells = [];
for (var i = 0; i < items.length; i++) {
var index = startIndex + i;
var item = items[i];
var itemKey = _this.props.getKey ? _this.props.getKey(item, index) : item && item.key;
if (itemKey === null || itemKey === undefined) {
itemKey = index;
}
var renderCell = onRenderCellConditional !== null && onRenderCellConditional !== void 0 ? onRenderCellConditional : onRenderCell;
var cell = (_a = renderCell === null || renderCell === void 0 ? void 0 : renderCell(item, index, !_this.props.ignoreScrollingState ? _this.state.isScrolling : undefined)) !== null && _a !== void 0 ? _a : null;
if (!onRenderCellConditional || cell) {
cells.push(React.createElement("div", { role: cellRole, className: 'ms-List-cell', key: itemKey, "data-list-index": index, "data-automationid": "ListCell" }, cell));
}
}
return React.createElement("div", tslib_1.__assign({}, divProps), cells);
};
(0, Utilities_1.initializeComponentRef)(_this);
_this.state = {
pages: [],
isScrolling: false,
getDerivedStateFromProps: _this._getDerivedStateFromProps,
hasMounted: false,
};
_this._estimatedPageHeight = 0;
_this._totalEstimates = 0;
_this._requiredWindowsAhead = 0;
_this._requiredWindowsBehind = 0;
// Track the measure version for everything.
_this._measureVersion = 0;
_this._cachedPageHeights = {};
_this._estimatedPageHeight = 0;
_this._focusedIndex = -1;
_this._pageCache = {};
return _this;
}
// private _debugRenderer: ListDebugRenderer;
// private _debugRafId: number | undefined = undefined;
List.getDerivedStateFromProps = function (nextProps, previousState) {
return previousState.getDerivedStateFromProps(nextProps, previousState);
};
Object.defineProperty(List.prototype, "pageRefs", {
get: function () {
return this._pageRefs;
},
enumerable: false,
configurable: true
});
/**
* Scroll to the given index. By default will bring the page the specified item is on into the view. If a callback
* to measure the height of an individual item is specified, will only scroll to bring the specific item into view.
*
* Note: with items of variable height and no passed in `getPageHeight` method, the list might jump after scrolling
* when windows before/ahead are being rendered, and the estimated height is replaced using actual elements.
*
* @param index - Index of item to scroll to
* @param measureItem - Optional callback to measure the height of an individual item
* @param scrollToMode - Optional defines where in the window the item should be positioned to when scrolling
*/
List.prototype.scrollToIndex = function (index, measureItem, scrollToMode) {
if (scrollToMode === void 0) { scrollToMode = List_types_1.ScrollToMode.auto; }
var startIndex = this.props.startIndex;
var renderCount = this._getRenderCount();
var endIndex = startIndex + renderCount;
var allowedRect = this._allowedRect;
var scrollTop = 0;
var itemsPerPage = 1;
for (var itemIndex = startIndex; itemIndex < endIndex; itemIndex += itemsPerPage) {
var pageSpecification = this._getPageSpecification(this.props, itemIndex, allowedRect);
var pageHeight = pageSpecification.height;
itemsPerPage = pageSpecification.itemCount;
var requestedIndexIsInPage = itemIndex <= index && itemIndex + itemsPerPage > index;
if (requestedIndexIsInPage) {
// We have found the page. If the user provided a way to measure an individual item, we will try to scroll in
// just the given item, otherwise we'll only bring the page into view
if (measureItem && this._scrollElement) {
var scrollRect = _measureScrollRect(this._scrollElement);
var scrollPosition = (0, scroll_1.getScrollYPosition)(this._scrollElement);
var scrollWindow = {
top: scrollPosition,
bottom: scrollPosition + scrollRect.height,
};
// Adjust for actual item position within page
var itemPositionWithinPage = index - itemIndex;
for (var itemIndexInPage = 0; itemIndexInPage < itemPositionWithinPage; ++itemIndexInPage) {
scrollTop += measureItem(itemIndex + itemIndexInPage);
}
var scrollBottom = scrollTop + measureItem(index);
// If scrollToMode is set to something other than auto, we always want to
// scroll the item into a specific position on the page.
switch (scrollToMode) {
case List_types_1.ScrollToMode.top:
(0, scroll_1.setScrollYPosition)(this._scrollElement, scrollTop);
return;
case List_types_1.ScrollToMode.bottom:
(0, scroll_1.setScrollYPosition)(this._scrollElement, scrollBottom - scrollRect.height);
return;
case List_types_1.ScrollToMode.center:
(0, scroll_1.setScrollYPosition)(this._scrollElement, (scrollTop + scrollBottom - scrollRect.height) / 2);
return;
case List_types_1.ScrollToMode.auto:
default:
break;
}
var itemIsFullyVisible = scrollTop >= scrollWindow.top && scrollBottom <= scrollWindow.bottom;
if (itemIsFullyVisible) {
// Item is already visible, do nothing.
return;
}
var itemIsPartiallyAbove = scrollTop < scrollWindow.top;
var itemIsPartiallyBelow = scrollBottom > scrollWindow.bottom;
if (itemIsPartiallyAbove) {
// We will just scroll to 'scrollTop'
// .------. - scrollTop
// |Item |
// | .----|-. - scrollWindow.top
// '------' |
// | |
// '------'
}
else if (itemIsPartiallyBelow) {
// Adjust scrollTop position to just bring in the element
// .------. - scrollTop
// | |
// | .------.
// '-|----' | - scrollWindow.bottom
// | Item |
// '------' - scrollBottom
scrollTop = scrollBottom - scrollRect.height;
}
}
if (this._scrollElement) {
(0, scroll_1.setScrollYPosition)(this._scrollElement, scrollTop);
}
return;
}
scrollTop += pageHeight;
}
};
List.prototype.getStartItemIndexInView = function (measureItem) {
var pages = this.state.pages || [];
for (var _i = 0, pages_1 = pages; _i < pages_1.length; _i++) {
var page = pages_1[_i];
var isPageVisible = !page.isSpacer && (this._scrollTop || 0) >= page.top && (this._scrollTop || 0) <= page.top + page.height;
if (isPageVisible) {
if (!measureItem) {
var rowHeight = Math.floor(page.height / page.itemCount);
return page.startIndex + Math.floor((this._scrollTop - page.top) / rowHeight);
}
else {
var totalRowHeight = 0;
for (var itemIndex = page.startIndex; itemIndex < page.startIndex + page.itemCount; itemIndex++) {
var rowHeight = measureItem(itemIndex);
if (page.top + totalRowHeight <= this._scrollTop &&
this._scrollTop < page.top + totalRowHeight + rowHeight) {
return itemIndex;
}
else {
totalRowHeight += rowHeight;
}
}
}
}
}
return 0;
};
List.prototype.componentDidMount = function () {
this._async = new Utilities_1.Async(this);
this._events = new Utilities_1.EventGroup(this);
// Ensure that scrolls are lazy updated.
this._onAsyncScrollDebounced = this._async.debounce(this._onAsyncScroll, MIN_SCROLL_UPDATE_DELAY, {
leading: false,
maxWait: MAX_SCROLL_UPDATE_DELAY,
});
this._onAsyncIdleDebounced = this._async.debounce(this._onAsyncIdle, IDLE_DEBOUNCE_DELAY, {
leading: false,
});
this._onAsyncResizeDebounced = this._async.debounce(this._onAsyncResize, RESIZE_DELAY, {
leading: false,
});
this._onScrollingDoneDebounced = this._async.debounce(this._onScrollingDone, DONE_SCROLLING_WAIT, {
leading: false,
});
this._scrollElement = (0, Utilities_1.findScrollableParent)(this._root.current);
this._scrollTop = 0;
this.setState(tslib_1.__assign(tslib_1.__assign({}, this._updatePages(this.props, this.state)), { hasMounted: true }));
this._measureVersion++;
var win = (0, dom_1.getWindowEx)(this.context);
this._events.on(win, 'resize', this._onAsyncResizeDebounced);
if (this._root.current) {
this._events.on(this._root.current, 'focus', this._onFocus, true);
}
if (this._scrollElement) {
this._events.on(this._scrollElement, 'scroll', this._onScroll);
this._events.on(this._scrollElement, 'scroll', this._onAsyncScrollDebounced);
}
// this._debugRenderer = new ListDebugRenderer();
// const debugRender = () => {
// this._debugRenderer.render({
// visibleRect: this._visibleRect,
// allowedRect: this._allowedRect,
// requiredRect: this._requiredRect,
// materializedRect: this._materializedRect,
// surfaceRect: this._surfaceRect,
// totalListHeight: this.getTotalListHeight(),
// pages: this.state.pages,
// scrollTop: Math.abs(this._scrollTop - getScrollYPosition(this._scrollElement)),
// estimatedLine: this._estimatedPageHeight * SCROLL_RATIO,
// scrollY: getScrollYPosition(this._scrollElement),
// });
// this._debugRafId = requestAnimationFrame(debugRender);
// };
// debugRender();
};
List.prototype.componentDidUpdate = function (previousProps, previousState) {
// Multiple updates may have been queued, so the callback will reflect all of them.
// Re-fetch the current props and states to avoid using a stale props or state captured in the closure.
var finalProps = this.props;
var finalState = this.state;
if (this.state.pagesVersion !== previousState.pagesVersion) {
// If we weren't provided with the page height, measure the pages
if (!finalProps.getPageHeight) {
// If measured version is invalid since we've updated the DOM
var heightsChanged = this._updatePageMeasurements(finalState.pages);
// On first render, we should re-measure so that we don't get a visual glitch.
if (heightsChanged) {
this._materializedRect = null;
if (!this._hasCompletedFirstRender) {
this._hasCompletedFirstRender = true;
this.setState(this._updatePages(finalProps, finalState));
}
else {
this._onAsyncScrollDebounced();
}
}
else {
// Enqueue an idle bump.
this._onAsyncIdleDebounced();
}
}
else {
// Enqueue an idle bump
this._onAsyncIdleDebounced();
}
// Notify the caller that rendering the new pages has completed
if (finalProps.onPagesUpdated) {
finalProps.onPagesUpdated(finalState.pages);
}
}
};
List.prototype.componentWillUnmount = function () {
var _a, _b;
(_a = this._async) === null || _a === void 0 ? void 0 : _a.dispose();
(_b = this._events) === null || _b === void 0 ? void 0 : _b.dispose();
delete this._scrollElement;
// this._debugRenderer.dispose();
// if (this._debugRafId) {
// cancelAnimationFrame(this._debugRafId);
// this._debugRafId = undefined;
// }
};
List.prototype.shouldComponentUpdate = function (newProps, newState) {
var oldPages = this.state.pages;
var newPages = newState.pages;
var shouldComponentUpdate = false;
// Update if the page stops scrolling
if (!newState.isScrolling && this.state.isScrolling) {
return true;
}
if (newProps.version !== this.props.version) {
return true;
}
if (newProps.className !== this.props.className) {
return true;
}
if (newProps.items === this.props.items && oldPages.length === newPages.length) {
for (var i = 0; i < oldPages.length; i++) {
var oldPage = oldPages[i];
var newPage = newPages[i];
if (oldPage.key !== newPage.key || oldPage.itemCount !== newPage.itemCount) {
shouldComponentUpdate = true;
break;
}
}
}
else {
shouldComponentUpdate = true;
}
return shouldComponentUpdate;
};
List.prototype.forceUpdate = function () {
this._invalidatePageCache();
// Ensure that when the list is force updated we update the pages first before render.
this._updateRenderRects(this.props, this.state, true);
this.setState(this._updatePages(this.props, this.state));
this._measureVersion++;
_super.prototype.forceUpdate.call(this);
};
/**
* Get the current height the list and it's pages.
*/
List.prototype.getTotalListHeight = function () {
return this._surfaceRect.height;
};
List.prototype.render = function () {
var _a = this.props, className = _a.className, _b = _a.role, role = _b === void 0 ? 'list' : _b, onRenderSurface = _a.onRenderSurface, onRenderRoot = _a.onRenderRoot;
var _c = this.state.pages, pages = _c === void 0 ? [] : _c;
var pageElements = [];
var divProps = (0, Utilities_1.getNativeProps)(this.props, Utilities_1.divProperties);
for (var _i = 0, pages_2 = pages; _i < pages_2.length; _i++) {
var page = pages_2[_i];
pageElements.push(this._renderPage(page));
}
var finalOnRenderSurface = onRenderSurface
? (0, Utilities_2.composeRenderFunction)(onRenderSurface, this._onRenderSurface)
: this._onRenderSurface;
var finalOnRenderRoot = onRenderRoot
? (0, Utilities_2.composeRenderFunction)(onRenderRoot, this._onRenderRoot)
: this._onRenderRoot;
return finalOnRenderRoot({
rootRef: this._root,
pages: pages,
surfaceElement: finalOnRenderSurface({
surfaceRef: this._surface,
pages: pages,
pageElements: pageElements,
divProps: {
role: 'presentation',
className: 'ms-List-surface',
},
}),
divProps: tslib_1.__assign(tslib_1.__assign({}, divProps), { className: (0, Utilities_1.css)('ms-List', className), role: pageElements.length > 0 ? role : undefined, 'aria-label': pageElements.length > 0 ? divProps['aria-label'] : undefined }),
});
};
List.prototype._shouldVirtualize = function (props) {
if (props === void 0) { props = this.props; }
var onShouldVirtualize = props.onShouldVirtualize;
return !onShouldVirtualize || onShouldVirtualize(props);
};
/**
* when props.items change or forceUpdate called, throw away cached pages
*/
List.prototype._invalidatePageCache = function () {
this._pageCache = {};
};
List.prototype._renderPage = function (page) {
var _this = this;
var usePageCache = this.props.usePageCache;
var cachedPage;
// if usePageCache is set and cached page element can be found, just return cached page
if (usePageCache) {
cachedPage = this._pageCache[page.key];
if (cachedPage && cachedPage.pageElement) {
return cachedPage.pageElement;
}
}
var pageStyle = this._getPageStyle(page);
var _a = this.props.onRenderPage, onRenderPage = _a === void 0 ? this._onRenderPage : _a;
var pageElement = onRenderPage({
page: page,
className: 'ms-List-page',
key: page.key,
ref: function (newRef) {
_this._pageRefs[page.key] = newRef;
},
style: pageStyle,
role: 'presentation',
}, this._onRenderPage);
// cache the first page for now since it is re-rendered a lot times unnecessarily.
// todo: a more aggresive caching mechanism is to cache pages constaining the items not changed.
// now we re-render pages too frequently, for example, props.items increased from 30 to 60, although the
// first 30 items did not change, we still re-rendered all of them in this props.items change.
if (usePageCache && page.startIndex === 0) {
this._pageCache[page.key] = {
page: page,
pageElement: pageElement,
};
}
return pageElement;
};
/** Generate the style object for the page. */
List.prototype._getPageStyle = function (page) {
var getPageStyle = this.props.getPageStyle;
return tslib_1.__assign(tslib_1.__assign({}, (getPageStyle ? getPageStyle(page) : {})), (!page.items
? {
height: page.height,
}
: {}));
};
/** Track the last item index focused so that we ensure we keep it rendered. */
List.prototype._onFocus = function (ev) {
var target = ev.target;
while (target !== this._surface.current) {
var indexString = target.getAttribute('data-list-index');
if (indexString) {
this._focusedIndex = Number(indexString);
break;
}
target = (0, Utilities_1.getParent)(target);
}
};
/**
* Called synchronously to reset the required render range to 0 on scrolling. After async scroll has executed,
* we will call onAsyncIdle which will reset it back to it's correct value.
*/
List.prototype._onScroll = function () {
if (!this.state.isScrolling && !this.props.ignoreScrollingState) {
this.setState({ isScrolling: true });
}
this._resetRequiredWindows();
this._onScrollingDoneDebounced();
};
List.prototype._resetRequiredWindows = function () {
this._requiredWindowsAhead = 0;
this._requiredWindowsBehind = 0;
};
/**
* Debounced method to asynchronously update the visible region on a scroll event.
*/
List.prototype._onAsyncScroll = function () {
this._updateRenderRects(this.props, this.state);
// Only update pages when the visible rect falls outside of the materialized rect.
if (!this._materializedRect || !_isContainedWithin(this._requiredRect, this._materializedRect)) {
this.setState(this._updatePages(this.props, this.state));
}
else {
// console.log('requiredRect contained in materialized', this._requiredRect, this._materializedRect);
}
};
/**
* This is an async debounced method that will try and increment the windows we render. If we can increment
* either, we increase the amount we render and re-evaluate.
*/
List.prototype._onAsyncIdle = function () {
var _a = this.props, renderedWindowsAhead = _a.renderedWindowsAhead, renderedWindowsBehind = _a.renderedWindowsBehind;
var _b = this, requiredWindowsAhead = _b._requiredWindowsAhead, requiredWindowsBehind = _b._requiredWindowsBehind;
var windowsAhead = Math.min(renderedWindowsAhead, requiredWindowsAhead + 1);
var windowsBehind = Math.min(renderedWindowsBehind, requiredWindowsBehind + 1);
if (windowsAhead !== requiredWindowsAhead || windowsBehind !== requiredWindowsBehind) {
// console.log('idling', windowsBehind, windowsAhead);
this._requiredWindowsAhead = windowsAhead;
this._requiredWindowsBehind = windowsBehind;
this._updateRenderRects(this.props, this.state);
this.setState(this._updatePages(this.props, this.state));
}
if (renderedWindowsAhead > windowsAhead || renderedWindowsBehind > windowsBehind) {
// Async increment on next tick.
this._onAsyncIdleDebounced();
}
};
/**
* Function to call when the list is done scrolling.
* This function is debounced.
*/
List.prototype._onScrollingDone = function () {
if (!this.props.ignoreScrollingState) {
this.setState({ isScrolling: false });
this._onAsyncIdle();
}
};
List.prototype._onAsyncResize = function () {
this.forceUpdate();
};
List.prototype._updatePages = function (nextProps, previousState) {
// console.log('updating pages');
if (!this._requiredRect) {
this._updateRenderRects(nextProps, previousState);
}
var newListState = this._buildPages(nextProps, previousState);
var oldListPages = previousState.pages;
this._notifyPageChanges(oldListPages, newListState.pages, this.props);
return tslib_1.__assign(tslib_1.__assign(tslib_1.__assign({}, previousState), newListState), { pagesVersion: {} });
};
/**
* Notify consumers that the rendered pages have changed
* @param oldPages - The old pages
* @param newPages - The new pages
* @param props - The props to use
*/
List.prototype._notifyPageChanges = function (oldPages, newPages, props) {
var onPageAdded = props.onPageAdded, onPageRemoved = props.onPageRemoved;
if (onPageAdded || onPageRemoved) {
var renderedIndexes = {};
for (var _i = 0, oldPages_1 = oldPages; _i < oldPages_1.length; _i++) {
var page = oldPages_1[_i];
if (page.items) {
renderedIndexes[page.startIndex] = page;
}
}
for (var _a = 0, newPages_1 = newPages; _a < newPages_1.length; _a++) {
var page = newPages_1[_a];
if (page.items) {
if (!renderedIndexes[page.startIndex]) {
this._onPageAdded(page);
}
else {
delete renderedIndexes[page.startIndex];
}
}
}
for (var index in renderedIndexes) {
if (renderedIndexes.hasOwnProperty(index)) {
this._onPageRemoved(renderedIndexes[index]);
}
}
}
};
List.prototype._updatePageMeasurements = function (pages) {
var heightChanged = false;
// when not in virtualize mode, we render all the items without page measurement
if (!this._shouldVirtualize()) {
return heightChanged;
}
for (var i = 0; i < pages.length; i++) {
var page = pages[i];
if (page.items) {
heightChanged = this._measurePage(page) || heightChanged;
}
}
return heightChanged;
};
/**
* Given a page, measure its dimensions, update cache.
* @returns True if the height has changed.
*/
List.prototype._measurePage = function (page) {
var hasChangedHeight = false;
var pageElement = this._pageRefs[page.key];
var cachedHeight = this._cachedPageHeights[page.startIndex];
// console.log(' * measure attempt', page.startIndex, cachedHeight);
if (pageElement &&
this._shouldVirtualize() &&
(!cachedHeight || cachedHeight.measureVersion !== this._measureVersion)) {
var newClientRect = {
width: pageElement.clientWidth,
height: pageElement.clientHeight,
};
if (newClientRect.height || newClientRect.width) {
hasChangedHeight = page.height !== newClientRect.height;
// console.warn(' *** expensive page measure', page.startIndex, page.height, newClientRect.height);
page.height = newClientRect.height;
this._cachedPageHeights[page.startIndex] = {
height: newClientRect.height,
measureVersion: this._measureVersion,
};
this._estimatedPageHeight = Math.round((this._estimatedPageHeight * this._totalEstimates + newClientRect.height) / (this._totalEstimates + 1));
this._totalEstimates++;
}
}
return hasChangedHeight;
};
/** Called when a page has been added to the DOM. */
List.prototype._onPageAdded = function (page) {
var onPageAdded = this.props.onPageAdded;
// console.log('page added', page.startIndex, this.state.pages.map(page => page.key).join(', '));
if (onPageAdded) {
onPageAdded(page);
}
};
/** Called when a page has been removed from the DOM. */
List.prototype._onPageRemoved = function (page) {
var onPageRemoved = this.props.onPageRemoved;
// console.log(' --- page removed', page.startIndex, this.state.pages.map(page => page.key).join(', '));
if (onPageRemoved) {
onPageRemoved(page);
}
};
/** Build up the pages that should be rendered. */
List.prototype._buildPages = function (props, state) {
var renderCount = props.renderCount;
var items = props.items, startIndex = props.startIndex, getPageHeight = props.getPageHeight;
renderCount = this._getRenderCount(props);
var materializedRect = tslib_1.__assign({}, EMPTY_RECT);
var pages = [];
var itemsPerPage = 1;
var pageTop = 0;
var currentSpacer = null;
var focusedIndex = this._focusedIndex;
var endIndex = startIndex + renderCount;
var shouldVirtualize = this._shouldVirtualize(props);
// First render is very important to track; when we render cells, we have no idea of estimated page height.
// So we should default to rendering only the first page so that we can get information.
// However if the user provides a measure function, let's just assume they know the right heights.
var isFirstRender = this._estimatedPageHeight === 0 && !getPageHeight;
var allowedRect = this._allowedRect;
var _loop_1 = function (itemIndex) {
var pageSpecification = this_1._getPageSpecification(props, itemIndex, allowedRect);
var pageHeight = pageSpecification.height;
var pageData = pageSpecification.data;
var key = pageSpecification.key;
itemsPerPage = pageSpecification.itemCount;
var pageBottom = pageTop + pageHeight - 1;
var isPageRendered = (0, Utilities_1.findIndex)(state.pages, function (page) { return !!page.items && page.startIndex === itemIndex; }) > -1;
var isPageInAllowedRange = !allowedRect || (pageBottom >= allowedRect.top && pageTop <= allowedRect.bottom);
var isPageInRequiredRange = !this_1._requiredRect || (pageBottom >= this_1._requiredRect.top && pageTop <= this_1._requiredRect.bottom);
var isPageVisible = (!isFirstRender && (isPageInRequiredRange || (isPageInAllowedRange && isPageRendered))) || !shouldVirtualize;
var isPageFocused = focusedIndex >= itemIndex && focusedIndex < itemIndex + itemsPerPage;
var isFirstPage = itemIndex === startIndex;
// Only render whats visible, focused, or first page,
// or when running in fast rendering mode (not in virtualized mode), we render all current items in pages
if (isPageVisible || isPageFocused || isFirstPage) {
if (currentSpacer) {
pages.push(currentSpacer);
currentSpacer = null;
}
var itemsInPage = Math.min(itemsPerPage, endIndex - itemIndex);
var newPage = this_1._createPage(key, items.slice(itemIndex, itemIndex + itemsInPage), itemIndex, undefined, undefined, pageData);
newPage.top = pageTop;
newPage.height = pageHeight;
if (this_1._visibleRect && this_1._visibleRect.bottom) {
newPage.isVisible = pageBottom >= this_1._visibleRect.top && pageTop <= this_1._visibleRect.bottom;
}
pages.push(newPage);
if (isPageInRequiredRange && this_1._allowedRect) {
_mergeRect(materializedRect, {
top: pageTop,
bottom: pageBottom,
height: pageHeight,
left: allowedRect.left,
right: allowedRect.right,
width: allowedRect.width,
});
}
}
else {
if (!currentSpacer) {
currentSpacer = this_1._createPage(SPACER_KEY_PREFIX + itemIndex, undefined, itemIndex, 0, undefined, pageData, true /*isSpacer*/);
}
currentSpacer.height = (currentSpacer.height || 0) + (pageBottom - pageTop) + 1;
currentSpacer.itemCount += itemsPerPage;
}
pageTop += pageBottom - pageTop + 1;
// in virtualized mode, we render need to render first page then break and measure,
// otherwise, we render all items without measurement to make rendering fast
if (isFirstRender && shouldVirtualize) {
return "break";
}
};
var this_1 = this;
for (var itemIndex = startIndex; itemIndex < endIndex; itemIndex += itemsPerPage) {
var state_1 = _loop_1(itemIndex);
if (state_1 === "break")
break;
}
if (currentSpacer) {
currentSpacer.key = SPACER_KEY_PREFIX + 'end';
pages.push(currentSpacer);
}
this._materializedRect = materializedRect;
// console.log('materialized: ', materializedRect);
return tslib_1.__assign(tslib_1.__assign({}, state), { pages: pages, measureVersion: this._measureVersion });
};
List.prototype._getPageSpecification = function (props, itemIndex, visibleRect) {
var getPageSpecification = props.getPageSpecification;
if (getPageSpecification) {
var pageData = getPageSpecification(itemIndex, visibleRect, props.items);
var _a = pageData.itemCount, itemCount = _a === void 0 ? this._getItemCountForPage(itemIndex, visibleRect) : _a;
var _b = pageData.height, height = _b === void 0 ? this._getPageHeight(itemIndex, visibleRect, itemCount) : _b;
return {
itemCount: itemCount,
height: height,
data: pageData.data,
key: pageData.key,
};
}
else {
var itemCount = this._getItemCountForPage(itemIndex, visibleRect);
return {
itemCount: itemCount,
height: this._getPageHeight(itemIndex, visibleRect, itemCount),
};
}
};
/**
* Get the pixel height of a give page. Will use the props getPageHeight first, and if not provided, fallback to
* cached height, or estimated page height, or default page height.
*/
List.prototype._getPageHeight = function (itemIndex, visibleRect, itemsPerPage) {
if (this.props.getPageHeight) {
return this.props.getPageHeight(itemIndex, visibleRect, itemsPerPage, this.props.items);
}
else {
var cachedHeight = this._cachedPageHeights[itemIndex];
return cachedHeight ? cachedHeight.height : this._estimatedPageHeight || DEFAULT_PAGE_HEIGHT;
}
};
List.prototype._getItemCountForPage = function (itemIndex, visibileRect) {
var itemsPerPage = this.props.getItemCountForPage
? this.props.getItemCountForPage(itemIndex, visibileRect)
: DEFAULT_ITEMS_PER_PAGE;
return itemsPerPage ? itemsPerPage : DEFAULT_ITEMS_PER_PAGE;
};
List.prototype._createPage = function (pageKey, items, startIndex, count, style, data, isSpacer) {
if (startIndex === void 0) { startIndex = -1; }
if (count === void 0) { count = items ? items.length : 0; }
if (style === void 0) { style = {}; }
pageKey = pageKey || PAGE_KEY_PREFIX + startIndex;
var cachedPage = this._pageCache[pageKey];
if (cachedPage && cachedPage.page) {
return cachedPage.page;
}
return {
key: pageKey,
startIndex: startIndex,
itemCount: count,
items: items,
style: style,
top: 0,
height: 0,
data: data,
isSpacer: isSpacer || false,
};
};
List.prototype._getRenderCount = function (props) {
var _a = props || this.props, items = _a.items, startIndex = _a.startIndex, renderCount = _a.renderCount;
return renderCount === undefined ? (items ? items.length - startIndex : 0) : renderCount;
};
/** Calculate the visible rect within the list where top: 0 and left: 0 is the top/left of the list. */
List.prototype._updateRenderRects = function (props, state, forceUpdate) {
var renderedWindowsAhead = props.renderedWindowsAhead, renderedWindowsBehind = props.renderedWindowsBehind;
var pages = state.pages;
// when not in virtualize mode, we render all items without measurement to optimize page rendering perf
if (!this._shouldVirtualize(props)) {
return;
}
var surfaceRect = this._surfaceRect || tslib_1.__assign({}, EMPTY_RECT);
var scrollHeight = (0, scroll_1.getScrollHeight)(this._scrollElement);
var scrollTop = (0, scroll_1.getScrollYPosition)(this._scrollElement);
// WARNING: EXPENSIVE CALL! We need to know the surface top relative to the window.
// This needs to be called to recalculate when new pages should be loaded.
// We check to see how far we've scrolled and if it's further than a third of a page we run it again.
if (this._surface.current &&
(forceUpdate ||
!pages ||
!this._surfaceRect ||
!scrollHeight ||
scrollHeight !== this._scrollHeight ||
Math.abs(this._scrollTop - scrollTop) > this._estimatedPageHeight * SCROLL_RATIO)) {
surfaceRect = this._surfaceRect = _measureSurfaceRect(this._surface.current);
this._scrollTop = scrollTop;
}
// If the scroll height has changed, something in the container likely resized and
// we should redo the page heights incase their content resized.
if (forceUpdate || !scrollHeight || scrollHeight !== this._scrollHeight) {
this._measureVersion++;
}
this._scrollHeight = scrollHeight || 0;
// If the surface is above the container top or below the container bottom, or if this is not the first
// render return empty rect.
// The first time the list gets rendered we need to calculate the rectangle. The width of the list is
// used to calculate the width of the list items.
var visibleTop = Math.max(0, -surfaceRect.top);
var win = (0, Utilities_1.getWindow)(this._root.current);
var visibleRect = {
top: visibleTop,
left: surfaceRect.left,
bottom: visibleTop + win.innerHeight,
right: surfaceRect.right,
width: surfaceRect.width,
height: win.innerHeight,
};
// The required/allowed rects are adjusted versions of the visible rect.
this._requiredRect = _expandRect(visibleRect, this._requiredWindowsBehind, this._requiredWindowsAhead);
this._allowedRect = _expandRect(visibleRect, renderedWindowsBehind, renderedWindowsAhead);
// store the visible rect for later use.
this._visibleRect = visibleRect;
};
List.defaultProps = {
startIndex: 0,
onRenderCell: function (item, index, containsFocus) { return React.createElement(React.Fragment, null, (item && item.name) || ''); },
onRenderCellConditional: undefined,
renderedWindowsAhead: DEFAULT_RENDERED_WINDOWS_AHEAD,
renderedWindowsBehind: DEFAULT_RENDERED_WINDOWS_BEHIND,
};
List.contextType = react_window_provider_1.WindowContext;
return List;
}(React.Component));
exports.List = List;
function _expandRect(rect, pagesBefore, pagesAfter) {
var top = rect.top - pagesBefore * rect.height;
var height = rect.height + (pagesBefore + pagesAfter) * rect.height;
return {
top: top,
bottom: top + height,
height: height,
left: rect.left,
right: rect.right,
width: rect.width,
};
}
function _isContainedWithin(innerRect, outerRect) {
return (innerRect.top >= outerRect.top &&
innerRect.left >= outerRect.left &&
innerRect.bottom <= outerRect.bottom &&
innerRect.right <= outerRect.right);
}
function _mergeRect(targetRect, newRect) {
targetRect.top = newRect.top < targetRect.top || targetRect.top === -1 ? newRect.top : targetRect.top;
targetRect.left = newRect.left < targetRect.left || targetRect.left === -1 ? newRect.left : targetRect.left;
targetRect.bottom =
newRect.bottom > targetRect.bottom || targetRect.bottom === -1 ? newRect.bottom : targetRect.bottom;
targetRect.right = newRect.right > targetRect.right || targetRect.right === -1 ? newRect.right : targetRect.right;
targetRect.width = targetRect.right - targetRect.left + 1;
targetRect.height = targetRect.bottom - targetRect.top + 1;
return targetRect;
}
//# sourceMappingURL=List.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,305 @@
import * as React from 'react';
import { List } from './List';
import type { IRefObject, IRectangle, IRenderFunction } from '../../Utilities';
import type { JSXElement } from '@fluentui/utilities';
/**
* {@docCategory List}
*/
export declare const ScrollToMode: {
/**
* Does not make any consideration to where in the viewport the item should align to.
*/
auto: 0;
/**
* Attempts to scroll the list so the top of the desired item is aligned with the top of the viewport.
*/
top: 1;
/**
* Attempts to scroll the list so the bottom of the desired item is aligned with the bottom of the viewport.
*/
bottom: 2;
/**
* Attempts to scroll the list so the desired item is in the exact center of the viewport.
*/
center: 3;
};
/**
* {@docCategory List}
*/
export type ScrollToMode = (typeof ScrollToMode)[keyof typeof ScrollToMode];
/**
* Props passed to the render override for the list root.
* {@docCategory List}
*/
export interface IListOnRenderRootProps<T> {
/**
* The ref to be applied to the list root.
* The `List` uses this element to track scroll position and sizing.
*/
rootRef: React.Ref<HTMLDivElement>;
/**
* Props to apply to the list root element.
*/
divProps: React.HTMLAttributes<HTMLDivElement>;
/**
* The active pages to be rendered into the list.
* These will have been rendered using `onRenderPage`.
*/
pages: IPage<T>[];
/**
* The content to be rendered as the list surface element.
* This will have been rendered using `onRenderSurface`.
*/
surfaceElement: JSXElement | null;
}
/**
* Props passed to the render override for the list surface.
* {@docCategory List}
*/
export interface IListOnRenderSurfaceProps<T> {
/**
* A ref to be applied to the surface element.
* The `List` uses this element to track content size and focus.
*/
surfaceRef: React.Ref<HTMLDivElement>;
/**
* Props to apply to the list surface element.
*/
divProps: React.HTMLAttributes<HTMLDivElement>;
/**
* The active pages to be rendered into the list.
* These will have been rendered using `onRenderPage`.
*/
pages: IPage<T>[];
/**
* The content to be rendered representing all active pages.
*/
pageElements: JSXElement[];
}
/**
* {@docCategory List}
*/
export interface IList {
/**
* Force the component to update.
*/
forceUpdate: () => void;
/**
* Get the current height the list and it's pages.
*/
getTotalListHeight?: () => number;
/**
* Scroll to the given index. By default will bring the page the specified item is on into the view. If a callback
* to measure the height of an individual item is specified, will only scroll to bring the specific item into view.
*
* Note: with items of variable height and no passed in `getPageHeight` method, the list might jump after scrolling
* when windows before/ahead are being rendered, and the estimated height is replaced using actual elements.
*
* @param index - Index of item to scroll to
* @param measureItem - Optional callback to measure the height of an individual item
* @param scrollToMode - Optional defines the behavior of the scrolling alignment. Defaults to auto.
* Note: The scrollToMode requires the measureItem callback is provided to function.
*/
scrollToIndex: (index: number, measureItem?: (itemIndex: number) => number, scrollToMode?: ScrollToMode) => void;
/**
* Get the start index of the page that is currently in view
*/
getStartItemIndexInView: () => number;
}
/**
* {@docCategory List}
*/
export interface IListProps<T = any> extends React.HTMLAttributes<List<T> | HTMLDivElement> {
/**
* Optional callback to access the IList interface. Use this instead of ref for accessing
* the public methods and properties of the component.
*/
componentRef?: IRefObject<IList>;
/** Optional classname to append to root list. */
className?: string;
/** Items to render. */
items?: T[];
/**
* Method to call when trying to render an item.
* @param item - The data associated with the cell that is being rendered.
* @param index - The index of the cell being rendered.
* @param isScrolling - True if the list is being scrolled. May be useful for rendering a placeholder if your cells
* are complex.
*/
onRenderCell?: (item?: T, index?: number, isScrolling?: boolean) => React.ReactNode;
/**
* Method to call when trying to render an item conditionally.
*
* When this method returns `null` the cell will be skipped in the render.
*
* This prop is mutually exclusive with `onRenderCell` and when `onRenderCellConditional` is set,
* `onRenderCell` will not be called.
*
* @param item - The data associated with the cell that is being rendered.
* @param index - The index of the cell being rendered.
* @param isScrolling - True if the list is being scrolled. May be useful for rendering a placeholder if your cells
* are complex.
*/
onRenderCellConditional?: (item?: T, index?: number, isScrolling?: boolean) => React.ReactNode | null;
/**
* Optional callback invoked when List rendering completed.
* This can be on initial mount or on re-render due to scrolling.
* This method will be called as a result of changes in List pages (added or removed),
* and after ALL the changes complete.
* To track individual page Add / Remove use onPageAdded / onPageRemoved instead.
* @param pages - The current array of pages in the List.
*/
onPagesUpdated?: (pages: IPage<T>[]) => void;
/** Optional callback for monitoring when a page is added. */
onPageAdded?: (page: IPage<T>) => void;
/** Optional callback for monitoring when a page is removed. */
onPageRemoved?: (page: IPage<T>) => void;
/** Optional callback to get the item key, to be used on render. */
getKey?: (item: T, index?: number) => string;
/**
* Called by the list to get the specification for a page.
* Use this method to provide an allocation of items per page,
* as well as an estimated rendered height for the page.
* The list will use this to optimize virtualization.
*/
getPageSpecification?: (itemIndex?: number, visibleRect?: IRectangle, items?: T[]) => IPageSpecification;
/**
* Method called by the list to get how many items to render per page from specified index.
* In general, use `getPageSpecification` instead.
*/
getItemCountForPage?: (itemIndex?: number, visibleRect?: IRectangle) => number;
/**
* Method called by the list to get the pixel height for a given page. By default, we measure the first
* page's height and default all other pages to that height when calculating the surface space. It is
* ideal to be able to adequately predict page heights in order to keep the surface space from jumping
* in pixels, which has been seen to cause browser performance issues.
* In general, use `getPageSpecification` instead.
*/
getPageHeight?: (itemIndex?: number, visibleRect?: IRectangle, itemCount?: number, items?: T[]) => number;
/**
* Method called by the list to derive the page style object. For spacer pages, the list will derive
* the height and passed in heights will be ignored.
*/
getPageStyle?: (page: IPage<T>) => any;
/**
* In addition to the visible window, how many windowHeights should we render ahead.
* @defaultvalue 2
*/
renderedWindowsAhead?: number;
/**
* In addition to the visible window, how many windowHeights should we render behind.
* @defaultvalue 2
*/
renderedWindowsBehind?: number;
/**
* Index in `items` array to start rendering from.
* @default 0
*/
startIndex?: number;
/**
* Number of items to render.
* @default items.length
*/
renderCount?: number;
/**
* Boolean value to enable render page caching. This is an experimental performance optimization
* that is off by default.
* @defaultvalue false
*/
usePageCache?: boolean;
/**
* Optional callback to determine whether the list should be rendered in full, or virtualized.
* Virtualization will add and remove pages of items as the user scrolls them into the visible range.
* This benefits larger list scenarios by reducing the DOM on the screen, but can negatively affect performance for
* smaller lists.
* The default implementation will virtualize when this callback is not provided.
*/
onShouldVirtualize?: (props: IListProps<T>) => boolean;
/**
* The role to assign to the list root element.
* Use this to override the default assignment of 'list' to the root and 'listitem' to the cells.
*/
role?: string;
/**
* Called when the List will render a page.
* Override this to control how cells are rendered within a page.
*/
onRenderPage?: IRenderFunction<IPageProps<T>>;
/**
* Render override for the element at the root of the `List`.
* Use this to apply some final attributes or structure to the content
* each time the list is updated with new active pages or items.
*/
onRenderRoot?: IRenderFunction<IListOnRenderRootProps<T>>;
/**
* Render override for the element representing the surface of the `List`.
* Use this to alter the structure of the rendered content if necessary on each update.
*/
onRenderSurface?: IRenderFunction<IListOnRenderSurfaceProps<T>>;
/**
* For perf reasons, List avoids re-rendering unless certain props have changed.
* Use this prop if you need to force it to re-render in other cases. You can pass any type of
* value as long as it only changes (`===` comparison) when a re-render should happen.
*/
version?: any;
/**
* Whether to disable scroll state updates. This causes the isScrolling arg in onRenderCell to always be undefined.
* This is a performance optimization to let List skip a render cycle by not updating its scrolling state.
*/
ignoreScrollingState?: boolean;
/**
* Whether to render the list earlier than the default.
* Use this in scenarios where the list is contained in a FocusZone or FocusTrapZone
* as in a Dialog.
*/
renderEarly?: boolean;
}
/**
* {@docCategory List}
*/
export interface IPage<T = any> {
key: string;
items: T[] | undefined;
startIndex: number;
itemCount: number;
style: React.CSSProperties;
top: number;
height: number;
data?: any;
isSpacer?: boolean;
isVisible?: boolean;
}
/**
* {@docCategory List}
*/
export interface IPageProps<T = any> extends React.HTMLAttributes<HTMLDivElement>, React.ClassAttributes<HTMLDivElement> {
/**
* The role being assigned to the rendered page element by the list.
*/
role?: string;
/**
* The allocation data for the page.
*/
page: IPage<T>;
}
/**
* {@docCategory List}
*/
export interface IPageSpecification {
/**
* The number of items to allocate to the page.
*/
itemCount?: number;
/**
* The estimated pixel height of the page.
*/
height?: number;
/**
* Data to pass through to the page when rendering.
*/
data?: any;
/**
* The key to use when creating the page.
*/
key?: string;
}
@@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScrollToMode = void 0;
/**
* {@docCategory List}
*/
exports.ScrollToMode = {
/**
* Does not make any consideration to where in the viewport the item should align to.
*/
auto: 0,
/**
* Attempts to scroll the list so the top of the desired item is aligned with the top of the viewport.
*/
top: 1,
/**
* Attempts to scroll the list so the bottom of the desired item is aligned with the bottom of the viewport.
*/
bottom: 2,
/**
* Attempts to scroll the list so the desired item is in the exact center of the viewport.
*/
center: 3,
};
//# sourceMappingURL=List.types.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
export * from './List';
export * from './List.types';
+6
View File
@@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
tslib_1.__exportStar(require("./List"), exports);
tslib_1.__exportStar(require("./List.types"), exports);
//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"../src/","sources":["components/List/index.ts"],"names":[],"mappings":";;;AAAA,iDAAuB;AACvB,uDAA6B","sourcesContent":["export * from './List';\nexport * from './List.types';\n"]}
@@ -0,0 +1,25 @@
import { IRectangle } from '@fluentui/utilities';
import { IPage } from '../List.types';
export type RenderParams = {
visibleRect: IRectangle | undefined;
allowedRect: IRectangle | null;
requiredRect: IRectangle | null;
materializedRect: IRectangle | null;
surfaceRect: IRectangle | undefined;
totalListHeight: number;
pages: IPage[] | undefined;
scrollTop: number;
estimatedLine: number;
scrollY: number;
};
export declare class ListDebugRenderer {
private _wrapper;
private _renderer;
private _doc;
constructor(doc?: Document);
dispose(): void;
render(params: RenderParams): void;
private _renderRect;
private _renderPage;
private _renderLine;
}
@@ -0,0 +1,87 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ListDebugRenderer = void 0;
var RENDER_COLOR_VISIBLE_RECT = 'hotpink';
var RENDER_COLOR_ALLOWED_RECT = 'green';
var RENDER_COLOR_REQUIRED_RECT = 'blue';
var RENDER_COLOR_MATERIALIZED_RECT = 'red';
var RENDER_COLOR_SURFACE_RECT = 'black';
var RENDER_COLOR_PAGE = 'purple';
var RENDER_COLOR_SPACER_PAGE = 'orange';
var ListDebugRenderer = /** @class */ (function () {
function ListDebugRenderer(doc) {
// eslint-disable-next-line no-restricted-globals
this._doc = doc || document;
this._wrapper = this._doc.createElement('div');
this._wrapper.style.position = 'fixed';
this._wrapper.style.top = '0';
this._wrapper.style.right = '0';
this._wrapper.style.width = '300px';
this._wrapper.style.bottom = '0';
var canvas = this._doc.createElement('canvas');
this._renderer = canvas.getContext('2d');
this._wrapper.appendChild(canvas);
this._doc.body.appendChild(this._wrapper);
}
ListDebugRenderer.prototype.dispose = function () {
this._doc.body.removeChild(this._wrapper);
};
ListDebugRenderer.prototype.render = function (params) {
var _this = this;
var visibleRect = params.visibleRect, allowedRect = params.allowedRect, requiredRect = params.requiredRect, materializedRect = params.materializedRect, surfaceRect = params.surfaceRect, totalListHeight = params.totalListHeight, pages = params.pages, scrollTop = params.scrollTop, estimatedLine = params.estimatedLine, scrollY = params.scrollY;
if (!surfaceRect) {
return;
}
var debugRendererHeight = this._wrapper.clientHeight;
var scaleFactor = debugRendererHeight / totalListHeight;
this._renderer.canvas.width = this._wrapper.clientWidth;
this._renderer.canvas.height = debugRendererHeight;
this._renderer.fillStyle = 'white';
this._renderer.fillRect(0, 0, this._wrapper.clientWidth, this._wrapper.clientHeight);
this._renderRect({ left: 0, top: 0, height: totalListHeight, width: surfaceRect.width }, RENDER_COLOR_SURFACE_RECT, scaleFactor);
if (visibleRect) {
this._renderRect(visibleRect, RENDER_COLOR_VISIBLE_RECT, scaleFactor, 10);
}
if (allowedRect) {
this._renderRect(allowedRect, RENDER_COLOR_ALLOWED_RECT, scaleFactor, 20);
}
if (requiredRect) {
this._renderRect(requiredRect, RENDER_COLOR_REQUIRED_RECT, scaleFactor, 30);
}
if (materializedRect) {
this._renderRect(materializedRect, RENDER_COLOR_MATERIALIZED_RECT, scaleFactor, 40);
}
if (pages) {
var top_1 = 0;
pages.forEach(function (page, i) {
var isSpacer = page.key.startsWith('spacer');
var t = isSpacer ? top_1 : page.top;
_this._renderPage(page, isSpacer ? RENDER_COLOR_SPACER_PAGE : RENDER_COLOR_PAGE, scaleFactor, surfaceRect.left, surfaceRect.width, t, 50 + i * 10);
top_1 += page.height;
});
}
this._renderLine(scrollTop, 'red', surfaceRect.width);
this._renderLine(estimatedLine, 'black', surfaceRect.width);
this._renderLine(scrollY, 'yellow', surfaceRect.width);
};
ListDebugRenderer.prototype._renderRect = function (rect, color, scaleFactor, offset) {
if (offset === void 0) { offset = 0; }
this._renderer.strokeStyle = color;
this._renderer.strokeRect(rect.left * scaleFactor + offset, rect.top * scaleFactor, rect.width * scaleFactor + offset, rect.height * scaleFactor);
};
ListDebugRenderer.prototype._renderPage = function (page, color, scaleFactor, left, width, top, offset) {
if (offset === void 0) { offset = 0; }
this._renderer.strokeStyle = color;
this._renderer.strokeRect(left * scaleFactor + offset, top * scaleFactor, width * scaleFactor + offset, page.height * scaleFactor);
};
ListDebugRenderer.prototype._renderLine = function (y, color, width) {
this._renderer.strokeStyle = color;
this._renderer.beginPath();
this._renderer.moveTo(0, y);
this._renderer.lineTo(width, y);
this._renderer.stroke();
};
return ListDebugRenderer;
}());
exports.ListDebugRenderer = ListDebugRenderer;
//# sourceMappingURL=ListDebugRenderer.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
export declare const getScrollHeight: (el?: HTMLElement | Window) => number;
export declare const getScrollYPosition: (el?: HTMLElement | Window) => number;
export declare const setScrollYPosition: (el: HTMLElement | Window, pos: number) => void;
@@ -0,0 +1,46 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setScrollYPosition = exports.getScrollYPosition = exports.getScrollHeight = void 0;
var getScrollHeight = function (el) {
if (el === undefined) {
return 0;
}
var scrollHeight = 0;
if ('scrollHeight' in el) {
scrollHeight = el.scrollHeight;
}
else if ('document' in el) {
scrollHeight = el.document.documentElement.scrollHeight;
}
// No need to round as scrollHeight is already rounded for us.
// See: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
return scrollHeight;
};
exports.getScrollHeight = getScrollHeight;
var getScrollYPosition = function (el) {
if (el === undefined) {
return 0;
}
var scrollPos = 0;
if ('scrollTop' in el) {
scrollPos = el.scrollTop;
}
else if ('scrollY' in el) {
scrollPos = el.scrollY;
}
// Round this value to an integer as it may be fractional.
// See: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop
// See: https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY
return Math.ceil(scrollPos);
};
exports.getScrollYPosition = getScrollYPosition;
var setScrollYPosition = function (el, pos) {
if ('scrollTop' in el) {
el.scrollTop = pos;
}
else if ('scrollY' in el) {
el.scrollTo(el.scrollX, pos);
}
};
exports.setScrollYPosition = setScrollYPosition;
//# sourceMappingURL=scroll.js.map
@@ -0,0 +1 @@
{"version":3,"file":"scroll.js","sourceRoot":"../src/","sources":["components/List/utils/scroll.ts"],"names":[],"mappings":";;;AAAO,IAAM,eAAe,GAAG,UAAC,EAAyB;IACvD,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;QACrB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,IAAI,cAAc,IAAI,EAAE,EAAE,CAAC;QACzB,YAAY,GAAG,EAAE,CAAC,YAAY,CAAC;IACjC,CAAC;SAAM,IAAI,UAAU,IAAI,EAAE,EAAE,CAAC;QAC5B,YAAY,GAAG,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC,YAAY,CAAC;IAC1D,CAAC;IAED,8DAA8D;IAC9D,6EAA6E;IAC7E,OAAO,YAAY,CAAC;AACtB,CAAC,CAAC;AAfW,QAAA,eAAe,mBAe1B;AAEK,IAAM,kBAAkB,GAAG,UAAC,EAAyB;IAC1D,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;QACrB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,WAAW,IAAI,EAAE,EAAE,CAAC;QACtB,SAAS,GAAG,EAAE,CAAC,SAAS,CAAC;IAC3B,CAAC;SAAM,IAAI,SAAS,IAAI,EAAE,EAAE,CAAC;QAC3B,SAAS,GAAG,EAAE,CAAC,OAAO,CAAC;IACzB,CAAC;IAED,0DAA0D;IAC1D,0EAA0E;IAC1E,uEAAuE;IACvE,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAC9B,CAAC,CAAC;AAhBW,QAAA,kBAAkB,sBAgB7B;AAEK,IAAM,kBAAkB,GAAG,UAAC,EAAwB,EAAE,GAAW;IACtE,IAAI,WAAW,IAAI,EAAE,EAAE,CAAC;QACtB,EAAE,CAAC,SAAS,GAAG,GAAG,CAAC;IACrB,CAAC;SAAM,IAAI,SAAS,IAAI,EAAE,EAAE,CAAC;QAC3B,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC,CAAC;AANW,QAAA,kBAAkB,sBAM7B","sourcesContent":["export const getScrollHeight = (el?: HTMLElement | Window): number => {\n if (el === undefined) {\n return 0;\n }\n\n let scrollHeight = 0;\n if ('scrollHeight' in el) {\n scrollHeight = el.scrollHeight;\n } else if ('document' in el) {\n scrollHeight = el.document.documentElement.scrollHeight;\n }\n\n // No need to round as scrollHeight is already rounded for us.\n // See: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight\n return scrollHeight;\n};\n\nexport const getScrollYPosition = (el?: HTMLElement | Window): number => {\n if (el === undefined) {\n return 0;\n }\n\n let scrollPos = 0;\n if ('scrollTop' in el) {\n scrollPos = el.scrollTop;\n } else if ('scrollY' in el) {\n scrollPos = el.scrollY;\n }\n\n // Round this value to an integer as it may be fractional.\n // See: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop\n // See: https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY\n return Math.ceil(scrollPos);\n};\n\nexport const setScrollYPosition = (el: HTMLElement | Window, pos: number): void => {\n if ('scrollTop' in el) {\n el.scrollTop = pos;\n } else if ('scrollY' in el) {\n el.scrollTo(el.scrollX, pos);\n }\n};\n"]}