diff --git a/common/changes/@uifabric/experiments/v-vibr-PlaceholderTile_2018-04-10-06-48.json b/common/changes/@uifabric/experiments/v-vibr-PlaceholderTile_2018-04-10-06-48.json new file mode 100644 index 00000000000000..a9a6c24c8de698 --- /dev/null +++ b/common/changes/@uifabric/experiments/v-vibr-PlaceholderTile_2018-04-10-06-48.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/experiments", + "comment": "Refactoring Shimmer and adding ShimmerTile + Implements Shimmer in TilesList.", + "type": "minor" + } + ], + "packageName": "@uifabric/experiments", + "email": "v-vibr@microsoft.com" +} \ No newline at end of file diff --git a/packages/experiments/src/components/Shimmer/Shimmer.base.tsx b/packages/experiments/src/components/Shimmer/Shimmer.base.tsx index 4d7c4b60eea86b..af50bd05af2d11 100644 --- a/packages/experiments/src/components/Shimmer/Shimmer.base.tsx +++ b/packages/experiments/src/components/Shimmer/Shimmer.base.tsx @@ -18,80 +18,38 @@ import { IStyleSet } from '../../Styling'; import { ShimmerLine } from './ShimmerLine/ShimmerLine'; +import { ShimmerGap } from './ShimmerGap/ShimmerGap'; import { ShimmerCircle } from './ShimmerCircle/ShimmerCircle'; const LINE_DEFAULT_HEIGHT = 16; +const GAP_DEFAULT_HEIGHT = 16; const CIRCLE_DEFAULT_HEIGHT = 24; const getClassNames = classNamesFunction(); export class ShimmerBase extends BaseComponent { public static defaultProps: IShimmerProps = { - width: 100, isDataLoaded: false, isBaseStyle: false }; - private _classNames: {[key in keyof IShimmerStyles]: string}; + private _classNames: { [key in keyof IShimmerStyles]: string }; constructor(props: IShimmerProps) { super(props); } public render(): JSX.Element { const { getStyles, width, lineElements, children, isDataLoaded, isBaseStyle } = this.props; - const maxHeight: number | undefined = lineElements ? this._findMaxHeight(lineElements) : undefined; - this._classNames = getClassNames(getStyles!, { width, maxHeight, isDataLoaded, isBaseStyle }); - - const elements: JSX.Element[] | JSX.Element = lineElements ? - lineElements.map((elem: ICircle | ILine | IGap, index: number): JSX.Element => { - switch (elem.type) { - case ShimmerElementType.CIRCLE: - if (!elem.height) { - elem.height = CIRCLE_DEFAULT_HEIGHT; - } - return ( - - ); - case ShimmerElementType.GAP: - const gapWidth = elem.widthInPercentage || elem.widthInPixel ? - elem.widthInPercentage ? elem.widthInPercentage + '%' : elem.widthInPixel + 'px' - : '5px'; - return ( -
- ); - case ShimmerElementType.LINE: - if (!elem.height) { - elem.height = LINE_DEFAULT_HEIGHT; - } - return ( - - ); - } - }) : ( - - ); + + const rowHeight: number | undefined = lineElements ? findMaxElementHeight(lineElements) : undefined; + + this._classNames = getClassNames(getStyles!, { width, rowHeight, isDataLoaded, isBaseStyle }); + + const renderedElements: React.ReactNode = getRenderedElements(lineElements, rowHeight); return (
- { !!isBaseStyle ? children : elements } + { !!isBaseStyle ? children : renderedElements }
{ !!isDataLoaded && @@ -102,60 +60,97 @@ export class ShimmerBase extends BaseComponent {
); } +} - private _findMaxHeight(items: Array): number { - const itemsDefaulted: Array = items.map((item: ICircle | IGap | ILine): ICircle | IGap | ILine => { - switch (item.type) { +export function getRenderedElements(lineElements?: Array, rowHeight?: number): React.ReactNode { + const renderedElements: React.ReactNode = lineElements ? + lineElements.map((elem: ICircle | ILine | IGap, index: number): JSX.Element => { + switch (elem.type) { case ShimmerElementType.CIRCLE: - if (!item.height) { - item.height = CIRCLE_DEFAULT_HEIGHT; - } + return ( + + ); + case ShimmerElementType.GAP: + return ( + + ); case ShimmerElementType.LINE: - if (!item.height) { - item.height = LINE_DEFAULT_HEIGHT; - } + return ( + + ); } - return item; - }); - - const maxHeight = itemsDefaulted.reduce((acc: number, next: ICircle | IGap | ILine): number => { - return next.height ? - next.height > acc ? next.height : acc - : acc; - }, 0); - return maxHeight; + }) : ( + + ); + + return renderedElements; +} + +export function getBorderStyles(elem: ICircle | IGap | ILine, rowHeight?: number): IStyleSet | undefined { + const elemHeight: number | undefined = elem.height; + + const dif: number = rowHeight && elemHeight ? rowHeight - elemHeight : 0; + + let borderStyle: IStyleSet | undefined; + + if (!elem.verticalAlign || elem.verticalAlign === ShimmerElementVerticalAlign.CENTER) { + borderStyle = { + borderBottom: `${dif ? Math.floor(dif / 2) : 0}px solid ${DefaultPalette.white}`, + borderTop: `${dif ? Math.ceil(dif / 2) : 0}px solid ${DefaultPalette.white}` + }; + } else if (elem.verticalAlign && elem.verticalAlign === ShimmerElementVerticalAlign.TOP) { + borderStyle = { + borderBottom: `${dif ? dif : 0}px solid ${DefaultPalette.white}`, + borderTop: `0px solid ${DefaultPalette.white}` + }; + } else if (elem.verticalAlign && elem.verticalAlign === ShimmerElementVerticalAlign.BOTTOM) { + borderStyle = { + borderBottom: `0px solid ${DefaultPalette.white}`, + borderTop: `${dif ? dif : 0}px solid ${DefaultPalette.white}` + }; } - private _getBorderAlignStyles(maxHeight: number | undefined, elem: ICircle | IGap | ILine): IStyleSet | undefined { - const elemHeight: number | undefined = elem.height; - - const dif: number | undefined = maxHeight && elemHeight ? - maxHeight - elemHeight > 0 ? - maxHeight - elemHeight : undefined - : undefined; - - let borderStyle: IStyleSet | undefined; - const hasVerticalAlign: boolean = elem.verticalAlign ? true : false; - - if (elem.verticalAlign === ShimmerElementVerticalAlign.CENTER || !hasVerticalAlign) { - borderStyle = { - alignSelf: 'center', - borderBottom: `${dif ? dif / 2 : 0}px solid ${DefaultPalette.white}`, - borderTop: `${dif ? dif / 2 : 0}px solid ${DefaultPalette.white}` - }; - } else if (elem.verticalAlign === ShimmerElementVerticalAlign.TOP && hasVerticalAlign) { - borderStyle = { - alignSelf: 'top', - borderBottom: `${dif ? dif : 0}px solid ${DefaultPalette.white}`, - borderTop: `0px solid ${DefaultPalette.white}` - }; - } else if (elem.verticalAlign === ShimmerElementVerticalAlign.BOTTOM && hasVerticalAlign) { - borderStyle = { - alignSelf: 'bottom', - borderBottom: `0px solid ${DefaultPalette.white}`, - borderTop: `${dif ? dif : 0}px solid ${DefaultPalette.white}` - }; + return borderStyle; +} + +export function findMaxElementHeight(elements: Array): number { + const itemsDefaulted: Array = elements.map((elem: ICircle | IGap | ILine): ICircle | IGap | ILine => { + switch (elem.type) { + case ShimmerElementType.CIRCLE: + if (!elem.height) { + elem.height = CIRCLE_DEFAULT_HEIGHT; + } + case ShimmerElementType.LINE: + if (!elem.height) { + elem.height = LINE_DEFAULT_HEIGHT; + } + case ShimmerElementType.GAP: + if (!elem.height) { + elem.height = GAP_DEFAULT_HEIGHT; + } } - return borderStyle; - } + return elem; + }); + + const rowHeight = itemsDefaulted.reduce((acc: number, next: ICircle | IGap | ILine): number => { + return next.height ? + next.height > acc ? next.height : acc + : acc; + }, 0); + + return rowHeight; } \ No newline at end of file diff --git a/packages/experiments/src/components/Shimmer/Shimmer.styles.ts b/packages/experiments/src/components/Shimmer/Shimmer.styles.ts index 78dd50cdf8fd26..3e07eb4c8d6f45 100644 --- a/packages/experiments/src/components/Shimmer/Shimmer.styles.ts +++ b/packages/experiments/src/components/Shimmer/Shimmer.styles.ts @@ -7,7 +7,7 @@ import { export function getStyles(props: IShimmerStyleProps): IShimmerStyles { const { width, - maxHeight, + rowHeight, isDataLoaded, isBaseStyle } = props; @@ -31,7 +31,7 @@ export function getStyles(props: IShimmerStyleProps): IShimmerStyles { margin: '10px', width: 'auto', boxSizing: 'content-box', - minHeight: maxHeight ? `${maxHeight}px` : '16px' + minHeight: rowHeight ? `${rowHeight}px` : '16px' }, isBaseStyle && { margin: '0', @@ -46,9 +46,12 @@ export function getStyles(props: IShimmerStyleProps): IShimmerStyles { display: 'flex', position: 'absolute', top: '0', + bottom: '0', + left: '0', + right: '0', alignItems: 'center', alignContent: 'space-between', - width: `${width}%`, + width: width ? `${width}%` : '100%', height: 'auto', boxSizing: 'border-box', background: `${DefaultPalette.neutralLighter} @@ -73,7 +76,7 @@ export function getStyles(props: IShimmerStyleProps): IShimmerStyles { }, isBaseStyle && { position: 'static', - width: 'auto' + width: width ? `${width}px` : 'auto' } ], dataWrapper: [ diff --git a/packages/experiments/src/components/Shimmer/Shimmer.types.ts b/packages/experiments/src/components/Shimmer/Shimmer.types.ts index 8b8d01f8380a9a..6fe6293f998417 100644 --- a/packages/experiments/src/components/Shimmer/Shimmer.types.ts +++ b/packages/experiments/src/components/Shimmer/Shimmer.types.ts @@ -106,6 +106,11 @@ export interface ICircle extends IShimmerElement { } export interface IGap extends IShimmerElement { + /** + * Sets the height of the shimmer gap in pixels. + * @default 16px + */ + height?: number; /** * The value will be calculated as '%' relative the to shimmer wrapper. */ @@ -113,14 +118,14 @@ export interface IGap extends IShimmerElement { /** * Sets the width of the Gap to an exact value in pixels. - * @default 5px + * @default 10px */ widthInPixel?: number; } export interface IShimmerStyleProps { width?: number; - maxHeight?: number; + rowHeight?: number; isDataLoaded?: boolean; isBaseStyle?: boolean; } diff --git a/packages/experiments/src/components/Shimmer/ShimmerCircle/ShimmerCircle.base.tsx b/packages/experiments/src/components/Shimmer/ShimmerCircle/ShimmerCircle.base.tsx index 3c045acd15f1be..273d5d606f440d 100644 --- a/packages/experiments/src/components/Shimmer/ShimmerCircle/ShimmerCircle.base.tsx +++ b/packages/experiments/src/components/Shimmer/ShimmerCircle/ShimmerCircle.base.tsx @@ -19,8 +19,8 @@ export class ShimmerCircleBase extends BaseComponent { } public render(): JSX.Element { - const { height, getStyles, borderAlignStyle } = this.props; - this._classNames = getClassNames(getStyles!, { height, borderAlignStyle }); + const { height, getStyles, borderStyle } = this.props; + this._classNames = getClassNames(getStyles!, { height, borderStyle }); return (
diff --git a/packages/experiments/src/components/Shimmer/ShimmerCircle/ShimmerCircle.styles.ts b/packages/experiments/src/components/Shimmer/ShimmerCircle/ShimmerCircle.styles.ts index b2c9056b23fb03..872d0586ba60f2 100644 --- a/packages/experiments/src/components/Shimmer/ShimmerCircle/ShimmerCircle.styles.ts +++ b/packages/experiments/src/components/Shimmer/ShimmerCircle/ShimmerCircle.styles.ts @@ -10,14 +10,14 @@ import { export function getStyles(props: IShimmerCircleStyleProps): IShimmerCircleStyles { const { height, - borderAlignStyle + borderStyle } = props; - const styles: IStyleSet = !!borderAlignStyle ? borderAlignStyle : {}; + const styles: IStyleSet = !!borderStyle ? borderStyle : {}; return { root: [ - 'ms-ShimmerCircle-wrapper', + 'ms-ShimmerCircle-root', { width: `${height}px`, height: `${height}px`, diff --git a/packages/experiments/src/components/Shimmer/ShimmerCircle/ShimmerCircle.types.ts b/packages/experiments/src/components/Shimmer/ShimmerCircle/ShimmerCircle.types.ts index 75b4dc9e80477c..ce8294cc1fd270 100644 --- a/packages/experiments/src/components/Shimmer/ShimmerCircle/ShimmerCircle.types.ts +++ b/packages/experiments/src/components/Shimmer/ShimmerCircle/ShimmerCircle.types.ts @@ -28,7 +28,7 @@ export interface IShimmerCircleProps extends React.AllHTMLAttributes(); + +export class ShimmerGapBase extends BaseComponent { + private _classNames: {[key in keyof IShimmerGapStyles]: string}; + + constructor(props: IShimmerGapProps) { + super(props); + } + + public render(): JSX.Element { + const { height, getStyles, widthInPercentage, widthInPixel, borderStyle } = this.props; + + this._classNames = getClassNames(getStyles!, { height, widthInPixel, widthInPercentage, borderStyle }); + + return ( +
+ ); + } +} \ No newline at end of file diff --git a/packages/experiments/src/components/Shimmer/ShimmerGap/ShimmerGap.styles.ts b/packages/experiments/src/components/Shimmer/ShimmerGap/ShimmerGap.styles.ts new file mode 100644 index 00000000000000..2cb14db58a6e2f --- /dev/null +++ b/packages/experiments/src/components/Shimmer/ShimmerGap/ShimmerGap.styles.ts @@ -0,0 +1,30 @@ +import { + IShimmerGapStyleProps, + IShimmerGapStyles +} from './ShimmerGap.types'; +import { IStyleSet, DefaultPalette } from 'office-ui-fabric-react/lib/Styling'; + +export function getStyles(props: IShimmerGapStyleProps): IShimmerGapStyles { + const { + height, + widthInPercentage, + widthInPixel, + borderStyle + } = props; + + const styles: IStyleSet = !!borderStyle ? borderStyle : {}; + const ACTUAL_WIDTH = widthInPercentage ? widthInPercentage + '%' : widthInPixel ? widthInPixel + 'px' : '10px'; + + return { + root: [ + 'ms-ShimmerGap-root', + { + backgroundColor: `${DefaultPalette.white}`, + width: ACTUAL_WIDTH, + height: `${height}px`, + boxSizing: 'content-box', + }, + styles + ] + }; +} diff --git a/packages/experiments/src/components/Shimmer/ShimmerGap/ShimmerGap.tsx b/packages/experiments/src/components/Shimmer/ShimmerGap/ShimmerGap.tsx new file mode 100644 index 00000000000000..dff5780583ed55 --- /dev/null +++ b/packages/experiments/src/components/Shimmer/ShimmerGap/ShimmerGap.tsx @@ -0,0 +1,15 @@ +import { + styled +} from '../../../Utilities'; +import { + IShimmerGapProps, + IShimmerGapStyleProps, + IShimmerGapStyles +} from './ShimmerGap.types'; +import { ShimmerGapBase } from './ShimmerGap.base'; +import { getStyles } from './ShimmerGap.styles'; + +export const ShimmerGap = styled( + ShimmerGapBase, + getStyles +); diff --git a/packages/experiments/src/components/Shimmer/ShimmerGap/ShimmerGap.types.ts b/packages/experiments/src/components/Shimmer/ShimmerGap/ShimmerGap.types.ts new file mode 100644 index 00000000000000..325adaff895d0c --- /dev/null +++ b/packages/experiments/src/components/Shimmer/ShimmerGap/ShimmerGap.types.ts @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { + IStyle, + IStyleSet +} from '../../../Styling'; +import { IStyleFunction } from '../../../Utilities'; + +export interface IShimmerGap { + +} + +/** + * ShimmerGap component props. + */ +export interface IShimmerGapProps extends React.AllHTMLAttributes { + /** + * Optional callback to access the IShimmerGap interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: (component: IShimmerGap | null) => void; + + /** + * Sets the height of the gap. + * @default 16px + */ + height?: number; + + /** + * Sets width of the element in percentages. + * @default 100% + */ + widthInPercentage?: number; + + /** + * Sets width of the element in pixels. + * @default 50px + */ + widthInPixel?: number; + + /** + * Sets custom styling of the gap. + */ + borderStyle?: IStyleSet; + + /** + * Call to provide customized styling that will layer on top of the variant rules. + */ + getStyles?: IStyleFunction; +} + +export interface IShimmerGapStyleProps { + height?: number; + widthInPercentage?: number; + widthInPixel?: number; + borderStyle?: IStyleSet; +} + +export interface IShimmerGapStyles { + root?: IStyle; +} \ No newline at end of file diff --git a/packages/experiments/src/components/Shimmer/ShimmerLine/ShimmerLine.base.tsx b/packages/experiments/src/components/Shimmer/ShimmerLine/ShimmerLine.base.tsx index 150c36af83f3fd..6fe9ad6c3bfcbe 100644 --- a/packages/experiments/src/components/Shimmer/ShimmerLine/ShimmerLine.base.tsx +++ b/packages/experiments/src/components/Shimmer/ShimmerLine/ShimmerLine.base.tsx @@ -8,24 +8,20 @@ import { IShimmerLineStyleProps, IShimmerLineStyles } from './ShimmerLine.types'; -import { ShimmerElementVerticalAlign } from '../Shimmer.types'; const getClassNames = classNamesFunction(); export class ShimmerLineBase extends BaseComponent { - public static defaultProps: IShimmerLineProps = { - verticalAlign: ShimmerElementVerticalAlign.CENTER, - }; - private _classNames: {[key in keyof IShimmerLineStyles]: string}; + private _classNames: { [key in keyof IShimmerLineStyles]: string }; constructor(props: IShimmerLineProps) { super(props); } public render(): JSX.Element { - const { height, getStyles, widthInPercentage, widthInPixel, borderAlignStyle } = this.props; + const { height, getStyles, widthInPercentage, widthInPixel, borderStyle } = this.props; - this._classNames = getClassNames(getStyles!, { height, widthInPixel, widthInPercentage, borderAlignStyle }); + this._classNames = getClassNames(getStyles!, { height, widthInPixel, widthInPercentage, borderStyle }); return (
diff --git a/packages/experiments/src/components/Shimmer/ShimmerLine/ShimmerLine.styles.ts b/packages/experiments/src/components/Shimmer/ShimmerLine/ShimmerLine.styles.ts index 2199c92478948c..60015d7b943fda 100644 --- a/packages/experiments/src/components/Shimmer/ShimmerLine/ShimmerLine.styles.ts +++ b/packages/experiments/src/components/Shimmer/ShimmerLine/ShimmerLine.styles.ts @@ -9,17 +9,16 @@ export function getStyles(props: IShimmerLineStyleProps): IShimmerLineStyles { height, widthInPercentage, widthInPixel, - borderAlignStyle + borderStyle } = props; - const styles: IStyleSet = !!borderAlignStyle ? borderAlignStyle : {}; + const styles: IStyleSet = !!borderStyle ? borderStyle : {}; const ACTUAL_WIDTH = widthInPercentage ? widthInPercentage + '%' : widthInPixel ? widthInPixel + 'px' : '100%'; return { root: [ - 'ms-ShimmerLine-line', + 'ms-ShimmerLine-root', { - color: 'transparent', width: ACTUAL_WIDTH, height: `${height}px`, boxSizing: 'content-box', diff --git a/packages/experiments/src/components/Shimmer/ShimmerLine/ShimmerLine.types.ts b/packages/experiments/src/components/Shimmer/ShimmerLine/ShimmerLine.types.ts index 12306c1f21f455..1d8c2262c19fdd 100644 --- a/packages/experiments/src/components/Shimmer/ShimmerLine/ShimmerLine.types.ts +++ b/packages/experiments/src/components/Shimmer/ShimmerLine/ShimmerLine.types.ts @@ -37,15 +37,10 @@ export interface IShimmerLineProps extends React.AllHTMLAttributes */ widthInPixel?: number; - /** - * @default center - */ - verticalAlign?: string; - /** * Sets custom styling of the rectangle. */ - borderAlignStyle?: IStyleSet; + borderStyle?: IStyleSet; /** * Call to provide customized styling that will layer on top of the variant rules. @@ -55,10 +50,9 @@ export interface IShimmerLineProps extends React.AllHTMLAttributes export interface IShimmerLineStyleProps { height?: number; - verticalAlign?: string; widthInPercentage?: number; widthInPixel?: number; - borderAlignStyle?: IStyleSet; + borderStyle?: IStyleSet; } export interface IShimmerLineStyles { diff --git a/packages/experiments/src/components/Shimmer/ShimmerTile/ShimmerTile.base.tsx b/packages/experiments/src/components/Shimmer/ShimmerTile/ShimmerTile.base.tsx new file mode 100644 index 00000000000000..ec32842026ae12 --- /dev/null +++ b/packages/experiments/src/components/Shimmer/ShimmerTile/ShimmerTile.base.tsx @@ -0,0 +1,222 @@ +import * as React from 'react'; +import { + BaseComponent, + classNamesFunction, +} from '../../../Utilities'; +import { + IShimmerTileProps, + IShimmerTileStyleProps, + IShimmerTileStyles +} from './ShimmerTile.types'; +import { TileLayoutSizes, TileSize } from 'experiments/lib/Tile'; +import { ShimmerGap } from '../ShimmerGap/ShimmerGap'; +import { getRenderedElements } from '../Shimmer.base'; +import { ShimmerElementType as ElemType } from 'experiments/lib/Shimmer'; + +const enum ShimmerTileLayoutValues { + largeSquareWidth = 96, + largeSquareHeight = 96, + largeNameWidth = 144, + largeNameHeight = 7, + largeActivityWidth = 96, + largeActivityHeight = 7, + smallSquareWidth = 62, + smallSquareHeight = 61, + smallNameWidth = 106, + smallNameHeight = 5, + smallActivityWidth = 62, + smallActivityHeight = 5, +} + +const PLACEHOLDER_SIZES: { + [P in TileSize]: { + squareWidth: number; + squareHeight: number; + nameWidth: number; + nameHeight: number; + activityWidth: number; + activityHeight: number; + }; +} = { + small: { + squareWidth: ShimmerTileLayoutValues.smallSquareWidth, + squareHeight: ShimmerTileLayoutValues.smallSquareHeight, + nameWidth: ShimmerTileLayoutValues.smallNameWidth, + nameHeight: ShimmerTileLayoutValues.smallNameHeight, + activityWidth: ShimmerTileLayoutValues.smallActivityWidth, + activityHeight: ShimmerTileLayoutValues.smallActivityHeight + }, + large: { + squareWidth: ShimmerTileLayoutValues.largeSquareWidth, + squareHeight: ShimmerTileLayoutValues.largeSquareHeight, + nameWidth: ShimmerTileLayoutValues.largeNameWidth, + nameHeight: ShimmerTileLayoutValues.largeNameHeight, + activityWidth: ShimmerTileLayoutValues.largeActivityWidth, + activityHeight: ShimmerTileLayoutValues.largeActivityHeight + } + }; + +const getClassNames = classNamesFunction(); + +export class ShimmerTileBase extends BaseComponent { + private _classNames: {[key in keyof IShimmerTileStyles]: string}; + + constructor(props: IShimmerTileProps) { + super(props); + } + + public render(): JSX.Element { + const { + getStyles, + contentSize = { width: 176, height: 171 }, + itemActivity = true, + itemName = true, + itemThumbnail = true, + tileSize = 'large' + } = this.props; + + const { + nameplatePadding, + nameplateMargin, + nameplateActivityHeight, + nameplateNameHeight + } = TileLayoutSizes[tileSize]; + + const { + squareWidth, + squareHeight, + nameWidth, + nameHeight, + activityWidth, + activityHeight + } = PLACEHOLDER_SIZES[tileSize]; + + let nameplateHeight = 0; + + if (itemName || itemActivity) { + nameplateHeight += nameplatePadding * 2; + if (itemName) { + nameplateHeight += nameplateNameHeight; + } + if (itemActivity) { + nameplateHeight += nameplateActivityHeight + nameplateMargin; + } + } + + this._classNames = getClassNames(getStyles!, {}); + + return ( +
+ +
+ { + getRenderedElements( + [ + { + type: ElemType.GAP, + widthInPixel: (contentSize.width - squareWidth) / 2, + height: squareHeight + }, + itemThumbnail ? + { + type: ElemType.LINE, + widthInPixel: squareWidth, + height: squareHeight + } : + { + type: ElemType.GAP, + widthInPixel: squareWidth, + height: squareHeight + }, + { + type: ElemType.GAP, + widthInPixel: (contentSize.width - squareWidth) / 2, + height: squareHeight + } + ], + squareHeight + ) + } +
+ { + itemActivity || itemName ? +
+ + { + itemName ? +
+ { + getRenderedElements( + [ + { + type: ElemType.GAP, + widthInPixel: (contentSize.width - nameWidth) / 2, + height: nameplateNameHeight + }, + { + type: ElemType.LINE, + widthInPixel: nameWidth, + height: nameHeight + }, + { + type: ElemType.GAP, + widthInPixel: (contentSize.width - nameWidth) / 2, + height: nameplateNameHeight + } + ], + nameplateNameHeight + ) + } +
: null + } + { + itemActivity ? +
+ { + getRenderedElements( + [ + { + type: ElemType.GAP, + widthInPixel: (contentSize.width - activityWidth) / 2, + height: nameplateActivityHeight + }, + { + type: ElemType.LINE, + widthInPixel: activityWidth, + height: activityHeight + }, + { + type: ElemType.GAP, + widthInPixel: (contentSize.width - activityWidth) / 2, + height: nameplateActivityHeight + } + ], + nameplateActivityHeight + ) + } +
: null + } + +
: null + } +
+ ); + } +} \ No newline at end of file diff --git a/packages/experiments/src/components/Shimmer/ShimmerTile/ShimmerTile.styles.ts b/packages/experiments/src/components/Shimmer/ShimmerTile/ShimmerTile.styles.ts new file mode 100644 index 00000000000000..5eb5def859bc8c --- /dev/null +++ b/packages/experiments/src/components/Shimmer/ShimmerTile/ShimmerTile.styles.ts @@ -0,0 +1,27 @@ +import { + IShimmerTileStyleProps, + IShimmerTileStyles +} from './ShimmerTile.types'; + +export function getStyles(props: IShimmerTileStyleProps): IShimmerTileStyles { + const { + } = props; + + return { + root: [ + 'ms-ShimmerTile-root', + { + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column' + }, + ], + flexDiv: [ + 'ms-ShimmerTile-flexRow', + { + display: 'flex' + } + ] + }; +} diff --git a/packages/experiments/src/components/Shimmer/ShimmerTile/ShimmerTile.tsx b/packages/experiments/src/components/Shimmer/ShimmerTile/ShimmerTile.tsx new file mode 100644 index 00000000000000..4244fd0d72900f --- /dev/null +++ b/packages/experiments/src/components/Shimmer/ShimmerTile/ShimmerTile.tsx @@ -0,0 +1,15 @@ +import { + styled +} from '../../../Utilities'; +import { + IShimmerTileProps, + IShimmerTileStyleProps, + IShimmerTileStyles +} from './ShimmerTile.types'; +import { ShimmerTileBase } from './ShimmerTile.base'; +import { getStyles } from './ShimmerTile.styles'; + +export const ShimmerTile = styled( + ShimmerTileBase, + getStyles +); diff --git a/packages/experiments/src/components/Shimmer/ShimmerTile/ShimmerTile.types.ts b/packages/experiments/src/components/Shimmer/ShimmerTile/ShimmerTile.types.ts new file mode 100644 index 00000000000000..111897b785bba2 --- /dev/null +++ b/packages/experiments/src/components/Shimmer/ShimmerTile/ShimmerTile.types.ts @@ -0,0 +1,67 @@ +import * as React from 'react'; +import { + IStyle +} from 'office-ui-fabric-react/lib/Styling'; +import { + ISize, + IStyleFunction +} from 'office-ui-fabric-react/lib/Utilities'; +import { TileSize } from 'experiments/lib/Tile'; + +export interface IShimmerTile { + +} + +/** + * ShimmerTile component props. + */ +export interface IShimmerTileProps extends React.AllHTMLAttributes { + /** + * Optional callback to access the IShimmerTile interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: (component: IShimmerTile | null) => void; + + /** + * The intended dimensions for the Tile. + */ + contentSize?: ISize; + + /** + * The breakpoint size for the Tile. + * @default large + */ + tileSize?: TileSize; + + /** + * Set to false if you choose not to display a name on the nameplate for the tile. + * @default true + */ + itemName?: boolean; + + /** + * Set to false if you choose not to display a activity on the nameplate for the tile. + * @default true + */ + itemActivity?: boolean; + + /** + * Set to false if you choose not to display a thumbnail of item type above the nameplate for the tile. + * @default true + */ + itemThumbnail?: boolean; + + /** + * Call to provide customized styling that will layer on top of the variant rules. + */ + getStyles?: IStyleFunction; +} + +export interface IShimmerTileStyleProps { + contentSize?: ISize; +} + +export interface IShimmerTileStyles { + root?: IStyle; + flexDiv?: IStyle; +} \ No newline at end of file diff --git a/packages/experiments/src/components/TilesList/TilesList.scss b/packages/experiments/src/components/TilesList/TilesList.scss index a20f279914f551..dc4afa3fca990c 100644 --- a/packages/experiments/src/components/TilesList/TilesList.scss +++ b/packages/experiments/src/components/TilesList/TilesList.scss @@ -4,6 +4,7 @@ .listCell { display: block; margin: 0; + border-color: $ms-color-white; } .listPage { diff --git a/packages/experiments/src/components/TilesList/TilesList.tsx b/packages/experiments/src/components/TilesList/TilesList.tsx index 97aadcceac4ff9..72e7a7d3b5bfa1 100644 --- a/packages/experiments/src/components/TilesList/TilesList.tsx +++ b/packages/experiments/src/components/TilesList/TilesList.tsx @@ -5,6 +5,7 @@ import { List, IPageProps } from 'office-ui-fabric-react/lib/List'; import { FocusZone, FocusZoneDirection } from 'office-ui-fabric-react/lib/FocusZone'; import { css, IRenderFunction, IRectangle } from 'office-ui-fabric-react/lib/Utilities'; import * as TilesListStylesModule from './TilesList.scss'; +import { Shimmer } from '../Shimmer/Shimmer'; // tslint:disable-next-line:no-any const TilesListStyles: any = TilesListStylesModule; @@ -14,6 +15,8 @@ const CELLS_PER_PAGE = 100; const MIN_ASPECT_RATIO = 0.5; const MAX_ASPECT_RATIO = 3; +const ROW_OF_PLACEHOLDER_CELLS = 3; + export interface ITilesListState { cells: ITileCell[]; } @@ -26,6 +29,7 @@ export interface ITileGrid { marginTop: number; marginBottom: number; key: string; + isPlaceholder?: boolean; } export interface ITileCell { @@ -33,6 +37,7 @@ export interface ITileCell { content: TItem; aspectRatio: number; grid: ITileGrid; + isPlaceholder?: boolean; onRender(content: TItem, finalSize: { width: number; height: number; }): React.ReactNode | React.ReactNode[]; } @@ -201,10 +206,14 @@ export class TilesList extends React.Component, IT let currentRow: IRowData | undefined; + let shimmerWrapperWidth = 0; + for (let i = 0; i < endIndex;) { // For each cell at the start of a grid. const grid = cells[i].grid; + const isPlaceholder = grid.isPlaceholder; + const renderedCells: React.ReactNode[] = []; const width = data.pageWidths[page.startIndex + i]; @@ -257,21 +266,37 @@ export class TilesList extends React.Component, IT } } - renderedCells.push( -
- { this._onRenderCell(cell, finalSize) } -
- ); + const renderedCell = (keyOffset?: number): JSX.Element => { + return ( +
+ { this._onRenderCell(cell, finalSize) } +
+ ); + }; + + if (cell.isPlaceholder && grid.mode !== TilesGridMode.none) { + let cellsPerRow = Math.floor(width / (grid.spacing + finalSize.width)); + let totalPlaceholderItems = cellsPerRow * ROW_OF_PLACEHOLDER_CELLS; + shimmerWrapperWidth = (cellsPerRow * finalSize.width) + (grid.spacing * (cellsPerRow - 1)); + for (let j = 0; j < totalPlaceholderItems; j++) { + renderedCells.push(renderedCell(j)); + } + } else { + shimmerWrapperWidth = finalSize.width / 3; + renderedCells.push(renderedCell()); + } } const isOpenStart = previousCell && previousCell.grid === grid; @@ -279,7 +304,7 @@ export class TilesList extends React.Component, IT const margin = grid.spacing / 2; - grids.push( + const finalGrid: JSX.Element = (
extends React.Component, IT { renderedCells }
); + + grids.push( + isPlaceholder ? + ( + + { finalGrid } + + ) : + finalGrid + ); } return ( @@ -534,7 +573,9 @@ export class TilesList extends React.Component, IT return { flex: isFill ? `${itemWidthOverHeight} ${itemWidthOverHeight} ${width}px` : `0 0 ${width}px`, maxWidth: `${maxWidth}px`, - margin: `${margin}px` + margin: !item.isPlaceholder ? `${margin}px` : 0, + borderStyle: item.isPlaceholder ? 'solid' : 'none', + borderWidth: item.isPlaceholder ? `${margin}px` : 0 }; } @@ -564,8 +605,9 @@ export class TilesList extends React.Component, IT mode: item.mode, key: `grid-${item.key}`, maxScaleFactor: maxScaleFactor, - marginTop: marginTop, - marginBottom: marginBottom + marginTop: item.isPlaceholder ? 0 : marginTop, + marginBottom: item.isPlaceholder ? 0 : marginBottom, + isPlaceholder: item.isPlaceholder }; for (const gridItem of item.items) { @@ -584,7 +626,8 @@ export class TilesList extends React.Component, IT content: gridItem.content, onRender: gridItem.onRender, grid: grid, - key: gridItem.key + key: gridItem.key, + isPlaceholder: gridItem.isPlaceholder }); } } else { @@ -600,9 +643,11 @@ export class TilesList extends React.Component, IT key: `grid-header-${item.key}`, maxScaleFactor: 1, marginBottom: 0, - marginTop: 0 + marginTop: 0, + isPlaceholder: item.isPlaceholder }, - key: `header-${item.key}` + key: `header-${item.key}`, + isPlaceholder: item.isPlaceholder }); } } diff --git a/packages/experiments/src/components/TilesList/TilesList.types.ts b/packages/experiments/src/components/TilesList/TilesList.types.ts index d61e6d3ebcb0fa..8d98a021d4b7cc 100644 --- a/packages/experiments/src/components/TilesList/TilesList.types.ts +++ b/packages/experiments/src/components/TilesList/TilesList.types.ts @@ -19,6 +19,10 @@ export interface ITilesGridItem { * If not provided, this is assumed to be a square equivalent to the current row height. */ desiredSize?: { width: number; height: number; }; + /** + * Set to true if the item is intended to be a placeholder + */ + isPlaceholder?: boolean; /** * Invoked to render the virtual DOM for the item. * This content will be rendered inside the cell allocated for the item. @@ -88,6 +92,10 @@ export interface ITilesGridSegment { * The maximum aspect ratio for an item in the grid. */ maxAspectRatio?: number; + /** + * Set to true if the item is intended to be a placeholder + */ + isPlaceholder?: boolean; } export { ISize as ITileSize }; diff --git a/packages/experiments/src/components/TilesList/TilesListPage.tsx b/packages/experiments/src/components/TilesList/TilesListPage.tsx index 4f1c39c13df010..919bde9b67efe4 100644 --- a/packages/experiments/src/components/TilesList/TilesListPage.tsx +++ b/packages/experiments/src/components/TilesList/TilesListPage.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { Link } from 'office-ui-fabric-react/lib/Link'; +import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; import { ExampleCard, IComponentDemoPageProps, @@ -24,21 +25,44 @@ const TilesListMediaExampleCode = require( '!raw-loader!experiments/src/components/TilesList/examples/TilesList.Media.Example.tsx' ) as string; -export class TilesListPage extends React.Component { +export interface ITilesListPageState { + size: 'small' | 'large'; +} + +export class TilesListPage extends React.Component { + constructor(props: {}) { + super(props); + + this.state = { + size: 'large' + }; + } + public render(): JSX.Element { + const { size } = this.state; + return ( - + - - + + + - +
@@ -83,4 +107,10 @@ export class TilesListPage extends React.Component /> ); } + + private _onIsLargeChanged = (event: React.FormEvent, checked: boolean): void => { + this.setState({ + size: checked ? 'large' : 'small' + }); + } } diff --git a/packages/experiments/src/components/TilesList/examples/ExampleHelpers.tsx b/packages/experiments/src/components/TilesList/examples/ExampleHelpers.tsx index c6267a4d0974dc..29d7b4076ece3c 100644 --- a/packages/experiments/src/components/TilesList/examples/ExampleHelpers.tsx +++ b/packages/experiments/src/components/TilesList/examples/ExampleHelpers.tsx @@ -79,13 +79,16 @@ export function createGroup(items: IExampleItem[], type: 'document' | 'media', i export function getTileCells(groups: IExampleGroup[], { onRenderCell, onRenderHeader, - size = 'large' + size = 'large', + shimmerMode = false }: { onRenderHeader: (item: IExampleItem) => JSX.Element; onRenderCell: (item: IExampleItem, finalSize?: ITileSize) => JSX.Element; - size?: 'large' | 'small' + size?: 'large' | 'small'; + shimmerMode?: boolean; }): (ITilesGridSegment | ITilesGridItem)[] { const items: (ITilesGridSegment | ITilesGridItem)[] = []; + const isLargeSize: boolean = size === 'large' ? true : false; for (const group of groups) { const header: ITilesGridItem = { @@ -96,7 +99,8 @@ export function getTileCells(groups: IExampleGroup[], { name: group.name, index: group.index }, - onRender: onRenderHeader + onRender: onRenderHeader, + isPlaceholder: shimmerMode }; items.push(header); @@ -107,26 +111,43 @@ export function getTileCells(groups: IExampleGroup[], { key: item.key, content: item, desiredSize: group.type === 'document' ? { - width: 176, - height: 171 + width: isLargeSize ? 176 : 138, + height: isLargeSize ? 171 : 135 } : { - width: 171 * item.aspectRatio, - height: 171 + width: isLargeSize ? 171 * item.aspectRatio : 135 * item.aspectRatio, + height: isLargeSize ? 171 : 135 }, - onRender: onRenderCell + onRender: onRenderCell, + isPlaceholder: shimmerMode }; }), - spacing: 8, - marginBottom: 40, - minRowHeight: 171, + spacing: isLargeSize ? 8 : 12, + marginBottom: shimmerMode ? 0 : 40, + minRowHeight: isLargeSize ? 171 : 135, mode: group.type === 'document' ? size === 'small' ? TilesGridMode.fillHorizontal : TilesGridMode.stack : TilesGridMode.fill, - key: group.key + key: group.key, + isPlaceholder: shimmerMode }); } return items; } + +export function createShimmerGroups(type: 'document' | 'media', index: number): IExampleGroup[] { + return [{ + items: [{ + key: `shimmerItem-${index}`, + name: lorem(4), + index: index, + aspectRatio: 1 + }], + index: index, + name: lorem(4), + key: `shimmerGroup-${index}`, + type: type + }]; +} diff --git a/packages/experiments/src/components/TilesList/examples/TilesList.Document.Example.tsx b/packages/experiments/src/components/TilesList/examples/TilesList.Document.Example.tsx index db7ccc2fc82875..bb457ddae2d39b 100644 --- a/packages/experiments/src/components/TilesList/examples/TilesList.Document.Example.tsx +++ b/packages/experiments/src/components/TilesList/examples/TilesList.Document.Example.tsx @@ -5,12 +5,30 @@ import { ITilesGridItem, ITilesGridSegment } from '../../TilesList'; -import { Tile } from '../../../Tile'; +import { + Tile +} from '../../../Tile'; import { Toggle } from 'office-ui-fabric-react/lib/Toggle'; import { Selection, SelectionZone } from 'office-ui-fabric-react/lib/Selection'; import { MarqueeSelection } from 'office-ui-fabric-react/lib/MarqueeSelection'; import { AnimationClassNames } from 'office-ui-fabric-react/lib/Styling'; -import { IExampleGroup, IExampleItem, createGroup, createDocumentItems, getTileCells } from './ExampleHelpers'; +import { + IExampleGroup, + IExampleItem, + createGroup, + createDocumentItems, + getTileCells, + createShimmerGroups +} from './ExampleHelpers'; +import { ISize } from 'experiments/lib/Utilities'; +import { ShimmerTile } from '../../Shimmer/ShimmerTile/ShimmerTile'; +import { getRenderedElements } from '../../Shimmer/Shimmer.base'; +import { + ShimmerElementType as ElemType, +} from 'experiments/lib/Shimmer'; + +const HEADER_VERTICAL_PADDING = 13; +const HEADER_FONT_SIZE = 18; function createGroups(): IExampleGroup[] { let offset = 0; @@ -30,6 +48,8 @@ function createGroups(): IExampleGroup[] { const GROUPS = createGroups(); +const SHIMMER_GROUPS = createShimmerGroups('document', 0); + const ITEMS = ([] as IExampleItem[]).concat(...GROUPS.map((group: { items: IExampleItem[]; }) => group.items)); declare class TilesListClass extends TilesList { } @@ -38,13 +58,18 @@ const TilesListType: typeof TilesListClass = TilesList; export interface ITilesListDocumentExampleState { isModalSelection: boolean; + isDataLoaded: boolean; cells: (ITilesGridItem | ITilesGridSegment)[]; } -export class TilesListDocumentExample extends React.Component<{}, ITilesListDocumentExampleState> { +export interface ITilesListDocumentExampleProps { + tileSize: 'large' | 'small'; +} + +export class TilesListDocumentExample extends React.Component { private _selection: Selection; - constructor(props: {}) { + constructor(props: ITilesListDocumentExampleProps) { super(props); this._selection = new Selection({ @@ -56,13 +81,40 @@ export class TilesListDocumentExample extends React.Component<{}, ITilesListDocu this.state = { isModalSelection: this._selection.isModal(), - cells: getTileCells(GROUPS, { - onRenderCell: this._onRenderDocumentCell, - onRenderHeader: this._onRenderHeader + isDataLoaded: false, + cells: getTileCells(SHIMMER_GROUPS, { + onRenderCell: this._onRenderShimmerCell, + onRenderHeader: this._onRenderShimmerHeader, + size: props.tileSize, + shimmerMode: true }) }; } + public componentDidUpdate(previousProps: ITilesListDocumentExampleProps): void { + const { isDataLoaded } = this.state; + if (this.props.tileSize !== previousProps.tileSize) { + if (!isDataLoaded) { + this.setState({ + cells: getTileCells(SHIMMER_GROUPS, { + onRenderCell: this._onRenderShimmerCell, + onRenderHeader: this._onRenderShimmerHeader, + size: this.props.tileSize, + shimmerMode: true + }) + }); + } else { + this.setState({ + cells: getTileCells(GROUPS, { + onRenderCell: this._onRenderDocumentCell, + onRenderHeader: this._onRenderHeader, + size: this.props.tileSize + }) + }); + } + } + } + public render(): JSX.Element { return ( // tslint:disable-next-line:jsx-ban-props @@ -74,6 +126,13 @@ export class TilesListDocumentExample extends React.Component<{}, ITilesListDocu onText='Modal' offText='Normal' /> + { + const { tileSize } = this.props; + const { isDataLoaded } = this.state; + let { cells } = this.state; + + if (cells.length && !cells[0].isPlaceholder) { + cells = getTileCells(SHIMMER_GROUPS, { + onRenderCell: this._onRenderShimmerCell, + onRenderHeader: this._onRenderShimmerHeader, + shimmerMode: true, + size: tileSize + }); + } else { + cells = getTileCells(GROUPS, { + onRenderCell: this._onRenderDocumentCell, + onRenderHeader: this._onRenderHeader, + size: tileSize + }); + } + + this.setState({ + isDataLoaded: !isDataLoaded, + cells: cells, + }); + } + private _onSelectionChange = (): void => { this.setState({ isModalSelection: this._selection.isModal() @@ -108,6 +193,9 @@ export class TilesListDocumentExample extends React.Component<{}, ITilesListDocu } private _onRenderDocumentCell = (item: IExampleItem): JSX.Element => { + const { tileSize } = this.props; + let imgSize = tileSize === 'large' ? 64 : 48; + return ( @@ -135,14 +223,55 @@ export class TilesListDocumentExample extends React.Component<{}, ITilesListDocu showForegroundFrame={ true } itemName={ item.name } itemActivity={ item.key } + tileSize={ tileSize } + /> + ); + } + + private _onRenderShimmerCell = (item: IExampleItem, finalSize: ISize): JSX.Element => { + const { tileSize } = this.props; + + return ( + ); } private _onRenderHeader = (item: IExampleItem): JSX.Element => { return ( -
-

{ item.name }

+
+ { item.name } +
+ ); + } + + private _onRenderShimmerHeader = (item: IExampleItem): JSX.Element => { + return ( +
+ { + getRenderedElements( + [ + { type: ElemType.LINE, height: HEADER_FONT_SIZE, widthInPercentage: 100 }, + ], + HEADER_VERTICAL_PADDING * 2 + HEADER_FONT_SIZE + ) + }
); }