@uifabric/experiments
Version:
Experimental React components for building experiences for Microsoft 365.
576 lines • 30.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var React = require("react");
var List_1 = require("office-ui-fabric-react/lib-commonjs/List");
var FocusZone_1 = require("office-ui-fabric-react/lib-commonjs/FocusZone");
var Utilities_1 = require("office-ui-fabric-react/lib-commonjs/Utilities");
var TilesListStylesModule = require("./TilesList.scss");
var Shimmer_1 = require("office-ui-fabric-react/lib-commonjs/Shimmer");
var TilesListStyles = TilesListStylesModule;
var MAX_TILE_STRETCH = 1.5;
var CELLS_PER_PAGE = 100;
var MIN_ASPECT_RATIO = 0.5;
var MAX_ASPECT_RATIO = 3;
var ROWS_OF_PLACEHOLDER_CELLS = 3;
/**
* Component which renders a virtualized flexbox list of 'tiles', which have arbitrary width and height
* and which support scaling to fill rows when needed.
*/
var TilesList = /** @class */ (function (_super) {
tslib_1.__extends(TilesList, _super);
function TilesList(props, context) {
var _this = _super.call(this, props, context) || this;
_this._onRenderListRoot = function (props, defaultRender) {
var onRenderRoot = _this.props.onRenderRoot;
if (!defaultRender) {
return null;
}
var pages = props.pages;
var rowCount = 0;
var maxColCount = 0;
for (var _i = 0, pages_1 = pages; _i < pages_1.length; _i++) {
var page = pages_1[_i];
var data = page.data;
if (data) {
for (var _a = 0, _b = Object.keys(data.rows); _a < _b.length; _a++) {
var key = _b[_a];
var rowData = data.rows[Number(key)];
rowCount++;
maxColCount = Math.max(maxColCount, rowData.cellCount);
}
}
}
var baseOnRenderRoot = function (baseProps) {
return defaultRender(tslib_1.__assign(tslib_1.__assign({}, props), { divProps: tslib_1.__assign(tslib_1.__assign({}, props.divProps), baseProps.divProps), surfaceElement: baseProps.surfaceElement }));
};
var finalOnRenderRoot = onRenderRoot ? Utilities_1.composeRenderFunction(onRenderRoot, baseOnRenderRoot) : baseOnRenderRoot;
return finalOnRenderRoot({
surfaceElement: props.surfaceElement,
divProps: props.divProps,
rowCount: rowCount,
columnCount: maxColCount,
});
};
/**
* Renders a single list page using a flexbox layout.
* By default, List provides no special formatting for a list page. For Tiles, the parent element
* needs flexbox metadata and padding to support the alignment rules.
*/
_this._onRenderPage = function (pageProps, defaultRender) {
if (!pageProps) {
return null;
}
var _a = _this.props, role = _a.role, onRenderRow = _a.onRenderRow;
var finalOnRenderRow = onRenderRow ? Utilities_1.composeRenderFunction(onRenderRow, _this._renderRow) : _this._renderRow;
var page = pageProps.page, pageClassName = pageProps.className, divProps = tslib_1.__rest(pageProps, ["page", "className"]);
var items = page.items;
var data = page.data;
var cells = items || [];
var grids = [];
var previousCell = _this.state.cells[page.startIndex - 1];
var nextCell = _this.state.cells[page.startIndex + page.itemCount];
var endIndex = cells.length;
var currentRow;
var currentRowCells = [];
var shimmerWrapperWidth = 0;
var _loop_1 = function (i) {
var _a, _b;
// For each cell at the start of a grid.
var grid = cells[i].grid;
var isPlaceholder = grid.isPlaceholder, maxRowCount = grid.maxRowCount;
var renderedCells = [];
var width = data.pageWidths[page.startIndex + i];
var rowCount = 0;
var isAtMaxRowCount = false;
var columnIndex = 0;
var _loop_2 = function () {
// For each cell in the current grid.
var cell = cells[i];
var index = page.startIndex + i;
var cellAsFirstRow = data.rows[index];
if (cellAsFirstRow) {
if (currentRowCells.length > 0) {
renderedCells.push(finalOnRenderRow({
cellElements: currentRowCells,
divProps: {
className: TilesListStyles.row,
role: 'presentation',
},
}));
currentRowCells = [];
}
if (cellAsFirstRow !== currentRow) {
rowCount++;
}
if (typeof maxRowCount === 'number' && rowCount > maxRowCount) {
isAtMaxRowCount = true;
return "break";
}
currentRow = cellAsFirstRow;
columnIndex = 0;
}
var finalSize = data.cellSizes[index];
if (currentRow) {
var scaleFactor = currentRow.scaleFactor, isLastRow = currentRow.isLastRow, currentRowMaxScaleFactor = currentRow.maxScaleFactor;
if (currentRowMaxScaleFactor) {
// If the current row has its own max scale factor,
// compute final size from the provided value.
var finalScaleFactor = Math.min(currentRowMaxScaleFactor, grid.maxScaleFactor);
finalSize = {
width: finalSize.width * finalScaleFactor,
height: grid.mode === 2 /* fill */ ? finalSize.height * finalScaleFactor : grid.minRowHeight,
};
}
else if ((grid.mode === 2 /* fill */ || grid.mode === 3 /* fillHorizontal */) &&
(!isLastRow || scaleFactor <= grid.maxScaleFactor)) {
// Compute the final size from the overall max scale factor, if present.
var finalScaleFactor = Math.min(grid.maxScaleFactor, scaleFactor);
finalSize = {
width: finalSize.width * finalScaleFactor,
height: grid.mode === 2 /* fill */ ? finalSize.height * finalScaleFactor : grid.minRowHeight,
};
}
}
var renderedCell = function (keyOffset) {
var _a;
if (keyOffset === void 0) { keyOffset = 0; }
return (React.createElement("div", { key: grid.key + "-item-" + cell.key + (keyOffset ? '-' + keyOffset : ''), "data-list-index": index, role: role ? 'presentation' : 'listitem', className: Utilities_1.css('ms-List-cell', _this._onGetCellClassName(), (_a = {},
_a["ms-TilesList-cell--firstInRow " + TilesListStyles.cellFirstInRow] = !!cellAsFirstRow,
_a)), "data-automationid": "ListCell", style: tslib_1.__assign({}, _this._onGetCellStyle(cell, currentRow)) }, _this._onRenderCell(cell, finalSize, columnIndex + keyOffset)));
};
if (cell.isPlaceholder && grid.mode !== 0 /* none */) {
var cellsPerRow = Math.floor(width / (grid.spacing + finalSize.width));
var totalPlaceholderItems = cellsPerRow * ROWS_OF_PLACEHOLDER_CELLS;
shimmerWrapperWidth = cellsPerRow * finalSize.width + grid.spacing * (cellsPerRow - 1);
for (var j = 0; j < totalPlaceholderItems; j++) {
currentRowCells.push(renderedCell(j));
}
}
else {
shimmerWrapperWidth = finalSize.width / 3;
currentRowCells.push(renderedCell());
}
columnIndex++;
};
for (; i < endIndex && cells[i].grid === grid; i++) {
var state_1 = _loop_2();
if (state_1 === "break")
break;
}
if (isAtMaxRowCount) {
for (; i < endIndex && cells[i].grid === grid; i++) {
// Consume the rest of the grid.
}
}
var isOpenStart = previousCell && previousCell.grid === grid;
var isOpenEnd = nextCell && nextCell.grid === grid;
var margin = grid.spacing / 2;
if (currentRowCells.length > 0) {
renderedCells.push(finalOnRenderRow({
cellElements: currentRowCells,
divProps: {
className: Utilities_1.css(TilesListStyles.row, (_a = {},
_a[TilesListStyles.headerRow] = grid.mode === 0 /* none */,
_a)),
role: 'presentation',
},
}));
currentRowCells = [];
}
var finalGrid = (React.createElement("div", { key: grid.key, role: "presentation", className: Utilities_1.css('ms-TilesList-grid', (_b = {},
_b["" + TilesListStyles.grid] = grid.mode !== 0 /* none */,
_b["" + TilesListStyles.shimmeredList] = isPlaceholder,
_b)), style: {
width: width + "px",
margin: -margin + "px",
marginTop: isOpenStart ? '0' : grid.marginTop - margin + "px",
marginBottom: isOpenEnd ? '0' : grid.marginBottom - margin + "px",
} }, renderedCells));
grids.push(isPlaceholder ? React.createElement(Shimmer_1.Shimmer, { key: i, customElementsGroup: finalGrid, width: shimmerWrapperWidth }) : finalGrid);
out_i_1 = i;
};
var out_i_1;
for (var i = 0; i < endIndex;) {
_loop_1(i);
i = out_i_1;
}
return (React.createElement("div", tslib_1.__assign({ role: "presentation" }, divProps, { className: Utilities_1.css(pageClassName, _this._onGetPageClassName()) }), grids));
};
/**
* Gets the specification for the list page, which requires pre-calculating the flexbox layout
* to determine the set of tiles which fit neatly within a rectangle. Any tiles left dangling
* at the end of a page are overflowed into the next page unless they are just before a grid
* boundary.
*/
_this._getPageSpecification = function (startIndex, bounds) {
if (_this._pageSpecificationCache) {
if (_this._pageSpecificationCache.width !== bounds.width) {
_this._pageSpecificationCache = undefined;
}
}
if (!_this._pageSpecificationCache) {
_this._pageSpecificationCache = {
width: bounds.width,
byIndex: {},
};
}
var pageSpecificationCache = _this._pageSpecificationCache;
if (pageSpecificationCache.byIndex[startIndex]) {
// If the page specification has already been calculated, return it.
// List recalculates all pages if any input changes, so this memoization
// cuts down on calculation of individual pages without changes.
return pageSpecificationCache.byIndex[startIndex];
}
var cells = _this.state.cells;
var _a = _this.props.cellsPerPage, cellsPerPage = _a === void 0 ? CELLS_PER_PAGE : _a;
var endIndex = Math.min(cells.length, startIndex + cellsPerPage);
var rowWidth = 0;
var rowStart = 0;
var i = startIndex;
var isAtGridEnd = true;
var startCells = {};
var extraCells;
var cellSizes = {};
var widths = {};
for (; i < endIndex;) {
// For each cell at the start of a grid.
var grid = cells[i].grid;
var maxRowCount = grid.maxRowCount;
rowWidth = 0;
rowStart = i;
var boundsWidth = bounds.width + grid.spacing;
widths[i] = boundsWidth;
var currentRow = (startCells[i] = {
scaleFactor: 1,
cellCount: 0,
});
if (grid.mode === 0 /* none */) {
// The current "grid" just takes up the full width.
// No flex calculations necessary.
isAtGridEnd = true;
cellSizes[i] = {
width: bounds.width,
height: 0,
};
currentRow.cellCount++;
i++;
continue;
}
var rowCount = 0;
var isAtMaxRowCount = false;
for (; i < endIndex && cells[i].grid === grid; i++) {
if (typeof maxRowCount === 'number' && rowCount >= maxRowCount) {
isAtMaxRowCount = true;
break;
}
// For each cell in the current grid.
var aspectRatio = cells[i].aspectRatio;
var width = aspectRatio * grid.minRowHeight + grid.spacing;
if (rowWidth + width > boundsWidth) {
var totalMargin = grid.spacing * (i - rowStart);
currentRow.scaleFactor = (boundsWidth - totalMargin) / (rowWidth - totalMargin);
}
rowWidth += width;
cellSizes[i] = {
// Assign the expected base size of the cell.
// Scaling will be handled at render time.
width: aspectRatio * grid.minRowHeight,
height: grid.minRowHeight,
};
if (rowWidth > boundsWidth) {
rowWidth = width;
rowStart = i;
// Add a marker for a new row, with the default scale factor.
currentRow = startCells[i] = {
scaleFactor: 1,
cellCount: 0,
};
rowCount++;
}
currentRow.cellCount++;
}
if (!cells[i] || cells[i].grid !== grid || isAtMaxRowCount) {
// If the next cell is part of a different grid.
currentRow.isLastRow = true;
}
else {
isAtGridEnd = false;
}
if (rowWidth < boundsWidth) {
var totalMargin = grid.spacing * (i - rowStart);
currentRow.scaleFactor = (boundsWidth - totalMargin) / (rowWidth - totalMargin);
if ((grid.mode === 2 /* fill */ || grid.mode === 3 /* fillHorizontal */) && currentRow.isLastRow) {
if (i - rowStart > 0) {
// If the grid is in 'fill' mode, and there is underflow in the last row, then by default, flexbox will
// scale all widths to the maximum possible, which may cause regularly-sized items to be larger than
// those in previous rows.
// A way to counter that is to pretend that the last row is actually filled with more items, and calculate
// the resulting scale factor. Then pass the new maximum width to flexbox.
// The result should be perfectly-aligned final items.
// The 'phantom' items are not actually rendered in the list.
// Project the average tile width across the rest of the row.
var width = (rowWidth - totalMargin) / (i - rowStart) + grid.spacing;
var phantomRowWidth = rowWidth;
for (var j = i;; j++) {
if (phantomRowWidth + width > boundsWidth) {
// The final phantom item has been added, so the row is complete.
var phantomTotalMargin = grid.spacing * (j - rowStart);
// Set the new scale factor based on the total width including the phantom items.
currentRow.maxScaleFactor = (boundsWidth - phantomTotalMargin) / (phantomRowWidth - phantomTotalMargin);
break;
}
phantomRowWidth += width;
}
}
}
}
if (!isAtGridEnd &&
currentRow.scaleFactor >
(grid.mode === 2 /* fill */ || grid.mode === 3 /* fillHorizontal */ ? grid.maxScaleFactor : 1)) {
// If the last computed row is not the end of the grid, and the content cannot scale to fit the width,
// declare these cells as 'extra' and let them be pushed into the next page.
extraCells = cells.slice(rowStart, i);
}
if (isAtMaxRowCount) {
while (i < cells.length && cells[i].grid === grid) {
// Consume the rest of the cells in the grid if the max row count has been achieved.
i++;
}
}
}
// If there are extra cells, cut off the page so the extra cells will be pushed into the next page.
// Otherwise, take all the cells.
var itemCount = i - (extraCells ? extraCells.length : 0) - startIndex;
var pageSpecification = {
itemCount: itemCount,
data: {
pageWidths: widths,
rows: startCells,
extraCells: extraCells,
cellSizes: cellSizes,
},
};
pageSpecificationCache.byIndex[startIndex] = pageSpecification;
return pageSpecification;
};
_this._renderRow = function (props) {
var cellElements = props.cellElements, divProps = props.divProps;
return (React.createElement("div", tslib_1.__assign({ role: "presentation" }, divProps), cellElements));
};
_this._onGetCellClassName = function () {
return TilesListStyles.listCell;
};
_this._onGetPageClassName = function () {
return TilesListStyles.listPage;
};
/**
* Get the style to be applied to a single list cell, which will specify the flex behavior
* within the flexbox layout.
*/
_this._onGetCellStyle = function (item, currentRow) {
var _a = item.grid, gridMode = _a.mode, maxScaleFactor = _a.maxScaleFactor, grid = item.grid;
if (gridMode === 0 /* none */) {
return {};
}
var itemWidthOverHeight = item.aspectRatio || 1;
var margin = grid.spacing / 2;
var isFill = gridMode === 2 /* fill */ || gridMode === 3 /* fillHorizontal */;
var width = itemWidthOverHeight * grid.minRowHeight;
var maxWidth;
if (currentRow && currentRow.maxScaleFactor) {
// If the row has its own max scale factor, force flexbox to limit at that value.
// This typically happens if there is underflow in the final row of a grid.
maxWidth = width * Math.min(currentRow.maxScaleFactor, maxScaleFactor);
}
else if (isFill && (!currentRow || !currentRow.isLastRow || currentRow.scaleFactor <= maxScaleFactor)) {
// If the entire grid has a max scale factor, use that limit.
maxWidth = width * maxScaleFactor;
}
else {
maxWidth = width;
}
return {
flex: isFill ? itemWidthOverHeight + " " + itemWidthOverHeight + " " + width + "px" : "0 0 " + width + "px",
maxWidth: maxWidth + "px",
margin: !item.isPlaceholder ? margin + "px" : 0,
borderStyle: item.isPlaceholder ? 'solid' : 'none',
borderWidth: item.isPlaceholder ? margin + "px" : 0,
};
};
_this.listRef = React.createRef();
_this.state = {
cells: _this._getCells(props.items),
};
return _this;
}
TilesList.prototype.UNSAFE_componentWillReceiveProps = function (nextProps) {
if (nextProps.items !== this.props.items) {
this.setState({
cells: this._getCells(nextProps.items),
});
}
};
TilesList.prototype.UNSAFE_componentWillUpdate = function (nextProps, nextState) {
if (nextState.cells !== this.state.cells) {
this._pageSpecificationCache = undefined;
}
};
TilesList.prototype.render = function () {
var cells = this.state.cells;
var _a = this.props, className = _a.className, onActiveElementChanged = _a.onActiveElementChanged, items = _a.items, cellsPerPage = _a.cellsPerPage, ref = _a.ref, role = _a.role, focusZoneComponentRef = _a.focusZoneComponentRef, _b = _a.listProps, listProps = _b === void 0 ? {} : _b, divProps = tslib_1.__rest(_a, ["className", "onActiveElementChanged", "items", "cellsPerPage", "ref", "role", "focusZoneComponentRef", "listProps"]);
var onRenderRoot = listProps.onRenderRoot, onRenderPage = listProps.onRenderPage, otherListProps = tslib_1.__rest(listProps, ["onRenderRoot", "onRenderPage"]);
var finalOnRenderRoot = onRenderRoot
? Utilities_1.composeRenderFunction(onRenderRoot, this._onRenderListRoot)
: this._onRenderListRoot;
var finalOnRenderPage = onRenderPage
? Utilities_1.composeRenderFunction(onRenderPage, this._onRenderPage)
: this._onRenderPage;
return (React.createElement(FocusZone_1.FocusZone, tslib_1.__assign({}, divProps, { ref: ref, componentRef: focusZoneComponentRef, className: Utilities_1.css('ms-TilesList', className), direction: FocusZone_1.FocusZoneDirection.bidirectional, onActiveElementChanged: this.props.onActiveElementChanged }),
React.createElement(Utilities_1.FocusRects, null),
React.createElement(List_1.List, tslib_1.__assign({ items: cells, role: role, onRenderRoot: finalOnRenderRoot, getPageSpecification: this._getPageSpecification, onRenderPage: finalOnRenderPage, ref: this.listRef, usePageCache: true }, otherListProps))));
};
TilesList.prototype.scrollToIndex = function (index, mode) {
var _this = this;
if (mode === void 0) { mode = List_1.ScrollToMode.auto; }
if (this.listRef && this.listRef.current) {
if (this.state.cells[index].grid.mode === 0 /* none */) {
// if we are using grid mode none, we reliably know the height of the cell,
// so we can implement the measureItem callback.
this.listRef.current.scrollToIndex(index, function (itemIndex) {
var cell = _this.state.cells[index];
if (cell && cell.desiredHeight !== undefined) {
return cell.desiredHeight;
}
return 0;
}, mode);
}
else {
// otherwise, we do not implement the measure item callback,
// then the List will just scroll to the nearest page
this.listRef.current.scrollToIndex(index, undefined, mode);
}
}
};
TilesList.prototype.getTotalListHeight = function () {
if (this.listRef && this.listRef.current && this.listRef.current.getTotalListHeight) {
return this.listRef.current.getTotalListHeight();
}
return 0; // Stub
};
TilesList.prototype._onRenderCell = function (item, finalSize, column) {
if (item.grid.mode === 0 /* none */) {
return (React.createElement("div", { role: "presentation", className: Utilities_1.css(TilesListStyles.header) }, item.onRender({
item: item.content,
finalSize: { width: 0, height: 0 },
position: {
column: column,
},
})));
}
var itemWidthOverHeight = item.aspectRatio;
var itemHeightOverWidth = 1 / itemWidthOverHeight;
return (React.createElement("div", { role: "presentation", className: Utilities_1.css(TilesListStyles.cell), style: item.grid.mode === 3 /* fillHorizontal */
? {
height: item.grid.minRowHeight + "px",
}
: {
paddingTop: (100 * itemHeightOverWidth).toFixed(2) + "%",
} },
React.createElement("div", { role: "presentation", className: Utilities_1.css(TilesListStyles.cellContent) }, item.onRender({
item: item.content,
finalSize: finalSize,
position: {
column: column,
},
}))));
};
/**
* Flattens the grid and item specifications into a cell list. List will partition the cells into
* pages use `getPageSpecification`, so each cell is marked up with metadata to assist the flexbox
* algorithm.
*/
TilesList.prototype._getCells = function (items) {
var cells = [];
var _loop_3 = function (item) {
if (isGridSegment(item)) {
// The item is a grid of child items.
var _a = item.spacing, spacing = _a === void 0 ? 0 : _a, _b = item.maxScaleFactor, maxScaleFactor = _b === void 0 ? MAX_TILE_STRETCH : _b, _c = item.marginBottom, marginBottom = _c === void 0 ? 0 : _c, _d = item.marginTop, marginTop = _d === void 0 ? 0 : _d, _e = item.minAspectRatio, minAspectRatio = _e === void 0 ? MIN_ASPECT_RATIO : _e, _f = item.maxAspectRatio, maxAspectRatio = _f === void 0 ? MAX_ASPECT_RATIO : _f;
var grid = {
minRowHeight: item.minRowHeight,
spacing: spacing,
mode: item.mode,
key: "grid-" + item.key,
maxScaleFactor: maxScaleFactor,
marginTop: item.isPlaceholder ? 0 : marginTop,
marginBottom: item.isPlaceholder ? 0 : marginBottom,
isPlaceholder: item.isPlaceholder,
maxRowCount: item.maxRowCount,
};
var _loop_4 = function (gridItem) {
var desiredSize = gridItem.desiredSize, itemOnRender = gridItem.onRender, onRenderCell = gridItem.onRenderCell;
var aspectRatio = Math.min(maxAspectRatio, Math.max(minAspectRatio, (desiredSize && desiredSize.width / desiredSize.height) || 1));
var onRender = onRenderCell ||
(function (props) {
if (!itemOnRender) {
return null;
}
return itemOnRender(props.item, props.finalSize);
});
cells.push({
aspectRatio: aspectRatio,
content: gridItem.content,
onRender: onRender,
grid: grid,
key: gridItem.key,
isPlaceholder: gridItem.isPlaceholder,
desiredHeight: desiredSize ? desiredSize.height : undefined,
});
};
for (var _i = 0, _g = item.items; _i < _g.length; _i++) {
var gridItem = _g[_i];
_loop_4(gridItem);
}
}
else {
var onRenderCell = item.onRenderCell, itemOnRender_1 = item.onRender;
var onRender = onRenderCell ||
(function (props) {
if (!itemOnRender_1) {
return null;
}
return itemOnRender_1(props.item, props.finalSize);
});
// The item is not part of the grid, and should take up a whole row.
cells.push({
aspectRatio: 1,
content: item.content,
onRender: onRender,
grid: {
minRowHeight: 0,
spacing: 0,
mode: 0 /* none */,
key: "grid-header-" + item.key,
maxScaleFactor: 1,
marginBottom: 0,
marginTop: 0,
isPlaceholder: item.isPlaceholder,
},
key: "header-" + item.key,
isPlaceholder: item.isPlaceholder,
});
}
};
for (var _i = 0, items_1 = items; _i < items_1.length; _i++) {
var item = items_1[_i];
_loop_3(item);
}
return cells;
};
return TilesList;
}(React.Component));
exports.TilesList = TilesList;
function isGridSegment(item) {
return !!item.items;
}
//# sourceMappingURL=TilesList.js.map