diff --git a/apps/fabric-website/src/components/App/AppState.tsx b/apps/fabric-website/src/components/App/AppState.tsx index e27d0c9e6c9bc8..162ceac49185aa 100644 --- a/apps/fabric-website/src/components/App/AppState.tsx +++ b/apps/fabric-website/src/components/App/AppState.tsx @@ -364,6 +364,12 @@ export const AppState: IAppState = { component: () => , getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/SearchBoxComponentPage').SearchBoxComponentPage)) }, + { + title: 'Shimmer', + url: '#/components/shimmer', + component: () => , + getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/ShimmerComponentPage').ShimmerComponentPage)) + }, { title: 'Slider', url: '#/components/slider', diff --git a/apps/fabric-website/src/pages/Components/ShimmerComponentPage.tsx b/apps/fabric-website/src/pages/Components/ShimmerComponentPage.tsx new file mode 100644 index 00000000000000..2640254bdabf1e --- /dev/null +++ b/apps/fabric-website/src/pages/Components/ShimmerComponentPage.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { ShimmerPage } from 'office-ui-fabric-react/lib/components/Shimmer/ShimmerPage'; +import { PageHeader } from '../../components/PageHeader/PageHeader'; +import { ComponentPage } from '../../components/ComponentPage/ComponentPage'; +const pageStyles: any = require('../PageStyles.module.scss'); + +export class ShimmerComponentPage extends React.Component { + public render(): JSX.Element { + return ( +
+ + + + +
+ ); + } +} diff --git a/apps/vr-tests/src/stories/Shimmer.stories.tsx b/apps/vr-tests/src/stories/Shimmer.stories.tsx new file mode 100644 index 00000000000000..40ed46c4fee551 --- /dev/null +++ b/apps/vr-tests/src/stories/Shimmer.stories.tsx @@ -0,0 +1,56 @@ +/*! Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. */ +import * as React from 'react'; +import Screener, { Steps } from 'screener-storybook/src/screener'; +import { storiesOf } from '@storybook/react'; +import { FabricDecorator } from '../utilities'; +import { Shimmer, ShimmerElementType as ElemType, ShimmerElementsGroup } from 'office-ui-fabric-react'; + +storiesOf('Shimmer', module) + .addDecorator(story => ( + // Shimmer without a specified width needs a container with a fixed width or it's collapsing. + // tslint:disable-next-line:jsx-ban-props +
{story()}
+ )) + .addDecorator(FabricDecorator) + .addDecorator(story => {story()}) + .add('Basic', () => ) + .add('50% width', () => ) + .add('Circle Gap Line', () => ( + + )) + .add('Custom elements', () => ( + + + + + } + width={300} + /> + )) + .add('Data not loaded', () => ( + +
Example content
+
+ )) + .add('Data loaded', () => ( + +
Example content
+
+ )); diff --git a/common/changes/@uifabric/fabric-website/v-vibr-OUFR_Shimmer_2018-08-09-23-14.json b/common/changes/@uifabric/fabric-website/v-vibr-OUFR_Shimmer_2018-08-09-23-14.json new file mode 100644 index 00000000000000..39731a9f98f3dc --- /dev/null +++ b/common/changes/@uifabric/fabric-website/v-vibr-OUFR_Shimmer_2018-08-09-23-14.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/fabric-website", + "comment": "Adds new Shimmer component to fabric-website.", + "type": "minor" + } + ], + "packageName": "@uifabric/fabric-website", + "email": "v-vibr@microsoft.com" +} \ No newline at end of file diff --git a/common/changes/office-ui-fabric-react/v-vibr-OUFR_Shimmer_2018-08-09-23-12.json b/common/changes/office-ui-fabric-react/v-vibr-OUFR_Shimmer_2018-08-09-23-12.json new file mode 100644 index 00000000000000..cdf94cb2d4eea0 --- /dev/null +++ b/common/changes/office-ui-fabric-react/v-vibr-OUFR_Shimmer_2018-08-09-23-12.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "Adds a new Shimmer component to OUFR 5.0", + "type": "minor" + } + ], + "packageName": "office-ui-fabric-react", + "email": "v-vibr@microsoft.com" +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/Shimmer.ts b/packages/office-ui-fabric-react/src/Shimmer.ts new file mode 100644 index 00000000000000..7a69f81856f73e --- /dev/null +++ b/packages/office-ui-fabric-react/src/Shimmer.ts @@ -0,0 +1 @@ +export * from './components/Shimmer/index'; diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.base.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.base.tsx new file mode 100644 index 00000000000000..f30cefd89cead0 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.base.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; +import { + BaseComponent, + classNamesFunction, + customizable, + DelayedRender, + getNativeProps, + divProperties +} from '../../Utilities'; +import { IShimmerProps, IShimmerStyleProps, IShimmerStyles } from './Shimmer.types'; +import { ShimmerElementsGroup } from './ShimmerElementsGroup/ShimmerElementsGroup'; + +export interface IShimmerState { + /** + * Flag for knowing when to remove the shimmerWrapper from the DOM. + */ + contentLoaded?: boolean; +} + +const TRANSITION_ANIMATION_INTERVAL = 200; /* ms */ + +const getClassNames = classNamesFunction(); + +@customizable('Shimmer', ['theme']) +export class ShimmerBase extends BaseComponent { + public static defaultProps: IShimmerProps = { + isDataLoaded: false + }; + + private _classNames: { [key in keyof IShimmerStyles]: string }; + private _lastTimeoutId: number | undefined; + + constructor(props: IShimmerProps) { + super(props); + + this.state = { + contentLoaded: props.isDataLoaded + }; + } + + public componentWillReceiveProps(nextProps: IShimmerProps): void { + const { isDataLoaded } = nextProps; + + if (this._lastTimeoutId !== undefined) { + this._async.clearTimeout(this._lastTimeoutId); + this._lastTimeoutId = undefined; + } + if (isDataLoaded) { + this._lastTimeoutId = this._async.setTimeout(() => { + this.setState({ + contentLoaded: isDataLoaded + }); + this._lastTimeoutId = undefined; + }, TRANSITION_ANIMATION_INTERVAL); + } else { + this.setState({ + contentLoaded: isDataLoaded + }); + } + } + + public render(): JSX.Element { + const { + getStyles, + shimmerElements, + children, + isDataLoaded, + width, + className, + customElementsGroup, + theme, + ariaLabel + } = this.props; + + const { contentLoaded } = this.state; + + this._classNames = getClassNames(getStyles!, { + theme: theme!, + isDataLoaded, + className, + transitionAnimationInterval: TRANSITION_ANIMATION_INTERVAL + }); + + const divProps = getNativeProps(this.props, divProperties); + + return ( +
+ { !contentLoaded && ( +
+ { customElementsGroup ? customElementsGroup : } +
+ ) } + { children &&
{ children }
} + { ariaLabel && + !isDataLoaded && ( +
+ +
{ ariaLabel }
+
+
+ ) } +
+ ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.checklist.ts b/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.checklist.ts new file mode 100644 index 00000000000000..e738f413c7fb97 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.checklist.ts @@ -0,0 +1,9 @@ +import { ChecklistStatus } from '../../demo/ComponentStatus/ComponentStatus.types'; + +export const ShimmerStatus = { + keyboardAccessibilitySupport: ChecklistStatus.notApplicable, + markupSupport: ChecklistStatus.unknown, + highContrastSupport: ChecklistStatus.pass, + rtlSupport: ChecklistStatus.pass, + testCoverage: ChecklistStatus.unknown +}; diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.styles.ts b/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.styles.ts new file mode 100644 index 00000000000000..193f3f3a1e3c0f --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.styles.ts @@ -0,0 +1,108 @@ +import { IShimmerStyleProps, IShimmerStyles } from './Shimmer.types'; +import { keyframes, getGlobalClassNames, hiddenContentStyle, HighContrastSelector } from '../../Styling'; +import { getRTL } from '../../Utilities'; + +const GlobalClassNames = { + root: 'ms-Shimmer-container', + shimmerWrapper: 'ms-Shimmer-shimmerWrapper', + dataWrapper: 'ms-Shimmer-dataWrapper' +}; + +const BACKGROUND_OFF_SCREEN_POSITION = '1000%'; + +const shimmerAnimation: string = keyframes({ + '0%': { + backgroundPosition: `-${BACKGROUND_OFF_SCREEN_POSITION}` + }, + '100%': { + backgroundPosition: BACKGROUND_OFF_SCREEN_POSITION + } +}); + +const shimmerAnimationRTL: string = keyframes({ + '100%': { + backgroundPosition: `-${BACKGROUND_OFF_SCREEN_POSITION}` + }, + '0%': { + backgroundPosition: BACKGROUND_OFF_SCREEN_POSITION + } +}); + +export function getStyles(props: IShimmerStyleProps): IShimmerStyles { + const { isDataLoaded, className, theme, transitionAnimationInterval } = props; + + const { palette } = theme; + const classNames = getGlobalClassNames(GlobalClassNames, theme); + + const isRTL = getRTL(); + + return { + root: [ + classNames.root, + { + position: 'relative', + height: 'auto' + }, + className + ], + shimmerWrapper: [ + classNames.shimmerWrapper, + { + background: `${palette.neutralLighter} + linear-gradient( + to right, + ${palette.neutralLighter} 0%, + ${palette.neutralLight} 50%, + ${palette.neutralLighter} 100%) + 0 0 / 90% 100% + no-repeat`, + animationDuration: '2s', + animationTimingFunction: 'ease-in-out', + animationDirection: 'normal', + animationIterationCount: 'infinite', + animationName: isRTL ? shimmerAnimationRTL : shimmerAnimation, + transition: `opacity ${transitionAnimationInterval}ms`, + selectors: { + [HighContrastSelector]: { + background: `WindowText + linear-gradient( + to right, + transparent 0%, + Window 50%, + transparent 100%) + 0 0 / 90% 100% + no-repeat` + } + } + }, + isDataLoaded && { + opacity: '0', + position: 'absolute', + top: '0', + bottom: '0', + left: '0', + right: '0' + } + ], + dataWrapper: [ + classNames.dataWrapper, + { + position: 'absolute', + top: '0', + bottom: '0', + left: '0', + right: '0', + opacity: '0', + background: 'none', + backgroundColor: 'transparent', + border: 'none', + transition: `opacity ${transitionAnimationInterval}ms` + }, + isDataLoaded && { + opacity: '1', + position: 'static' + } + ], + screenReaderText: hiddenContentStyle + }; +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.test.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.test.tsx new file mode 100644 index 00000000000000..140cec839e3b56 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.test.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; +import * as renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; + +import { Shimmer } from './Shimmer'; +import { ShimmerElementType as ElemType, IShimmer } from './Shimmer.types'; +import { ShimmerElementsGroup } from './ShimmerElementsGroup/ShimmerElementsGroup'; + +describe('Shimmer', () => { + it('renders Shimmer correctly', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders Shimmer with custom elements correctly', () => { + const customElements: JSX.Element = ( +
+ + +
+ ); + + const component = renderer.create(); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('removes Shimmer animation div when data is loaded', () => { + let shimmerComponent: any; + const setRef = (ref: IShimmer): void => { + shimmerComponent = ref; + }; + const shimmer = mount( + +
TEST DATA
+
+ ); + + expect(shimmerComponent).toBeDefined(); + + // moved initialization of fake timers below the mount() as it caused and extra setTimeout call registered. + jest.useFakeTimers(); + + expect(shimmer.find('.ms-Shimmer-container').children()).toHaveLength(3); + + // update props to trigger the setTimeout in componentWillReceiveProps + const newProps = { isDataLoaded: true }; + shimmer.setProps(newProps); + shimmer.update(); + + // assert that setTimeout was called exactly once + expect(setTimeout).toHaveBeenCalledTimes(1); + // assert that the 2nd argument to the call to setTimeout is 200 + expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 200); + + jest.runAllTimers(); + + expect(shimmer.find('.ms-Shimmer-container').children()).toHaveLength(2); + shimmer.unmount(); + }); +}); diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.ts b/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.ts new file mode 100644 index 00000000000000..6f5f2aabb5234e --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.ts @@ -0,0 +1,6 @@ +import { styled } from '../../Utilities'; +import { IShimmerProps, IShimmerStyleProps, IShimmerStyles } from './Shimmer.types'; +import { getStyles } from './Shimmer.styles'; +import { ShimmerBase } from './Shimmer.base'; + +export const Shimmer = styled(ShimmerBase, getStyles); diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.types.ts b/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.types.ts new file mode 100644 index 00000000000000..e8ff45b6b39567 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/Shimmer.types.ts @@ -0,0 +1,171 @@ +import * as React from 'react'; +import { IStyle, ITheme } from '../../Styling'; +import { IStyleFunction } from '../../Utilities'; + +export interface IShimmer { } + +/** + * Shimmer component props. + */ +export interface IShimmerProps extends React.AllHTMLAttributes { + /** + * Optional callback to access the IShimmer interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: (component: IShimmer | null) => void; + + /** + * Sets the width value of the shimmer wave wrapper. + * @default 100% + */ + width?: number | string; + + /** + * Controls when the shimmer is swapped with actual data through an animated transition. + * @default false + */ + isDataLoaded?: boolean; + + /** + * Elements to render in one line of the Shimmer. + */ + shimmerElements?: IShimmerElement[]; + + /** + * Custom elements when necessary to build complex placeholder skeletons. + */ + customElementsGroup?: React.ReactNode; + + /** + * Localized string of the status label for screen reader + */ + ariaLabel?: string; + + /** + * Call to provide customized styling that will layer on top of the variant rules. + */ + getStyles?: IStyleFunction; + + /** + * Additional CSS class(es) to apply to the Shimmer container. + */ + className?: string; + + /** + * Theme provided by High-Order Component. + */ + theme?: ITheme; +} + +/** + * Shimmer Elements Interface + */ +export interface IShimmerElement { + /** + * Required for every element you intend to use. + */ + type: ShimmerElementType; + + /** + * The height of the element (ICircle, ILine, IGap) in pixels. + * Read more details for each specific element. + */ + height?: number; + + /** + * The width value of the element (ILine, IGap) in pixels. + * Read more details for each specific element. + */ + width?: number | string; + + /** + * The vertical alignemt of the element (ICircle, ILine). + * @default center + */ + verticalAlign?: 'top' | 'center' | 'bottom'; +} + +export interface ILine extends IShimmerElement { + /** + * Sets the height of the shimmer line in pixels. + * @default 16px + */ + height?: number; + + /** + * Line width value. + * @default 100% + */ + width?: number | string; +} + +export interface ICircle extends IShimmerElement { + /** + * Sets the height of the shimmer circle in pixels. + * Minimum supported 10px. + * @default 24px + */ + height?: number; +} + +export interface IGap extends IShimmerElement { + /** + * Sets the height of the shimmer gap in pixels. + * @default 16px + */ + height?: number; + + /** + * Gap width value. + * @default 10px + */ + width?: number | string; +} + +export interface IShimmerStyleProps { + isDataLoaded?: boolean; + className?: string; + theme: ITheme; + transitionAnimationInterval?: number; +} + +export interface IShimmerStyles { + root?: IStyle; + shimmerWrapper?: IStyle; + dataWrapper?: IStyle; + screenReaderText?: IStyle; +} + +export enum ShimmerElementType { + /** + * Line element type + */ + line = 1, + + /** + * Circle element type + */ + circle = 2, + + /** + * Gap element type + */ + gap = 3 +} + +export enum ShimmerElementsDefaultHeights { + /** + * Default height of the line element when not provided by user: 16px + */ + line = 16, + + /** + * Default height of the gap element when not provided by user: 16px + */ + gap = 16, + + /** + * Default height of the circle element when not provided by user: 24px + */ + circle = 24 +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerCircle/ShimmerCircle.base.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerCircle/ShimmerCircle.base.tsx new file mode 100644 index 00000000000000..f81eb195761a86 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerCircle/ShimmerCircle.base.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { BaseComponent, classNamesFunction, customizable } from '../../../Utilities'; +import { IShimmerCircleProps, IShimmerCircleStyleProps, IShimmerCircleStyles } from './ShimmerCircle.types'; + +const getClassNames = classNamesFunction(); + +@customizable('ShimmerCircle', ['theme']) +export class ShimmerCircleBase extends BaseComponent { + private _classNames: { [key in keyof IShimmerCircleStyles]: string }; + + constructor(props: IShimmerCircleProps) { + super(props); + } + + public render(): JSX.Element { + const { height, getStyles, borderStyle, theme } = this.props; + this._classNames = getClassNames(getStyles!, { + theme: theme!, + height, + borderStyle + }); + + return ( +
+ + + +
+ ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerCircle/ShimmerCircle.styles.ts b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerCircle/ShimmerCircle.styles.ts new file mode 100644 index 00000000000000..661279be29dca4 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerCircle/ShimmerCircle.styles.ts @@ -0,0 +1,49 @@ +import { IShimmerCircleStyleProps, IShimmerCircleStyles } from './ShimmerCircle.types'; +import { IRawStyle, getGlobalClassNames, HighContrastSelector } from '../../../Styling'; + +const GlobalClassNames = { + root: 'ms-ShimmerCircle-root', + svg: 'ms-ShimmerCircle-svg' +}; + +export function getStyles(props: IShimmerCircleStyleProps): IShimmerCircleStyles { + const { height, borderStyle, theme } = props; + + const { palette } = theme; + const globalClassNames = getGlobalClassNames(GlobalClassNames, theme); + + const borderStyles: IRawStyle = !!borderStyle ? borderStyle : {}; + + return { + root: [ + globalClassNames.root, + { + width: `${height}px`, + height: `${height}px`, + minWidth: `${height}px`, // Fix for IE11 flex items + boxSizing: 'content-box', + borderTopStyle: 'solid', + borderBottomStyle: 'solid', + borderColor: palette.white, + selectors: { + [HighContrastSelector]: { + borderColor: 'Window' + } + } + }, + borderStyles + ], + svg: [ + globalClassNames.svg, + { + display: 'block', + fill: palette.white, + selectors: { + [HighContrastSelector]: { + fill: 'Window' + } + } + } + ] + }; +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerCircle/ShimmerCircle.ts b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerCircle/ShimmerCircle.ts new file mode 100644 index 00000000000000..33f5fc9c5bf6eb --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerCircle/ShimmerCircle.ts @@ -0,0 +1,9 @@ +import { styled } from '../../../Utilities'; +import { getStyles } from './ShimmerCircle.styles'; +import { IShimmerCircleProps, IShimmerCircleStyleProps, IShimmerCircleStyles } from './ShimmerCircle.types'; +import { ShimmerCircleBase } from './ShimmerCircle.base'; + +export const ShimmerCircle = styled( + ShimmerCircleBase, + getStyles +); diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerCircle/ShimmerCircle.types.ts b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerCircle/ShimmerCircle.types.ts new file mode 100644 index 00000000000000..ebcbf577c86248 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerCircle/ShimmerCircle.types.ts @@ -0,0 +1,74 @@ +import * as React from 'react'; +import { IStyle, IRawStyle, ITheme } from '../../../Styling'; +import { IStyleFunction } from '../../../Utilities'; + +export interface IShimmerCircle { } + +/** + * ShimmerCircle component props. + */ +export interface IShimmerCircleProps extends React.AllHTMLAttributes { + /** + * Optional callback to access the IShimmerCircle interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: (component: IShimmerCircle | null) => void; + + /** + * Sets the height of the circle. + * @default 24px + */ + height?: number; + + /** + * Theme provided by High-Order Component. + */ + theme?: ITheme; + + /** + * Call to provide customized styling that will layer on top of the variant rules. + */ + getStyles?: IStyleFunction; + + /** + * Use to set custom styling of the shimmerCircle borders. + * @deprecated Use 'styles' prop to leverage mergeStyle API. + */ + borderStyle?: IRawStyle; +} + +/** + * Props needed to construct styles. + */ +export type IShimmerCircleStyleProps = { + /** + * Theme values passed to the component. + */ + theme: ITheme; + + /** + * Needed to provide a height to the root of the control. + */ + height?: number; + + /** + * Styles to override borderStyles with custom ones. + * @deprecated in favor of mergeStyles API. + */ + borderStyle?: IRawStyle; +}; + +/** + * Represents the stylable areas of the control. + */ +export interface IShimmerCircleStyles { + /** + * Root of the ShimmerCircle component. + */ + root?: IStyle; + + /** + * Style for the circle SVG of the ShimmerCircle component. + */ + svg?: IStyle; +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerElementsGroup/ShimmerElementsGroup.base.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerElementsGroup/ShimmerElementsGroup.base.tsx new file mode 100644 index 00000000000000..fe60f911206529 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerElementsGroup/ShimmerElementsGroup.base.tsx @@ -0,0 +1,139 @@ +import * as React from 'react'; +import { BaseComponent, classNamesFunction, customizable } from '../../../Utilities'; +import { + IShimmerElementsGroupProps, + IShimmerElementsGroupStyleProps, + IShimmerElementsGroupStyles +} from './ShimmerElementsGroup.types'; +import { IStyleSet } from '../../../Styling'; +import { ShimmerElementType, ShimmerElementsDefaultHeights, IShimmerElement } from '../Shimmer.types'; +import { ShimmerLine } from '../ShimmerLine/ShimmerLine'; +import { IShimmerLineStyles, IShimmerLineStyleProps } from '../ShimmerLine/ShimmerLine.types'; +import { ShimmerGap } from '../ShimmerGap/ShimmerGap'; +import { IShimmerGapStyles, IShimmerGapStyleProps } from '../ShimmerGap/ShimmerGap.types'; +import { ShimmerCircle } from '../ShimmerCircle/ShimmerCircle'; +import { IShimmerCircleStyles, IShimmerCircleStyleProps } from '../ShimmerCircle/ShimmerCircle.types'; + +const getClassNames = classNamesFunction(); + +@customizable('ShimmerElementsGroup', ['theme']) +export class ShimmerElementsGroupBase extends BaseComponent { + public static defaultProps: IShimmerElementsGroupProps = { + flexWrap: false + }; + + private _classNames: { [key in keyof IShimmerElementsGroupStyles]: string }; + + constructor(props: IShimmerElementsGroupProps) { + super(props); + } + + public render(): JSX.Element { + const { getStyles, width, shimmerElements, rowHeight, flexWrap, theme } = this.props; + + this._classNames = getClassNames(getStyles!, { + theme: theme!, + flexWrap + }); + + const height = rowHeight ? rowHeight : this._findMaxElementHeight(shimmerElements ? shimmerElements : []); + + return ( + // tslint:disable-next-line:jsx-ban-props +
+ { this._getRenderedElements(shimmerElements, height) } +
+ ); + } + + private _getRenderedElements = (shimmerElements?: IShimmerElement[], rowHeight?: number): React.ReactNode => { + const renderedElements: React.ReactNode = shimmerElements ? ( + shimmerElements.map( + (elem: IShimmerElement, index: number): JSX.Element => { + const { type, ...filteredElem } = elem; + switch (elem.type) { + case ShimmerElementType.circle: + return ; + case ShimmerElementType.gap: + return ; + case ShimmerElementType.line: + return ; + } + } + ) + ) : ( + + ); + + return renderedElements; + } + + private _getBorderStyles = ( + elem: IShimmerElement, + rowHeight?: number + ): (props: IShimmerCircleStyleProps | IShimmerGapStyleProps | IShimmerLineStyleProps) => IShimmerCircleStyles | IShimmerGapStyles | IShimmerLineStyles => { + return (props: IShimmerCircleStyleProps | IShimmerGapStyleProps | IShimmerLineStyleProps): IShimmerCircleStyles | IShimmerGapStyles | IShimmerLineStyles => { + const elemHeight: number | undefined = elem.height; + const dif: number = rowHeight && elemHeight ? rowHeight - elemHeight : 0; + + let borderStyle: IStyleSet | undefined; + + if (!elem.verticalAlign || elem.verticalAlign === 'center') { + borderStyle = { + borderBottomWidth: `${dif ? Math.floor(dif / 2) : 0}px`, + borderTopWidth: `${dif ? Math.ceil(dif / 2) : 0}px` + }; + } else if (elem.verticalAlign && elem.verticalAlign === 'top') { + borderStyle = { + borderBottomWidth: `${dif ? dif : 0}px`, + borderTopWidth: `0px` + }; + } else if (elem.verticalAlign && elem.verticalAlign === 'bottom') { + borderStyle = { + borderBottomWidth: `0px`, + borderTopWidth: `${dif ? dif : 0}px` + }; + } + + return { + root: [{ ...borderStyle }] + }; + }; + } + + /** + * User should not worry to provide which of the elements is the highest, we do the calculation for him. + * Plus if user forgot to specify the height we assign their defaults. + */ + private _findMaxElementHeight = (elements: IShimmerElement[]): number => { + const itemsDefaulted: IShimmerElement[] = elements.map( + (elem: IShimmerElement): IShimmerElement => { + switch (elem.type) { + case ShimmerElementType.circle: + if (!elem.height) { + elem.height = ShimmerElementsDefaultHeights.circle; + } + case ShimmerElementType.line: + if (!elem.height) { + elem.height = ShimmerElementsDefaultHeights.line; + } + case ShimmerElementType.gap: + if (!elem.height) { + elem.height = ShimmerElementsDefaultHeights.gap; + } + } + return elem; + } + ); + + const rowHeight = itemsDefaulted.reduce((acc: number, next: IShimmerElement): number => { + return next.height ? (next.height > acc ? next.height : acc) : acc; + }, 0); + + return rowHeight; + } + + private _getDefaultLineStyle = (props: IShimmerLineStyleProps): IShimmerLineStyles => { + return { root: [{ borderWidth: '0px' }] }; + } +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerElementsGroup/ShimmerElementsGroup.styles.ts b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerElementsGroup/ShimmerElementsGroup.styles.ts new file mode 100644 index 00000000000000..fe12bc5a50ea7c --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerElementsGroup/ShimmerElementsGroup.styles.ts @@ -0,0 +1,23 @@ +import { IShimmerElementsGroupStyleProps, IShimmerElementsGroupStyles } from './ShimmerElementsGroup.types'; +import { getGlobalClassNames } from '../../../Styling'; + +const GlobalClassNames = { + root: 'ms-ShimmerElementsGroup-root' +}; + +export function getStyles(props: IShimmerElementsGroupStyleProps): IShimmerElementsGroupStyles { + const { flexWrap, theme } = props; + + const classNames = getGlobalClassNames(GlobalClassNames, theme); + + return { + root: [ + classNames.root, + { + display: 'flex', + alignItems: 'center', + flexWrap: flexWrap ? 'wrap' : 'nowrap' + } + ] + }; +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerElementsGroup/ShimmerElementsGroup.ts b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerElementsGroup/ShimmerElementsGroup.ts new file mode 100644 index 00000000000000..1827138d30d4dd --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerElementsGroup/ShimmerElementsGroup.ts @@ -0,0 +1,14 @@ +import { styled } from '../../../Utilities'; +import { + IShimmerElementsGroupProps, + IShimmerElementsGroupStyleProps, + IShimmerElementsGroupStyles +} from './ShimmerElementsGroup.types'; +import { ShimmerElementsGroupBase } from './ShimmerElementsGroup.base'; +import { getStyles } from './ShimmerElementsGroup.styles'; + +export const ShimmerElementsGroup = styled< + IShimmerElementsGroupProps, + IShimmerElementsGroupStyleProps, + IShimmerElementsGroupStyles + >(ShimmerElementsGroupBase, getStyles); diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerElementsGroup/ShimmerElementsGroup.types.ts b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerElementsGroup/ShimmerElementsGroup.types.ts new file mode 100644 index 00000000000000..cbc77d0f535248 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerElementsGroup/ShimmerElementsGroup.types.ts @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { IStyle, ITheme } from '../../../Styling'; +import { IStyleFunction } from '../../../Utilities'; +import { IShimmerElement } from '../Shimmer.types'; + +export interface IShimmerElementsGroup { } + +/** + * ShimmerElementsGroup component props. + */ +export interface IShimmerElementsGroupProps extends React.AllHTMLAttributes { + /** + * Optional callback to access the IShimmerElementsGroup interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: (component: IShimmerElementsGroup | null) => void; + + /** + * Optional maximum row height of the shimmerElements container. + */ + rowHeight?: number; + + /** + * Elements to render in one group of the Shimmer. + */ + shimmerElements?: IShimmerElement[]; + + /** + * Optional boolean for enabling flexWrap of the container containing the shimmerElements. + * @default false + */ + flexWrap?: boolean; + + /** + * Optional width for ShimmerElements container. + */ + width?: string; + + /** + * Theme provided by High-Order Component. + */ + theme?: ITheme; + + /** + * Call to provide customized styling that will layer on top of the variant rules. + */ + getStyles?: IStyleFunction; +} + +export interface IShimmerElementsGroupStyleProps { + flexWrap?: boolean; + theme: ITheme; +} + +export interface IShimmerElementsGroupStyles { + root?: IStyle; +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerGap/ShimmerGap.base.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerGap/ShimmerGap.base.tsx new file mode 100644 index 00000000000000..88254d4bb75d22 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerGap/ShimmerGap.base.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { BaseComponent, classNamesFunction, customizable } from '../../../Utilities'; +import { IShimmerGapProps, IShimmerGapStyleProps, IShimmerGapStyles } from './ShimmerGap.types'; + +const getClassNames = classNamesFunction(); + +@customizable('ShimmerGap', ['theme']) +export class ShimmerGapBase extends BaseComponent { + private _classNames: { [key in keyof IShimmerGapStyles]: string }; + + constructor(props: IShimmerGapProps) { + super(props); + } + + public render(): JSX.Element { + const { height, getStyles, width, borderStyle, theme } = this.props; + + this._classNames = getClassNames(getStyles!, { + theme: theme!, + height, + borderStyle + }); + + return ( +
+ ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerGap/ShimmerGap.styles.ts b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerGap/ShimmerGap.styles.ts new file mode 100644 index 00000000000000..64e46b989a3419 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerGap/ShimmerGap.styles.ts @@ -0,0 +1,36 @@ +import { IShimmerGapStyleProps, IShimmerGapStyles } from './ShimmerGap.types'; +import { IRawStyle, getGlobalClassNames, HighContrastSelector } from '../../../Styling'; + +const GlobalClassNames = { + root: 'ms-ShimmerGap-root' +}; + +export function getStyles(props: IShimmerGapStyleProps): IShimmerGapStyles { + const { height, borderStyle, theme } = props; + + const { palette } = theme; + const globalClassNames = getGlobalClassNames(GlobalClassNames, theme); + + const borderStyles: IRawStyle = !!borderStyle ? borderStyle : {}; + + return { + root: [ + globalClassNames.root, + { + backgroundColor: palette.white, + height: `${height}px`, + boxSizing: 'content-box', + borderTopStyle: 'solid', + borderBottomStyle: 'solid', + borderColor: palette.white, + selectors: { + [HighContrastSelector]: { + backgroundColor: 'Window', + borderColor: 'Window' + } + } + }, + borderStyles + ] + }; +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerGap/ShimmerGap.ts b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerGap/ShimmerGap.ts new file mode 100644 index 00000000000000..6cf09216263f5f --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerGap/ShimmerGap.ts @@ -0,0 +1,6 @@ +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/office-ui-fabric-react/src/components/Shimmer/ShimmerGap/ShimmerGap.types.ts b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerGap/ShimmerGap.types.ts new file mode 100644 index 00000000000000..a23b7e43c94522 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerGap/ShimmerGap.types.ts @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { IStyle, IRawStyle, ITheme } 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 value of the gap. + * @default 10px + */ + width?: number | string; + + /** + * Theme provided by High-Order Component. + */ + theme?: ITheme; + + /** + * Call to provide customized styling that will layer on top of the variant rules. + */ + getStyles?: IStyleFunction; + + /** + * Use to set custom styling of the shimmerGap borders. + * @deprecated Use 'styles' prop to leverage mergeStyle API. + */ + borderStyle?: IRawStyle; +} + +/** + * Props needed to construct styles. + */ +export type IShimmerGapStyleProps = { + /** + * Theme values passed to the component. + */ + theme: ITheme; + + /** + * Needed to provide a height to the root of the control. + */ + height?: number; + + /** + * Styles to override borderStyles with custom ones. + * @deprecated in favor of mergeStyles API. + */ + borderStyle?: IRawStyle; +}; + +/** + * Represents the stylable areas of the control. + */ +export interface IShimmerGapStyles { + /** + * Root of the ShimmerGap component. + */ + root?: IStyle; +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerLine/ShimmerLine.base.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerLine/ShimmerLine.base.tsx new file mode 100644 index 00000000000000..ebc7321bf22b2b --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerLine/ShimmerLine.base.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { BaseComponent, classNamesFunction, customizable } from '../../../Utilities'; +import { IShimmerLineProps, IShimmerLineStyleProps, IShimmerLineStyles } from './ShimmerLine.types'; + +const getClassNames = classNamesFunction(); + +@customizable('ShimmerLine', ['theme']) +export class ShimmerLineBase extends BaseComponent { + private _classNames: { [key in keyof IShimmerLineStyles]: string }; + + constructor(props: IShimmerLineProps) { + super(props); + } + + public render(): JSX.Element { + const { height, getStyles, width, borderStyle, theme } = this.props; + + this._classNames = getClassNames(getStyles!, { + theme: theme!, + height, + borderStyle + }); + + return ( +
+ + + + + + + + + + + + +
+ ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerLine/ShimmerLine.styles.ts b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerLine/ShimmerLine.styles.ts new file mode 100644 index 00000000000000..5943f7f67c8b77 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerLine/ShimmerLine.styles.ts @@ -0,0 +1,81 @@ +import { IShimmerLineStyleProps, IShimmerLineStyles } from './ShimmerLine.types'; +import { IRawStyle, getGlobalClassNames, HighContrastSelector } from '../../../Styling'; + +const GlobalClassNames = { + root: 'ms-ShimmerLine-root', + topLeftCorner: 'ms-ShimmerLine-topLeftCorner', + topRightCorner: 'ms-ShimmerLine-topRightCorner', + bottomLeftCorner: 'ms-ShimmerLine-bottomLeftCorner', + bottomRightCorner: 'ms-ShimmerLine-bottomRightCorner' +}; + +export function getStyles(props: IShimmerLineStyleProps): IShimmerLineStyles { + const { height, borderStyle, theme } = props; + + const { palette } = theme; + const globalClassNames = getGlobalClassNames(GlobalClassNames, theme); + + const borderStyles: IRawStyle = !!borderStyle ? borderStyle : {}; + + const sharedCornerStyles: IRawStyle = { + position: 'absolute', + fill: palette.white + }; + + return { + root: [ + globalClassNames.root, + { + height: `${height}px`, + boxSizing: 'content-box', + position: 'relative', + borderTopStyle: 'solid', + borderBottomStyle: 'solid', + borderColor: palette.white, + selectors: { + [HighContrastSelector]: { + borderColor: 'Window', + selectors: { + '> *': { + fill: 'Window' + } + } + } + } + }, + borderStyles + ], + topLeftCorner: [ + globalClassNames.topLeftCorner, + { + top: '0', + left: '0' + }, + sharedCornerStyles + ], + topRightCorner: [ + globalClassNames.topRightCorner, + { + top: '0', + right: '0' + }, + sharedCornerStyles + ], + bottomRightCorner: [ + globalClassNames.bottomRightCorner, + { + bottom: '0', + right: '0' + }, + sharedCornerStyles + ], + bottomLeftCorner: [ + globalClassNames.bottomLeftCorner, + { + bottom: '0', + left: '0' + }, + sharedCornerStyles + ] + }; +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerLine/ShimmerLine.ts b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerLine/ShimmerLine.ts new file mode 100644 index 00000000000000..6624ccc6ef4ec9 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerLine/ShimmerLine.ts @@ -0,0 +1,9 @@ +import { styled } from '../../../Utilities'; +import { IShimmerLineProps, IShimmerLineStyleProps, IShimmerLineStyles } from './ShimmerLine.types'; +import { ShimmerLineBase } from './ShimmerLine.base'; +import { getStyles } from './ShimmerLine.styles'; + +export const ShimmerLine = styled( + ShimmerLineBase, + getStyles +); diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerLine/ShimmerLine.types.ts b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerLine/ShimmerLine.types.ts new file mode 100644 index 00000000000000..14c7071264e346 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerLine/ShimmerLine.types.ts @@ -0,0 +1,95 @@ +import * as React from 'react'; +import { IStyle, IRawStyle, ITheme } from '../../../Styling'; +import { IStyleFunction } from '../../../Utilities'; + +export interface IShimmerLine { } + +/** + * ShimmerLine component props. + */ +export interface IShimmerLineProps extends React.AllHTMLAttributes { + /** + * Optional callback to access the IShimmerLine interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ + componentRef?: (component: IShimmerLine | null) => void; + + /** + * Sets the height of the rectangle. + * @default 16px + */ + height?: number; + + /** + * Sets width value of the line. + * @default 100% + */ + width?: number | string; + + /** + * Theme provided by High-Order Component. + */ + theme?: ITheme; + + /** + * Call to provide customized styling that will layer on top of the variant rules. + */ + getStyles?: IStyleFunction; + + /** + * Use to set custom styling of the shimmerLine borders. + * @deprecated Use 'styles' prop to leverage mergeStyle API. + */ + borderStyle?: IRawStyle; +} + +/** + * Props needed to construct styles. + */ +export type IShimmerLineStyleProps = { + /** + * Theme values passed to the component. + */ + theme: ITheme; + + /** + * Needed to provide a height to the root of the control. + */ + height?: number; + + /** + * Styles to override borderStyles with custom ones. + * @deprecated in favor of mergeStyles API. + */ + borderStyle?: IRawStyle; +}; + +/** + * Represents the stylable areas of the control. + */ +export interface IShimmerLineStyles { + /** + * Root of the ShimmerLine component. + */ + root?: IStyle; + + /** + * Top-left corner SVG of the ShimmerLine component. + */ + topLeftCorner?: IStyle; + + /** + * Top-right corner SVG of the ShimmerLine component. + */ + topRightCorner?: IStyle; + + /** + * Bottom-right corner SVG of the ShimmerLine component. + */ + bottomRightCorner?: IStyle; + + /** + * Bottom-left corner SVG of the ShimmerLine component. + */ + bottomLeftCorner?: IStyle; +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerPage.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerPage.tsx new file mode 100644 index 00000000000000..d21cc968a453f7 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/ShimmerPage.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import { + ExampleCard, + ComponentPage, + IComponentDemoPageProps, + PropertiesTableSet, + PageMarkdown +} from '@uifabric/example-app-base'; +import { ShimmerBasicExample } from './examples/Shimmer.Basic.Example'; +import { ShimmerCustomElementsExample } from './examples/Shimmer.CustomElements.Example'; +import { ShimmerLoadDataExample } from './examples/Shimmer.LoadData.Example'; +import { ShimmerApplicationExample } from './examples/Shimmer.Application.Example'; +import { ShimmerStylingExample } from './examples/Shimmer.Styling.Example'; +import { ComponentStatus } from '../../demo/ComponentStatus/ComponentStatus'; +import { ShimmerStatus } from './Shimmer.checklist'; + +const ShimmerBasicExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Basic.Example.tsx') as string; + +const ShimmerCustomExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.CustomElements.Example.tsx') as string; + +const ShimmerStylingExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Styling.Example.tsx') as string; + +const ShimmerLoadDataExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.LoadData.Example.tsx') as string; + +const ShimmerApplicationExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Application.Example.tsx') as string; + +export class ShimmerPage extends React.Component { + public render(): JSX.Element { + return ( + + + + + + + + + + + + + + + + +
+ } + propertiesTables={ + ('!raw-loader!office-ui-fabric-react/src/components/Shimmer/Shimmer.types.ts')] } + /> + } + overview={ + + { require('!raw-loader!office-ui-fabric-react/src/components/Shimmer/docs/ShimmerOverview.md') } + + } + bestPractices={
} + dos={ + + { require('!raw-loader!office-ui-fabric-react/src/components/Shimmer/docs/ShimmerDos.md') } + + } + donts={ + + { require('!raw-loader!office-ui-fabric-react/src/components/Shimmer/docs/ShimmerDonts.md') } + + } + isHeaderVisible={ this.props.isHeaderVisible } + componentStatus={ } + /> + ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/__snapshots__/Shimmer.test.tsx.snap b/packages/office-ui-fabric-react/src/components/Shimmer/__snapshots__/Shimmer.test.tsx.snap new file mode 100644 index 00000000000000..3f1fc14aac7cb5 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/__snapshots__/Shimmer.test.tsx.snap @@ -0,0 +1,625 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Shimmer renders Shimmer correctly 1`] = ` +
+
+
+
+ + + +
+
+
* { + fill: Window; + } + style={ + Object { + "minWidth": "auto", + "width": "100%", + } + } + > + + + + + + + + + + + + +
+
+
+
+`; + +exports[`Shimmer renders Shimmer with custom elements correctly 1`] = ` +
+
+
+
+
* { + fill: Window; + } + style={ + Object { + "minWidth": "40px", + "width": 40, + } + } + > + + + + + + + + + + + + +
+
+
+
+
* { + fill: Window; + } + style={ + Object { + "minWidth": "300px", + "width": 300, + } + } + > + + + + + + + + + + + + +
+
* { + fill: Window; + } + style={ + Object { + "minWidth": "200px", + "width": 200, + } + } + > + + + + + + + + + + + + +
+
+
+
+
+
+`; diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/docs/ShimmerDonts.md b/packages/office-ui-fabric-react/src/components/Shimmer/docs/ShimmerDonts.md new file mode 100644 index 00000000000000..0620b2f865f79e --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/docs/ShimmerDonts.md @@ -0,0 +1,5 @@ + +- Use on the same element both types of widths. It will always default to just one of them. See documentation below. +- Build Shimmer UI should with a lot of details. Circles and rectangles are really as detailed as you want to get. Adding more detail will result in confusion once the UI loads. +- Use shimmer if you are confident that the UI will take less than a second to load. +- Use shimmer as a way to not make improvements in your code to improve performance. \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/docs/ShimmerDos.md b/packages/office-ui-fabric-react/src/components/Shimmer/docs/ShimmerDos.md new file mode 100644 index 00000000000000..d9b01f9b9be5fd --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/docs/ShimmerDos.md @@ -0,0 +1,7 @@ + +- Use shimmer to help ease a UI transition when we know the service will potentially take a longer amount of time to retrieve the data. +- Provide widths for each of the shimmer elements you used to build a skeleton layout looking as close as possible to real content it is replacing. +- Use `isDataLoaded` prop to trigger the transition once we have the data from the service. The Shimmer UI should Fade out while the real UI Fades In. +- Use shimmer if you know the UI loading time is longer than 1 second. +- Provide an ETA as quickly as possible to help the user understand that the system isn’t broken if you use shimmer and the delay is longer than 10 seconds. +- Provide shimmer designs for the breakpoints that your experience is supported in. \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/docs/ShimmerOverview.md b/packages/office-ui-fabric-react/src/components/Shimmer/docs/ShimmerOverview.md new file mode 100644 index 00000000000000..0a1c3769aaa674 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/docs/ShimmerOverview.md @@ -0,0 +1,5 @@ +Shimmer is a temporary animation placeholder for when data from the service call takes time to get back and we don't want to block rendering the rest of the UI. + +When Shimmer is not wrapping the actual component to be rendered while data is fetching, `shimmerElements` or `customElementsGroup` props should be used, and later just replace the Shimmer UI with the intended content. Otherwise, if smooth transition from Shimmer UI to content is wanted, wrap the content node with Shimmer tags and use `isDataLoaded` prop to trigger the transition. For reference use the examples provided below. + +For cases when your application supports theming, Shimmer component is equiped with everything you need to just load the custom theme to the application, and as long as the color palette you provide has an overried for the two Fabric colors used in Shimmer, everything should be ok. If no theming is supported, then follow the example showing the use of the `styles` prop. \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/examples/ExampleHelper.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/examples/ExampleHelper.tsx new file mode 100644 index 00000000000000..fa23ca7d6ce012 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/examples/ExampleHelper.tsx @@ -0,0 +1,8 @@ +const baseProductionCdnUrl = 'https://static2.sharepointonline.com/files/fabric/office-ui-fabric-react-assets/'; + +export const PersonaDetails = { + imageUrl: baseProductionCdnUrl + 'persona-female.png', + imageInitials: 'AL', + primaryText: 'Annie Lindqvist', + secondaryText: 'Software Engineer' +}; diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Application.Example.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Application.Example.tsx new file mode 100644 index 00000000000000..1a9a793ebd8dd2 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Application.Example.tsx @@ -0,0 +1,224 @@ +/* tslint:disable:no-unused-variable */ +import * as React from 'react'; +/* tslint:enable:no-unused-variable */ +import { BaseComponent } from 'office-ui-fabric-react/lib/Utilities'; +import { createListItems } from '@uifabric/example-app-base/lib/utilities/data'; +import { + IColumn, + DetailsList, + buildColumns, + SelectionMode, + Toggle, + IDetailsRowProps, + DetailsRow +} from 'office-ui-fabric-react/lib/index'; +import { Shimmer } from 'office-ui-fabric-react/lib/Shimmer'; + +import * as ShimmerExampleStyles from './Shimmer.Example.scss'; + +export interface IItem { + [index: string]: string | number; + thumbnail: string; + key: string; + name: string; + description: string; + color: string; + shape: string; + location: string; + width: number; + height: number; +} + +const fileIcons: { name: string }[] = [ + { name: 'accdb' }, + { name: 'csv' }, + { name: 'docx' }, + { name: 'dotx' }, + { name: 'mpp' }, + { name: 'mpt' }, + { name: 'odp' }, + { name: 'ods' }, + { name: 'odt' }, + { name: 'one' }, + { name: 'onepkg' }, + { name: 'onetoc' }, + { name: 'potx' }, + { name: 'ppsx' }, + { name: 'pptx' }, + { name: 'pub' }, + { name: 'vsdx' }, + { name: 'vssx' }, + { name: 'vstx' }, + { name: 'xls' }, + { name: 'xlsx' }, + { name: 'xltx' }, + { name: 'xsn' } +]; + +const ITEMS_COUNT = 500; +const ITEMS_BATCH_SIZE = 10; +const PAGING_DELAY = 2500; + +// tslint:disable-next-line:no-any +let _items: any[]; + +export interface IShimmerApplicationExampleState { + items?: IItem[]; + columns?: IColumn[]; + isDataLoaded?: boolean; + isModalSelection?: boolean; + isCompactMode?: boolean; +} + +export class ShimmerApplicationExample extends BaseComponent<{}, IShimmerApplicationExampleState> { + private _isFetchingItems: boolean; + private _lastTimeoutId: number; + + constructor(props: {}) { + super(props); + + this.state = { + items: new Array(), + columns: _buildColumns(), + isDataLoaded: false, + isModalSelection: false, + isCompactMode: false + }; + } + + public render(): JSX.Element { + const { items, columns, isDataLoaded, isModalSelection, isCompactMode } = this.state; + + return ( +
+
+ + + +
+
+ +
+
+ ); + } + + private _onRenderMissingItem = (index: number, rowProps: IDetailsRowProps): React.ReactNode => { + const { isDataLoaded } = this.state; + isDataLoaded && this._onDataMiss(index as number); + + const shimmerRow: JSX.Element = ; + + return ; + } + + // Simulating asynchronus data loading each 2.5 sec + private _onDataMiss = (index: number): void => { + index = Math.floor(index / ITEMS_BATCH_SIZE) * ITEMS_BATCH_SIZE; + if (!this._isFetchingItems) { + this._isFetchingItems = true; + this._lastTimeoutId = this._async.setTimeout(() => { + this._isFetchingItems = false; + // tslint:disable-next-line:no-any + const itemsCopy = ([] as any[]).concat(this.state.items); + itemsCopy.splice.apply( + itemsCopy, + [index, ITEMS_BATCH_SIZE].concat(_items.slice(index, index + ITEMS_BATCH_SIZE)) + ); + this.setState({ + items: itemsCopy + }); + }, PAGING_DELAY); + } + } + + private _onLoadData = (checked: boolean): void => { + if (!_items) { + _items = createListItems(ITEMS_COUNT); + _items.map((item: IItem) => { + const randomFileType = this._randomFileIcon(); + item.thumbnail = randomFileType.url; + }); + } + + let items: IItem[]; + if (checked) { + items = _items.slice(0, ITEMS_BATCH_SIZE).concat(new Array(ITEMS_COUNT - ITEMS_BATCH_SIZE)); + } else { + items = new Array(); + this._async.clearTimeout(this._lastTimeoutId); + } + this.setState({ + isDataLoaded: checked, + items: items + }); + } + + private _onChangeModalSelection = (checked: boolean): void => { + this.setState({ isModalSelection: checked }); + } + + private _onChangeCompactMode = (checked: boolean): void => { + this.setState({ isCompactMode: checked }); + } + + private _onRenderItemColumn = (item: IItem, index: number, column: IColumn): JSX.Element | string | number => { + if (column.key === 'thumbnail') { + return ; + } + + return item[column.key]; + } + + private _randomFileIcon(): { docType: string; url: string } { + const docType: string = fileIcons[Math.floor(Math.random() * fileIcons.length) + 0].name; + return { + docType, + url: `https://static2.sharepointonline.com/files/fabric/assets/brand-icons/document/svg/${docType}_16x1.svg` + }; + } +} + +function _buildColumns(): IColumn[] { + const _item = createListItems(1); + const columns: IColumn[] = buildColumns(_item); + + columns.forEach((column: IColumn) => { + if (column.key === 'thumbnail') { + column.name = 'FileType'; + column.minWidth = 16; + column.maxWidth = 16; + column.isIconOnly = true; + column.iconName = 'Page'; + } + }); + return columns; +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Basic.Example.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Basic.Example.tsx new file mode 100644 index 00000000000000..0b70f078d027d7 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Basic.Example.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; + +import { Shimmer, ShimmerElementType as ElemType } from 'office-ui-fabric-react/lib/Shimmer'; + +import * as ShimmerExampleStyles from './Shimmer.Example.scss'; + +export class ShimmerBasicExample extends React.Component<{}, {}> { + constructor(props: {}) { + super(props); + } + + public render(): JSX.Element { + return ( +
+ Basic Shimmer with no elements provided. It defaults to a line of 16px height. + + + + Basic Shimmer with elements provided. + + + + Variations of vertical alignment for Circles and Lines. + +
+ ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.CustomElements.Example.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.CustomElements.Example.tsx new file mode 100644 index 00000000000000..0b7a90dd4d6d45 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.CustomElements.Example.tsx @@ -0,0 +1,112 @@ +import * as React from 'react'; + +import { Shimmer, ShimmerElementsGroup, ShimmerElementType as ElemType } from 'office-ui-fabric-react/lib/Shimmer'; + +import * as ShimmerExampleStyles from './Shimmer.Example.scss'; + +export class ShimmerCustomElementsExample extends React.Component<{}, {}> { + constructor(props: {}) { + super(props); + } + + public render(): JSX.Element { + return ( +
+ Using ShimmerElementsGroup component to build complex structures of the placeholder you need. + + + +
+ ); + } + + private _getCustomElementsExampleOne = (): JSX.Element => { + return ( +
+ + +
+ ); + } + + private _getCustomElementsExampleTwo = (): JSX.Element => { + return ( +
+ + +
+ ); + } + + private _getCustomElementsExampleThree = (): JSX.Element => { + return ( +
+ +
+ + + +
+
+ ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Example.scss b/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Example.scss new file mode 100644 index 00000000000000..494d6cfca41ca2 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Example.scss @@ -0,0 +1,15 @@ +.shimmerExampleContainer { + padding: 2px; + + & > * { + margin: 10px 0; + } +} + +.shimmerExampleFlexGroup { + display: flex; + + & > * { + margin-right: 30px; + } +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.LoadData.Example.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.LoadData.Example.tsx new file mode 100644 index 00000000000000..dfd976e9b8a50d --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.LoadData.Example.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import { Shimmer, ShimmerElementsGroup, ShimmerElementType as ElemType } from 'office-ui-fabric-react/lib/Shimmer'; +import { Persona, PersonaSize, PersonaPresence, IPersonaProps } from 'office-ui-fabric-react/lib/Persona'; +import { Toggle } from 'office-ui-fabric-react/lib/Toggle'; +import { PersonaDetails } from './ExampleHelper'; + +import * as ShimmerExampleStyles from './Shimmer.Example.scss'; + +export interface IShimmerLoadDataExampleState { + isDataLoadedOne?: boolean; + isDataLoadedTwo?: boolean; + contentOne?: string; + examplePersona?: IPersonaProps; +} + +export class ShimmerLoadDataExample extends React.Component<{}, IShimmerLoadDataExampleState> { + constructor(props: {}) { + super(props); + this.state = { + isDataLoadedOne: false, + isDataLoadedTwo: false, + contentOne: '', + examplePersona: {} + }; + } + + public render(): JSX.Element { + const { isDataLoadedOne, isDataLoadedTwo, contentOne, examplePersona } = this.state; + + return ( +
+ + +
+ { contentOne } + { contentOne } + { contentOne } +
+
+ + + + +
+ ); + } + + private _getContentOne = (checked: boolean): void => { + const { isDataLoadedOne } = this.state; + this.setState({ + isDataLoadedOne: checked, + contentOne: !isDataLoadedOne ? 'Congratulations!!! You have successfully loaded the content. ' : '' + }); + } + + private _getContentTwo = (checked: boolean): void => { + const { isDataLoadedTwo } = this.state; + this.setState({ + isDataLoadedTwo: checked, + examplePersona: !isDataLoadedTwo ? { ...PersonaDetails } : {} + }); + } + + private _getCustomElements = (): JSX.Element => { + return ( +
+ + +
+ ); + } +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Styling.Example.tsx b/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Styling.Example.tsx new file mode 100644 index 00000000000000..bffcc9f199936c --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/examples/Shimmer.Styling.Example.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; + +import { Shimmer, IShimmerStyleProps, IShimmerStyles } from 'office-ui-fabric-react/lib/Shimmer'; + +import * as ShimmerExampleStyles from './Shimmer.Example.scss'; + +export class ShimmerStylingExample extends React.Component<{}, {}> { + constructor(props: {}) { + super(props); + } + + public render(): JSX.Element { + return ( +
+ + + + + +
+ ); + } + + private _getShimmerStyles = (props: IShimmerStyleProps): IShimmerStyles => { + return { + shimmerWrapper: [ + { + backgroundColor: '#deecf9', + backgroundImage: + 'linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #c7e0f4 50%, rgba(255, 255, 255, 0) 100%)' + } + ] + }; + } +} diff --git a/packages/office-ui-fabric-react/src/components/Shimmer/index.ts b/packages/office-ui-fabric-react/src/components/Shimmer/index.ts new file mode 100644 index 00000000000000..cfb95664f46ca9 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Shimmer/index.ts @@ -0,0 +1,10 @@ +export * from './Shimmer'; +export * from './Shimmer.base'; +export * from './Shimmer.types'; +export * from './ShimmerLine/ShimmerLine'; +export * from './ShimmerLine/ShimmerLine.types'; +export * from './ShimmerCircle/ShimmerCircle'; +export * from './ShimmerCircle/ShimmerCircle.types'; +export * from './ShimmerGap/ShimmerGap'; +export * from './ShimmerGap/ShimmerGap.types'; +export * from './ShimmerElementsGroup/ShimmerElementsGroup'; diff --git a/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx b/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx index a1a9ab1c9b291c..912254c709bc50 100644 --- a/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx +++ b/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx @@ -251,7 +251,14 @@ export const AppDefinition: IAppDefinition = { url: '#/examples/searchbox' }, { - component: require('../components/SelectedItemsList/SelectedPeopleList/SelectedPeopleListPage').SelectedPeopleListPage, + component: require('../components/Shimmer/ShimmerPage').ShimmerPage, + key: 'Shimmer', + name: 'Shimmer', + url: '#/examples/shimmer' + }, + { + component: require('../components/SelectedItemsList/SelectedPeopleList/SelectedPeopleListPage') + .SelectedPeopleListPage, key: 'SelectedPeopleList', name: 'SelectedPeopleList', url: '#examples/selectedpeoplelist' diff --git a/packages/office-ui-fabric-react/src/demo/ComponentStatus/AllComponents.checklist.ts b/packages/office-ui-fabric-react/src/demo/ComponentStatus/AllComponents.checklist.ts index 055d491471f919..a14b749573b51f 100644 --- a/packages/office-ui-fabric-react/src/demo/ComponentStatus/AllComponents.checklist.ts +++ b/packages/office-ui-fabric-react/src/demo/ComponentStatus/AllComponents.checklist.ts @@ -44,6 +44,7 @@ export const AllComponentsStatus: IComponentStatusState = { ResizeGroup: require('../../components/ResizeGroup/ResizeGroup.checklist').ResizeGroupStatus, ScrollablePane: require('../../components/ScrollablePane/ScrollablePane.checklist').ScrollablePaneStatus, SearchBox: require('../../components/SearchBox/SearchBox.checklist').SearchBoxStatus, + Shimmer: require('../../components/Shimmer/Shimmer.checklist').ShimmerStatus, Slider: require('../../components/Slider/Slider.checklist').SliderStatus, Spinner: require('../../components/Spinner/Spinner.checklist').SpinnerStatus, SpinButton: require('../../components/SpinButton/SpinButton.checklist').SpinButtonStatus, diff --git a/packages/office-ui-fabric-react/src/index.ts b/packages/office-ui-fabric-react/src/index.ts index a2bcbfe47b40c1..704478b159dd59 100644 --- a/packages/office-ui-fabric-react/src/index.ts +++ b/packages/office-ui-fabric-react/src/index.ts @@ -53,6 +53,7 @@ export * from './ResizeGroup'; export * from './ScrollablePane'; export * from './SearchBox'; export * from './SelectedItemsList'; +export * from './Shimmer'; export * from './Slider'; export * from './SpinButton'; export * from './Spinner';