Skip to content

Commit

Permalink
GroupedList: fix virtualization (unstable preview) (#24460)
Browse files Browse the repository at this point in the history
* GroupedList: add new version to address virtualization issues

Introduces a new component, GroupedListV2, that is a drop-in replacement
for GroupedList. GroupedListV2 addresses a bug with the virtualization
implementation in GroupedList. As it is a significant re-write of the
internals we've decided to make it a new component so users can opt in
to the new component/behavior as needed rather than risk significant
breakage with the existing GroupedList implementation.

---

Virtualization in GroupedList is powered by List and groups in GroupedList
are nested Lists. When nested with two or more levels of groups issues
can arise with virtualization that result in the vertical size of the
Lists being miscalculated resulting in items not rendering, the scrollbar
repeatedly resizing (causing the list to "jump" about), or both.

List does work asynchronously which contributes to the issue itself and
makes debugging practically impossible as even a simple GroupedList will
contain many Lists all of which are virtualized and rendering async.

To address this issue we are introducing GroupedListV2 which is a drop-in
replacement for GropedList (V1) as it adheres to the same API.

Internally GroupedListV2 flattens virtualization into a single List
eliminating the virtualization bug described above and making the list
easier to reason about and debug.

* List: add conditional rendering option

Adds support to conditionally render cells in List which helps when
rendering flattend GroupedLists as we don't really know if we need to
render certain parts of the list (e.g., footers) until we call the
render function.

Ensure GroupedList <--> GroupedListV2 compatibility.

* DetailsList: allow for custom GroupedList renderer

This change allows users to provide a custom GroupedList renderer like
GroupedListV2.

* Update @fluentui/react API snapshot
Add GroupedListV2 tests
Add DetailsList tests
A dd support for ungrouped lists

* add perf tests for groupedlist/groupedlistv2

* change files

* better types and refactor render functions.

* refactor grouped items

* typescript

* WIP debugging

* fix issues from tests

- Add proper `getKey()` handling.
- Remove selection dependency for "show all" and footer rendering.

* Mark GroupedListV2 as unstable

* groupedlistv2: update naming

- Rename to GroupedListV2_unstable
- Update tests to use this name

* update api snapshot

* update groupedlistv2 import for perf-test

* update snapshots

* pr feedback

* update test snapshot
  • Loading branch information
spmonahan authored Sep 16, 2022
1 parent be154fe commit b7395d6
Show file tree
Hide file tree
Showing 23 changed files with 11,765 additions and 18 deletions.
1 change: 1 addition & 0 deletions apps/perf-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@fluentui/eslint-plugin": "*"
},
"dependencies": {
"@fluentui/example-data": "^8.4.2",
"@fluentui/react": "^8.95.1",
"@fluentui/scripts": "^1.0.0",
"@microsoft/load-themed-styles": "^1.10.26",
Expand Down
5 changes: 5 additions & 0 deletions apps/perf-test/src/scenarioIterations.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const scenarioIterations = {
ComboBox: 1000,
Persona: 1000,
ContextualMenu: 1000,
/* List performance is generally more influenced by the size
* of the list rather than the number of lists on a page.
*/
GroupedList: 2,
GroupedListV2: 2,
};

module.exports = scenarioIterations;
2 changes: 2 additions & 0 deletions apps/perf-test/src/scenarioRenderTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const DefaultRenderTypes = ['mount'];

const scenarioRenderTypes = {
ThemeProvider: AllRenderTypes,
GroupedList: AllRenderTypes,
GroupedListV2: AllRenderTypes,
};

module.exports = {
Expand Down
55 changes: 55 additions & 0 deletions apps/perf-test/src/scenarios/GroupedList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as React from 'react';
import { createListItems, createGroups, IExampleItem } from '@fluentui/example-data';
import { GroupedList, Selection, SelectionMode, DetailsRow, IGroup, IColumn } from '@fluentui/react';

const groupCount = 5;
const groupDepth = 5;
const items = createListItems(Math.pow(groupCount, groupDepth + 1));
const groups = createGroups(groupCount, groupDepth, 0, groupCount);

const columns = Object.keys(items[0])
.slice(0, 3)
.map(
(key: string): IColumn => ({
key: key,
name: key,
fieldName: key,
minWidth: 300,
}),
);

const selection = new Selection();
selection.setItems(items);

const onRenderCell = (
nestingDepth?: number,
item?: IExampleItem,
itemIndex?: number,
group?: IGroup,
): React.ReactNode => {
return item && typeof itemIndex === 'number' && itemIndex > -1 ? (
<DetailsRow
columns={columns}
groupNestingDepth={nestingDepth}
item={item}
itemIndex={itemIndex}
selection={selection}
selectionMode={SelectionMode.multiple}
group={group}
/>
) : null;
};

const Scenario = () => {
return (
<GroupedList
items={items}
groups={groups}
onRenderCell={onRenderCell}
selection={selection}
selectionMode={SelectionMode.multiple}
/>
);
};

export default Scenario;
62 changes: 62 additions & 0 deletions apps/perf-test/src/scenarios/GroupedListV2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as React from 'react';
import { createListItems, createGroups, IExampleItem } from '@fluentui/example-data';
import {
GroupedListV2_unstable as GroupedListV2,
Selection,
SelectionMode,
DetailsRow,
IGroup,
IColumn,
} from '@fluentui/react';

const groupCount = 5;
const groupDepth = 5;
const items = createListItems(Math.pow(groupCount, groupDepth + 1));
const groups = createGroups(groupCount, groupDepth, 0, groupCount);

const columns = Object.keys(items[0])
.slice(0, 3)
.map(
(key: string): IColumn => ({
key: key,
name: key,
fieldName: key,
minWidth: 300,
}),
);

const selection = new Selection();
selection.setItems(items);

const onRenderCell = (
nestingDepth?: number,
item?: IExampleItem,
itemIndex?: number,
group?: IGroup,
): React.ReactNode => {
return item && typeof itemIndex === 'number' && itemIndex > -1 ? (
<DetailsRow
columns={columns}
groupNestingDepth={nestingDepth}
item={item}
itemIndex={itemIndex}
selection={selection}
selectionMode={SelectionMode.multiple}
group={group}
/>
) : null;
};

const Scenario = () => {
return (
<GroupedListV2
items={items}
groups={groups}
onRenderCell={onRenderCell}
selection={selection}
selectionMode={SelectionMode.multiple}
/>
);
};

export default Scenario;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: improve groupedlist virtualization",
"packageName": "@fluentui/react",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from 'react';
import { IGroup } from '@fluentui/react/lib/GroupedList';
import { GroupedListV2_unstable as GroupedListV2 } from '@fluentui/react/lib/GroupedListV2';
import { IColumn, DetailsRow } from '@fluentui/react/lib/DetailsList';
import { Selection, SelectionMode, SelectionZone } from '@fluentui/react/lib/Selection';
import { Toggle, IToggleStyles } from '@fluentui/react/lib/Toggle';
import { useBoolean, useConst } from '@fluentui/react-hooks';
import { createListItems, createGroups, IExampleItem } from '@fluentui/example-data';

const toggleStyles: Partial<IToggleStyles> = { root: { marginBottom: '20px' } };
const groupCount = 3;
const groupDepth = 3;
const items = createListItems(Math.pow(groupCount, groupDepth + 1));
const columns = Object.keys(items[0])
.slice(0, 3)
.map(
(key: string): IColumn => ({
key: key,
name: key,
fieldName: key,
minWidth: 300,
}),
);

const groups = createGroups(groupCount, groupDepth, 0, groupCount);

export const GroupedListV2BasicExample: React.FunctionComponent = () => {
const [isCompactMode, { toggle: toggleIsCompactMode }] = useBoolean(false);
const selection = useConst(() => {
const s = new Selection();
s.setItems(items, true);
return s;
});

const onRenderCell = (
nestingDepth?: number,
item?: IExampleItem,
itemIndex?: number,
group?: IGroup,
): React.ReactNode => {
return item && typeof itemIndex === 'number' && itemIndex > -1 ? (
<DetailsRow
columns={columns}
groupNestingDepth={nestingDepth}
item={item}
itemIndex={itemIndex}
selection={selection}
selectionMode={SelectionMode.multiple}
compact={isCompactMode}
group={group}
/>
) : null;
};

return (
<div>
<Toggle
label="Enable compact mode"
checked={isCompactMode}
onChange={toggleIsCompactMode}
onText="Compact"
offText="Normal"
styles={toggleStyles}
/>
<SelectionZone selection={selection} selectionMode={SelectionMode.multiple}>
<GroupedListV2
items={items}
// eslint-disable-next-line react/jsx-no-bind
onRenderCell={onRenderCell}
selection={selection}
selectionMode={SelectionMode.multiple}
groups={groups}
compact={isCompactMode}
/>
</SelectionZone>
</div>
);
};

// @ts-expect-error Storybook
GroupedListV2BasicExample.storyName = 'V2 Basic';
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as React from 'react';
import { IGroup, IGroupHeaderProps, IGroupFooterProps } from '@fluentui/react/lib/GroupedList';
import { GroupedListV2_unstable as GroupedListV2 } from '@fluentui/react/lib/GroupedListV2';
import { Link } from '@fluentui/react/lib/Link';
import { createListItems, createGroups, IExampleItem } from '@fluentui/example-data';
import { getTheme, mergeStyleSets, IRawStyle } from '@fluentui/react/lib/Styling';

const theme = getTheme();
const headerAndFooterStyles: IRawStyle = {
minWidth: 300,
minHeight: 40,
lineHeight: 40,
paddingLeft: 16,
};
const classNames = mergeStyleSets({
header: [headerAndFooterStyles, theme.fonts.xLarge],
footer: [headerAndFooterStyles, theme.fonts.large],
name: {
display: 'inline-block',
overflow: 'hidden',
height: 24,
cursor: 'default',
padding: 8,
boxSizing: 'border-box',
verticalAlign: 'top',
background: 'none',
backgroundColor: 'transparent',
border: 'none',
paddingLeft: 32,
},
});

const onRenderHeader = (props?: IGroupHeaderProps): JSX.Element | null => {
if (props) {
const toggleCollapse = (): void => {
props.onToggleCollapse!(props.group!);
};
return (
<div className={classNames.header}>
This is a custom header for {props.group!.name}
&nbsp; (
<Link
// eslint-disable-next-line react/jsx-no-bind
onClick={toggleCollapse}
>
{props.group!.isCollapsed ? 'Expand' : 'Collapse'}
</Link>
)
</div>
);
}

return null;
};

const onRenderCell = (nestingDepth?: number, item?: IExampleItem, itemIndex?: number): React.ReactNode => {
return item ? (
<div role="row" data-selection-index={itemIndex}>
<span role="cell" className={classNames.name}>
{item.name}
</span>
</div>
) : null;
};

const onRenderFooter = (props?: IGroupFooterProps): JSX.Element | null => {
return props ? <div className={classNames.footer}>This is a custom footer for {props.group!.name}</div> : null;
};

const groupedListProps = {
onRenderHeader,
onRenderFooter,
};
const items: IExampleItem[] = createListItems(20);
const groups: IGroup[] = createGroups(4, 0, 0, 5);

export const GroupedListV2CustomExample: React.FunctionComponent = () => (
<GroupedListV2 items={items} onRenderCell={onRenderCell} groupProps={groupedListProps} groups={groups} />
);

// @ts-expect-error Storybook
GroupedListV2CustomExample.storyName = 'V2 Custom';
Loading

0 comments on commit b7395d6

Please sign in to comment.