From 70dc26741ab711717c60e70eae8c84aa595cdcb5 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Fri, 12 Aug 2022 14:18:37 -0700 Subject: [PATCH 01/18] 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. --- .../GroupedList/GroupedList.Basic.Example.tsx | 4 + .../GroupedListV2.Basic.Example.tsx | 82 ++++ packages/react/etc/react.api.md | 51 ++ .../GroupedList/GroupedListV2.base.tsx | 457 ++++++++++++++++++ .../components/GroupedList/GroupedListV2.ts | 15 + .../react/src/components/GroupedList/index.ts | 3 + 6 files changed, 612 insertions(+) create mode 100644 packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx create mode 100644 packages/react/src/components/GroupedList/GroupedListV2.base.tsx create mode 100644 packages/react/src/components/GroupedList/GroupedListV2.ts diff --git a/packages/react-examples/src/react/GroupedList/GroupedList.Basic.Example.tsx b/packages/react-examples/src/react/GroupedList/GroupedList.Basic.Example.tsx index cd34ff05805070..767ea3eb0ee510 100644 --- a/packages/react-examples/src/react/GroupedList/GroupedList.Basic.Example.tsx +++ b/packages/react-examples/src/react/GroupedList/GroupedList.Basic.Example.tsx @@ -70,6 +70,10 @@ export const GroupedListBasicExample: React.FunctionComponent = () => { selectionMode={SelectionMode.multiple} groups={groups} compact={isCompactMode} + groupProps={{ + footerProps: { footerText: 'cats' }, + getGroupItemLimit: (group: any) => (group.key === 'group0-0-0' && !group.isShowingAll ? 1 : group.count), + }} /> diff --git a/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx b/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx new file mode 100644 index 00000000000000..cd708f647c717a --- /dev/null +++ b/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { GroupedListV2, IGroup } from '@fluentui/react/lib/GroupedList'; +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 = { 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 ? ( + + ) : null; + }; + + return ( +
+ + + + +
+ ); +}; + +// @ts-expect-error Storybook +GroupedListV2BasicExample.story = { + name: 'V2 Basic', +}; diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index cec44a35208fad..ec129a8d1cdb15 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -2012,6 +2012,29 @@ export class GroupedListSection extends React_2.Component; + +// @public (undocumented) +export const GroupedListV2FC: React_2.FC; + +// @public (undocumented) +export class GroupedListV2Wrapper extends React_2.Component implements IGroupedList { + constructor(props: IGroupedListProps); + // (undocumented) + forceUpdate(): void; + // (undocumented) + static getDerivedStateFromProps(nextProps: IGroupedListProps, previousState: IGroupedListV2State): IGroupedListV2State; + // (undocumented) + getStartItemIndexInView(): number; + // (undocumented) + render(): JSX.Element; + // (undocumented) + scrollToIndex(index: number, measureItem?: (itemIndex: number) => number, scrollToMode?: ScrollToMode): void; + // (undocumented) + toggleCollapseAll(allCollapsed: boolean): void; +} + // @public (undocumented) export const GroupFooter: React_2.FunctionComponent; @@ -6117,6 +6140,34 @@ export interface IGroupedListStyles { root: IStyle; } +// @public (undocumented) +export interface IGroupedListV2Props extends IGroupedListProps { + // (undocumented) + groupExpandedVersion: {}; + // (undocumented) + listRef: React_2.Ref; + // (undocumented) + version: {}; +} + +// @public (undocumented) +export interface IGroupedListV2State { + // (undocumented) + compact?: IGroupedListProps['compact']; + // (undocumented) + groupExpandedVersion: {}; + // (undocumented) + groups?: IGroup[]; + // (undocumented) + items?: IGroupedListProps['items']; + // (undocumented) + listProps?: IGroupedListProps['listProps']; + // (undocumented) + selectionMode?: IGroupedListProps['selectionMode']; + // (undocumented) + version: {}; +} + // @public (undocumented) export interface IGroupFooterProps extends IGroupDividerProps { styles?: IStyleFunctionOrObject; diff --git a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx new file mode 100644 index 00000000000000..3bac4997ec3c0e --- /dev/null +++ b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx @@ -0,0 +1,457 @@ +import * as React from 'react'; +import { initializeComponentRef, classNamesFunction, KeyCodes, getRTLSafeKeyCode, css, getId } from '../../Utilities'; +import { List, ScrollToMode } from '../../List'; +import { SelectionMode } from '../../Selection'; +import { FocusZone, FocusZoneDirection } from '../../FocusZone'; +import type { IProcessedStyleSet } from '../../Styling'; +import type { + IGroupedList, + IGroupedListProps, + IGroup, + IGroupRenderProps, + IGroupedListStyleProps, + IGroupedListStyles, +} from './GroupedList.types'; +import { GroupHeader } from './GroupHeader'; +import { GroupShowAll } from './GroupShowAll'; +import { GroupFooter } from './GroupFooter'; +import type { IGroupHeaderProps } from './GroupHeader'; +import type { IGroupShowAllProps } from './GroupShowAll.styles'; +import type { IGroupFooterProps } from './GroupFooter.types'; +import { useMount } from '@fluentui/react-hooks'; + +export interface IGroupedListV2State { + selectionMode?: IGroupedListProps['selectionMode']; + compact?: IGroupedListProps['compact']; + groups?: IGroup[]; + items?: IGroupedListProps['items']; + listProps?: IGroupedListProps['listProps']; + version: {}; + groupExpandedVersion: {}; +} + +export interface IGroupedListV2Props extends IGroupedListProps { + listRef: React.Ref; + version: {}; + groupExpandedVersion: {}; +} + +type IGroupedItem = { + group?: IGroup; + groupId?: string; + item?: any; + itemIndex?: number; + type: 'group' | 'item' | 'showAll' | 'footer'; +}; + +type FlattenItemsFn = ( + groups: IGroup[] | undefined, + items: any[], + memoItems: IGroupedItem[], + groupProps: IGroupRenderProps, +) => IGroupedItem[]; + +const flattenItems: FlattenItemsFn = (groups, items, memoItems, groupProps) => { + if (groups === undefined) { + return items; + } + + if (memoItems.length < 1) { + // Not the exact final size but gets us in the ballpark. + // This helps avoid trashing memory when building + // the flattened list. + memoItems = new Array(items.length); + } + + let index = 0; + + const stack: IGroup[] = []; + let j = groups.length - 1; + while (j >= 0) { + stack.push(groups[j]); + j--; + } + + while (stack.length > 0) { + let group = stack.pop()!; + memoItems[index] = { + group, + groupId: getId('GroupedListSection'), + type: 'group', + }; + + index++; + + while (group.isCollapsed !== true && group?.children && group.children.length > 0) { + j = group.children.length - 1; + while (j > 0) { + stack.push(group.children[j]); + j--; + } + group = group.children[0]; + memoItems[index] = { + group, + groupId: getId('GroupedListSection'), + type: 'group', + }; + index++; + } + + if (group.isCollapsed !== true) { + let itemIndex = group.startIndex; + const renderCount = groupProps.getGroupItemLimit ? groupProps.getGroupItemLimit(group) : Infinity; + const itemEnd = itemIndex + Math.min(group.count, renderCount); + while (itemIndex < itemEnd) { + memoItems[index] = { + group, + item: items[itemIndex], + itemIndex, // track the index in `item` for later rendering/selection + type: 'item', + }; + itemIndex++; + index++; + } + + const isShowAllVisible = + !group.children && + !group.isCollapsed && + !group.isShowingAll && + (group.count > renderCount || group.hasMoreData); + + if (isShowAllVisible) { + memoItems[index] = { + group, + type: 'showAll', + }; + index++; + } + + if (groupProps.footerProps?.footerText) { + memoItems[index] = { + group, + type: 'footer', + }; + index++; + } + } + } + + memoItems.length = index; + + console.log('MEMO ITEMS', memoItems); + + return memoItems; +}; + +const computeIsSomeGroupExpanded = (groups: IGroup[] | undefined): boolean => { + return !!( + groups && groups.some(group => (group.children ? computeIsSomeGroupExpanded(group.children) : !group.isCollapsed)) + ); +}; + +const setGroupsCollapsedState = (groups: IGroup[] | undefined, isCollapsed: boolean): void => { + if (groups === undefined) { + return; + } + for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) { + groups[groupIndex].isCollapsed = isCollapsed; + } +}; + +const isInnerZoneKeystroke = (ev: React.KeyboardEvent): boolean => { + // eslint-disable-next-line deprecation/deprecation + return ev.which === getRTLSafeKeyCode(KeyCodes.right); +}; + +const getClassNames = classNamesFunction(); + +const renderGroupHeader = (props: IGroupHeaderProps): JSX.Element => { + return ; +}; + +const renderGroupShowAll = (props: IGroupShowAllProps): JSX.Element => { + return ; +}; + +const renderGroupFooter = (props: IGroupFooterProps): JSX.Element => { + return ; +}; + +export const GroupedListV2FC: React.FC = props => { + const { + selection, + selectionMode = SelectionMode.multiple, + groupProps = {}, + compact = false, + items = [], + groups, + onGroupExpandStateChanged, + + className, + usePageCache, + onShouldVirtualize, + theme, + role = 'treegrid', + styles, + focusZoneProps = {}, + rootListProps = {}, + onRenderCell, + viewport, + listRef, + groupExpandedVersion, + } = props; + + const { + onRenderHeader = renderGroupHeader, + onRenderFooter = renderGroupFooter, + onRenderShowAll = renderGroupShowAll, + } = groupProps; + + const classNames: IProcessedStyleSet = getClassNames(styles, { + theme: theme!, + className, + compact, + }); + + const flatList = React.useRef([]); + const isSomeGroupExpanded = React.useRef(computeIsSomeGroupExpanded(groups)); + + const [version, setVersion] = React.useState({}); + const [toggleVersion, setToggleVersion] = React.useState({}); + + // eslint-disable-next-line deprecation/deprecation + const { shouldEnterInnerZone = isInnerZoneKeystroke } = focusZoneProps; + + const listView = React.useMemo(() => { + return flattenItems(groups, items, flatList.current, groupProps); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [groups, groupProps, items, toggleVersion, flatList]); + + useMount(() => { + if (groupProps?.isAllGroupsCollapsed) { + setGroupsCollapsedState(groups, groupProps.isAllGroupsCollapsed); + } + }); + + React.useEffect(() => { + setVersion({}); + }, [compact]); + + React.useEffect(() => { + const newIsSomeGroupExpanded = computeIsSomeGroupExpanded(groups); + if (newIsSomeGroupExpanded !== isSomeGroupExpanded.current) { + isSomeGroupExpanded.current = newIsSomeGroupExpanded; + onGroupExpandStateChanged?.(newIsSomeGroupExpanded); + } + }, [groups, toggleVersion, onGroupExpandStateChanged, groupExpandedVersion]); + + const onToggleCollapse = React.useCallback( + (group: IGroup): void => { + const onToggleCollapseFn = groupProps?.headerProps?.onToggleCollapse; + + if (group) { + onToggleCollapseFn?.(group); + group.isCollapsed = !group.isCollapsed; + setToggleVersion({}); + setVersion({}); + } + }, + [setToggleVersion, groupProps], + ); + + const onToggleSelectGroup = React.useCallback( + (group: IGroup): void => { + if (group && selection && selectionMode === SelectionMode.multiple) { + selection.toggleRangeSelected(group.startIndex, group.count); + } + }, + [selection, selectionMode], + ); + + const onToggleSummarize = React.useCallback( + (group: IGroup): void => { + const onToggleSummarizeFn = groupProps?.showAllProps?.onToggleSummarize; + + if (onToggleSummarizeFn) { + onToggleSummarizeFn(group); + } else { + if (group) { + group.isShowingAll = !group.isShowingAll; + } + + setVersion({}); + setToggleVersion({}); + } + }, + [groupProps], + ); + + const renderItem = React.useCallback( + (item: IGroupedItem, flattenedIndex: number): React.ReactNode => { + const group = item.group; + + const level = group?.level ? group.level + 1 : 1; + const isSelected = selection && group ? selection.isRangeSelected(group.startIndex, group.count) : false; + + const dividerProps = { + group, + groupIndex: flattenedIndex, + groupLevel: group?.level ?? 0, + isSelected, + selected: isSelected, + viewport, + selectionMode, + groups, + compact, + onToggleSelectGroup, + onToggleCollapse, + onToggleSummarize, + }; + + if (item.type === 'group') { + const groupHeaderProps = { + ...groupProps!.headerProps, + ...dividerProps, + key: group!.key, + groupedListId: item.groupId!, + ariaLevel: level, + ariaSetSize: groups ? groups.length : undefined, + ariaPosInSet: flattenedIndex !== undefined ? flattenedIndex + 1 : undefined, + }; + + return onRenderHeader(groupHeaderProps, renderGroupHeader); + } else if (item.type === 'showAll') { + const groupShowAllProps = { + ...groupProps!.showAllProps, + ...dividerProps, + key: group?.key ? `${group.key}-show-all` : undefined, + }; + + return onRenderShowAll(groupShowAllProps, renderGroupShowAll); + } else if (item.type === 'footer') { + const groupFooterProps = { + ...groupProps!.footerProps, + ...dividerProps, + key: group?.key ? `${group.key}-footer` : undefined, + }; + + return onRenderFooter(groupFooterProps, renderGroupFooter); + } else { + return onRenderCell(level, item.item, item.itemIndex ?? flattenedIndex); + } + }, + [ + onRenderCell, + groups, + groupProps, + selection, + selectionMode, + compact, + viewport, + onToggleCollapse, + onToggleSelectGroup, + onToggleSummarize, + onRenderHeader, + onRenderShowAll, + onRenderFooter, + ], + ); + + return ( + + + + ); +}; + +export class GroupedListV2Wrapper + extends React.Component + implements IGroupedList { + private _list = React.createRef(); + + public static getDerivedStateFromProps( + nextProps: IGroupedListProps, + previousState: IGroupedListV2State, + ): IGroupedListV2State { + const { groups } = nextProps; + + if (groups !== previousState.groups) { + return { + ...previousState, + groups, + }; + } + + return previousState; + } + + constructor(props: IGroupedListProps) { + super(props); + initializeComponentRef(this); + + const { listProps: { version = {} } = {}, groups } = props; + this.state = { + version, + groupExpandedVersion: {}, + groups, + }; + } + + public scrollToIndex(index: number, measureItem?: (itemIndex: number) => number, scrollToMode?: ScrollToMode): void { + if (this._list.current) { + this._list.current.scrollToIndex(index, measureItem, scrollToMode); + } + } + + public getStartItemIndexInView(): number { + return this._list.current!.getStartItemIndexInView() || 0; + } + + public render(): JSX.Element { + return ; + } + + public forceUpdate() { + super.forceUpdate(); + this._forceListUpdate(); + } + + public toggleCollapseAll(allCollapsed: boolean): void { + const { groups } = this.state; + const { groupProps } = this.props; + + if (groups && groups.length > 0) { + groupProps?.onToggleCollapseAll?.(allCollapsed); + + setGroupsCollapsedState(groups, allCollapsed); + this.setState({ + groupExpandedVersion: {}, + }); + + this.forceUpdate(); + } + } + + private _forceListUpdate(): void { + this.setState({ + version: {}, + }); + } +} diff --git a/packages/react/src/components/GroupedList/GroupedListV2.ts b/packages/react/src/components/GroupedList/GroupedListV2.ts new file mode 100644 index 00000000000000..1007927900e01c --- /dev/null +++ b/packages/react/src/components/GroupedList/GroupedListV2.ts @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { styled } from '../../Utilities'; +import { getStyles } from './GroupedList.styles'; +import { GroupedListV2Wrapper } from './GroupedListV2.base'; +import type { IGroupedListProps, IGroupedListStyles, IGroupedListStyleProps } from './GroupedList.types'; + +export const GroupedListV2: React.FunctionComponent = styled< + IGroupedListProps, + IGroupedListStyleProps, + IGroupedListStyles +>(GroupedListV2Wrapper, getStyles, undefined, { + scope: 'GroupedListV2', +}); + +export type { IGroupedListProps }; diff --git a/packages/react/src/components/GroupedList/index.ts b/packages/react/src/components/GroupedList/index.ts index b012ba695051b1..9aa720d297d2ba 100644 --- a/packages/react/src/components/GroupedList/index.ts +++ b/packages/react/src/components/GroupedList/index.ts @@ -10,3 +10,6 @@ export * from './GroupedListSection'; export type { IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupHeaderCheckboxProps } from './GroupHeader.types'; export type { IGroupFooterStyleProps, IGroupFooterStyles } from './GroupFooter.types'; export type { IGroupShowAllStyleProps, IGroupShowAllStyles } from './GroupShowAll.types'; + +export * from './GroupedListV2'; +export * from './GroupedListV2.base'; From 55cbe948a7e92aabc9a014d62bf7437f4b2f42f0 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Fri, 19 Aug 2022 11:24:23 -0700 Subject: [PATCH 02/18] 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. --- .../GroupedList/GroupedList.Basic.Example.tsx | 4 - .../GroupedListV2.Basic.Example.tsx | 4 +- .../GroupedListV2.Custom.Example.tsx | 81 +++++++++++++++++ .../GroupedListV2.CustomCheckbox.Example.tsx | 88 +++++++++++++++++++ .../GroupedList/GroupedListV2.base.tsx | 56 ++++++++---- packages/react/src/components/List/List.tsx | 33 ++++--- .../react/src/components/List/List.types.ts | 15 ++++ 7 files changed, 246 insertions(+), 35 deletions(-) create mode 100644 packages/react-examples/src/react/GroupedList/GroupedListV2.Custom.Example.tsx create mode 100644 packages/react-examples/src/react/GroupedList/GroupedListV2.CustomCheckbox.Example.tsx diff --git a/packages/react-examples/src/react/GroupedList/GroupedList.Basic.Example.tsx b/packages/react-examples/src/react/GroupedList/GroupedList.Basic.Example.tsx index 767ea3eb0ee510..cd34ff05805070 100644 --- a/packages/react-examples/src/react/GroupedList/GroupedList.Basic.Example.tsx +++ b/packages/react-examples/src/react/GroupedList/GroupedList.Basic.Example.tsx @@ -70,10 +70,6 @@ export const GroupedListBasicExample: React.FunctionComponent = () => { selectionMode={SelectionMode.multiple} groups={groups} compact={isCompactMode} - groupProps={{ - footerProps: { footerText: 'cats' }, - getGroupItemLimit: (group: any) => (group.key === 'group0-0-0' && !group.isShowingAll ? 1 : group.count), - }} /> diff --git a/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx b/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx index cd708f647c717a..6e42daf6b20678 100644 --- a/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx +++ b/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx @@ -77,6 +77,4 @@ export const GroupedListV2BasicExample: React.FunctionComponent = () => { }; // @ts-expect-error Storybook -GroupedListV2BasicExample.story = { - name: 'V2 Basic', -}; +GroupedListV2BasicExample.storyName = 'V2 Basic'; diff --git a/packages/react-examples/src/react/GroupedList/GroupedListV2.Custom.Example.tsx b/packages/react-examples/src/react/GroupedList/GroupedListV2.Custom.Example.tsx new file mode 100644 index 00000000000000..9554250a929035 --- /dev/null +++ b/packages/react-examples/src/react/GroupedList/GroupedListV2.Custom.Example.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { GroupedListV2, IGroup, IGroupHeaderProps, IGroupFooterProps } from '@fluentui/react/lib/GroupedList'; +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 ( +
+ This is a custom header for {props.group!.name} +   ( + + {props.group!.isCollapsed ? 'Expand' : 'Collapse'} + + ) +
+ ); + } + + return null; +}; + +const onRenderCell = (nestingDepth?: number, item?: IExampleItem, itemIndex?: number): React.ReactNode => { + return item ? ( +
+ + {item.name} + +
+ ) : null; +}; + +const onRenderFooter = (props?: IGroupFooterProps): JSX.Element | null => { + return props ?
This is a custom footer for {props.group!.name}
: null; +}; + +const groupedListProps = { + onRenderHeader, + onRenderFooter, +}; +const items: IExampleItem[] = createListItems(20); +const groups: IGroup[] = createGroups(4, 0, 0, 5); + +export const GroupedListV2CustomExample: React.FunctionComponent = () => ( + +); + +// @ts-expect-error Storybook +GroupedListV2CustomExample.storyName = 'V2 Custom'; diff --git a/packages/react-examples/src/react/GroupedList/GroupedListV2.CustomCheckbox.Example.tsx b/packages/react-examples/src/react/GroupedList/GroupedListV2.CustomCheckbox.Example.tsx new file mode 100644 index 00000000000000..3140675bfab0d2 --- /dev/null +++ b/packages/react-examples/src/react/GroupedList/GroupedListV2.CustomCheckbox.Example.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import { + GroupHeader, + GroupedListV2, + IGroupHeaderCheckboxProps, + IGroupHeaderProps, + IGroupRenderProps, + IGroup, +} from '@fluentui/react/lib/GroupedList'; +import { IColumn, IObjectWithKey, DetailsRow } from '@fluentui/react/lib/DetailsList'; +import { FocusZone } from '@fluentui/react/lib/FocusZone'; +import { Selection, SelectionMode, SelectionZone } from '@fluentui/react/lib/Selection'; +import { Icon } from '@fluentui/react/lib/Icon'; +import { useConst } from '@fluentui/react-hooks'; +import { createListItems, createGroups, IExampleItem } from '@fluentui/example-data'; + +const groupCount = 3; +const groupDepth = 1; + +const groupProps: IGroupRenderProps = { + onRenderHeader: (props?: IGroupHeaderProps): JSX.Element => ( + + ), +}; + +/* This is rendered within a checkbox, so it must not be interactive itself. */ +const onRenderGroupHeaderCheckbox = (props?: IGroupHeaderCheckboxProps) => { + const iconStyles = { root: { fontSize: '36px' } }; + + return props?.checked ? ( + + ) : ( + + ); +}; + +export const GroupedListV2CustomCheckboxExample: React.FunctionComponent = () => { + const items: IObjectWithKey[] = useConst(() => createListItems(Math.pow(groupCount, groupDepth + 1))); + const groups = useConst(() => createGroups(groupCount, groupDepth, 0, groupCount)); + const columns = useConst(() => + Object.keys(items[0]) + .slice(0, 3) + .map( + (key: string): IColumn => ({ + key: key, + name: key, + fieldName: key, + minWidth: 300, + }), + ), + ); + const selection = useConst(() => new Selection({ items })); + + const onRenderCell = React.useCallback( + (nestingDepth?: number, item?: IExampleItem, itemIndex?: number, group?: IGroup): React.ReactNode => ( + + ), + [columns, selection], + ); + + return ( +
+ + + + + +
+ ); +}; + +// @ts-expect-error Storybook +GroupedListV2CustomCheckboxExample.storyName = 'V2 Custom Checkbox'; diff --git a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx index 3bac4997ec3c0e..094bf94e73510e 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx +++ b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx @@ -1,7 +1,15 @@ import * as React from 'react'; -import { initializeComponentRef, classNamesFunction, KeyCodes, getRTLSafeKeyCode, css, getId } from '../../Utilities'; +import { + initializeComponentRef, + classNamesFunction, + KeyCodes, + getRTLSafeKeyCode, + css, + getId, + EventGroup, +} from '../../Utilities'; import { List, ScrollToMode } from '../../List'; -import { SelectionMode } from '../../Selection'; +import { SelectionMode, SELECTION_CHANGE } from '../../Selection'; import { FocusZone, FocusZoneDirection } from '../../FocusZone'; import type { IProcessedStyleSet } from '../../Styling'; import type { @@ -18,7 +26,7 @@ import { GroupFooter } from './GroupFooter'; import type { IGroupHeaderProps } from './GroupHeader'; import type { IGroupShowAllProps } from './GroupShowAll.styles'; import type { IGroupFooterProps } from './GroupFooter.types'; -import { useMount } from '@fluentui/react-hooks'; +import { useMount, useUnmount } from '@fluentui/react-hooks'; export interface IGroupedListV2State { selectionMode?: IGroupedListProps['selectionMode']; @@ -125,20 +133,21 @@ const flattenItems: FlattenItemsFn = (groups, items, memoItems, groupProps) => { }; index++; } - - if (groupProps.footerProps?.footerText) { - memoItems[index] = { - group, - type: 'footer', - }; - index++; - } } + // Placeholder for a potential footer. + // Whether or not a footer is displayed is resolved + // by the footer render function so this is just a marker + // for where a footer may go. + memoItems[index] = { + group, + type: 'footer', + }; + index++; } memoItems.length = index; - console.log('MEMO ITEMS', memoItems); + // console.log('MEMO ITEMS', memoItems); return memoItems; }; @@ -173,8 +182,12 @@ const renderGroupShowAll = (props: IGroupShowAllProps): JSX.Element => { return ; }; -const renderGroupFooter = (props: IGroupFooterProps): JSX.Element => { - return ; +const renderGroupFooter = (props: IGroupFooterProps): JSX.Element | null => { + if (props.group && props.footerText) { + return ; + } + + return null; }; export const GroupedListV2FC: React.FC = props => { @@ -213,6 +226,7 @@ export const GroupedListV2FC: React.FC = props => { compact, }); + const events = React.useRef(); const flatList = React.useRef([]); const isSomeGroupExpanded = React.useRef(computeIsSomeGroupExpanded(groups)); @@ -231,6 +245,18 @@ export const GroupedListV2FC: React.FC = props => { if (groupProps?.isAllGroupsCollapsed) { setGroupsCollapsedState(groups, groupProps.isAllGroupsCollapsed); } + events.current = new EventGroup(this); + if (selection) { + events.current.on(selection, SELECTION_CHANGE, onSelectionChange); + } + }); + + const onSelectionChange = React.useCallback(() => { + setVersion({}); + }, [setVersion]); + + useUnmount(() => { + events.current?.dispose(); }); React.useEffect(() => { @@ -371,7 +397,7 @@ export const GroupedListV2FC: React.FC = props => { ref={listRef} role={role} items={listView} - onRenderCell={renderItem} + onRenderCellConditional={renderItem} usePageCache={usePageCache} onShouldVirtualize={onShouldVirtualize} version={version} diff --git a/packages/react/src/components/List/List.tsx b/packages/react/src/components/List/List.tsx index cf7a254c158227..b7864cae96533d 100644 --- a/packages/react/src/components/List/List.tsx +++ b/packages/react/src/components/List/List.tsx @@ -98,6 +98,7 @@ export class List extends React.Component, IListState> public static defaultProps = { startIndex: 0, onRenderCell: (item: any, index: number, containsFocus: boolean) => <>{(item && item.name) || ''}, + onRenderCellConditional: undefined, renderedWindowsAhead: DEFAULT_RENDERED_WINDOWS_AHEAD, renderedWindowsBehind: DEFAULT_RENDERED_WINDOWS_BEHIND, }; @@ -593,7 +594,7 @@ export class List extends React.Component, IListState> } private _onRenderPage = (pageProps: IPageProps, defaultRender?: IRenderFunction>): any => { - const { onRenderCell, role } = this.props; + const { onRenderCell, onRenderCellConditional, role } = this.props; const { page: { items = [], startIndex }, @@ -614,18 +615,24 @@ export class List extends React.Component, IListState> itemKey = index; } - cells.push( -
- {onRenderCell && - onRenderCell(item, index, !this.props.ignoreScrollingState ? this.state.isScrolling : undefined)} -
, - ); + const renderCell = onRenderCellConditional ? onRenderCellConditional : onRenderCell; + + const cell = + renderCell?.(item, index, !this.props.ignoreScrollingState ? this.state.isScrolling : undefined) ?? null; + + if (!onRenderCellConditional || (onRenderCellConditional! && cell !== null)) { + cells.push( +
+ {cell} +
, + ); + } } return
{cells}
; diff --git a/packages/react/src/components/List/List.types.ts b/packages/react/src/components/List/List.types.ts index 8af810f754ca25..d2b922663770d9 100644 --- a/packages/react/src/components/List/List.types.ts +++ b/packages/react/src/components/List/List.types.ts @@ -139,6 +139,21 @@ export interface IListProps extends React.HTMLAttributes | HTML */ onRenderCell?: (item?: T, index?: number, isScrolling?: boolean) => React.ReactNode; + /** + * Method to call when trying to render an item conditionally. + * + * When this method returns `null` the cell will be skipped in the render. + * + * This prop is mutually exclusive with `onRenderCell` and when `onRenderCellConditional` is set, + * `onRenderCell` will not be called. + * + * @param item - The data associated with the cell that is being rendered. + * @param index - The index of the cell being rendered. + * @param isScrolling - True if the list is being scrolled. May be useful for rendering a placeholder if your cells + * are complex. + */ + onRenderCellConditional?: (item?: T, index?: number, isScrolling?: boolean) => React.ReactNode | null; + /** * Optional callback invoked when List rendering completed. * This can be on initial mount or on re-render due to scrolling. From d248cf84bf87d150b277dda2faa7a8875309091f Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Fri, 19 Aug 2022 11:49:00 -0700 Subject: [PATCH 03/18] DetailsList: allow for custom GroupedList renderer This change allows users to provide a custom GroupedList renderer like GroupedListV2. --- .../react/src/components/DetailsList/DetailsList.base.tsx | 4 +++- .../react/src/components/DetailsList/DetailsList.types.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/DetailsList/DetailsList.base.tsx b/packages/react/src/components/DetailsList/DetailsList.base.tsx index 47e41c1c84a02d..cec7bea46ef2a8 100644 --- a/packages/react/src/components/DetailsList/DetailsList.base.tsx +++ b/packages/react/src/components/DetailsList/DetailsList.base.tsx @@ -597,8 +597,10 @@ const DetailsListInner: React.ComponentType = ( onBlur: focusZoneProps && focusZoneProps.onBlur ? focusZoneProps.onBlur : onBlur, }; + const FinalGroupedList = groups && groupProps?.groupedListAs ? groupProps.groupedListAs : GroupedList; + const list = groups ? ( - ; onRenderHeader?: IRenderFunction; + groupedListAs?: IComponentAs; } /** From 00e0f36cf0f8abe2f17c71e714a275abd173cdb2 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Fri, 19 Aug 2022 15:06:10 -0700 Subject: [PATCH 04/18] Update @fluentui/react API snapshot Add GroupedListV2 tests Add DetailsList tests A dd support for ungrouped lists --- packages/react/etc/react.api.md | 6 + packages/react/src/GroupedListV2.ts | 1 + .../DetailsList/DetailsListV2.test.tsx | 1131 ++ .../__snapshots__/DetailsListV2.test.tsx.snap | 9253 +++++++++++++++++ .../GroupedList/GroupedList.test.tsx | 8 +- .../GroupedList/GroupedListV2.base.tsx | 32 +- .../GroupedList/GroupedListV2.test.tsx | 350 + 7 files changed, 10775 insertions(+), 6 deletions(-) create mode 100644 packages/react/src/GroupedListV2.ts create mode 100644 packages/react/src/components/DetailsList/DetailsListV2.test.tsx create mode 100644 packages/react/src/components/DetailsList/__snapshots__/DetailsListV2.test.tsx.snap create mode 100644 packages/react/src/components/GroupedList/GroupedListV2.test.tsx diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index ec129a8d1cdb15..29eb3bc101e050 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -2022,6 +2022,8 @@ export const GroupedListV2FC: React_2.FC; export class GroupedListV2Wrapper extends React_2.Component implements IGroupedList { constructor(props: IGroupedListProps); // (undocumented) + static displayName: string; + // (undocumented) forceUpdate(): void; // (undocumented) static getDerivedStateFromProps(nextProps: IGroupedListProps, previousState: IGroupedListV2State): IGroupedListV2State; @@ -4558,6 +4560,8 @@ export interface IDetailsGroupDividerProps extends IGroupDividerProps, IDetailsI // @public (undocumented) export interface IDetailsGroupRenderProps extends IGroupRenderProps { + // (undocumented) + groupedListAs?: IComponentAs; // (undocumented) onRenderFooter?: IRenderFunction; // (undocumented) @@ -6786,6 +6790,7 @@ export interface IListProps extends React_2.HTMLAttributes | HT onPageRemoved?: (page: IPage) => void; onPagesUpdated?: (pages: IPage[]) => void; onRenderCell?: (item?: T, index?: number, isScrolling?: boolean) => React_2.ReactNode; + onRenderCellConditional?: (item?: T, index?: number, isScrolling?: boolean) => React_2.ReactNode | null; onRenderPage?: IRenderFunction>; onRenderRoot?: IRenderFunction>; onRenderSurface?: IRenderFunction>; @@ -9820,6 +9825,7 @@ export class List extends React_2.Component, IListState JSX.Element; + onRenderCellConditional: undefined; renderedWindowsAhead: number; renderedWindowsBehind: number; }; diff --git a/packages/react/src/GroupedListV2.ts b/packages/react/src/GroupedListV2.ts new file mode 100644 index 00000000000000..c55c281b2c37c2 --- /dev/null +++ b/packages/react/src/GroupedListV2.ts @@ -0,0 +1 @@ +export * from './components/GroupedList/index'; diff --git a/packages/react/src/components/DetailsList/DetailsListV2.test.tsx b/packages/react/src/components/DetailsList/DetailsListV2.test.tsx new file mode 100644 index 00000000000000..a3370d485a151e --- /dev/null +++ b/packages/react/src/components/DetailsList/DetailsListV2.test.tsx @@ -0,0 +1,1131 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import * as renderer from 'react-test-renderer'; +import { ReactWrapper } from 'enzyme'; +import { safeMount } from '@fluentui/test-utilities'; +import { EventGroup, KeyCodes, resetIds } from '../../Utilities'; +import { SelectionMode, Selection, SelectionZone } from '../../Selection'; +import { getTheme } from '../../Styling'; +import { DetailsHeader } from './DetailsHeader'; +import { DetailsList } from './DetailsList'; +import { DetailsListBase } from './DetailsList.base'; +import { CheckboxVisibility, DetailsListLayoutMode, IDetailsGroupRenderProps } from './DetailsList.types'; +import { DetailsRow } from './DetailsRow'; +import { DetailsRowCheck } from './DetailsRowCheck'; +import type { IDragDropEvents } from '../../DragDrop'; +import type { IGroup } from '../../GroupedList'; +import type { IRenderFunction } from '../../Utilities'; +import type { IDetailsColumnProps } from './DetailsColumn'; +import type { IDetailsHeaderProps } from './DetailsHeader'; +import type { IColumn, IDetailsGroupDividerProps, IDetailsList } from './DetailsList.types'; +import type { IDetailsRowProps } from './DetailsRow'; +import { GroupedListV2 } from '../GroupedList/GroupedListV2'; + +// Populate mock data for testing +function mockData(count: number, isColumn: boolean = false, customDivider: boolean = false): any { + const data = []; + let _data = {}; + + for (let i = 0; i < count; i++) { + _data = { + key: i, + name: 'Item ' + i, + value: i, + }; + if (isColumn) { + _data = { + ..._data, + key: `column_key_${i}`, + ariaLabel: `column_${i}`, + onRenderDivider: customDivider ? customColumnDivider : columnDividerWrapper, + }; + } + data.push(_data); + } + + return data; +} + +// Wrapper function which calls the defaultRenderer with the corresponding params +function columnDividerWrapper( + iDetailsColumnProps: IDetailsColumnProps, + defaultRenderer: (props?: IDetailsColumnProps) => JSX.Element | null, +): any { + return defaultRenderer(iDetailsColumnProps); +} + +// Using a bar sign as a custom divider along with the default divider +function customColumnDivider( + iDetailsColumnProps: IDetailsColumnProps, + defaultRenderer: (props?: IDetailsColumnProps) => JSX.Element | null, +): any { + return ( + + | + {defaultRenderer(iDetailsColumnProps)} + + ); +} + +const groupProps: IDetailsGroupRenderProps = { + groupedListAs: GroupedListV2, +}; + +/** + * NOTE: There isn't actually a DetailsListV2 control, rather + * this control uses GroupedListV2 for rendering. + */ +describe('DetailsListV2', () => { + let spy: jest.SpyInstance; + beforeAll(() => { + /* eslint-disable-next-line @typescript-eslint/no-empty-function */ + spy = jest.spyOn(window, 'scrollTo').mockImplementation(() => {}); + }); + + afterAll(() => { + spy.mockRestore(); + }); + + beforeEach(() => { + resetIds(); + }); + + afterEach(() => { + if (((setTimeout as unknown) as jest.Mock).mock) { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + } + }); + + it('renders List correctly', () => { + const component = renderer.create( + null} + skipViewportMeasures={true} + onShouldVirtualize={() => false} + groupProps={groupProps} + />, + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders List correctly with onRenderDivider props', () => { + const component = renderer.create( + null} + skipViewportMeasures={true} + onShouldVirtualize={() => false} + groupProps={groupProps} + />, + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders List with custom icon as column divider', () => { + const component = renderer.create( + null} + skipViewportMeasures={true} + onShouldVirtualize={() => false} + groupProps={groupProps} + />, + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders List in fixed constrained layout correctly', () => { + const component = renderer.create( + null} + layoutMode={DetailsListLayoutMode.fixedColumns} + skipViewportMeasures={true} + onShouldVirtualize={() => false} + groupProps={groupProps} + />, + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders a single proportional column with correct width', () => { + jest.useFakeTimers(); + + let component: IDetailsList | null; + safeMount( + (component = ref)} + layoutMode={DetailsListLayoutMode.fixedColumns} + flexMargin={-640} + skipViewportMeasures={false} + onShouldVirtualize={() => false} + groupProps={groupProps} + />, + () => { + expect(component).toBeTruthy(); + component!.focusIndex(2); + setTimeout(() => { + const elements = (document.activeElement as HTMLElement).querySelectorAll('div[role=columnheader]'); + elements.forEach((element: Element, index: number) => { + if (index === 0) { + return; + } + + const style = element.getAttribute('style')!; + expect(style).toBeDefined(); + + const width = style.match(/(?<=width: )\d+/g)!; + expect(width).toBeDefined(); + expect(width[0]).toBeDefined(); + + if (index === 1) { + expect(width[0]).toBe('121'); + } else if (index === 2) { + expect(width[0]).toBe('348'); + } else if (index === 3) { + expect(width[0]).toBe('123'); + } else { + fail('Unexpected index.'); + } + }); + }, 0); + jest.runOnlyPendingTimers(); + }, + ); + }); + + it('renders proportional columns with proper width ratios', () => { + jest.useFakeTimers(); + + let component: IDetailsList | null; + safeMount( + (component = ref)} + layoutMode={DetailsListLayoutMode.fixedColumns} + flexMargin={-640} + skipViewportMeasures={false} + onShouldVirtualize={() => false} + groupProps={groupProps} + />, + () => { + expect(component).toBeTruthy(); + component!.focusIndex(2); + setTimeout(() => { + const elements = (document.activeElement as HTMLElement).querySelectorAll('div[role=columnheader]'); + elements.forEach((element: Element, index: number) => { + if (index === 0) { + return; + } + + const style = element.getAttribute('style')!; + expect(style).toBeDefined(); + + const width = style.match(/(?<=width: )\d+/g)!; + expect(width).toBeDefined(); + expect(width[0]).toBeDefined(); + + if (index === 1) { + expect(width[0]).toBe('336'); + } else if (index === 2) { + expect(width[0]).toBe('255'); + } else { + fail('Unexpected index.'); + } + }); + }, 0); + jest.runOnlyPendingTimers(); + }, + ); + }); + + it('renders proportional columns with proper width ratios when delayFirstMeasure', () => { + jest.useFakeTimers(); + + let component: IDetailsList | null; + safeMount( + (component = ref)} + layoutMode={DetailsListLayoutMode.fixedColumns} + flexMargin={-640} + skipViewportMeasures={false} + onShouldVirtualize={() => false} + delayFirstMeasure + groupProps={groupProps} + />, + () => { + expect(component).toBeTruthy(); + component!.focusIndex(2); + setTimeout(() => { + const elements = (document.activeElement as HTMLElement).querySelectorAll('div[role=columnheader]'); + elements.forEach((element: Element, index: number) => { + if (index === 0) { + return; + } + + const style = element.getAttribute('style')!; + expect(style).toBeDefined(); + + const width = style.match(/(?<=width: )\d+/g)!; + expect(width).toBeDefined(); + expect(width[0]).toBeDefined(); + + if (index === 1) { + expect(width[0]).toBe('336'); + } else if (index === 2) { + expect(width[0]).toBe('255'); + } else { + fail('Unexpected index.'); + } + }); + }, 0); + jest.runOnlyPendingTimers(); + }, + ); + }); + + it('renders List in compact mode correctly', () => { + const component = renderer.create( + null} + compact={true} + skipViewportMeasures={true} + onShouldVirtualize={() => false} + groupProps={groupProps} + />, + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders List with hidden checkboxes correctly', () => { + const component = renderer.create( + false} + groups={[ + { + key: 'group0', + name: 'Group 0', + startIndex: 0, + count: 2, + }, + { + key: 'group1', + name: 'Group 1', + startIndex: 2, + count: 3, + }, + ]} + checkboxVisibility={CheckboxVisibility.hidden} + groupProps={groupProps} + />, + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('focuses row by index', () => { + jest.useFakeTimers(); + + let component: IDetailsList | null; + safeMount( + (component = ref)} + skipViewportMeasures={true} + onShouldVirtualize={() => false} + groupProps={groupProps} + />, + () => { + expect(component).toBeTruthy(); + component!.focusIndex(2); + jest.runAllTimers(); + expect( + (document.activeElement as HTMLElement).querySelector('[data-automationid=DetailsRowCell]')!.textContent, + ).toEqual('2'); + expect((document.activeElement as HTMLElement).className.split(' ')).toContain('ms-DetailsRow'); + }, + true /* attach */, + ); + }); + + it('focuses row by arrow key', () => { + jest.useFakeTimers(); + + let component: IDetailsList | null; + const onSelectionChanged = jest.fn(); + const selection = new Selection({ + onSelectionChanged, + }); + safeMount( + (component = ref)} + items={mockData(5)} + selection={selection} + skipViewportMeasures={true} + onShouldVirtualize={() => false} + groupProps={groupProps} + />, + wrapper => { + expect(component).toBeTruthy(); + component!.focusIndex(0); + jest.runAllTimers(); + + onSelectionChanged.mockClear(); + wrapper.find('.ms-DetailsList-headerWrapper').simulate('keyDown', { which: KeyCodes.down }); + expect(onSelectionChanged).toHaveBeenCalledTimes(1); + }, + ); + }); + + it('does not focus by arrow key when isSelectedOnFocus is `false`', () => { + jest.useFakeTimers(); + + let component: IDetailsList | null; + const onSelectionChanged = jest.fn(); + const selection = new Selection({ + onSelectionChanged, + }); + safeMount( + (component = ref)} + items={mockData(5)} + selection={selection} + skipViewportMeasures={true} + onShouldVirtualize={() => false} + isSelectedOnFocus={false} + groupProps={groupProps} + />, + wrapper => { + expect(component).toBeTruthy(); + component!.focusIndex(0); + jest.runAllTimers(); + + onSelectionChanged.mockClear(); + wrapper.find('.ms-DetailsList-headerWrapper').simulate('keyDown', { which: KeyCodes.down }); + expect(onSelectionChanged).toHaveBeenCalledTimes(0); + }, + ); + }); + + it('clears selection when escape key is pressed and isSelectedOnFocus is `true`', () => { + jest.useFakeTimers(); + + let component: IDetailsList | null; + const onSelectionChanged = jest.fn(); + const selection = new Selection({ + onSelectionChanged, + }); + safeMount( + (component = ref)} + items={mockData(5)} + selection={selection} + skipViewportMeasures={true} + onShouldVirtualize={() => false} + groupProps={groupProps} + />, + wrapper => { + expect(component).toBeTruthy(); + selection.setAllSelected(true); + jest.runAllTimers(); + + onSelectionChanged.mockClear(); + wrapper.find('.ms-SelectionZone').simulate('keyDown', { which: KeyCodes.escape }); + expect(onSelectionChanged).toHaveBeenCalledTimes(1); + expect(selection.getSelectedCount()).toEqual(0); + }, + ); + }); + + it('does not clear selection when escape key is pressed and isSelectedOnFocus is `false`', () => { + jest.useFakeTimers(); + + let component: IDetailsList | null; + const onSelectionChanged = jest.fn(); + const selection = new Selection({ + onSelectionChanged, + }); + safeMount( + (component = ref)} + items={mockData(5)} + selection={selection} + skipViewportMeasures={true} + onShouldVirtualize={() => false} + isSelectedOnFocus={false} + groupProps={groupProps} + />, + wrapper => { + expect(component).toBeTruthy(); + selection.setAllSelected(true); + jest.runAllTimers(); + + onSelectionChanged.mockClear(); + wrapper.find('.ms-SelectionZone').simulate('keyDown', { which: KeyCodes.escape }); + expect(onSelectionChanged).toHaveBeenCalledTimes(0); + expect(selection.getSelectedCount()).toEqual(5); + }, + ); + }); + + it('invokes optional onRenderMissingItem prop once per missing item rendered', () => { + const onRenderMissingItem = jest.fn(); + const items = [...mockData(5), null, null]; + + safeMount( + , + () => { + expect(onRenderMissingItem).toHaveBeenCalledTimes(2); + }, + ); + }); + + it('does not invoke optional onRenderMissingItem prop if no missing items are rendered', () => { + const onRenderMissingItem = jest.fn(); + const items = mockData(5); + + safeMount( + , + () => { + expect(onRenderMissingItem).toHaveBeenCalledTimes(0); + }, + ); + }); + + it('executes onItemInvoked when double click is pressed', () => { + const items = mockData(5); + const onItemInvoked = jest.fn(); + + safeMount( + , + (wrapper: ReactWrapper) => { + wrapper.find('.ms-DetailsRow').first().simulate('dblclick'); + + expect(onItemInvoked).toHaveBeenCalledTimes(1); + }, + ); + }); + + it('executes onItemInvoked when enter is pressed', () => { + const items = mockData(5); + const onItemInvoked = jest.fn(); + + safeMount( + , + (wrapper: ReactWrapper) => { + wrapper.find('.ms-DetailsRow').first().simulate('keydown', { which: KeyCodes.enter }); + + expect(onItemInvoked).toHaveBeenCalledTimes(1); + }, + ); + }); + + it('respects changed dragDropEvents prop on re-renders.', () => { + const _dragDropEvents: IDragDropEvents = { + canDrag: jest.fn().mockReturnValue(true), + canDrop: jest.fn().mockReturnValue(true), + onDragEnd: jest.fn(), + onDragEnter: jest.fn(), + onDragLeave: jest.fn(), + onDragStart: jest.fn(), + onDrop: jest.fn(), + }; + + const _dragDropEvents2: IDragDropEvents = { + canDrag: jest.fn().mockReturnValue(true), + canDrop: jest.fn().mockReturnValue(true), + onDragEnd: jest.fn(), + onDragEnter: jest.fn(), + onDragLeave: jest.fn(), + onDragStart: jest.fn(), + onDrop: jest.fn(), + }; + + const _RaiseEvent = (target: any, _eventName: string, _clientX: number) => { + EventGroup.raise( + target, + _eventName, + { + clientX: _clientX, + button: 0, + } as DragEvent, + true, + ); + }; + + const container = document.createElement('div'); + const items = mockData(5); + const columns = mockData(5, true); + + ReactDOM.render( + , + container, + ); + + let detailsRowSource = container.querySelector('div[aria-rowindex="2"][role="row"]') as HTMLDivElement; + + _RaiseEvent(detailsRowSource, 'mousedown', 270); + _RaiseEvent(detailsRowSource, 'dragstart', 270); + + // original eventhandler should be fired + expect(_dragDropEvents.onDragStart).toHaveBeenCalledTimes(1); + expect(_dragDropEvents2.onDragStart).toHaveBeenCalledTimes(0); + + ReactDOM.render( + , + container, + ); + + detailsRowSource = container.querySelector('div[aria-rowindex="2"][role="row"]') as HTMLDivElement; + + _RaiseEvent(detailsRowSource, 'mousedown', 270); + _RaiseEvent(detailsRowSource, 'dragstart', 270); + + expect(_dragDropEvents.onDragStart).toHaveBeenCalledTimes(2); + expect(_dragDropEvents2.onDragStart).toHaveBeenCalledTimes(0); + + ReactDOM.render( + , + container, + ); + + detailsRowSource = container.querySelector('div[aria-rowindex="2"][role="row"]') as HTMLDivElement; + + _RaiseEvent(detailsRowSource, 'mousedown', 270); + _RaiseEvent(detailsRowSource, 'dragstart', 270); + + expect(_dragDropEvents.onDragStart).toHaveBeenCalledTimes(2); + expect(_dragDropEvents2.onDragStart).toHaveBeenCalledTimes(1); + }); + + it('focuses into row element', () => { + jest.useFakeTimers(); + + const onRenderColumn = (item: any, index: number, column: IColumn) => { + let value = item && column && column.fieldName ? item[column.fieldName] : ''; + if (value === null || value === undefined) { + value = ''; + } + return ( +
+ {value} +
+ ); + }; + + const getCellValueKey = (item: any, index: number, column: IColumn) => { + const valueKey = item && column && column.fieldName ? item[column.fieldName] : column.key + index; + return valueKey; + }; + + let component: IDetailsList | null; + safeMount( + (component = ref)} + skipViewportMeasures={true} + onShouldVirtualize={() => false} + onRenderItemColumn={onRenderColumn} + getCellValueKey={getCellValueKey} + groupProps={groupProps} + />, + () => { + expect(component).toBeTruthy(); + component!.focusIndex(3); + jest.runOnlyPendingTimers(); + expect( + (document.activeElement as HTMLElement).querySelector('[data-automationid=DetailsRowCell]')!.textContent, + ).toEqual('3'); + expect((document.activeElement as HTMLElement).className.split(' ')).toContain('ms-DetailsRow'); + + // Set element visibility manually as a test workaround + component!.focusIndex(4); + jest.runOnlyPendingTimers(); + ((document.activeElement as HTMLElement).children[1] as any).isVisible = true; + ((document.activeElement as HTMLElement).children[1].children[0] as any).isVisible = true; + ((document.activeElement as HTMLElement).children[1].children[0].children[0] as any).isVisible = true; + + component!.focusIndex(4, true); + jest.runOnlyPendingTimers(); + expect((document.activeElement as HTMLElement).textContent).toEqual('4'); + expect((document.activeElement as HTMLElement).className.split(' ')).toContain('test-column'); + }, + true /* attach */, + ); + }); + + it('reset focusedItemIndex when setKey updates', () => { + jest.useFakeTimers(); + + let component: DetailsListBase | null; + + safeMount( + (component = value as any)} + skipViewportMeasures={true} + onShouldVirtualize={() => false} + groupProps={groupProps} + />, + (wrapper: ReactWrapper) => { + expect(component).toBeTruthy(); + component!.focusIndex(3); + jest.runAllTimers(); + expect(component!.state.focusedItemIndex).toEqual(3); + + // update props to new setKey + const newProps = { items: mockData(7), setKey: 'set2', initialFocusedIndex: 0 }; + wrapper.setProps(newProps); + wrapper.update(); + + // verify that focusedItemIndex is reset to 0 and 0th row is focused + jest.runAllTimers(); + expect(component!.state.focusedItemIndex).toEqual(0); + expect( + (document.activeElement as HTMLElement).querySelector('[data-automationid=DetailsRowCell]')!.textContent, + ).toEqual('0'); + expect((document.activeElement as HTMLElement).className.split(' ')).toContain('ms-DetailsRow'); + }, + true /* attach */, + ); + }); + + it('invokes optional onColumnResize callback per IColumn if defined when columns are adjusted', () => { + jest.useFakeTimers(); + + const columns: IColumn[] = mockData(2, true); + columns[0].onColumnResize = jest.fn(); + columns[1].onColumnResize = jest.fn(); + + safeMount( + false} groupProps={groupProps} />, + () => { + jest.runOnlyPendingTimers(); + expect(columns[0].onColumnResize).toHaveBeenCalledTimes(1); + expect(columns[1].onColumnResize).toHaveBeenCalledTimes(1); + }, + ); + }); + + it('invokes optional onRenderDetailsHeader prop to customize DetailsHeader rendering when provided', () => { + const onRenderDetailsHeaderMock = jest.fn(); + + safeMount( + false} + onRenderDetailsHeader={onRenderDetailsHeaderMock} + groupProps={groupProps} + />, + () => { + expect(onRenderDetailsHeaderMock).toHaveBeenCalledTimes(1); + }, + ); + }); + + it('invokes onRenderColumnHeaderTooltip to customize DetailsColumn tooltip rendering when provided', () => { + const NUM_COLUMNS = 2; + const onRenderColumnHeaderTooltipMock = jest.fn(); + const onRenderDetailsHeader = ( + props: IDetailsHeaderProps, + defaultRenderer?: IRenderFunction, + ) => { + return ; + }; + + safeMount( + false} + onRenderDetailsHeader={onRenderDetailsHeader} + groupProps={groupProps} + />, + () => { + expect(onRenderColumnHeaderTooltipMock).toHaveBeenCalledTimes(4); + }, + ); + }); + + it('invokes optional onRenderCheckbox callback to customize checkbox rendering when provided', () => { + const onRenderCheckboxMock = jest.fn(); + const selection = new Selection(); + const theme = getTheme(); + + safeMount( + false} + onRenderCheckbox={onRenderCheckboxMock} + checkboxVisibility={CheckboxVisibility.always} + selectionMode={SelectionMode.multiple} + selection={selection} + groupProps={groupProps} + />, + () => { + expect(onRenderCheckboxMock).toHaveBeenCalledTimes(3); + expect(onRenderCheckboxMock.mock.calls[2][0]).toEqual({ checked: false, theme }); + + selection.setAllSelected(true); + + expect(onRenderCheckboxMock).toHaveBeenCalledTimes(6); + expect(onRenderCheckboxMock.mock.calls[5][0]).toEqual({ checked: true, theme }); + }, + ); + }); + + it('initializes the selection mode object with the selectionMode prop', () => { + safeMount( + , + (wrapper: ReactWrapper) => { + const selectionZone = wrapper.find(SelectionZone); + + expect(selectionZone.props().selection.mode).toEqual(SelectionMode.none); + }, + ); + }); + + it('handles updates to items and groups', () => { + const tableOneItems = [ + { f1: 'A1', f2: 'B1', f3: 'C1' }, + { f1: 'A2', f2: 'B2', f3: 'C2' }, + { f1: 'A3', f2: 'B3', f3: 'C3' }, + { f1: 'A4', f2: 'B4', f3: 'C4' }, + ]; + const tableTwoItems = [ + { f1: 'D1', f2: 'E1', f3: 'F1' }, + { f1: 'D2', f2: 'E2', f3: 'F2' }, + { f1: 'D3', f2: 'E3', f3: 'F3' }, + { f1: 'D4', f2: 'E4', f3: 'F4' }, + ]; + + const groupOneGroups: IGroup[] = [ + { key: 'one-1', name: 'one 1', count: 1, startIndex: 0 }, + { key: 'one-2', name: 'one 2', count: 1, startIndex: 1 }, + { key: 'one-3', name: 'one 3', count: 1, startIndex: 2 }, + { key: 'one-4', name: 'one 4', count: 1, startIndex: 3 }, + ]; + + const groupTwoGroups: IGroup[] = [ + { key: 'two-1', name: 'two 1', count: 2, startIndex: 0 }, + { key: 'two-2', name: 'two 2', count: 2, startIndex: 2 }, + ]; + + const onRenderDetailsHeader: IRenderFunction = (headerProps: IDetailsHeaderProps) => { + return ( +
+ {headerProps.columns.map((column: IColumn) => { + return
{column.name}
; + })} +
+ ); + }; + + const onRenderRow = (rowProps: IDetailsRowProps) => { + return ( +
+ {rowProps.columns.map((column: IColumn) => { + return
{rowProps.item[column.key]}
; + })} +
+ ); + }; + + const onRenderGroupHeader: IRenderFunction = ( + groupDividerProps: IDetailsGroupDividerProps, + ) => { + return
{groupDividerProps.group?.name}
; + }; + + const component = renderer.create( + , + ); + + expect(component.toJSON()).toMatchSnapshot(); + + // New items, same groups + + component.update( + , + ); + + expect(component.toJSON()).toMatchSnapshot(); + + // Same items, new groups + + component.update( + , + ); + + expect(component.toJSON()).toMatchSnapshot(); + + // New items, same groups + + component.update( + , + ); + + expect(component.toJSON()).toMatchSnapshot(); + }); + + it('handles paged updates to items within groups', () => { + const roundOneItems = [{ f1: 'A1', f2: 'B1', f3: 'C1' }, undefined, { f1: 'A3', f2: 'B3', f3: 'C3' }, undefined]; + const roundTwoItems = [ + { f1: 'A1', f2: 'B1', f3: 'C1' }, + { f1: 'A2', f2: 'B2', f3: 'C2' }, + { f1: 'A3', f2: 'B3', f3: 'C3' }, + { f1: 'A4', f2: 'B4', f3: 'C4' }, + ]; + + const groups: IGroup[] = [ + { key: 'two-1', name: 'two 1', count: 2, startIndex: 0 }, + { key: 'two-2', name: 'two 2', count: 2, startIndex: 2 }, + ]; + + const onRenderDetailsHeader: IRenderFunction = (headerProps: IDetailsHeaderProps) => { + return ( +
+ {headerProps.columns.map((column: IColumn) => { + return
{column.name}
; + })} +
+ ); + }; + + const onRenderRow = (rowProps: IDetailsRowProps) => { + return ( +
+ {rowProps.columns.map((column: IColumn) => { + return
{rowProps.item[column.key]}
; + })} +
+ ); + }; + + const onRenderMissingItem = () => { + return
Placeholder
; + }; + + const onRenderGroupHeader: IRenderFunction = ( + groupDividerProps: IDetailsGroupDividerProps, + ) => { + return
{groupDividerProps.group?.name}
; + }; + + const component = renderer.create( + , + ); + + expect(component.toJSON()).toMatchSnapshot(); + + // New items, same groups + + component.update( + , + ); + + expect(component.toJSON()).toMatchSnapshot(); + }); + + it('returns an element with the correct text based on the second id passed in aria-labelledby', () => { + const container = document.createElement('div'); + const columns = [ + { + key: 'column1', + name: 'Name', + fieldName: 'name', + minWidth: 100, + maxWidth: 200, + isResizable: true, + isRowHeader: true, + }, + { key: 'column2', name: 'Value', fieldName: 'value', minWidth: 100, maxWidth: 200, isResizable: true }, + ]; + const items = mockData(1); + + ReactDOM.render( + , + container, + ); + + const checkbox = container.querySelector('div[role="checkbox"][aria-label="select row"]') as HTMLElement; + const rowHeaderId = checkbox?.getAttribute('aria-labelledby')?.split(' ')[1]; + + expect(container.querySelector(`#${rowHeaderId}`)!.textContent).toEqual(items[0].name); + }); + + it('has an aria-labelledby checkboxId that matches the id of the checkbox', () => { + const component = renderer.create( + , + ); + + const detailsRowSource = component.root.findAllByType(DetailsRow)[0]; + const detailsRowCheckSource = detailsRowSource.findByType(DetailsRowCheck).props; + const checkboxId = detailsRowCheckSource.id; + + expect(checkboxId).toEqual(detailsRowCheckSource[`aria-labelledby`].split(' ')[0]); + }); + + it('names group header checkboxes based on checkButtonGroupAriaLabel', () => { + const container = document.createElement('div'); + ReactDOM.render( + , + container, + ); + + const checkbox = container.querySelector('[role="checkbox"][aria-label="select section"]') as HTMLElement; + expect(checkbox).not.toBeNull(); + + const groupNameId = checkbox.getAttribute('aria-labelledby')?.split(' ')[1]; + expect(container.querySelector(`#${groupNameId} span`)!.textContent).toEqual('Group 0'); + }); +}); diff --git a/packages/react/src/components/DetailsList/__snapshots__/DetailsListV2.test.tsx.snap b/packages/react/src/components/DetailsList/__snapshots__/DetailsListV2.test.tsx.snap new file mode 100644 index 00000000000000..0bb281a0918183 --- /dev/null +++ b/packages/react/src/components/DetailsList/__snapshots__/DetailsListV2.test.tsx.snap @@ -0,0 +1,9253 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DetailsListV2 handles paged updates to items within groups 1`] = ` +
+
+
+
+
+
+ f1 +
+
+ f2 +
+
+ f3 +
+
+
+
+
+
+
+
+
+
+
+ two 1 +
+
+
+
+
+ A1 +
+
+ B1 +
+
+ C1 +
+
+
+
+
+ Placeholder +
+
+
+
+ two 2 +
+
+
+
+
+ A3 +
+
+ B3 +
+
+ C3 +
+
+
+
+
+ Placeholder +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DetailsListV2 handles paged updates to items within groups 2`] = ` +
+
+
+
+
+
+ f1 +
+
+ f2 +
+
+ f3 +
+
+
+
+
+
+
+
+
+
+
+ two 1 +
+
+
+
+
+ A1 +
+
+ B1 +
+
+ C1 +
+
+
+
+
+
+ A2 +
+
+ B2 +
+
+ C2 +
+
+
+
+
+ two 2 +
+
+
+
+
+ A3 +
+
+ B3 +
+
+ C3 +
+
+
+
+
+
+ A4 +
+
+ B4 +
+
+ C4 +
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DetailsListV2 handles updates to items and groups 1`] = ` +
+
+
+
+
+
+ f1 +
+
+ f2 +
+
+ f3 +
+
+
+
+
+
+
+
+
+
+
+ one 1 +
+
+
+
+
+ A1 +
+
+ B1 +
+
+ C1 +
+
+
+
+
+ one 2 +
+
+
+
+
+ A2 +
+
+ B2 +
+
+ C2 +
+
+
+
+
+ one 3 +
+
+
+
+
+ A3 +
+
+ B3 +
+
+ C3 +
+
+
+
+
+ one 4 +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DetailsListV2 handles updates to items and groups 2`] = ` +
+
+
+
+
+
+ f1 +
+
+ f2 +
+
+ f3 +
+
+
+
+
+
+
+
+
+
+
+ one 1 +
+
+
+
+
+ D1 +
+
+ E1 +
+
+ F1 +
+
+
+
+
+ one 2 +
+
+
+
+
+ D2 +
+
+ E2 +
+
+ F2 +
+
+
+
+
+ one 3 +
+
+
+
+
+ D3 +
+
+ E3 +
+
+ F3 +
+
+
+
+
+ one 4 +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DetailsListV2 handles updates to items and groups 3`] = ` +
+
+
+
+
+
+ f1 +
+
+ f2 +
+
+ f3 +
+
+
+
+
+
+
+
+
+
+
+ two 1 +
+
+
+
+
+ D1 +
+
+ E1 +
+
+ F1 +
+
+
+
+
+
+ D2 +
+
+ E2 +
+
+ F2 +
+
+
+
+
+ two 2 +
+
+
+
+
+ D3 +
+
+ E3 +
+
+ F3 +
+
+
+
+
+
+ D4 +
+
+ E4 +
+
+ F4 +
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DetailsListV2 handles updates to items and groups 4`] = ` +
+
+
+
+
+
+ f1 +
+
+ f2 +
+
+ f3 +
+
+
+
+
+
+
+
+
+
+
+ two 1 +
+
+
+
+
+ A1 +
+
+ B1 +
+
+ C1 +
+
+
+
+
+
+ A2 +
+
+ B2 +
+
+ C2 +
+
+
+
+
+ two 2 +
+
+
+
+
+ A3 +
+
+ B3 +
+
+ C3 +
+
+
+
+
+
+ A4 +
+
+ B4 +
+
+ C4 +
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DetailsListV2 renders List correctly 1`] = ` +
+
+
+
+
+
+ + + +
+
+ + + + key + + + +
+
+
+ + + + name + + + +
+
+
+ + + + value + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DetailsListV2 renders List correctly with onRenderDivider props 1`] = ` +
+
+
+
+
+
+ + + +
+
+ + + + Item 0 + + + +
+
+ + + + Item 1 + + + +
+
+ + + + Item 2 + + + +
+
+ + + + Item 3 + + + +
+
+ + + + Item 4 + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DetailsListV2 renders List in compact mode correctly 1`] = ` +
+
+
+
+
+
+ + + +
+
+ + + + key + + + +
+
+
+ + + + name + + + +
+
+
+ + + + value + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DetailsListV2 renders List in fixed constrained layout correctly 1`] = ` +
+
+
+
+
+
+ + + +
+
+ + + + key + + + +
+
+
+ + + + name + + + +
+
+
+ + + + value + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DetailsListV2 renders List with custom icon as column divider 1`] = ` +
+
+
+
+
+
+ + + +
+
+ + + + Item 0 + + + +
+ + | + +
+ + + + Item 1 + + + +
+ + | + +
+ + + + Item 2 + + + +
+ + | + +
+ + + + Item 3 + + + +
+ + | + +
+ + + + Item 4 + + + +
+ + | + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`DetailsListV2 renders List with hidden checkboxes correctly 1`] = ` +
+
+
+
+
+
+ +  + + +
+
+ + + + key + + + +
+
+
+ + + + name + + + +
+
+
+ + + + value + + + +
+
+
+
+
+
+
+
+
+
+
+
.ms-GroupHeader .ms-GroupHeader-dropIcon { + opacity: 1; + transform: rotate(0.2deg) scale(1); + transition-delay: 0.367s; + transition: transform 0.467s cubic-bezier(0.075, 0.820, 0.165, 1.000) opacity 0.167s cubic-bezier(0.390, 0.575, 0.565, 1.000); + } + .ms-GroupedList-group.is-dropping .ms-GroupHeader-check { + opacity: 0; + } + data-is-focusable={true} + data-selection-index={0} + data-selection-span={2} + onKeyUp={[Function]} + role="row" + style={Object {}} + > +
+
+ +  + +
+
+ +
+
+ + Group 0 + + + ( + 2 + ) + +
+
+
+
+
+
.ms-Link { + color: #004578; + } + &:hover .ms-DetailsRow-check { + opacity: 1; + } + .ms-Fabric--isFocusVisible &:focus .ms-DetailsRow-check { + opacity: 1; + } + & .ms-GroupSpacer { + flex-grow: 0; + flex-shrink: 0; + } + data-automationid="DetailsRow" + data-focuszone-id="FocusZone8" + data-is-focusable={true} + data-item-index={0} + data-selection-index={0} + data-selection-touch-invoke={true} + id="row0-0" + onFocus={[Function]} + onKeyDown={[Function]} + onMouseDownCapture={[Function]} + role="row" + style={ + Object { + "minWidth": 300, + } + } + > + +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="key" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + 0 +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="name" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + Item 0 +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="value" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + 0 +
+
+ +
+
+
+
.ms-Link { + color: #004578; + } + &:hover .ms-DetailsRow-check { + opacity: 1; + } + .ms-Fabric--isFocusVisible &:focus .ms-DetailsRow-check { + opacity: 1; + } + & .ms-GroupSpacer { + flex-grow: 0; + flex-shrink: 0; + } + data-automationid="DetailsRow" + data-focuszone-id="FocusZone10" + data-is-focusable={true} + data-item-index={1} + data-selection-index={1} + data-selection-touch-invoke={true} + id="row0-1" + onFocus={[Function]} + onKeyDown={[Function]} + onMouseDownCapture={[Function]} + role="row" + style={ + Object { + "minWidth": 300, + } + } + > + +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="key" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + 1 +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="name" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + Item 1 +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="value" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + 1 +
+
+ +
+
+
+
.ms-GroupHeader .ms-GroupHeader-dropIcon { + opacity: 1; + transform: rotate(0.2deg) scale(1); + transition-delay: 0.367s; + transition: transform 0.467s cubic-bezier(0.075, 0.820, 0.165, 1.000) opacity 0.167s cubic-bezier(0.390, 0.575, 0.565, 1.000); + } + .ms-GroupedList-group.is-dropping .ms-GroupHeader-check { + opacity: 0; + } + data-is-focusable={true} + data-selection-index={2} + data-selection-span={3} + onKeyUp={[Function]} + role="row" + style={Object {}} + > +
+
+ +  + +
+
+ +
+
+ + Group 1 + + + ( + 3 + ) + +
+
+
+
+
+
.ms-Link { + color: #004578; + } + &:hover .ms-DetailsRow-check { + opacity: 1; + } + .ms-Fabric--isFocusVisible &:focus .ms-DetailsRow-check { + opacity: 1; + } + & .ms-GroupSpacer { + flex-grow: 0; + flex-shrink: 0; + } + data-automationid="DetailsRow" + data-focuszone-id="FocusZone13" + data-is-focusable={true} + data-item-index={2} + data-selection-index={2} + data-selection-touch-invoke={true} + id="row0-2" + onFocus={[Function]} + onKeyDown={[Function]} + onMouseDownCapture={[Function]} + role="row" + style={ + Object { + "minWidth": 300, + } + } + > + +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="key" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + 2 +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="name" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + Item 2 +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="value" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + 2 +
+
+ +
+
+
+
.ms-Link { + color: #004578; + } + &:hover .ms-DetailsRow-check { + opacity: 1; + } + .ms-Fabric--isFocusVisible &:focus .ms-DetailsRow-check { + opacity: 1; + } + & .ms-GroupSpacer { + flex-grow: 0; + flex-shrink: 0; + } + data-automationid="DetailsRow" + data-focuszone-id="FocusZone15" + data-is-focusable={true} + data-item-index={3} + data-selection-index={3} + data-selection-touch-invoke={true} + id="row0-3" + onFocus={[Function]} + onKeyDown={[Function]} + onMouseDownCapture={[Function]} + role="row" + style={ + Object { + "minWidth": 300, + } + } + > + +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="key" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + 3 +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="name" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + Item 3 +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="value" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + 3 +
+
+ +
+
+
+
.ms-Link { + color: #004578; + } + &:hover .ms-DetailsRow-check { + opacity: 1; + } + .ms-Fabric--isFocusVisible &:focus .ms-DetailsRow-check { + opacity: 1; + } + & .ms-GroupSpacer { + flex-grow: 0; + flex-shrink: 0; + } + data-automationid="DetailsRow" + data-focuszone-id="FocusZone17" + data-is-focusable={true} + data-item-index={4} + data-selection-index={4} + data-selection-touch-invoke={true} + id="row0-4" + onFocus={[Function]} + onKeyDown={[Function]} + onMouseDownCapture={[Function]} + role="row" + style={ + Object { + "minWidth": 300, + } + } + > + +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="key" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + 4 +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="name" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + Item 4 +
+
button { + max-width: 100%; + } + & [data-is-focusable='true'] { + outline: transparent; + position: relative; + } + & [data-is-focusable='true']::-moz-focus-inner { + border: 0; + } + { + padding-right: 8px; + } + data-automation-key="value" + data-automationid="DetailsRowCell" + role="gridcell" + style={ + Object { + "width": 120, + } + } + > + 4 +
+
+ +
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/packages/react/src/components/GroupedList/GroupedList.test.tsx b/packages/react/src/components/GroupedList/GroupedList.test.tsx index 47f64615aac177..721a10988e7e9d 100644 --- a/packages/react/src/components/GroupedList/GroupedList.test.tsx +++ b/packages/react/src/components/GroupedList/GroupedList.test.tsx @@ -129,7 +129,7 @@ describe('GroupedList', () => { it("renders the number of rows specified by a group's count when startIndex is not zero", () => { const _selection = new Selection(); - const _items: Array<{ key: string }> = [{ key: '1' }, { key: '2' }, { key: '3' }]; + const _items: Array<{ key: string }> = [{ key: '1' }, { key: '2' }, { key: '3' }, { key: '4' }, { key: '5' }]; const _groups: Array = [ { count: 3, @@ -172,7 +172,11 @@ describe('GroupedList', () => { ); const listRows = wrapper.find(DetailsRow); - expect(listRows).toHaveLength(1); + expect(listRows).toHaveLength(3); + + expect(listRows.at(0).parent().key()).toBe('3'); + expect(listRows.at(1).parent().key()).toBe('4'); + expect(listRows.at(2).parent().key()).toBe('5'); wrapper.unmount(); }); diff --git a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx index 094bf94e73510e..167b597efd244c 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx +++ b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx @@ -8,7 +8,7 @@ import { getId, EventGroup, } from '../../Utilities'; -import { List, ScrollToMode } from '../../List'; +import { List, ScrollToMode, IListProps } from '../../List'; import { SelectionMode, SELECTION_CHANGE } from '../../Selection'; import { FocusZone, FocusZoneDirection } from '../../FocusZone'; import type { IProcessedStyleSet } from '../../Styling'; @@ -60,7 +60,7 @@ type FlattenItemsFn = ( ) => IGroupedItem[]; const flattenItems: FlattenItemsFn = (groups, items, memoItems, groupProps) => { - if (groups === undefined) { + if (!groups) { return items; } @@ -108,7 +108,8 @@ const flattenItems: FlattenItemsFn = (groups, items, memoItems, groupProps) => { if (group.isCollapsed !== true) { let itemIndex = group.startIndex; const renderCount = groupProps.getGroupItemLimit ? groupProps.getGroupItemLimit(group) : Infinity; - const itemEnd = itemIndex + Math.min(group.count, renderCount); + const count = !group.isShowingAll ? group.count : items.length; + const itemEnd = itemIndex + Math.min(count, renderCount); while (itemIndex < itemEnd) { memoItems[index] = { group, @@ -174,6 +175,16 @@ const isInnerZoneKeystroke = (ev: React.KeyboardEvent): boolean => const getClassNames = classNamesFunction(); +const getKey: IListProps['getKey'] = (item, _index) => { + if (item.type === 'group' && item.group) { + return item.group.key; + } else if (item.type === 'item' && item.item) { + return item.item.key; + } + + return null; +}; + const renderGroupHeader = (props: IGroupHeaderProps): JSX.Element => { return ; }; @@ -241,6 +252,16 @@ export const GroupedListV2FC: React.FC = props => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [groups, groupProps, items, toggleVersion, flatList]); + const getPageSpecification = React.useCallback( + (flattenedIndex: number): { key?: string } => { + const pageGroup = listView[flattenedIndex]; + return { + key: pageGroup.type === 'group' ? pageGroup.group!.key : undefined, + }; + }, + [listView], + ); + useMount(() => { if (groupProps?.isAllGroupsCollapsed) { setGroupsCollapsedState(groups, groupProps.isAllGroupsCollapsed); @@ -363,7 +384,7 @@ export const GroupedListV2FC: React.FC = props => { return onRenderFooter(groupFooterProps, renderGroupFooter); } else { - return onRenderCell(level, item.item, item.itemIndex ?? flattenedIndex); + return onRenderCell(level, item.item ?? item, item.itemIndex ?? flattenedIndex); } }, [ @@ -400,7 +421,9 @@ export const GroupedListV2FC: React.FC = props => { onRenderCellConditional={renderItem} usePageCache={usePageCache} onShouldVirtualize={onShouldVirtualize} + getPageSpecification={getPageSpecification} version={version} + getKey={getKey} {...rootListProps} /> @@ -410,6 +433,7 @@ export const GroupedListV2FC: React.FC = props => { export class GroupedListV2Wrapper extends React.Component implements IGroupedList { + public static displayName: string = 'GroupedListV2'; private _list = React.createRef(); public static getDerivedStateFromProps( diff --git a/packages/react/src/components/GroupedList/GroupedListV2.test.tsx b/packages/react/src/components/GroupedList/GroupedListV2.test.tsx new file mode 100644 index 00000000000000..3e83f5e20bd934 --- /dev/null +++ b/packages/react/src/components/GroupedList/GroupedListV2.test.tsx @@ -0,0 +1,350 @@ +import * as React from 'react'; +import { mount } from 'enzyme'; +import { SelectionMode, Selection } from '../../Selection'; +import { GroupedListV2 } from './GroupedListV2'; +import { DetailsRow } from '../DetailsList/DetailsRow'; +import { List } from '../../List'; +import { GroupShowAll } from './GroupShowAll'; +import { Link } from '../../Link'; +import { GroupHeader } from './GroupHeader'; +import { getTheme } from '../../Styling'; +import * as path from 'path'; +import { isConformant } from '../../common/isConformant'; +import type { IGroup } from './GroupedList.types'; +import type { IColumn } from '../DetailsList/DetailsList.types'; + +describe('GroupedListV2', () => { + isConformant({ + Component: GroupedListV2, + displayName: 'GroupedListV2', + componentPath: path.join(__dirname, 'GroupedListV2.ts'), + requiredProps: { + items: [], + onRenderCell: () => { + return
; + }, + }, + // Problem: Ref is not supported + // Solution: Convert to FunctionComponent and support using forwardRef + disabledTests: ['component-handles-ref', 'component-has-root-ref', 'has-top-level-file'], + }); + + it("sets inner List page key to IGroup's key attribute for uniqueness", () => { + const _selection = new Selection(); + const _items: Array = [ + { + key: 'item1', + name: 'item1', + value: 1, + }, + ]; + const _groups: Array = [ + { + count: 1, + key: 'group0', + name: 'group 0', + startIndex: 0, + level: 0, + children: [], + }, + ]; + + function _onRenderCell(nestingDepth: number, item: any, itemIndex: number): JSX.Element { + return ( + { + return { + key: value, + name: value, + fieldName: value, + minWidth: 300, + }; + }, + )} + groupNestingDepth={nestingDepth} + item={item} + itemIndex={itemIndex} + selection={_selection} + selectionMode={SelectionMode.multiple} + /> + ); + } + + const wrapper = mount( + , + ); + const listPage = wrapper.find(List).find('.ms-List-page').first(); + expect(listPage.key()).toBe('group0'); + + wrapper.unmount(); + }); + + it("renders the number of rows specified by a group's count when startIndex is zero", () => { + const _selection = new Selection(); + const _items: Array<{ key: string }> = [{ key: '1' }, { key: '2' }, { key: '3' }]; + const _groups: Array = [ + { + count: 3, + hasMoreData: true, + isCollapsed: false, + key: 'group0', + name: 'group 0', + startIndex: 0, + level: 0, + children: [], + }, + ]; + + function _onRenderCell(nestingDepth: number, item: any, itemIndex: number): JSX.Element { + return ( + { + return { + key: value, + name: value, + fieldName: value, + minWidth: 300, + }; + }, + )} + groupNestingDepth={nestingDepth} + item={item} + itemIndex={itemIndex} + selection={_selection} + selectionMode={SelectionMode.multiple} + /> + ); + } + + const wrapper = mount( + , + ); + + const listRows = wrapper.find(DetailsRow); + expect(listRows).toHaveLength(3); + + wrapper.unmount(); + }); + + it("renders the number of rows specified by a group's count when startIndex is not zero", () => { + const _selection = new Selection(); + const _items: Array<{ key: string }> = [{ key: '1' }, { key: '2' }, { key: '3' }, { key: '4' }, { key: '5' }]; + const _groups: Array = [ + { + count: 3, + hasMoreData: true, + isCollapsed: false, + key: 'group0', + name: 'group 0', + startIndex: 2, + level: 0, + children: [], + }, + ]; + + function _onRenderCell(nestingDepth: number, item: any, itemIndex: number): JSX.Element { + return ( + { + return { + key: value, + name: value, + fieldName: value, + minWidth: 300, + }; + }, + )} + groupNestingDepth={nestingDepth} + item={item} + itemIndex={itemIndex} + selection={_selection} + selectionMode={SelectionMode.multiple} + /> + ); + } + + const wrapper = mount( + , + ); + + const listRows = wrapper.find(DetailsRow); + expect(listRows).toHaveLength(3); + + expect(listRows.at(0).parent().key()).toBe('3'); + expect(listRows.at(1).parent().key()).toBe('4'); + expect(listRows.at(2).parent().key()).toBe('5'); + + wrapper.unmount(); + }); + + it('renders no rows if group is collapsed', () => { + const _selection = new Selection(); + const _items: Array<{ key: string }> = [{ key: '1' }, { key: '2' }, { key: '3' }]; + const _groups: Array = [ + { + count: 3, + hasMoreData: true, + isCollapsed: true, + key: 'group0', + name: 'group 0', + startIndex: 0, + level: 0, + children: [], + }, + ]; + + function _onRenderCell(nestingDepth: number, item: any, itemIndex: number): JSX.Element { + return ( + { + return { + key: value, + name: value, + fieldName: value, + minWidth: 300, + }; + }, + )} + groupNestingDepth={nestingDepth} + item={item} + itemIndex={itemIndex} + selection={_selection} + selectionMode={SelectionMode.multiple} + /> + ); + } + + const wrapper = mount( + , + ); + + const listRows = wrapper.find(DetailsRow); + expect(listRows).toHaveLength(0); + + wrapper.unmount(); + }); + + // eslint-disable-next-line @fluentui/max-len + it('renders the specified count of rows if "Show All" is to be displayed and all rows once "Show All" is clicked', () => { + const _selection = new Selection(); + const _items: Array<{ key: string }> = [{ key: '1' }, { key: '2' }, { key: '3' }]; + const _groups: Array = [ + { + count: 1, + hasMoreData: true, + isCollapsed: false, + key: 'group0', + name: 'group 0', + startIndex: 0, + level: 0, + }, + ]; + + function _onRenderCell(nestingDepth: number, item: any, itemIndex: number): JSX.Element { + return ( + { + return { + key: value, + name: value, + fieldName: value, + minWidth: 300, + }; + }, + )} + groupNestingDepth={nestingDepth} + item={item} + itemIndex={itemIndex} + selection={_selection} + selectionMode={SelectionMode.multiple} + /> + ); + } + + const wrapper = mount( + , + ); + + let listRows = wrapper.find(DetailsRow); + expect(listRows).toHaveLength(1); + + const groupShowAllElement = wrapper.find(GroupShowAll); + + groupShowAllElement.find(Link).simulate('click'); + + listRows = wrapper.find(DetailsRow); + expect(listRows).toHaveLength(3); + + wrapper.unmount(); + }); + + it('renders group header with custom checkbox render', () => { + const onRenderCheckboxMock = jest.fn(); + + mount( + , + ); + + expect(onRenderCheckboxMock).toHaveBeenCalledTimes(1); + expect(onRenderCheckboxMock.mock.calls[0][0]).toEqual({ checked: false, theme: getTheme() }); + }); + + it('re-renders when items change back to the initial items', () => { + const initialItems: Array<{ key: string }> = [{ key: 'initial' }]; + const nextItems: Array<{ key: string }> = [{ key: 'changed' }]; + const _groups: Array = [ + { + count: 1, + hasMoreData: true, + isCollapsed: false, + key: 'group0', + name: 'group 0', + startIndex: 0, + level: 0, + }, + ]; + + function _onRenderCell(nestingDepth: number, item: { key: string }, itemIndex: number): JSX.Element { + const id = `rendered-item-${item.key}`; + return
; + } + + const wrapper = mount(); + expect(wrapper.contains(
)).toEqual(true); + + wrapper.setProps({ items: nextItems }); + expect(wrapper.contains(
)).toEqual(true); + expect(wrapper.contains(
)).toEqual(false); + + wrapper.setProps({ items: initialItems }); + expect(wrapper.contains(
)).toEqual(true); + }); +}); From b6780472e90eab07598a132409025e42f50eb6a1 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Mon, 22 Aug 2022 13:43:23 -0700 Subject: [PATCH 05/18] add perf tests for groupedlist/groupedlistv2 --- apps/perf-test/package.json | 1 + apps/perf-test/src/scenarioIterations.js | 5 ++ apps/perf-test/src/scenarioRenderTypes.js | 2 + apps/perf-test/src/scenarios/GroupedList.tsx | 55 +++++++++++++++++++ .../perf-test/src/scenarios/GroupedListV2.tsx | 55 +++++++++++++++++++ 5 files changed, 118 insertions(+) create mode 100644 apps/perf-test/src/scenarios/GroupedList.tsx create mode 100644 apps/perf-test/src/scenarios/GroupedListV2.tsx diff --git a/apps/perf-test/package.json b/apps/perf-test/package.json index 4ca4d106d19c80..1d61c92396111d 100644 --- a/apps/perf-test/package.json +++ b/apps/perf-test/package.json @@ -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", diff --git a/apps/perf-test/src/scenarioIterations.js b/apps/perf-test/src/scenarioIterations.js index db017b5bbb8968..1e81dfe7943cbc 100644 --- a/apps/perf-test/src/scenarioIterations.js +++ b/apps/perf-test/src/scenarioIterations.js @@ -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; diff --git a/apps/perf-test/src/scenarioRenderTypes.js b/apps/perf-test/src/scenarioRenderTypes.js index 2cdb377ceeced3..e30c3c4aaff06a 100644 --- a/apps/perf-test/src/scenarioRenderTypes.js +++ b/apps/perf-test/src/scenarioRenderTypes.js @@ -13,6 +13,8 @@ const DefaultRenderTypes = ['mount']; const scenarioRenderTypes = { ThemeProvider: AllRenderTypes, + GroupedList: AllRenderTypes, + GroupedListV2: AllRenderTypes, }; module.exports = { diff --git a/apps/perf-test/src/scenarios/GroupedList.tsx b/apps/perf-test/src/scenarios/GroupedList.tsx new file mode 100644 index 00000000000000..ed051e2130eea6 --- /dev/null +++ b/apps/perf-test/src/scenarios/GroupedList.tsx @@ -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 ? ( + + ) : null; +}; + +const Scenario = () => { + return ( + + ); +}; + +export default Scenario; diff --git a/apps/perf-test/src/scenarios/GroupedListV2.tsx b/apps/perf-test/src/scenarios/GroupedListV2.tsx new file mode 100644 index 00000000000000..15cb2e11502471 --- /dev/null +++ b/apps/perf-test/src/scenarios/GroupedListV2.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { createListItems, createGroups, IExampleItem } from '@fluentui/example-data'; +import { 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 ? ( + + ) : null; +}; + +const Scenario = () => { + return ( + + ); +}; + +export default Scenario; From c7d792898a249583529418b6fcb645ca7af666c6 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Mon, 22 Aug 2022 13:44:31 -0700 Subject: [PATCH 06/18] change files --- ...luentui-react-6c6ea224-d7e4-405c-92cc-8268da44ee7a.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-6c6ea224-d7e4-405c-92cc-8268da44ee7a.json diff --git a/change/@fluentui-react-6c6ea224-d7e4-405c-92cc-8268da44ee7a.json b/change/@fluentui-react-6c6ea224-d7e4-405c-92cc-8268da44ee7a.json new file mode 100644 index 00000000000000..36c71653ac682a --- /dev/null +++ b/change/@fluentui-react-6c6ea224-d7e4-405c-92cc-8268da44ee7a.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: improve groupedlist virtualization", + "packageName": "@fluentui/react", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} From db0867e9aa45213959281f97605ed844f5142d0a Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Tue, 23 Aug 2022 13:09:51 -0700 Subject: [PATCH 07/18] better types and refactor render functions. --- .../GroupedList/GroupedListV2.base.tsx | 217 +++++++++++++----- 1 file changed, 162 insertions(+), 55 deletions(-) diff --git a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx index 167b597efd244c..a97ea8743bc88f 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx +++ b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx @@ -44,14 +44,31 @@ export interface IGroupedListV2Props extends IGroupedListProps { groupExpandedVersion: {}; } -type IGroupedItem = { - group?: IGroup; - groupId?: string; - item?: any; - itemIndex?: number; - type: 'group' | 'item' | 'showAll' | 'footer'; +type IITemGroupedItem = { + type: 'item'; + group: IGroup; + item: any; + itemIndex: number; }; +type IShowAllGroupedItem = { + type: 'showAll'; + group: IGroup; +}; + +type IFooterGroupedItem = { + type: 'footer'; + group: IGroup; +}; + +type IHeaderGroupedItem = { + type: 'header'; + group: IGroup; + groupId: string; +}; + +type IGroupedItem = IITemGroupedItem | IShowAllGroupedItem | IFooterGroupedItem | IHeaderGroupedItem; + type FlattenItemsFn = ( groups: IGroup[] | undefined, items: any[], @@ -85,7 +102,7 @@ const flattenItems: FlattenItemsFn = (groups, items, memoItems, groupProps) => { memoItems[index] = { group, groupId: getId('GroupedListSection'), - type: 'group', + type: 'header', }; index++; @@ -100,7 +117,7 @@ const flattenItems: FlattenItemsFn = (groups, items, memoItems, groupProps) => { memoItems[index] = { group, groupId: getId('GroupedListSection'), - type: 'group', + type: 'header', }; index++; } @@ -333,17 +350,14 @@ export const GroupedListV2FC: React.FC = props => { [groupProps], ); - const renderItem = React.useCallback( - (item: IGroupedItem, flattenedIndex: number): React.ReactNode => { - const group = item.group; - - const level = group?.level ? group.level + 1 : 1; + const getDividerProps = React.useCallback( + (group: IGroup, flattenedIndex: number) => { const isSelected = selection && group ? selection.isRangeSelected(group.startIndex, group.count) : false; const dividerProps = { group, groupIndex: flattenedIndex, - groupLevel: group?.level ?? 0, + groupLevel: group.level ?? 0, isSelected, selected: isSelected, viewport, @@ -355,55 +369,148 @@ export const GroupedListV2FC: React.FC = props => { onToggleSummarize, }; - if (item.type === 'group') { - const groupHeaderProps = { - ...groupProps!.headerProps, - ...dividerProps, - key: group!.key, - groupedListId: item.groupId!, - ariaLevel: level, - ariaSetSize: groups ? groups.length : undefined, - ariaPosInSet: flattenedIndex !== undefined ? flattenedIndex + 1 : undefined, - }; + return dividerProps; + }, + [compact, groups, onToggleCollapse, onToggleSelectGroup, onToggleSummarize, selection, selectionMode, viewport], + ); - return onRenderHeader(groupHeaderProps, renderGroupHeader); - } else if (item.type === 'showAll') { - const groupShowAllProps = { - ...groupProps!.showAllProps, - ...dividerProps, - key: group?.key ? `${group.key}-show-all` : undefined, - }; + const renderHeader = React.useCallback( + (item: IHeaderGroupedItem, flattenedIndex: number): React.ReactNode => { + const group = item.group; - return onRenderShowAll(groupShowAllProps, renderGroupShowAll); - } else if (item.type === 'footer') { - const groupFooterProps = { - ...groupProps!.footerProps, - ...dividerProps, - key: group?.key ? `${group.key}-footer` : undefined, - }; + const level = group.level ? group.level + 1 : 1; - return onRenderFooter(groupFooterProps, renderGroupFooter); - } else { + const groupHeaderProps = { + ...groupProps!.headerProps, + ...getDividerProps(group, flattenedIndex), + key: group.key, + groupedListId: item.groupId, + ariaLevel: level, + ariaSetSize: groups ? groups.length : undefined, + ariaPosInSet: flattenedIndex !== undefined ? flattenedIndex + 1 : undefined, + }; + + return onRenderHeader(groupHeaderProps, renderGroupHeader); + }, + [onRenderHeader, groupProps, groups, getDividerProps], + ); + + const renderShowAll = React.useCallback( + (item: IShowAllGroupedItem, flattenedIndex: number): React.ReactNode => { + const group = item.group; + const groupShowAllProps = { + ...groupProps!.showAllProps, + ...getDividerProps(group, flattenedIndex), + key: group?.key ? `${group.key}-show-all` : undefined, + }; + + return onRenderShowAll(groupShowAllProps, renderGroupShowAll); + }, + + [onRenderShowAll, groupProps, getDividerProps], + ); + + const renderFooter = React.useCallback( + (item: IFooterGroupedItem, flattenedIndex: number): React.ReactNode => { + const group = item.group; + const groupFooterProps = { + ...groupProps!.footerProps, + ...getDividerProps(group, flattenedIndex), + key: group?.key ? `${group.key}-footer` : undefined, + }; + + return onRenderFooter(groupFooterProps, renderGroupFooter); + }, + [onRenderFooter, groupProps, getDividerProps], + ); + + const renderItem = React.useCallback( + (item: IGroupedItem, flattenedIndex: number): React.ReactNode => { + if (item.type === 'header') { + return renderHeader(item, flattenedIndex); + } else if (item.type === 'showAll') { + return renderShowAll(item, flattenedIndex); + } else if (item.type === 'footer') { + return renderFooter(item, flattenedIndex); + } else if (item.type === 'item') { + const level = item.group.level ? item.group.level + 1 : 1; return onRenderCell(level, item.item ?? item, item.itemIndex ?? flattenedIndex); } }, - [ - onRenderCell, - groups, - groupProps, - selection, - selectionMode, - compact, - viewport, - onToggleCollapse, - onToggleSelectGroup, - onToggleSummarize, - onRenderHeader, - onRenderShowAll, - onRenderFooter, - ], + [onRenderCell, renderHeader, renderShowAll, renderFooter], ); + // const renderItem = React.useCallback( + // (item: IGroupedItem, flattenedIndex: number): React.ReactNode => { + // const group = item.group; + + // const level = group.level ? group.level + 1 : 1; + // const isSelected = selection && group ? selection.isRangeSelected(group.startIndex, group.count) : false; + + // const dividerProps = { + // group, + // groupIndex: flattenedIndex, + // groupLevel: group.level ?? 0, + // isSelected, + // selected: isSelected, + // viewport, + // selectionMode, + // groups, + // compact, + // onToggleSelectGroup, + // onToggleCollapse, + // onToggleSummarize, + // }; + + // if (item.type === 'header') { + // const groupHeaderProps = { + // ...groupProps!.headerProps, + // ...dividerProps, + // key: group.key, + // groupedListId: item.groupId, + // ariaLevel: level, + // ariaSetSize: groups ? groups.length : undefined, + // ariaPosInSet: flattenedIndex !== undefined ? flattenedIndex + 1 : undefined, + // }; + + // return onRenderHeader(groupHeaderProps, renderGroupHeader); + // } else if (item.type === 'showAll') { + // const groupShowAllProps = { + // ...groupProps!.showAllProps, + // ...dividerProps, + // key: group?.key ? `${group.key}-show-all` : undefined, + // }; + + // return onRenderShowAll(groupShowAllProps, renderGroupShowAll); + // } else if (item.type === 'footer') { + // const groupFooterProps = { + // ...groupProps!.footerProps, + // ...dividerProps, + // key: group?.key ? `${group.key}-footer` : undefined, + // }; + + // return onRenderFooter(groupFooterProps, renderGroupFooter); + // } else if (item.type === 'item') { + // return onRenderCell(level, item.item ?? item, item.itemIndex ?? flattenedIndex); + // } + // }, + // [ + // onRenderCell, + // groups, + // groupProps, + // selection, + // selectionMode, + // compact, + // viewport, + // onToggleCollapse, + // onToggleSelectGroup, + // onToggleSummarize, + // onRenderHeader, + // onRenderShowAll, + // onRenderFooter, + // ], + // ); + return ( Date: Tue, 23 Aug 2022 15:38:51 -0700 Subject: [PATCH 08/18] refactor grouped items --- .../GroupedList/GroupedListV2.base.tsx | 184 +++++++++--------- 1 file changed, 88 insertions(+), 96 deletions(-) diff --git a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx index a97ea8743bc88f..f2556f98c6f059 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx +++ b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx @@ -7,9 +7,10 @@ import { css, getId, EventGroup, + IRenderFunction, } from '../../Utilities'; import { List, ScrollToMode, IListProps } from '../../List'; -import { SelectionMode, SELECTION_CHANGE } from '../../Selection'; +import { ISelection, SelectionMode, SELECTION_CHANGE } from '../../Selection'; import { FocusZone, FocusZoneDirection } from '../../FocusZone'; import type { IProcessedStyleSet } from '../../Styling'; import type { @@ -170,6 +171,26 @@ const flattenItems: FlattenItemsFn = (groups, items, memoItems, groupProps) => { return memoItems; }; +const useIsGroupSelected = (startIndex, count, selection, eventGroup) => { + const [isSelected, setIsSelected] = React.useState(selection?.isRangeSelected(startIndex, count) ?? false); + + React.useEffect(() => { + const changeHandler = () => { + setIsSelected(selection?.isRangeSelected(startIndex, count) ?? false); + }; + + if (selection) { + eventGroup.on(selection, SELECTION_CHANGE, changeHandler); + } + + return () => { + eventGroup?.off(selection, SELECTION_CHANGE, changeHandler); + }; + }, [startIndex, count, selection, eventGroup]); + + return isSelected; +}; + const computeIsSomeGroupExpanded = (groups: IGroup[] | undefined): boolean => { return !!( groups && groups.some(group => (group.children ? computeIsSomeGroupExpanded(group.children) : !group.isCollapsed)) @@ -273,7 +294,7 @@ export const GroupedListV2FC: React.FC = props => { (flattenedIndex: number): { key?: string } => { const pageGroup = listView[flattenedIndex]; return { - key: pageGroup.type === 'group' ? pageGroup.group!.key : undefined, + key: pageGroup.type === 'header' ? pageGroup.group.key : undefined, }; }, [listView], @@ -284,15 +305,8 @@ export const GroupedListV2FC: React.FC = props => { setGroupsCollapsedState(groups, groupProps.isAllGroupsCollapsed); } events.current = new EventGroup(this); - if (selection) { - events.current.on(selection, SELECTION_CHANGE, onSelectionChange); - } }); - const onSelectionChange = React.useCallback(() => { - setVersion({}); - }, [setVersion]); - useUnmount(() => { events.current?.dispose(); }); @@ -352,14 +366,10 @@ export const GroupedListV2FC: React.FC = props => { const getDividerProps = React.useCallback( (group: IGroup, flattenedIndex: number) => { - const isSelected = selection && group ? selection.isRangeSelected(group.startIndex, group.count) : false; - const dividerProps = { group, groupIndex: flattenedIndex, groupLevel: group.level ?? 0, - isSelected, - selected: isSelected, viewport, selectionMode, groups, @@ -371,28 +381,35 @@ export const GroupedListV2FC: React.FC = props => { return dividerProps; }, - [compact, groups, onToggleCollapse, onToggleSelectGroup, onToggleSummarize, selection, selectionMode, viewport], + [compact, groups, onToggleCollapse, onToggleSelectGroup, onToggleSummarize, selectionMode, viewport], ); const renderHeader = React.useCallback( (item: IHeaderGroupedItem, flattenedIndex: number): React.ReactNode => { const group = item.group; - const level = group.level ? group.level + 1 : 1; - - const groupHeaderProps = { + const headerProps = { ...groupProps!.headerProps, - ...getDividerProps(group, flattenedIndex), + ...getDividerProps(item.group, flattenedIndex), key: group.key, groupedListId: item.groupId, - ariaLevel: level, + ariaLevel: group.level ? group.level + 1 : 1, ariaSetSize: groups ? groups.length : undefined, ariaPosInSet: flattenedIndex !== undefined ? flattenedIndex + 1 : undefined, }; - return onRenderHeader(groupHeaderProps, renderGroupHeader); + return ( + + ); }, - [onRenderHeader, groupProps, groups, getDividerProps], + [onRenderHeader, groupProps, groups, getDividerProps, selection], ); const renderShowAll = React.useCallback( @@ -404,10 +421,19 @@ export const GroupedListV2FC: React.FC = props => { key: group?.key ? `${group.key}-show-all` : undefined, }; - return onRenderShowAll(groupShowAllProps, renderGroupShowAll); + return ( + + ); }, - [onRenderShowAll, groupProps, getDividerProps], + [onRenderShowAll, groupProps, getDividerProps, selection], ); const renderFooter = React.useCallback( @@ -419,9 +445,18 @@ export const GroupedListV2FC: React.FC = props => { key: group?.key ? `${group.key}-footer` : undefined, }; - return onRenderFooter(groupFooterProps, renderGroupFooter); + return ( + + ); }, - [onRenderFooter, groupProps, getDividerProps], + [onRenderFooter, groupProps, getDividerProps, selection], ); const renderItem = React.useCallback( @@ -440,77 +475,6 @@ export const GroupedListV2FC: React.FC = props => { [onRenderCell, renderHeader, renderShowAll, renderFooter], ); - // const renderItem = React.useCallback( - // (item: IGroupedItem, flattenedIndex: number): React.ReactNode => { - // const group = item.group; - - // const level = group.level ? group.level + 1 : 1; - // const isSelected = selection && group ? selection.isRangeSelected(group.startIndex, group.count) : false; - - // const dividerProps = { - // group, - // groupIndex: flattenedIndex, - // groupLevel: group.level ?? 0, - // isSelected, - // selected: isSelected, - // viewport, - // selectionMode, - // groups, - // compact, - // onToggleSelectGroup, - // onToggleCollapse, - // onToggleSummarize, - // }; - - // if (item.type === 'header') { - // const groupHeaderProps = { - // ...groupProps!.headerProps, - // ...dividerProps, - // key: group.key, - // groupedListId: item.groupId, - // ariaLevel: level, - // ariaSetSize: groups ? groups.length : undefined, - // ariaPosInSet: flattenedIndex !== undefined ? flattenedIndex + 1 : undefined, - // }; - - // return onRenderHeader(groupHeaderProps, renderGroupHeader); - // } else if (item.type === 'showAll') { - // const groupShowAllProps = { - // ...groupProps!.showAllProps, - // ...dividerProps, - // key: group?.key ? `${group.key}-show-all` : undefined, - // }; - - // return onRenderShowAll(groupShowAllProps, renderGroupShowAll); - // } else if (item.type === 'footer') { - // const groupFooterProps = { - // ...groupProps!.footerProps, - // ...dividerProps, - // key: group?.key ? `${group.key}-footer` : undefined, - // }; - - // return onRenderFooter(groupFooterProps, renderGroupFooter); - // } else if (item.type === 'item') { - // return onRenderCell(level, item.item ?? item, item.itemIndex ?? flattenedIndex); - // } - // }, - // [ - // onRenderCell, - // groups, - // groupProps, - // selection, - // selectionMode, - // compact, - // viewport, - // onToggleCollapse, - // onToggleSelectGroup, - // onToggleSummarize, - // onRenderHeader, - // onRenderShowAll, - // onRenderFooter, - // ], - // ); - return ( = props => { ); }; +interface IGroupItemProps { + props: T; + render: IRenderFunction; + defaultRender: (props?: T) => JSX.Element | null; + item: any; + selection: ISelection; + eventGroup: EventGroup; +} + +const GroupItem = ({ + render, + defaultRender, + item, + selection, + eventGroup, + props, +}: React.PropsWithChildren>): React.ReactElement | null => { + const group = item.group; + + const isSelected = useIsGroupSelected(group.startIndex, group.count, selection, eventGroup); + const mergedProps = { + ...props, + isSelected: isSelected, + selected: isSelected, + }; + return render(mergedProps, defaultRender); +}; + export class GroupedListV2Wrapper extends React.Component implements IGroupedList { From f18932dd249c34b59c5f440d37572bdfb22fb795 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Tue, 23 Aug 2022 17:17:52 -0700 Subject: [PATCH 09/18] typescript --- .../GroupedList/GroupedListV2.base.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx index f2556f98c6f059..29bb0ba9a061f0 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx +++ b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx @@ -171,7 +171,14 @@ const flattenItems: FlattenItemsFn = (groups, items, memoItems, groupProps) => { return memoItems; }; -const useIsGroupSelected = (startIndex, count, selection, eventGroup) => { +type UseIsGroupSelected = ( + startIndex: number, + count: number, + selection?: ISelection, + eventGroup?: EventGroup, +) => boolean; + +const useIsGroupSelected: UseIsGroupSelected = (startIndex, count, selection, eventGroup) => { const [isSelected, setIsSelected] = React.useState(selection?.isRangeSelected(startIndex, count) ?? false); React.useEffect(() => { @@ -180,7 +187,7 @@ const useIsGroupSelected = (startIndex, count, selection, eventGroup) => { }; if (selection) { - eventGroup.on(selection, SELECTION_CHANGE, changeHandler); + eventGroup?.on(selection, SELECTION_CHANGE, changeHandler); } return () => { @@ -506,11 +513,11 @@ interface IGroupItemProps { render: IRenderFunction; defaultRender: (props?: T) => JSX.Element | null; item: any; - selection: ISelection; - eventGroup: EventGroup; + selection?: ISelection; + eventGroup?: EventGroup; } -const GroupItem = ({ +const GroupItem = ({ render, defaultRender, item, From 963260603d78a33d55be1d3532b437e5bb519ef8 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Tue, 23 Aug 2022 17:32:15 -0700 Subject: [PATCH 10/18] WIP debugging --- .../GroupedListV2.Basic.Example.tsx | 125 ++++++++++++------ .../GroupedList/GroupedListV2.base.tsx | 6 +- .../GroupedList/GroupedListV2.test.tsx | 2 +- 3 files changed, 87 insertions(+), 46 deletions(-) diff --git a/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx b/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx index 6e42daf6b20678..0dc93f2283692d 100644 --- a/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx +++ b/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx @@ -24,56 +24,97 @@ const columns = Object.keys(items[0]) 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 _selection = new Selection(); + const _items: Array<{ key: string }> = [{ key: '1' }, { key: '2' }, { key: '3' }, { key: '4' }, { key: '5' }]; + const _groups: Array = [ + { + count: 3, + hasMoreData: true, + isCollapsed: false, + key: 'group0', + name: 'group 0', + startIndex: 2, + level: 0, + children: [], + }, + ]; - const onRenderCell = ( - nestingDepth?: number, - item?: IExampleItem, - itemIndex?: number, - group?: IGroup, - ): React.ReactNode => { - return item && typeof itemIndex === 'number' && itemIndex > -1 ? ( + function _onRenderCell(nestingDepth: number, item: any, itemIndex: number): JSX.Element { + return ( { + return { + key: value, + name: value, + fieldName: value, + minWidth: 300, + }; + }, + )} groupNestingDepth={nestingDepth} item={item} itemIndex={itemIndex} - selection={selection} + selection={_selection} selectionMode={SelectionMode.multiple} - compact={isCompactMode} - group={group} /> - ) : null; - }; + ); + } - return ( -
- - - - -
- ); + return ; + + // 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 ? ( + // + // ) : null; + // }; + + // return ( + //
+ // + // + // + // + //
+ // ); }; // @ts-expect-error Storybook diff --git a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx index 29bb0ba9a061f0..91b42ca12df29e 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx +++ b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx @@ -166,7 +166,7 @@ const flattenItems: FlattenItemsFn = (groups, items, memoItems, groupProps) => { memoItems.length = index; - // console.log('MEMO ITEMS', memoItems); + console.log('MEMO ITEMS', memoItems); return memoItems; }; @@ -425,7 +425,7 @@ export const GroupedListV2FC: React.FC = props => { const groupShowAllProps = { ...groupProps!.showAllProps, ...getDividerProps(group, flattenedIndex), - key: group?.key ? `${group.key}-show-all` : undefined, + key: group.key ? `${group.key}-show-all` : undefined, }; return ( @@ -449,7 +449,7 @@ export const GroupedListV2FC: React.FC = props => { const groupFooterProps = { ...groupProps!.footerProps, ...getDividerProps(group, flattenedIndex), - key: group?.key ? `${group.key}-footer` : undefined, + key: group.key ? `${group.key}-footer` : undefined, }; return ( diff --git a/packages/react/src/components/GroupedList/GroupedListV2.test.tsx b/packages/react/src/components/GroupedList/GroupedListV2.test.tsx index 3e83f5e20bd934..a47525843ea6ef 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.test.tsx +++ b/packages/react/src/components/GroupedList/GroupedListV2.test.tsx @@ -132,7 +132,7 @@ describe('GroupedListV2', () => { wrapper.unmount(); }); - it("renders the number of rows specified by a group's count when startIndex is not zero", () => { + it.only("renders the number of rows specified by a group's count when startIndex is not zero", () => { const _selection = new Selection(); const _items: Array<{ key: string }> = [{ key: '1' }, { key: '2' }, { key: '3' }, { key: '4' }, { key: '5' }]; const _groups: Array = [ From ff0057b9999998b9eeb20c3595aa7a8625be36ec Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Mon, 29 Aug 2022 10:00:42 -0700 Subject: [PATCH 11/18] fix issues from tests - Add proper `getKey()` handling. - Remove selection dependency for "show all" and footer rendering. --- .../GroupedListV2.Basic.Example.tsx | 125 ++++++------------ .../GroupedList/GroupedListV2.base.tsx | 47 +++---- .../GroupedList/GroupedListV2.test.tsx | 2 +- 3 files changed, 62 insertions(+), 112 deletions(-) diff --git a/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx b/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx index 0dc93f2283692d..6e42daf6b20678 100644 --- a/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx +++ b/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx @@ -24,97 +24,56 @@ const columns = Object.keys(items[0]) const groups = createGroups(groupCount, groupDepth, 0, groupCount); export const GroupedListV2BasicExample: React.FunctionComponent = () => { - const _selection = new Selection(); - const _items: Array<{ key: string }> = [{ key: '1' }, { key: '2' }, { key: '3' }, { key: '4' }, { key: '5' }]; - const _groups: Array = [ - { - count: 3, - hasMoreData: true, - isCollapsed: false, - key: 'group0', - name: 'group 0', - startIndex: 2, - level: 0, - children: [], - }, - ]; + const [isCompactMode, { toggle: toggleIsCompactMode }] = useBoolean(false); + const selection = useConst(() => { + const s = new Selection(); + s.setItems(items, true); + return s; + }); - function _onRenderCell(nestingDepth: number, item: any, itemIndex: number): JSX.Element { - return ( + const onRenderCell = ( + nestingDepth?: number, + item?: IExampleItem, + itemIndex?: number, + group?: IGroup, + ): React.ReactNode => { + return item && typeof itemIndex === 'number' && itemIndex > -1 ? ( { - return { - key: value, - name: value, - fieldName: value, - minWidth: 300, - }; - }, - )} + columns={columns} groupNestingDepth={nestingDepth} item={item} itemIndex={itemIndex} - selection={_selection} + selection={selection} selectionMode={SelectionMode.multiple} + compact={isCompactMode} + group={group} /> - ); - } + ) : null; + }; - return ; - - // 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 ? ( - // - // ) : null; - // }; - - // return ( - //
- // - // - // - // - //
- // ); + return ( +
+ + + + +
+ ); }; // @ts-expect-error Storybook diff --git a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx index 91b42ca12df29e..7701c10321ffc1 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx +++ b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx @@ -153,6 +153,7 @@ const flattenItems: FlattenItemsFn = (groups, items, memoItems, groupProps) => { index++; } } + // Placeholder for a potential footer. // Whether or not a footer is displayed is resolved // by the footer render function so this is just a marker @@ -166,7 +167,7 @@ const flattenItems: FlattenItemsFn = (groups, items, memoItems, groupProps) => { memoItems.length = index; - console.log('MEMO ITEMS', memoItems); + // console.log('MEMO ITEMS', memoItems); return memoItems; }; @@ -221,10 +222,18 @@ const isInnerZoneKeystroke = (ev: React.KeyboardEvent): boolean => const getClassNames = classNamesFunction(); const getKey: IListProps['getKey'] = (item, _index) => { - if (item.type === 'group' && item.group) { - return item.group.key; - } else if (item.type === 'item' && item.item) { - return item.item.key; + switch (item.type) { + case 'item': + return item.item?.key ?? null; + + case 'header': + return item.group.key; + + case 'footer': + return `${item.group.key}-footer`; + + case 'showAll': + return `${item.group.key}-showAll`; } return null; @@ -428,19 +437,10 @@ export const GroupedListV2FC: React.FC = props => { key: group.key ? `${group.key}-show-all` : undefined, }; - return ( - - ); + return onRenderShowAll(groupShowAllProps, renderGroupShowAll); }, - [onRenderShowAll, groupProps, getDividerProps, selection], + [onRenderShowAll, groupProps, getDividerProps], ); const renderFooter = React.useCallback( @@ -452,18 +452,9 @@ export const GroupedListV2FC: React.FC = props => { key: group.key ? `${group.key}-footer` : undefined, }; - return ( - - ); + return onRenderFooter(groupFooterProps, renderGroupFooter); }, - [onRenderFooter, groupProps, getDividerProps, selection], + [onRenderFooter, groupProps, getDividerProps], ); const renderItem = React.useCallback( @@ -476,7 +467,7 @@ export const GroupedListV2FC: React.FC = props => { return renderFooter(item, flattenedIndex); } else if (item.type === 'item') { const level = item.group.level ? item.group.level + 1 : 1; - return onRenderCell(level, item.item ?? item, item.itemIndex ?? flattenedIndex); + return onRenderCell(level, item.item, item.itemIndex ?? flattenedIndex); } }, [onRenderCell, renderHeader, renderShowAll, renderFooter], diff --git a/packages/react/src/components/GroupedList/GroupedListV2.test.tsx b/packages/react/src/components/GroupedList/GroupedListV2.test.tsx index a47525843ea6ef..3e83f5e20bd934 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.test.tsx +++ b/packages/react/src/components/GroupedList/GroupedListV2.test.tsx @@ -132,7 +132,7 @@ describe('GroupedListV2', () => { wrapper.unmount(); }); - it.only("renders the number of rows specified by a group's count when startIndex is not zero", () => { + it("renders the number of rows specified by a group's count when startIndex is not zero", () => { const _selection = new Selection(); const _items: Array<{ key: string }> = [{ key: '1' }, { key: '2' }, { key: '3' }, { key: '4' }, { key: '5' }]; const _groups: Array = [ From d07d8592e2129ed0fcce904b09782b4ef2f16ecc Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Thu, 1 Sep 2022 13:23:54 -0700 Subject: [PATCH 12/18] Mark GroupedListV2 as unstable --- .../GroupedListV2.Basic.Example.tsx | 3 +- .../GroupedListV2.Custom.Example.tsx | 3 +- .../GroupedListV2.CustomCheckbox.Example.tsx | 2 +- packages/react/etc/react.api.md | 53 ------------------- packages/react/package.json | 5 ++ packages/react/src/GroupedListV2.ts | 2 +- .../components/GroupedList/GroupedListV2.ts | 4 +- .../react/src/components/GroupedList/index.ts | 3 -- 8 files changed, 14 insertions(+), 61 deletions(-) diff --git a/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx b/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx index 6e42daf6b20678..8528c6e4b02465 100644 --- a/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx +++ b/packages/react-examples/src/react/GroupedList/GroupedListV2.Basic.Example.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { GroupedListV2, IGroup } from '@fluentui/react/lib/GroupedList'; +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'; diff --git a/packages/react-examples/src/react/GroupedList/GroupedListV2.Custom.Example.tsx b/packages/react-examples/src/react/GroupedList/GroupedListV2.Custom.Example.tsx index 9554250a929035..1b9c9840411274 100644 --- a/packages/react-examples/src/react/GroupedList/GroupedListV2.Custom.Example.tsx +++ b/packages/react-examples/src/react/GroupedList/GroupedListV2.Custom.Example.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { GroupedListV2, IGroup, IGroupHeaderProps, IGroupFooterProps } from '@fluentui/react/lib/GroupedList'; +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'; diff --git a/packages/react-examples/src/react/GroupedList/GroupedListV2.CustomCheckbox.Example.tsx b/packages/react-examples/src/react/GroupedList/GroupedListV2.CustomCheckbox.Example.tsx index 3140675bfab0d2..fc71599f6083c8 100644 --- a/packages/react-examples/src/react/GroupedList/GroupedListV2.CustomCheckbox.Example.tsx +++ b/packages/react-examples/src/react/GroupedList/GroupedListV2.CustomCheckbox.Example.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import { GroupHeader, - GroupedListV2, IGroupHeaderCheckboxProps, IGroupHeaderProps, IGroupRenderProps, IGroup, } from '@fluentui/react/lib/GroupedList'; +import { GroupedListV2_unstable as GroupedListV2 } from '@fluentui/react/lib/GroupedListV2'; import { IColumn, IObjectWithKey, DetailsRow } from '@fluentui/react/lib/DetailsList'; import { FocusZone } from '@fluentui/react/lib/FocusZone'; import { Selection, SelectionMode, SelectionZone } from '@fluentui/react/lib/Selection'; diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index 29eb3bc101e050..e5b43345775300 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -2012,31 +2012,6 @@ export class GroupedListSection extends React_2.Component; - -// @public (undocumented) -export const GroupedListV2FC: React_2.FC; - -// @public (undocumented) -export class GroupedListV2Wrapper extends React_2.Component implements IGroupedList { - constructor(props: IGroupedListProps); - // (undocumented) - static displayName: string; - // (undocumented) - forceUpdate(): void; - // (undocumented) - static getDerivedStateFromProps(nextProps: IGroupedListProps, previousState: IGroupedListV2State): IGroupedListV2State; - // (undocumented) - getStartItemIndexInView(): number; - // (undocumented) - render(): JSX.Element; - // (undocumented) - scrollToIndex(index: number, measureItem?: (itemIndex: number) => number, scrollToMode?: ScrollToMode): void; - // (undocumented) - toggleCollapseAll(allCollapsed: boolean): void; -} - // @public (undocumented) export const GroupFooter: React_2.FunctionComponent; @@ -6144,34 +6119,6 @@ export interface IGroupedListStyles { root: IStyle; } -// @public (undocumented) -export interface IGroupedListV2Props extends IGroupedListProps { - // (undocumented) - groupExpandedVersion: {}; - // (undocumented) - listRef: React_2.Ref; - // (undocumented) - version: {}; -} - -// @public (undocumented) -export interface IGroupedListV2State { - // (undocumented) - compact?: IGroupedListProps['compact']; - // (undocumented) - groupExpandedVersion: {}; - // (undocumented) - groups?: IGroup[]; - // (undocumented) - items?: IGroupedListProps['items']; - // (undocumented) - listProps?: IGroupedListProps['listProps']; - // (undocumented) - selectionMode?: IGroupedListProps['selectionMode']; - // (undocumented) - version: {}; -} - // @public (undocumented) export interface IGroupFooterProps extends IGroupDividerProps { styles?: IStyleFunctionOrObject; diff --git a/packages/react/package.json b/packages/react/package.json index eb218f95b2f01a..227d78f0deb3ca 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -507,6 +507,11 @@ "import": "./lib/components/GroupedList/GroupShowAll.styles.js", "require": "./lib-commonjs/components/GroupedList/GroupShowAll.styles.js" }, + "./lib/GroupedListV2": { + "types": "./lib/GroupedList.d.ts", + "import": "./lib/GroupedListV2.js", + "require": "./lib-commonjs/GroupedListV2.js" + }, "./lib/HoverCard": { "types": "./lib/HoverCard.d.ts", "import": "./lib/HoverCard.js", diff --git a/packages/react/src/GroupedListV2.ts b/packages/react/src/GroupedListV2.ts index c55c281b2c37c2..6c257b794af247 100644 --- a/packages/react/src/GroupedListV2.ts +++ b/packages/react/src/GroupedListV2.ts @@ -1 +1 @@ -export * from './components/GroupedList/index'; +export * from './components/GroupedList/GroupedListV2'; diff --git a/packages/react/src/components/GroupedList/GroupedListV2.ts b/packages/react/src/components/GroupedList/GroupedListV2.ts index 1007927900e01c..a80913cac28594 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.ts +++ b/packages/react/src/components/GroupedList/GroupedListV2.ts @@ -4,7 +4,7 @@ import { getStyles } from './GroupedList.styles'; import { GroupedListV2Wrapper } from './GroupedListV2.base'; import type { IGroupedListProps, IGroupedListStyles, IGroupedListStyleProps } from './GroupedList.types'; -export const GroupedListV2: React.FunctionComponent = styled< +const GroupedListV2: React.FunctionComponent = styled< IGroupedListProps, IGroupedListStyleProps, IGroupedListStyles @@ -12,4 +12,6 @@ export const GroupedListV2: React.FunctionComponent = styled< scope: 'GroupedListV2', }); +export { GroupedListV2 as GroupedListV2_unstable }; + export type { IGroupedListProps }; diff --git a/packages/react/src/components/GroupedList/index.ts b/packages/react/src/components/GroupedList/index.ts index 9aa720d297d2ba..b012ba695051b1 100644 --- a/packages/react/src/components/GroupedList/index.ts +++ b/packages/react/src/components/GroupedList/index.ts @@ -10,6 +10,3 @@ export * from './GroupedListSection'; export type { IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupHeaderCheckboxProps } from './GroupHeader.types'; export type { IGroupFooterStyleProps, IGroupFooterStyles } from './GroupFooter.types'; export type { IGroupShowAllStyleProps, IGroupShowAllStyles } from './GroupShowAll.types'; - -export * from './GroupedListV2'; -export * from './GroupedListV2.base'; From 758c06a17d1345e65526c9a0a125be6abfad2679 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Wed, 7 Sep 2022 13:23:03 -0700 Subject: [PATCH 13/18] groupedlistv2: update naming - Rename to GroupedListV2_unstable - Update tests to use this name --- .../components/DetailsList/DetailsListV2.test.tsx | 2 +- .../components/GroupedList/GroupedListV2.test.tsx | 4 ++-- .../src/components/GroupedList/GroupedListV2.ts | 13 +++++++++++-- packages/react/src/components/GroupedList/index.ts | 2 ++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/DetailsList/DetailsListV2.test.tsx b/packages/react/src/components/DetailsList/DetailsListV2.test.tsx index a3370d485a151e..0f02aff354d8cb 100644 --- a/packages/react/src/components/DetailsList/DetailsListV2.test.tsx +++ b/packages/react/src/components/DetailsList/DetailsListV2.test.tsx @@ -19,7 +19,7 @@ import type { IDetailsColumnProps } from './DetailsColumn'; import type { IDetailsHeaderProps } from './DetailsHeader'; import type { IColumn, IDetailsGroupDividerProps, IDetailsList } from './DetailsList.types'; import type { IDetailsRowProps } from './DetailsRow'; -import { GroupedListV2 } from '../GroupedList/GroupedListV2'; +import { GroupedListV2_unstable as GroupedListV2 } from '../GroupedList/GroupedListV2'; // Populate mock data for testing function mockData(count: number, isColumn: boolean = false, customDivider: boolean = false): any { diff --git a/packages/react/src/components/GroupedList/GroupedListV2.test.tsx b/packages/react/src/components/GroupedList/GroupedListV2.test.tsx index 3e83f5e20bd934..bb5bc0285f5f77 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.test.tsx +++ b/packages/react/src/components/GroupedList/GroupedListV2.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { mount } from 'enzyme'; import { SelectionMode, Selection } from '../../Selection'; -import { GroupedListV2 } from './GroupedListV2'; +import { GroupedListV2_unstable as GroupedListV2 } from './GroupedListV2'; import { DetailsRow } from '../DetailsList/DetailsRow'; import { List } from '../../List'; import { GroupShowAll } from './GroupShowAll'; @@ -16,7 +16,7 @@ import type { IColumn } from '../DetailsList/DetailsList.types'; describe('GroupedListV2', () => { isConformant({ Component: GroupedListV2, - displayName: 'GroupedListV2', + displayName: 'GroupedListV2_unstable', componentPath: path.join(__dirname, 'GroupedListV2.ts'), requiredProps: { items: [], diff --git a/packages/react/src/components/GroupedList/GroupedListV2.ts b/packages/react/src/components/GroupedList/GroupedListV2.ts index a80913cac28594..47ffef49965731 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.ts +++ b/packages/react/src/components/GroupedList/GroupedListV2.ts @@ -4,6 +4,15 @@ import { getStyles } from './GroupedList.styles'; import { GroupedListV2Wrapper } from './GroupedListV2.base'; import type { IGroupedListProps, IGroupedListStyles, IGroupedListStyleProps } from './GroupedList.types'; +/** + * NOTE: GroupedListV2 is "unstable" and meant for preview use. It passes + * the same test suite as GroupedList but it is an entirely new implementation + * so it may have bugs and implementation details may change without notice. + * + * GroupedListV2 is an API-compatible replacement for GroupedList with a new implementation + * that addresses issues GroupedList has with virtualizing nested lists under certain + * conditions. + */ const GroupedListV2: React.FunctionComponent = styled< IGroupedListProps, IGroupedListStyleProps, @@ -12,6 +21,6 @@ const GroupedListV2: React.FunctionComponent = styled< scope: 'GroupedListV2', }); -export { GroupedListV2 as GroupedListV2_unstable }; +GroupedListV2.displayName = 'GroupedListV2_unstable'; -export type { IGroupedListProps }; +export { GroupedListV2 as GroupedListV2_unstable }; diff --git a/packages/react/src/components/GroupedList/index.ts b/packages/react/src/components/GroupedList/index.ts index b012ba695051b1..14f3e2991f2129 100644 --- a/packages/react/src/components/GroupedList/index.ts +++ b/packages/react/src/components/GroupedList/index.ts @@ -10,3 +10,5 @@ export * from './GroupedListSection'; export type { IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupHeaderCheckboxProps } from './GroupHeader.types'; export type { IGroupFooterStyleProps, IGroupFooterStyles } from './GroupFooter.types'; export type { IGroupShowAllStyleProps, IGroupShowAllStyles } from './GroupShowAll.types'; + +export { GroupedListV2_unstable } from './GroupedListV2'; From 8fda518bd0edb6c757aefdf5f7f8dba52c82b1ef Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Wed, 7 Sep 2022 13:55:36 -0700 Subject: [PATCH 14/18] update api snapshot --- packages/react/etc/react.api.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index e5b43345775300..9ce91d036a9d99 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -2012,6 +2012,9 @@ export class GroupedListSection extends React_2.Component; + // @public (undocumented) export const GroupFooter: React_2.FunctionComponent; From f80314722bc2c21cd41231f6e0047002899818b3 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Thu, 8 Sep 2022 09:57:51 -0700 Subject: [PATCH 15/18] update groupedlistv2 import for perf-test --- apps/perf-test/src/scenarios/GroupedListV2.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/perf-test/src/scenarios/GroupedListV2.tsx b/apps/perf-test/src/scenarios/GroupedListV2.tsx index 15cb2e11502471..2e0d0123c6680f 100644 --- a/apps/perf-test/src/scenarios/GroupedListV2.tsx +++ b/apps/perf-test/src/scenarios/GroupedListV2.tsx @@ -1,6 +1,13 @@ import * as React from 'react'; import { createListItems, createGroups, IExampleItem } from '@fluentui/example-data'; -import { GroupedListV2, Selection, SelectionMode, DetailsRow, IGroup, IColumn } from '@fluentui/react'; +import { + GroupedListV2_unstable as GroupedListV2, + Selection, + SelectionMode, + DetailsRow, + IGroup, + IColumn, +} from '@fluentui/react'; const groupCount = 5; const groupDepth = 5; From 19fd396c0e31ce01375ef605ae833612963f1f29 Mon Sep 17 00:00:00 2001 From: Sean Monahan Date: Tue, 13 Sep 2022 08:58:31 -0700 Subject: [PATCH 16/18] update snapshots --- .../__snapshots__/DetailsListV2.test.tsx.snap | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/packages/react/src/components/DetailsList/__snapshots__/DetailsListV2.test.tsx.snap b/packages/react/src/components/DetailsList/__snapshots__/DetailsListV2.test.tsx.snap index 0bb281a0918183..062ed7981648a9 100644 --- a/packages/react/src/components/DetailsList/__snapshots__/DetailsListV2.test.tsx.snap +++ b/packages/react/src/components/DetailsList/__snapshots__/DetailsListV2.test.tsx.snap @@ -36,7 +36,6 @@ exports[`DetailsListV2 handles paged updates to items within groups 1`] = ` >
@@ -242,7 +241,6 @@ exports[`DetailsListV2 handles paged updates to items within groups 2`] = ` >
@@ -464,7 +462,6 @@ exports[`DetailsListV2 handles updates to items and groups 1`] = ` >
@@ -688,7 +685,6 @@ exports[`DetailsListV2 handles updates to items and groups 2`] = ` >
@@ -912,7 +908,6 @@ exports[`DetailsListV2 handles updates to items and groups 3`] = ` >
@@ -1134,7 +1129,6 @@ exports[`DetailsListV2 handles updates to items and groups 4`] = ` >
@@ -1355,7 +1349,6 @@ exports[`DetailsListV2 renders List correctly 1`] = ` >
@@ -2339,7 +2332,6 @@ exports[`DetailsListV2 renders List correctly with onRenderDivider props 1`] = ` >
@@ -3420,7 +3412,6 @@ exports[`DetailsListV2 renders List in compact mode correctly 1`] = ` >
@@ -4405,7 +4396,6 @@ exports[`DetailsListV2 renders List in fixed constrained layout correctly 1`] = >
@@ -5389,7 +5379,6 @@ exports[`DetailsListV2 renders List with custom icon as column divider 1`] = ` >
@@ -6484,7 +6473,6 @@ exports[`DetailsListV2 renders List with hidden checkboxes correctly 1`] = ` >
@@ -7612,7 +7600,6 @@ exports[`DetailsListV2 renders List with hidden checkboxes correctly 1`] = ` role="presentation" >