Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@uifabric/example-app-base",
"comment": "Add ability to disable scrolling for an ExampleCard",
"type": "minor"
}
],
"packageName": "@uifabric/example-app-base",
"email": "tmichon@microsoft.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@uifabric/experiments",
"comment": "Align Tiles in last row with previous rows",
"type": "patch"
}
],
"packageName": "@uifabric/experiments",
"email": "tmichon@microsoft.com"
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,18 @@
}

.ExampleCard-example {
-webkit-overflow-scrolling: touch;
max-height: 80vh;
overflow-x: hidden;
overflow-y: auto;
padding: 20px 4px;
position: relative;
}
&.is-scrollable {
-webkit-overflow-scrolling: touch;
max-height: 80vh;
overflow-x: hidden;
overflow-y: auto;
padding: 20px 4px;
position: relative;
}

.ExampleCard-example.is-right-aligned {
@include text-align(right);
&.is-right-aligned {
@include text-align(right);
}
}

.ExampleCard-code {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface IExampleCardProps {
isRightAligned?: boolean;
dos?: JSX.Element;
donts?: JSX.Element;
isScrollable?: boolean;
}

export interface IExampleCardState {
Expand All @@ -31,7 +32,7 @@ export class ExampleCard extends React.Component<IExampleCardProps, IExampleCard
}

public render(): JSX.Element {
const { title, code, children, isRightAligned } = this.props;
const { title, code, children, isRightAligned = false, isScrollable = true } = this.props;
const { isCodeVisible } = this.state;
let rootClass = 'ExampleCard' + (this.state.isCodeVisible ? ' is-codeVisible' : '');

Expand Down Expand Up @@ -66,9 +67,12 @@ export class ExampleCard extends React.Component<IExampleCardProps, IExampleCard
<div
className={ css(
'ExampleCard-example',
isRightAligned && ' is-right-aligned'
{
'is-right-aligned': isRightAligned,
'is-scrollable': isScrollable
}
) }
data-is-scrollable='true'
data-is-scrollable={ isScrollable }
>
{ children }
</div>
Expand Down
110 changes: 96 additions & 14 deletions packages/experiments/src/components/TilesList/TilesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface ITileCell<TItem> {
interface IRowData {
scaleFactor: number;
isLastRow?: boolean;
maxScaleFactor?: number;
}

interface IPageData<TItem> {
Expand Down Expand Up @@ -66,6 +67,10 @@ interface IPageSpecificationCache<TItem> {
width: number;
}

/**
* Component which renders a virtualized flexbox list of 'tiles', which have arbitrary width and height
* and which support scaling to fill rows when needed.
*/
export class TilesList<TItem> extends React.Component<ITilesListProps<TItem>, ITilesListState<TItem>> {
private _pageSpecificationCache: IPageSpecificationCache<TItem> | undefined;

Expand Down Expand Up @@ -167,6 +172,11 @@ export class TilesList<TItem> extends React.Component<ITilesListProps<TItem>, IT
);
}

/**
* Renders a single list page using a flexbox layout.
* By defualt, List provides no special formatting for a list page. For Tiles, the parent element
* needs flexbox metadata and padding to support the alignment rules.
*/
private _onRenderPage = (pageProps: IPageProps, defaultRender?: IRenderFunction<IPageProps>): JSX.Element => {
const {
page,
Expand Down Expand Up @@ -214,14 +224,29 @@ export class TilesList<TItem> extends React.Component<ITilesListProps<TItem>, IT

if (currentRow) {
const {
scaleFactor
scaleFactor,
isLastRow,
maxScaleFactor: currentRowMaxScaleFactor
} = currentRow;

if ((grid.mode === TilesGridMode.fill ||
if (currentRowMaxScaleFactor) {
// If the current row has its own max scale factor,
// compute final size from the provided value.
const finalScaleFactor = Math.min(currentRowMaxScaleFactor, grid.maxScaleFactor);

finalSize = {
width: finalSize.width * finalScaleFactor,
height: grid.mode === TilesGridMode.fill ?
finalSize.height * finalScaleFactor :
grid.minRowHeight
};
} else if ((grid.mode === TilesGridMode.fill ||
grid.mode === TilesGridMode.fillHorizontal) &&
(!currentRow.isLastRow ||
scaleFactor <= grid.maxScaleFactor)) {
const finalScaleFactor = Math.min(grid.maxScaleFactor, scaleFactor);
(!isLastRow || scaleFactor <= grid.maxScaleFactor)) {
// Compute the final size from the overall max scale factor, if present.
const finalScaleFactor = Math.min(
grid.maxScaleFactor,
scaleFactor);

finalSize = {
width: finalSize.width * finalScaleFactor,
Expand All @@ -241,9 +266,7 @@ export class TilesList<TItem> extends React.Component<ITilesListProps<TItem>, IT
}) }
// tslint:disable-next-line:jsx-ban-props
style={
{
...this._onGetCellStyle(cell, currentRow)
}
this._onGetCellStyle(cell, currentRow)
}
>
{ this._onRenderCell(cell, finalSize) }
Expand Down Expand Up @@ -272,7 +295,7 @@ export class TilesList<TItem> extends React.Component<ITilesListProps<TItem>, IT
}
}
>
{ ...renderedCells }
{ renderedCells }
</div>
);
}
Expand All @@ -287,6 +310,12 @@ export class TilesList<TItem> extends React.Component<ITilesListProps<TItem>, IT
);
}

/**
* 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.
*/
private _getPageSpecification = (startIndex: number, bounds: IRectangle): {
itemCount: number;
data: IPageData<TItem>;
Expand All @@ -307,6 +336,9 @@ export class TilesList<TItem> extends React.Component<ITilesListProps<TItem>, IT
const 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];
}

Expand Down Expand Up @@ -379,6 +411,7 @@ export class TilesList<TItem> extends React.Component<ITilesListProps<TItem>, IT
if (rowWidth > boundsWidth) {
rowWidth = width;
rowStart = i;
// Add a marker for a new row, with the default scale factor.
currentRow = startCells[i] = {
scaleFactor: 1
};
Expand All @@ -395,6 +428,35 @@ export class TilesList<TItem> extends React.Component<ITilesListProps<TItem>, IT
if (rowWidth < boundsWidth) {
const totalMargin = grid.spacing * (i - rowStart);
currentRow.scaleFactor = (boundsWidth - totalMargin) / (rowWidth - totalMargin);

if ((grid.mode === TilesGridMode.fill || grid.mode === TilesGridMode.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.
const width = (rowWidth - totalMargin) / (i - rowStart) + grid.spacing;

let phantomRowWidth = rowWidth;

for (let j = i; ; j++) {
if (phantomRowWidth + width > boundsWidth) {
// The final phantom item has been added, so the row is complete.
const 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 > (
Expand Down Expand Up @@ -434,6 +496,10 @@ export class TilesList<TItem> extends React.Component<ITilesListProps<TItem>, IT
return TilesListStyles.listPage;
}

/**
* Get the style to be applied to a single list cell, which will specify the flex behavior
* within the flexbox layout.
*/
private _onGetCellStyle = (item: ITileCell<TItem>, currentRow?: IRowData): React.CSSProperties => {
const {
grid: {
Expand All @@ -452,22 +518,37 @@ export class TilesList<TItem> extends React.Component<ITilesListProps<TItem>, IT
const isFill = gridMode === TilesGridMode.fill || gridMode === TilesGridMode.fillHorizontal;
const width = itemWidthOverHeight * grid.minRowHeight;

let maxWidth: number;

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: isFill && (!currentRow || !currentRow.isLastRow || currentRow.scaleFactor <= maxScaleFactor) ?
// Flexbox can scale the item to the maximum ratio.
`${width * maxScaleFactor}px` :
// The item must not be scaled.
`${width}px`,
maxWidth: `${maxWidth}px`,
margin: `${margin}px`
};
}

/**
* 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.
*/
private _getCells(items: (ITilesGridSegment<TItem> | ITilesGridItem<TItem>)[]): ITileCell<TItem>[] {
const cells: ITileCell<TItem>[] = [];

for (const item of items) {
if (isGridSegment(item)) {
// The item is a grid of child items.
const {
spacing = 0,
maxScaleFactor = MAX_TILE_STRETCH,
Expand Down Expand Up @@ -507,6 +588,7 @@ export class TilesList<TItem> extends React.Component<ITilesListProps<TItem>, IT
});
}
} else {
// The item is not part of the grid, and should take up a whole row.
cells.push({
aspectRatio: 1,
content: item.content,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ export class TilesListPage extends React.Component<IComponentDemoPageProps, {}>
componentName='TilesListExample'
exampleCards={
<div>
<ExampleCard title='TilesList with basic tiles' isOptIn={ true } code={ TilesListBasicExampleCode }>
<ExampleCard title='TilesList with basic tiles' isOptIn={ true } isScrollable={ false } code={ TilesListBasicExampleCode }>
<TilesListBasicExample />
</ExampleCard>
<ExampleCard title='TilesList with document tiles' isOptIn={ true } code={ TilesListDocumentExampleCode }>
<ExampleCard title='TilesList with document tiles' isOptIn={ true } isScrollable={ false } code={ TilesListDocumentExampleCode }>
<TilesListDocumentExample />
</ExampleCard>
<ExampleCard title='TilesList with media tiles' isOptIn={ true } code={ TilesListMediaExampleCode }>
<ExampleCard title='TilesList with media tiles' isOptIn={ true } isScrollable={ false } code={ TilesListMediaExampleCode }>
<TilesListMediaExample />
</ExampleCard>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
TilesList,
ITilesGridSegment,
ITilesGridItem,
TilesGridMode
TilesGridMode,
ITileSize
} from '../../TilesList';

export interface IBasicItem {
Expand All @@ -14,7 +15,7 @@ export interface IBasicItem {

const ITEMS: IBasicItem[] = [];

for (let i = 0; i < 500; i++) {
for (let i = 0; i < 27; i++) {
ITEMS.push({
color: ['red', 'blue', 'green', 'yellow', 'orange', 'brown', 'purple', 'gray'][Math.floor(Math.random() * 8)],
key: `item-${i}`
Expand Down Expand Up @@ -60,20 +61,25 @@ export class TilesListBasicExample extends React.Component<{}, ITilesListBasicEx
}
}

function renderItem(item: IBasicItem): JSX.Element {
function renderItem(item: IBasicItem, finalSize?: ITileSize): JSX.Element {
return (
<div
// tslint:disable-next-line:jsx-ban-props
style={
{
position: 'absolute',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
top: '0',
left: '0',
bottom: '0',
right: '0',
backgroundColor: item.color
}
}
/>
>
<span>{ finalSize ? `${finalSize.width.toFixed(1)}x${finalSize.height.toFixed(1)}` : '' }</span>
</div>
);
}