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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "office-ui-fabric-react",
"comment": "ShimmeredDetailsList: adds a new wrapper for DetailsList when needed to be used with Shimmer animation.",
"type": "minor"
}
],
"packageName": "office-ui-fabric-react",
"email": "v-vibr@microsoft.com"
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,22 @@ $detailsList-text-color: $listTextColor;
:global(.ms-List-cell) {
min-height: 32px;
}
}
}

/* Adding a fadding out overlay to emphasize that we don't know the number of items that will eventually be diplayed */
.shimmerFadeOut {
&::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-image: linear-gradient(
to bottom,
transparent 30%,
$ms-color-whiteTranslucent40 65%,
$ms-color-white 100%
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export class DetailsList extends BaseComponent<IDetailsListProps, IDetailsListSt
}

public componentWillReceiveProps(newProps: IDetailsListProps): void {
const { checkboxVisibility, items, setKey, selectionMode, columns, viewport } = this.props;
const { checkboxVisibility, items, setKey, selectionMode, columns, viewport, compact } = this.props;
const shouldResetSelection = newProps.setKey !== setKey || newProps.setKey === undefined;
let shouldForceUpdates = false;

Expand All @@ -245,7 +245,8 @@ export class DetailsList extends BaseComponent<IDetailsListProps, IDetailsListSt
if (
newProps.checkboxVisibility !== checkboxVisibility ||
newProps.columns !== columns ||
newProps.viewport!.width !== viewport!.width
newProps.viewport!.width !== viewport!.width ||
newProps.compact !== compact
) {
shouldForceUpdates = true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import * as React from 'react';

import { BaseComponent, css } from '../../Utilities';
import { SelectionMode } from '../../utilities/selection/interfaces';
import { IDetailsListProps, CheckboxVisibility } from './DetailsList.types';
import { DetailsList } from './DetailsList';
import { IDetailsRowProps } from './DetailsRow';
import { Shimmer, ShimmerElementsGroup, ShimmerElementType, IShimmerElement } from '../Shimmer';

import * as rowStyles from './DetailsRow.scss';
import * as listStyles from './DetailsList.scss';

export interface IShimmeredDetailsListProps extends IDetailsListProps {
shimmerLines?: number;
onRenderCustomPlaceholder?: () => React.ReactNode;
}

const SHIMMER_INITIAL_ITEMS = 10;
const DEFAULT_SHIMMER_HEIGHT = 7;

// This values are matching values from ./DetailsRow.css
const DEFAULT_SIDE_PADDING = 8;
const DEFAULT_ROW_HEIGHT = 42;
const COMPACT_ROW_HEIGHT = 32;

export class ShimmeredDetailsList extends BaseComponent<IShimmeredDetailsListProps, {}> {
private _shimmerItems: null[];

constructor(props: IShimmeredDetailsListProps) {
super(props);

this._shimmerItems = props.shimmerLines ? new Array(props.shimmerLines) : new Array(SHIMMER_INITIAL_ITEMS);
}

public render(): JSX.Element {
const { items, listProps } = this.props;
const { shimmerLines, onRenderCustomPlaceholder, enableShimmer, ...detailsListProps } = this.props;

// Adds to the optional listProp classname a fading out overlay classname only when shimmer enabled.
const shimmeredListClassname: string = css(
listProps && listProps.className,
enableShimmer && listStyles.shimmerFadeOut
);
const newListProps = { ...listProps, className: shimmeredListClassname };

return (
<DetailsList
{...detailsListProps}
items={enableShimmer ? this._shimmerItems : items}
onRenderMissingItem={this._onRenderShimmerPlaceholder}
listProps={newListProps}
/>
);
}

private _onRenderShimmerPlaceholder = (index: number, rowProps: IDetailsRowProps): React.ReactNode => {
const { onRenderCustomPlaceholder, compact } = this.props;
const { selectionMode, checkboxVisibility } = rowProps;
const showCheckbox = selectionMode !== SelectionMode.none && checkboxVisibility !== CheckboxVisibility.hidden;

const placeholderElements: React.ReactNode = onRenderCustomPlaceholder
? onRenderCustomPlaceholder()
: this._renderDefaultShimmerPlaceholder(rowProps);

return (
<div className={css(showCheckbox && rowStyles.shimmerLeftBorder, !compact && rowStyles.shimmerBottomBorder)}>
<Shimmer customElementsGroup={placeholderElements} />
</div>
);
};

private _renderDefaultShimmerPlaceholder = (rowProps: IDetailsRowProps): React.ReactNode => {
const { columns, compact } = rowProps;
const shimmerElementsRow: JSX.Element[] = [];
const gapHeight: number = compact ? COMPACT_ROW_HEIGHT : DEFAULT_ROW_HEIGHT;

columns.map((column, columnIdx) => {
const shimmerElements: IShimmerElement[] = [];
const groupWidth: number = DEFAULT_SIDE_PADDING * 2 + column.calculatedWidth!;

shimmerElements.push({
type: ShimmerElementType.gap,
width: DEFAULT_SIDE_PADDING,
height: gapHeight
});

if (column.isIconOnly) {
shimmerElements.push({
type: ShimmerElementType.line,
width: column.calculatedWidth!,
height: column.calculatedWidth!
});
shimmerElements.push({
type: ShimmerElementType.gap,
width: DEFAULT_SIDE_PADDING,
height: gapHeight
});
} else {
shimmerElements.push({
type: ShimmerElementType.line,
width: column.calculatedWidth! - DEFAULT_SIDE_PADDING * 3,
height: DEFAULT_SHIMMER_HEIGHT
});
shimmerElements.push({
type: ShimmerElementType.gap,
width: DEFAULT_SIDE_PADDING * 4,
height: gapHeight
});
}
shimmerElementsRow.push(
<ShimmerElementsGroup key={columnIdx} width={`${groupWidth}px`} shimmerElements={shimmerElements} />
);
});
// When resizing the window from narrow to wider, we need to cover the exposed Shimmer wave until the column resizing logic is done.
shimmerElementsRow.push(
<ShimmerElementsGroup
key={'endGap'}
width={'100%'}
shimmerElements={[{ type: ShimmerElementType.gap, width: '100%', height: gapHeight }]}
/>
);
return <div style={{ display: 'flex' }}>{shimmerElementsRow}</div>;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './DetailsList';
export * from './DetailsList.types';
export * from './DetailsRow';
export * from './DetailsRowCheck';
export * from './ShimmeredDetailsList';
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const ShimmerPageProps: IDocPageProps = {
title: 'Shimmer',
componentName: 'ShimmerExample',
componentUrl:
'https://githubcom/OfficeDev/office-ui-fabric-react/tree/master/packages/office-ui-fabric-react/src/components/Shimmer',
'https://github.com/OfficeDev/office-ui-fabric-react/tree/master/packages/office-ui-fabric-react/src/components/Shimmer',
examples: [
{
title: "Shimmer with basic elements using the 'shimmerElements' prop",
Expand All @@ -39,7 +39,7 @@ export const ShimmerPageProps: IDocPageProps = {
view: <ShimmerLoadDataExample />
},
{
title: 'Details List with 500 items simulating loading data in async manner and having Shimmer enabled.',
title: 'Shimmered Details List with 500 items simulating loading data in async manner.',
code: ShimmerApplicationExampleCode,
view: <ShimmerApplicationExample />
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,8 @@ import * as React from 'react';
/* tslint:enable:no-unused-variable */
import { BaseComponent } from 'office-ui-fabric-react/lib/Utilities';
import { createListItems } from '../../../utilities/exampleData';
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 { IColumn, buildColumns, SelectionMode, Toggle } from 'office-ui-fabric-react/lib/index';
import { ShimmeredDetailsList } from '../../DetailsList';

import * as ShimmerExampleStyles from './Shimmer.Example.scss';

Expand Down Expand Up @@ -56,8 +48,7 @@ const fileIcons: { name: string }[] = [
];

const ITEMS_COUNT = 500;
const ITEMS_BATCH_SIZE = 10;
const PAGING_DELAY = 2500;
const INTERVAL_DELAY = 2500;

// tslint:disable-next-line:no-any
let _items: any[];
Expand All @@ -71,8 +62,8 @@ export interface IShimmerApplicationExampleState {
}

export class ShimmerApplicationExample extends BaseComponent<{}, IShimmerApplicationExampleState> {
private _isFetchingItems: boolean;
private _lastTimeoutId: number;
private _lastIntervalId: number;
private _lastIndexWithData: number;

constructor(props: {}) {
super(props);
Expand All @@ -86,6 +77,10 @@ export class ShimmerApplicationExample extends BaseComponent<{}, IShimmerApplica
};
}

public componentWillUnmount(): void {
this._async.dispose();
}

public render(): JSX.Element {
const { items, columns, isDataLoaded, isModalSelection, isCompactMode } = this.state;

Expand Down Expand Up @@ -115,49 +110,37 @@ export class ShimmerApplicationExample extends BaseComponent<{}, IShimmerApplica
/>
</div>
<div>
<DetailsList
<ShimmeredDetailsList
setKey="items"
items={items!}
columns={columns}
compact={isCompactMode}
selectionMode={this.state.isModalSelection ? SelectionMode.multiple : SelectionMode.none}
onRenderItemColumn={this._onRenderItemColumn}
onRenderMissingItem={this._onRenderMissingItem}
enableShimmer={true}
enableShimmer={!isDataLoaded}
listProps={{ renderedWindowsAhead: 0, renderedWindowsBehind: 0 }}
/>
</div>
</div>
);
}

private _onRenderMissingItem = (index: number, rowProps: IDetailsRowProps): React.ReactNode => {
const { isDataLoaded } = this.state;
isDataLoaded && this._onDataMiss(index as number);

const shimmerRow: JSX.Element = <DetailsRow {...rowProps} shimmer={true} />;

return <Shimmer customElementsGroup={shimmerRow} />;
};

// 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 _loadData = (): void => {
this._lastIntervalId = this._async.setInterval(() => {
const randomQuantity: number = Math.floor(Math.random() * 10) + 1;
// tslint:disable-next-line:no-any
const itemsCopy = ([] as any[]).concat(this.state.items);
itemsCopy.splice.apply(
itemsCopy,
[this._lastIndexWithData, randomQuantity].concat(
_items.slice(this._lastIndexWithData, this._lastIndexWithData + randomQuantity)
)
);
this._lastIndexWithData += randomQuantity;
this.setState({
items: itemsCopy
});
}, INTERVAL_DELAY);
};

private _onLoadData = (checked: boolean): void => {
Expand All @@ -170,11 +153,14 @@ export class ShimmerApplicationExample extends BaseComponent<{}, IShimmerApplica
}

let items: IItem[];
const randomQuantity: number = Math.floor(Math.random() * 10) + 1;
if (checked) {
items = _items.slice(0, ITEMS_BATCH_SIZE).concat(new Array(ITEMS_COUNT - ITEMS_BATCH_SIZE));
items = _items.slice(0, randomQuantity).concat(new Array(ITEMS_COUNT - randomQuantity));
this._lastIndexWithData = randomQuantity;
this._loadData();
} else {
items = new Array();
this._async.clearTimeout(this._lastTimeoutId);
this._async.clearInterval(this._lastIntervalId);
}
this.setState({
isDataLoaded: checked,
Expand Down