diff --git a/apps/public-docsite/src/SiteDefinition/SiteDefinition.pages/Controls/web.tsx b/apps/public-docsite/src/SiteDefinition/SiteDefinition.pages/Controls/web.tsx index 43ecb10e92b001..182e6c3d5287e8 100644 --- a/apps/public-docsite/src/SiteDefinition/SiteDefinition.pages/Controls/web.tsx +++ b/apps/public-docsite/src/SiteDefinition/SiteDefinition.pages/Controls/web.tsx @@ -60,6 +60,7 @@ export const categories: { [name: string]: ICategory } = { LargeGrouped: { title: 'Large Grouped' }, GroupedV2: { title: 'Grouped V2' }, LargeGroupedV2: { title: 'Large Grouped V2' }, + ScrollToIndexGroupedV2: { title: 'Scroll To Index Grouped V2' }, CustomColumns: { title: 'Custom Item Columns', url: 'customitemcolumns' }, CustomRows: { title: 'Custom Item Rows', url: 'customitemrows' }, CustomFooter: { title: 'Custom Footer' }, diff --git a/apps/public-docsite/src/pages/Controls/DetailsListPage/DetailsListScrollToIndexGroupedV2Page.doc.ts b/apps/public-docsite/src/pages/Controls/DetailsListPage/DetailsListScrollToIndexGroupedV2Page.doc.ts new file mode 100644 index 00000000000000..83a60898bbf2af --- /dev/null +++ b/apps/public-docsite/src/pages/Controls/DetailsListPage/DetailsListScrollToIndexGroupedV2Page.doc.ts @@ -0,0 +1,10 @@ +import { TFabricPlatformPageProps } from '../../../interfaces/Platforms'; +import { DetailsListScrollToIndexGroupedV2PageProps as ExternalProps } from '@fluentui/react-examples/lib/react/DetailsList/DetailsList.doc'; + +export const DetailsListScrollToIndexGroupedV2PageProps: TFabricPlatformPageProps = { + web: { + ...(ExternalProps as any), + title: 'DetailsList - Scroll to index grouped V2', + isFeedbackVisible: false, + }, +}; diff --git a/apps/public-docsite/src/pages/Controls/DetailsListPage/DetailsListScrollToIndexGroupedV2Page.tsx b/apps/public-docsite/src/pages/Controls/DetailsListPage/DetailsListScrollToIndexGroupedV2Page.tsx new file mode 100644 index 00000000000000..1a39fabea167bb --- /dev/null +++ b/apps/public-docsite/src/pages/Controls/DetailsListPage/DetailsListScrollToIndexGroupedV2Page.tsx @@ -0,0 +1,7 @@ +import * as React from 'react'; +import { ControlsAreaPage, IControlsPageProps } from '../ControlsAreaPage'; +import { DetailsListScrollToIndexGroupedV2PageProps } from './DetailsListScrollToIndexGroupedV2Page.doc'; + +export const DetailsListScrollToIndexGroupedV2Page: React.FunctionComponent = props => { + return ; +}; diff --git a/change/@fluentui-react-d39b2c64-27d7-4851-b50f-8805e2664c0c.json b/change/@fluentui-react-d39b2c64-27d7-4851-b50f-8805e2664c0c.json new file mode 100644 index 00000000000000..67a2d47f103211 --- /dev/null +++ b/change/@fluentui-react-d39b2c64-27d7-4851-b50f-8805e2664c0c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: GroupedListV2 scrollToIndex scrolls to correct index", + "packageName": "@fluentui/react", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-examples/src/react/DetailsList/DetailsList.GroupedV2.Large.Example.tsx b/packages/react-examples/src/react/DetailsList/DetailsList.GroupedV2.Large.Example.tsx index 54dca9d9008dcd..1bed1b0ba5d401 100644 --- a/packages/react-examples/src/react/DetailsList/DetailsList.GroupedV2.Large.Example.tsx +++ b/packages/react-examples/src/react/DetailsList/DetailsList.GroupedV2.Large.Example.tsx @@ -40,7 +40,7 @@ const onRenderDetailsHeader = (props: IDetailsHeaderProps) => { return ; }; -export const DetailsListGroupedV2LargeExmaple: React.FC = () => { +export const DetailsListGroupedV2LargeExample: React.FC = () => { const [items] = React.useState(() => getInitialItems()); const [groups] = React.useState(() => getInitialGroups()); const [columns] = React.useState([ @@ -66,4 +66,4 @@ export const DetailsListGroupedV2LargeExmaple: React.FC = () => { }; // @ts-expect-error Storybook -DetailsListGroupedV2LargeExmaple.storyName = 'V2 Grouped Large'; +DetailsListGroupedV2LargeExample.storyName = 'V2 Grouped Large'; diff --git a/packages/react-examples/src/react/DetailsList/DetailsList.GroupedV2.ScrollToIndex.Example.tsx b/packages/react-examples/src/react/DetailsList/DetailsList.GroupedV2.ScrollToIndex.Example.tsx new file mode 100644 index 00000000000000..ffdb46d9932ef5 --- /dev/null +++ b/packages/react-examples/src/react/DetailsList/DetailsList.GroupedV2.ScrollToIndex.Example.tsx @@ -0,0 +1,303 @@ +import * as React from 'react'; +import { + ConstrainMode, + DefaultButton, + DetailsHeader, + DetailsList, + DetailsListLayoutMode, + DetailsRow, + Dropdown, + FocusZone, + FocusZoneDirection, + GroupedListV2_unstable as GroupedListV2, + ScrollToMode, + TextField, + mergeStyleSets, +} from '@fluentui/react'; + +import type { + IColumn, + IDetailsHeaderProps, + IDetailsList, + IGroup, + IGroupedItem, + IRenderFunction, + IDetailsListProps, + IDetailsRowStyles, + IRectangle, + IDetailsGroupRenderProps, + IDropdownOption, + IDetailsListStyles, +} from '@fluentui/react'; + +const rowHeight = 42; +const rowHeaderHeight = 48; +const defaultItemsPerPage = 10; +const dropdownOptions = [ + { key: 'auto', text: 'Auto' }, + { key: 'top', text: 'Top' }, + { key: 'bottom', text: 'Bottom' }, + { key: 'center', text: 'Center' }, +]; + +const gridStyles: Partial = { + root: { + overflowX: 'hidden', + selectors: { + '& [role=grid]': { + display: 'flex', + flexDirection: 'column', + alignItems: 'start', + height: '60vh', + }, + }, + }, + headerWrapper: { + flex: '0 0 auto', + }, + contentWrapper: { + flex: '1 1 auto', + overflow: 'hidden', + maxWidth: '100%', + }, +}; + +const classNames = mergeStyleSets({ + header: { + margin: 0, + }, + focusZone: { + height: '100%', + overflowY: 'auto', + overflowX: 'hidden', + maxWidth: '100%', + }, + selectionZone: { + height: '100%', + overflow: 'hidden', + }, +}); + +export interface IDetailsListGroupedExampleItem { + key: string; + name: string; + value: string; +} + +const getInitialItems = () => { + const items = []; + for (let i = 0; i < 1000; i++) { + items.push({ + key: i.toString(), + name: 'Item ' + i, + value: i.toString(), + }); + } + + return items; +}; + +const getInitialGroups = () => { + const groups = []; + for (let i = 0; i < 10; i++) { + groups.push({ + key: i.toString(), + name: i.toString(), + startIndex: i * 100, + count: 100, + level: 0, + }); + } + + return groups; +}; + +const onRenderColumn = (item: IDetailsListGroupedExampleItem, index: number, column: IColumn) => { + const value = + item && column && column.fieldName ? item[column.fieldName as keyof IDetailsListGroupedExampleItem] || '' : ''; + + return
{value}
; +}; + +const onRenderDetailsHeader = (props: IDetailsHeaderProps, defaultRender?: IRenderFunction) => { + return ; +}; + +const onRenderRow: IDetailsListProps['onRenderRow'] = props => { + const customStyles: Partial = {}; + if (props) { + customStyles.root = { height: rowHeight }; + return ; + } + return null; +}; + +const onRenderGroupHeader: IDetailsGroupRenderProps['onRenderHeader'] = (props, defaultRender) => { + if (props) { + const nextProps = { + ...props, + styles: { + root: { + height: rowHeaderHeight, + }, + }, + }; + + return defaultRender?.(nextProps) ?? null; + } + + return null; +}; + +/** + * + * @param index Index of the first item in the page + * @param _visibleRect + * @param itemsPerPage Number of items per page + * @param flatItems Flattened list of items rendered in the GroupedList (includes `groups` and `items`) + * @returns The height of the page. + */ +const getPageHeight = (index?: number, _visibleRect?: IRectangle, itemsPerPage?: number, flatItems?: any[]): number => { + if (typeof index === 'number' && typeof itemsPerPage === 'number' && Array.isArray(flatItems)) { + let h = 0; + + const listItems: IGroupedItem[] = flatItems as IGroupedItem[]; + + const count = Math.min(index + itemsPerPage, listItems.length); + for (let i = index; i < count; i++) { + const item = listItems[i]; + if (item.type === 'item') { + h += measureItem(item.itemIndex); + } else { + h += measureGroup(item.group); + } + } + + return h; + } + + return defaultItemsPerPage * rowHeight; +}; + +const measureItem = (index: number): number => { + return rowHeight; +}; + +const measureGroup = (group: IGroup): number => { + return rowHeaderHeight; +}; + +export const DetailsListGroupedV2ScrollToIndexExample: React.FC = () => { + const root = React.useRef(null); + + const [selectedIndex, setSelectedIndex] = React.useState(0); + const [scrollToMode, setScrollToMode] = React.useState(ScrollToMode.auto); + + const [items] = React.useState(() => getInitialItems()); + const [groups] = React.useState(() => getInitialGroups()); + const [columns] = React.useState([ + { key: 'name', name: 'Name', fieldName: 'name', minWidth: 100, maxWidth: 200, isResizable: true }, + { key: 'value', name: 'Value', fieldName: 'value', minWidth: 100, maxWidth: 200, isResizable: true }, + ]); + + const scroll = (index: number, propScrollToMode: ScrollToMode): void => { + const updatedSelectedIndex = Math.min(Math.max(index, 0), items.length - 1); + setSelectedIndex(updatedSelectedIndex); + setScrollToMode(propScrollToMode); + + root.current?.scrollToIndex(updatedSelectedIndex, measureItem, scrollToMode); + }; + + const scrollRelative = (delta: number): (() => void) => { + return (): void => { + scroll(selectedIndex + delta, scrollToMode); + }; + }; + + const onChangeText = (ev: React.FormEvent, value: string): void => { + scroll(parseInt(value, 10) || 0, scrollToMode); + }; + + const onDropdownChange = (event: React.FormEvent, option: IDropdownOption) => { + let scrollMode = scrollToMode; + switch (option.key) { + case 'auto': + scrollMode = ScrollToMode.auto; + break; + case 'top': + scrollMode = ScrollToMode.top; + break; + case 'bottom': + scrollMode = ScrollToMode.bottom; + break; + case 'center': + scrollMode = ScrollToMode.center; + break; + } + scroll(selectedIndex, scrollMode); + }; + const focusZoneProps = { + className: classNames.focusZone, + 'data-is-scrollable': 'true', + } as React.HTMLAttributes; + return ( + +
+ -10 + -1 + +1 + +10 +
+ +
+ Scroll item index: + +
+ + + {/* */} +
+ ); +}; + +// @ts-expect-error Storybook +DetailsListGroupedV2ScrollToIndexExample.storyName = 'V2 Grouped ScrollToIndex'; diff --git a/packages/react-examples/src/react/DetailsList/DetailsList.doc.tsx b/packages/react-examples/src/react/DetailsList/DetailsList.doc.tsx index 16d7daaec27fb9..eba06bcb1ba9e3 100644 --- a/packages/react-examples/src/react/DetailsList/DetailsList.doc.tsx +++ b/packages/react-examples/src/react/DetailsList/DetailsList.doc.tsx @@ -46,10 +46,14 @@ import { DetailsListGroupedLargeExample } from './DetailsList.Grouped.Large.Exam const DetailsListGroupedLargeExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/DetailsList/DetailsList.Grouped.Large.Example.tsx') as string; -import { DetailsListGroupedV2LargeExmaple } from './DetailsList.GroupedV2.Large.Example'; +import { DetailsListGroupedV2LargeExample } from './DetailsList.GroupedV2.Large.Example'; const DetailsListGroupedV2LargeExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/DetailsList/DetailsList.GroupedV2.Large.Example.tsx') as string; +import { DetailsListGroupedV2ScrollToIndexExample } from './DetailsList.GroupedV2.ScrollToIndex.Example'; +const DetailsListGroupedV2ScrollToIndexExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/DetailsList/DetailsList.GroupedV2.ScrollToIndex.Example.tsx') as string; + import { DetailsListDragDropExample } from './DetailsList.DragDrop.Example'; const DetailsListDragDropExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/DetailsList/DetailsList.DragDrop.Example.tsx') as string; @@ -144,7 +148,13 @@ export const DetailsListLargeGroupedPageProps: IDocPageProps = generateProps({ export const DetailsListLargeGroupedV2PageProps: IDocPageProps = generateProps({ title: 'Large grouped DetailsList V2', code: DetailsListGroupedV2LargeExampleCode, - view: , + view: , +}); + +export const DetailsListScrollToIndexGroupedV2PageProps: IDocPageProps = generateProps({ + title: 'Scroll To Index DetailsList V2', + code: DetailsListGroupedV2ScrollToIndexExampleCode, + view: , }); export const DetailsListCustomColumnsPageProps: IDocPageProps = generateProps({ diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index 79c22f95a835ee..46d51c6deb703b 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -5954,6 +5954,12 @@ export { IFontStyles } export { IFontWeight } +// @public +export type IFooterGroupedItem = { + type: 'footer'; + group: IGroup; +}; + export { iframeProperties } // @public @@ -6031,6 +6037,9 @@ export interface IGroupDividerProps { viewport?: IViewport; } +// @public +export type IGroupedItem = IItemGroupedItem | IShowAllGroupedItem | IFooterGroupedItem | IHeaderGroupedItem; + // @public (undocumented) export interface IGroupedList extends IList { forceUpdate: () => void; @@ -6139,8 +6148,17 @@ export interface IGroupedListStyles { root: IStyle; } +// @public (undocumented) +export interface IGroupedListV2 { + // (undocumented) + getStartItemIndexInView(): number; + // (undocumented) + scrollToIndex(index: number, measureItem?: (itemIndex: number) => number, scrollToMode?: ScrollToMode): void; +} + // @public (undocumented) export interface IGroupedListV2Props extends IGroupedListProps { + groupedListRef?: React_2.Ref; groupExpandedVersion?: {}; listRef?: React_2.Ref; onRenderCell: (nestingDepth?: number, item?: any, index?: number, group?: IGroup) => React_2.ReactNode; @@ -6279,6 +6297,14 @@ export interface IGroupSpacerStyles { root: IStyle; } +// @public +export type IHeaderGroupedItem = { + type: 'header'; + group: IGroup; + groupId: string; + groupIndex: number; +}; + // @public (undocumented) export interface IHoverCard { dismiss: (withTimeOut?: boolean) => void; @@ -6479,6 +6505,14 @@ export interface IInputProps extends React_2.InputHTMLAttributes extends React_2.HTMLAttributes | HT componentRef?: IRefObject; getItemCountForPage?: (itemIndex?: number, visibleRect?: IRectangle) => number; getKey?: (item: T, index?: number) => string; - getPageHeight?: (itemIndex?: number, visibleRect?: IRectangle, itemCount?: number) => number; - getPageSpecification?: (itemIndex?: number, visibleRect?: IRectangle) => IPageSpecification; + getPageHeight?: (itemIndex?: number, visibleRect?: IRectangle, itemCount?: number, items?: T[]) => number; + getPageSpecification?: (itemIndex?: number, visibleRect?: IRectangle, items?: T[]) => IPageSpecification; getPageStyle?: (page: IPage) => any; ignoreScrollingState?: boolean; items?: T[]; @@ -8546,6 +8580,12 @@ export interface IShimmerStyles { shimmerWrapper?: IStyle; } +// @public +export type IShowAllGroupedItem = { + type: 'showAll'; + group: IGroup; +}; + export { isIE11 } export { isInDateRangeArray } diff --git a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx index b8aced4ed7eca9..c146f58278cd67 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.base.tsx +++ b/packages/react/src/components/GroupedList/GroupedListV2.base.tsx @@ -20,7 +20,15 @@ import type { IGroupedListStyleProps, IGroupedListStyles, } from './GroupedList.types'; -import type { IGroupedListV2Props } from './GroupedListV2.types'; +import type { + IGroupedListV2, + IGroupedListV2Props, + IItemGroupedItem, + IShowAllGroupedItem, + IFooterGroupedItem, + IHeaderGroupedItem, + IGroupedItem, +} from './GroupedListV2.types'; import { GroupHeader } from './GroupHeader'; import { GroupShowAll } from './GroupShowAll'; import { GroupFooter } from './GroupFooter'; @@ -38,32 +46,6 @@ export interface IGroupedListV2State { groupExpandedVersion: {}; } -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; - groupIndex: number; -}; - -type IGroupedItem = IITemGroupedItem | IShowAllGroupedItem | IFooterGroupedItem | IHeaderGroupedItem; - type FlattenItems = ( groups: IGroup[] | undefined, items: any[], @@ -90,7 +72,6 @@ const flattenItems: FlattenItems = (groups, items, memoItems, getGroupItemLimit) let index = 0; - // const stack: IGroup[] = []; const stack: GroupStackItem[] = []; let j = groups.length - 1; while (j >= 0) { @@ -268,6 +249,7 @@ export const GroupedListV2FC: React.FC = props => { groups, onGroupExpandStateChanged, + listProps, className, usePageCache, onShouldVirtualize, @@ -278,7 +260,7 @@ export const GroupedListV2FC: React.FC = props => { rootListProps = {}, onRenderCell, viewport, - listRef, + groupedListRef, groupExpandedVersion, version: versionFromProps, } = props; @@ -298,6 +280,7 @@ export const GroupedListV2FC: React.FC = props => { const events = React.useRef(); const flatList = React.useRef([]); const isSomeGroupExpanded = React.useRef(computeIsSomeGroupExpanded(groups)); + const listRef = React.useRef(null); const [version, setVersion] = React.useState({}); const [toggleVersion, setToggleVersion] = React.useState({}); @@ -320,6 +303,49 @@ export const GroupedListV2FC: React.FC = props => { [listView], ); + React.useImperativeHandle( + groupedListRef, + () => { + let indexMap: number[] | undefined; + + return { + scrollToIndex: ( + index: number, + measureItem?: (itemIndex: number) => number, + scrollToMode?: ScrollToMode, + ): void => { + indexMap = + indexMap ?? + listView.reduce((map, item, listIndex) => { + if (item.type === 'item') { + map[item.itemIndex] = listIndex; + } + return map; + }, [] as number[]); + + const scrollIndex = indexMap[index]; + const measure = + typeof measureItem === 'function' + ? (itemIndex: number): number => { + if (listView[itemIndex]?.type === 'item') { + return measureItem((listView[itemIndex] as IItemGroupedItem).itemIndex); + } + + return 0; + } + : undefined; + + listRef.current?.scrollToIndex(scrollIndex, measure, scrollToMode); + }, + + getStartItemIndexInView: (): number => { + return listRef.current?.getStartItemIndexInView() || 0; + }, + }; + }, + [listView, listRef], + ); + React.useEffect(() => { if (groupProps?.isAllGroupsCollapsed) { setGroupsCollapsedState(groups, groupProps.isAllGroupsCollapsed); @@ -491,6 +517,7 @@ export const GroupedListV2FC: React.FC = props => { getPageSpecification={getPageSpecification} version={version} getKey={getKey} + {...listProps} {...rootListProps} /> @@ -530,7 +557,7 @@ export class GroupedListV2Wrapper implements IGroupedList { public static displayName: string = 'GroupedListV2'; - private _list = React.createRef(); + private _groupedList = React.createRef(); public static getDerivedStateFromProps( nextProps: IGroupedListV2Props, @@ -570,17 +597,15 @@ export class GroupedListV2Wrapper } public scrollToIndex(index: number, measureItem?: (itemIndex: number) => number, scrollToMode?: ScrollToMode): void { - if (this._list.current) { - this._list.current.scrollToIndex(index, measureItem, scrollToMode); - } + this._groupedList.current?.scrollToIndex(index, measureItem, scrollToMode); } public getStartItemIndexInView(): number { - return this._list.current?.getStartItemIndexInView() || 0; + return this._groupedList.current?.getStartItemIndexInView() || 0; } public render(): JSX.Element { - return ; + return ; } public forceUpdate() { diff --git a/packages/react/src/components/GroupedList/GroupedListV2.test.tsx b/packages/react/src/components/GroupedList/GroupedListV2.test.tsx index d3f5e9ccfc5540..59d91b4846cfd8 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.test.tsx +++ b/packages/react/src/components/GroupedList/GroupedListV2.test.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; import { SelectionMode, Selection } from '../../Selection'; import { GroupedListV2_unstable as GroupedListV2 } from './GroupedListV2'; @@ -411,4 +412,69 @@ describe('GroupedListV2', () => { wrapper.unmount(); }); + + it('scrolls to the correct index when calling `scrollToIndex`', () => { + 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 ref = React.createRef(); + const measureItem = jest.fn(); + + 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} + /> + ); + } + + act(() => { + const wrapper = mount( +
+ +
, + ); + + expect(typeof ref.current?.scrollToIndex).toBe('function'); + + ref.current?.scrollToIndex(4, measureItem); + + expect(measureItem).toHaveBeenCalled(); + expect(measureItem).toHaveBeenLastCalledWith(4); + + wrapper.unmount(); + }); + }); }); diff --git a/packages/react/src/components/GroupedList/GroupedListV2.types.ts b/packages/react/src/components/GroupedList/GroupedListV2.types.ts index 2b02fea016eb20..0d4f5b6bbc0c3c 100644 --- a/packages/react/src/components/GroupedList/GroupedListV2.types.ts +++ b/packages/react/src/components/GroupedList/GroupedListV2.types.ts @@ -1,12 +1,15 @@ import * as React from 'react'; import type { IGroupedListProps } from './GroupedList.types'; -import { List } from '../../List'; +import type { List, ScrollToMode } from '../../List'; import type { IGroup } from './GroupedList.types'; export interface IGroupedListV2Props extends IGroupedListProps { /** Ref to the underlying List control */ listRef?: React.Ref; + /** Ref to the underlying List control */ + groupedListRef?: React.Ref; + /** * For perf reasons, GroupedList avoids re-rendering unless certain props have changed. * Use this prop if you need to force it to re-render in other cases. You can pass any type of @@ -25,3 +28,49 @@ export interface IGroupedListV2Props extends IGroupedListProps { /** Rendering callback to render the group items. */ onRenderCell: (nestingDepth?: number, item?: any, index?: number, group?: IGroup) => React.ReactNode; } + +export interface IGroupedListV2 { + scrollToIndex(index: number, measureItem?: (itemIndex: number) => number, scrollToMode?: ScrollToMode): void; + getStartItemIndexInView(): number; +} + +/** + * An item rendered in a GroupedList. + */ +export type IItemGroupedItem = { + type: 'item'; + group: IGroup; + item: any; + itemIndex: number; +}; + +/** + * Item used for "show all" in a GroupedList. + */ +export type IShowAllGroupedItem = { + type: 'showAll'; + group: IGroup; +}; + +/** + * A footer in a GroupedList. + */ +export type IFooterGroupedItem = { + type: 'footer'; + group: IGroup; +}; + +/** + * A header in a GroupedList. + */ +export type IHeaderGroupedItem = { + type: 'header'; + group: IGroup; + groupId: string; + groupIndex: number; +}; + +/** + * Union of GroupedList item types. + */ +export type IGroupedItem = IItemGroupedItem | IShowAllGroupedItem | IFooterGroupedItem | IHeaderGroupedItem; diff --git a/packages/react/src/components/List/List.tsx b/packages/react/src/components/List/List.tsx index 0d224f99e2ab49..9ffc4d26f85b6c 100644 --- a/packages/react/src/components/List/List.tsx +++ b/packages/react/src/components/List/List.tsx @@ -1006,7 +1006,7 @@ export class List extends React.Component, IListState> const { getPageSpecification } = props; if (getPageSpecification) { - const pageData = getPageSpecification(itemIndex, visibleRect); + const pageData = getPageSpecification(itemIndex, visibleRect, props.items); const { itemCount = this._getItemCountForPage(itemIndex, visibleRect) } = pageData; @@ -1034,7 +1034,7 @@ export class List extends React.Component, IListState> */ private _getPageHeight(itemIndex: number, visibleRect: IRectangle, itemsPerPage: number): number { if (this.props.getPageHeight) { - return this.props.getPageHeight(itemIndex, visibleRect, itemsPerPage); + return this.props.getPageHeight(itemIndex, visibleRect, itemsPerPage, this.props.items); } else { const cachedHeight = this._cachedPageHeights[itemIndex]; diff --git a/packages/react/src/components/List/List.types.ts b/packages/react/src/components/List/List.types.ts index 772452b0edcb0e..fbcb198b827caa 100644 --- a/packages/react/src/components/List/List.types.ts +++ b/packages/react/src/components/List/List.types.ts @@ -179,7 +179,7 @@ export interface IListProps extends React.HTMLAttributes | HTML * as well as an estimated rendered height for the page. * The list will use this to optimize virtualization. */ - getPageSpecification?: (itemIndex?: number, visibleRect?: IRectangle) => IPageSpecification; + getPageSpecification?: (itemIndex?: number, visibleRect?: IRectangle, items?: T[]) => IPageSpecification; /** * Method called by the list to get how many items to render per page from specified index. @@ -194,7 +194,7 @@ export interface IListProps extends React.HTMLAttributes | HTML * in pixels, which has been seen to cause browser performance issues. * In general, use `getPageSpecification` instead. */ - getPageHeight?: (itemIndex?: number, visibleRect?: IRectangle, itemCount?: number) => number; + getPageHeight?: (itemIndex?: number, visibleRect?: IRectangle, itemCount?: number, items?: T[]) => number; /** * Method called by the list to derive the page style object. For spacer pages, the list will derive diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 5f3d49abe0686f..2ccb84a6355ba5 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -550,8 +550,14 @@ export type { IGroupedListSectionProps, IGroupedListSectionState, IGroupedListState, + IGroupedListV2, IGroupedListV2Props, IGroupedListV2State, + IItemGroupedItem, + IShowAllGroupedItem, + IFooterGroupedItem, + IHeaderGroupedItem, + IGroupedItem, } from './GroupedList'; export { ExpandingCard,