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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/fabric-website/src/components/App/AppState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,12 @@ export const AppState: IAppState = {
component: () => <LoadingComponent title='SearchBox' />,
getComponent: cb => require.ensure([], (require) => cb(require<any>('../../pages/Components/SearchBoxComponentPage').SearchBoxComponentPage))
},
{
title: 'Shimmer',
url: '#/components/shimmer',
component: () => <LoadingComponent title='Shimmer' />,
getComponent: cb => require.ensure([], (require) => cb(require<any>('../../pages/Components/ShimmerComponentPage').ShimmerComponentPage))
},
{
title: 'Slider',
url: '#/components/slider',
Expand Down
38 changes: 38 additions & 0 deletions apps/fabric-website/src/pages/Components/ShimmerComponentPage.tsx
Original file line number Diff line number Diff line change
@@ -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<any, any> {
public render(): JSX.Element {
return (
<div className={ pageStyles.basePage }>
<ComponentPage>
<PageHeader pageTitle='Shimmer' backgroundColor='#038387'
links={
[
{
'text': 'Overview',
'location': 'Overview'
},
{
'text': 'Best Practices',
'location': 'BestPractices'
},
{
'text': 'Variants',
'location': 'Variants'
},
{
'text': 'Implementation',
'location': 'Implementation'
}
]
} />
<ShimmerPage isHeaderVisible={ false } />
</ComponentPage>
</div>
);
}
}
56 changes: 56 additions & 0 deletions apps/vr-tests/src/stories/Shimmer.stories.tsx
Original file line number Diff line number Diff line change
@@ -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
<div style={{ width: '500px' }}>{story()}</div>
))
.addDecorator(FabricDecorator)
.addDecorator(story => <Screener steps={new Screener.Steps().snapshot('default').end()}>{story()}</Screener>)
.add('Basic', () => <Shimmer />)
.add('50% width', () => <Shimmer width={'50%'} />)
.add('Circle Gap Line', () => (
<Shimmer
shimmerElements={[{ type: ElemType.circle }, { type: ElemType.gap, width: '2%' }, { type: ElemType.line }]}
/>
))
.add('Custom elements', () => (
<Shimmer
customElementsGroup={
<div
// tslint:disable-next-line:jsx-ban-props
style={{ display: 'flex' }}
>
<ShimmerElementsGroup
shimmerElements={[{ type: ElemType.circle, height: 40 }, { type: ElemType.gap, width: 16, height: 40 }]}
/>
<ShimmerElementsGroup
flexWrap={true}
width={'100%'}
shimmerElements={[
{ type: ElemType.line, width: '100%', height: 10, verticalAlign: 'bottom' },
{ type: ElemType.line, width: '90%', height: 8 },
{ type: ElemType.gap, width: '10%', height: 20 }
]}
/>
</div>
}
width={300}
/>
))
.add('Data not loaded', () => (
<Shimmer isDataLoaded={false} ariaLabel={'Loading content'}>
<div>Example content</div>
</Shimmer>
))
.add('Data loaded', () => (
<Shimmer isDataLoaded={true} ariaLabel={'Loading content'}>
<div>Example content</div>
</Shimmer>
));
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions packages/office-ui-fabric-react/src/Shimmer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './components/Shimmer/index';
Original file line number Diff line number Diff line change
@@ -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<IShimmerStyleProps, IShimmerStyles>();

@customizable('Shimmer', ['theme'])
export class ShimmerBase extends BaseComponent<IShimmerProps, IShimmerState> {
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 (
<div { ...divProps } className={ this._classNames.root }>
{ !contentLoaded && (
<div style={ { width: width ? width : '100%' } } className={ this._classNames.shimmerWrapper }>
{ customElementsGroup ? customElementsGroup : <ShimmerElementsGroup shimmerElements={ shimmerElements } /> }
</div>
) }
{ children && <div className={ this._classNames.dataWrapper }>{ children }</div> }
{ ariaLabel &&
!isDataLoaded && (
<div role='status' aria-live='polite'>
<DelayedRender>
<div className={ this._classNames.screenReaderText }>{ ariaLabel }</div>
</DelayedRender>
</div>
) }
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -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
};
Original file line number Diff line number Diff line change
@@ -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
};
}
Loading