-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GroupedList: fix virtualization (unstable preview) (#24460)
* 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
Showing
23 changed files
with
11,765 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-6c6ea224-d7e4-405c-92cc-8268da44ee7a.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": "seanmonahan@microsoft.com", | ||
"dependentChangeType": "patch" | ||
} |
81 changes: 81 additions & 0 deletions
81
packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
82 changes: 82 additions & 0 deletions
82
packages/react-examples/src/react/GroupedList/GroupedListV2.Custom.Example.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
( | ||
<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'; |
Oops, something went wrong.