From 3bc95099ffd5356616b1504f423f770f2b2e9ef5 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Fri, 13 Jan 2023 18:26:49 +0100 Subject: [PATCH] add tests for flamegrill and v0 perf --- .../src/scenarios/TableAsDataGrid.tsx | 216 ++++++++++++++++++ .../src/scenarios/TablePrimitivesOnly.tsx | 93 ++++++++ .../src/scenarios/TableSelectionOnly.tsx | 188 +++++++++++++++ .../src/scenarios/DetailsListDataGrid.tsx | 83 +++++++ .../src/renderers/v8/detailsList.tsx | 2 +- .../src/shared/react/TestMount.tsx | 6 +- packages/fluentui/docs/package.json | 1 + .../Avatar/Performance/AvatarMinimal.perf.tsx | 9 - .../Avatar/Performance/DetailsList.perf.tsx | 85 +++++++ .../Performance/TableAsDataGrid.perf.tsx | 212 +++++++++++++++++ packages/fluentui/perf/src/index.tsx | 63 ++--- .../stories/Table/DataGrid.stories.tsx | 108 ++++----- .../stories/Table/Default.stories.tsx | 106 ++++----- .../stories/Table/MultipleSelect.stories.tsx | 98 ++++---- .../react-toolbar/src/index.ts | 9 +- .../DetailsListDataGrid.Example.tsx | 83 +++++++ .../gulp/src/webpack/webpack.config.perf.ts | 11 +- 17 files changed, 1143 insertions(+), 230 deletions(-) create mode 100644 apps/perf-test-react-components/src/scenarios/TableAsDataGrid.tsx create mode 100644 apps/perf-test-react-components/src/scenarios/TablePrimitivesOnly.tsx create mode 100644 apps/perf-test-react-components/src/scenarios/TableSelectionOnly.tsx create mode 100644 apps/perf-test/src/scenarios/DetailsListDataGrid.tsx delete mode 100644 packages/fluentui/docs/src/examples/components/Avatar/Performance/AvatarMinimal.perf.tsx create mode 100644 packages/fluentui/docs/src/examples/components/Avatar/Performance/DetailsList.perf.tsx create mode 100644 packages/fluentui/docs/src/examples/components/Avatar/Performance/TableAsDataGrid.perf.tsx create mode 100644 packages/react-examples/src/react/DetailsList/DetailsListDataGrid.Example.tsx diff --git a/apps/perf-test-react-components/src/scenarios/TableAsDataGrid.tsx b/apps/perf-test-react-components/src/scenarios/TableAsDataGrid.tsx new file mode 100644 index 0000000000000..4580e0aade52d --- /dev/null +++ b/apps/perf-test-react-components/src/scenarios/TableAsDataGrid.tsx @@ -0,0 +1,216 @@ +import * as React from 'react'; +import { useArrowNavigationGroup } from '@fluentui/react-tabster'; +import { + TableBody, + TableCell, + TableRow, + Table, + TableHeader, + TableHeaderCell, + TableSelectionCell, + TableCellLayout, + useTableFeatures, + ColumnDefinition, + useTableSelection, + useTableSort, + createColumn, + ColumnId, +} from '@fluentui/react-table'; +import { FluentProvider } from '@fluentui/react-provider'; +import { webLightTheme } from '@fluentui/react-theme'; + +type FileCell = { + label: string; +}; + +type LastUpdatedCell = { + label: string; + timestamp: number; +}; + +type LastUpdateCell = { + label: string; +}; + +type AuthorCell = { + label: string; +}; + +type Item = { + file: FileCell; + author: AuthorCell; + lastUpdated: LastUpdatedCell; + lastUpdate: LastUpdateCell; +}; + +const DataGrid = () => { + const items = React.useMemo(() => { + const baseItems: Item[] = [ + { + file: { label: 'Meeting notes' }, + author: { label: 'Max Mustermann' }, + lastUpdated: { label: '7h ago', timestamp: 3 }, + lastUpdate: { + label: 'You edited this', + }, + }, + { + file: { label: 'Thursday presentation' }, + author: { label: 'Erika Mustermann' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Training recording' }, + author: { label: 'John Doe' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Purchase order' }, + author: { label: 'Jane Doe' }, + lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 1 }, + lastUpdate: { + label: 'You shared this in a Teams chat', + }, + }, + ]; + + return new Array(15).fill(0).map((_, i) => baseItems[i % baseItems.length]); + }, []); + + const columns: ColumnDefinition[] = React.useMemo( + () => [ + createColumn({ + columnId: 'file', + compare: (a, b) => { + return a.file.label.localeCompare(b.file.label); + }, + }), + createColumn({ + columnId: 'author', + compare: (a, b) => { + return a.author.label.localeCompare(b.author.label); + }, + }), + createColumn({ + columnId: 'lastUpdated', + compare: (a, b) => { + return a.lastUpdated.timestamp - b.lastUpdated.timestamp; + }, + }), + createColumn({ + columnId: 'lastUpdate', + compare: (a, b) => { + return a.lastUpdate.label.localeCompare(b.lastUpdate.label); + }, + }), + ], + [], + ); + + const { + getRows, + selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, + sort: { getSortDirection, toggleColumnSort, sort }, + } = useTableFeatures( + { + columns, + items, + }, + [ + useTableSelection({ + selectionMode: 'multiselect', + defaultSelectedItems: new Set([0, 1]), + }), + useTableSort({ defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }), + ], + ); + + const rows = sort( + getRows(row => { + const selected = isRowSelected(row.rowId); + return { + ...row, + onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === ' ') { + e.preventDefault(); + toggleRow(e, row.rowId); + } + }, + selected, + appearance: selected ? ('brand' as const) : ('none' as const), + }; + }), + ); + + const headerSortProps = (columnId: ColumnId) => ({ + onClick: (e: React.MouseEvent) => { + toggleColumnSort(e, columnId); + }, + sortDirection: getSortDirection(columnId), + }); + + const keyboardNavAttr = useArrowNavigationGroup({ axis: 'grid' }); + + return ( + + + + + File + Author + Last updated + Last update + + + + {rows.map(({ item, selected, onClick, onKeyDown, appearance }) => ( + + + + {item.file.label} + + + {item.author.label} + + + {item.lastUpdated.label} + + + {item.lastUpdate.label} + + + ))} + +
+ ); +}; + +DataGrid.decorator = (props: { children: React.ReactNode }) => ( + {props.children} +); + +export default DataGrid; diff --git a/apps/perf-test-react-components/src/scenarios/TablePrimitivesOnly.tsx b/apps/perf-test-react-components/src/scenarios/TablePrimitivesOnly.tsx new file mode 100644 index 0000000000000..8fd9f214d27c2 --- /dev/null +++ b/apps/perf-test-react-components/src/scenarios/TablePrimitivesOnly.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import { + TableBody, + TableCell, + TableRow, + Table, + TableHeader, + TableHeaderCell, + TableCellLayout, +} from '@fluentui/react-table'; +import { FluentProvider } from '@fluentui/react-provider'; +import { webLightTheme } from '@fluentui/react-theme'; + +const columns = [ + { columnKey: 'file', label: 'File' }, + { columnKey: 'author', label: 'Author' }, + { columnKey: 'lastUpdated', label: 'Last updated' }, + { columnKey: 'lastUpdate', label: 'Last update' }, +]; + +const Default = () => { + const items = React.useMemo(() => { + const baseItems = [ + { + file: { label: 'Meeting notes' }, + author: { label: 'Max Mustermann' }, + lastUpdated: { label: '7h ago', timestamp: 3 }, + lastUpdate: { + label: 'You edited this', + }, + }, + { + file: { label: 'Thursday presentation' }, + author: { label: 'Erika Mustermann' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Training recording' }, + author: { label: 'John Doe' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Purchase order' }, + author: { label: 'Jane Doe' }, + lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 1 }, + lastUpdate: { + label: 'You shared this in a Teams chat', + }, + }, + ]; + + return new Array(15).fill(0).map((_, i) => baseItems[i % baseItems.length]); + }, []); + + return ( + + + + {columns.map(column => ( + {column.label} + ))} + + + + {items.map(item => ( + + + {item.file.label} + + + {item.author.label} + + {item.lastUpdated.label} + + {item.lastUpdate.label} + + + ))} + +
+ ); +}; + +Default.decorator = (props: { children: React.ReactNode }) => ( + {props.children} +); +export default Default; diff --git a/apps/perf-test-react-components/src/scenarios/TableSelectionOnly.tsx b/apps/perf-test-react-components/src/scenarios/TableSelectionOnly.tsx new file mode 100644 index 0000000000000..63a295cb7b93d --- /dev/null +++ b/apps/perf-test-react-components/src/scenarios/TableSelectionOnly.tsx @@ -0,0 +1,188 @@ +import * as React from 'react'; +import { + TableBody, + TableCell, + TableRow, + Table, + TableHeader, + TableHeaderCell, + TableSelectionCell, + TableCellLayout, + useTableFeatures, + ColumnDefinition, + useTableSelection, + createColumn, +} from '@fluentui/react-table'; +import { FluentProvider } from '@fluentui/react-provider'; +import { webLightTheme } from '@fluentui/react-theme'; + +type FileCell = { + label: string; +}; + +type LastUpdatedCell = { + label: string; + timestamp: number; +}; + +type LastUpdateCell = { + label: string; +}; + +type AuthorCell = { + label: string; +}; + +type Item = { + file: FileCell; + author: AuthorCell; + lastUpdated: LastUpdatedCell; + lastUpdate: LastUpdateCell; +}; + +const MultipleSelect = () => { + const items = React.useMemo(() => { + const baseItems: Item[] = [ + { + file: { label: 'Meeting notes' }, + author: { label: 'Max Mustermann' }, + lastUpdated: { label: '7h ago', timestamp: 3 }, + lastUpdate: { + label: 'You edited this', + }, + }, + { + file: { label: 'Thursday presentation' }, + author: { label: 'Erika Mustermann' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Training recording' }, + author: { label: 'John Doe' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Purchase order' }, + author: { label: 'Jane Doe' }, + lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 1 }, + lastUpdate: { + label: 'You shared this in a Teams chat', + }, + }, + ]; + + return new Array(15).fill(0).map((_, i) => baseItems[i % baseItems.length]); + }, []); + const columns: ColumnDefinition[] = React.useMemo( + () => [ + createColumn({ + columnId: 'file', + }), + createColumn({ + columnId: 'author', + }), + createColumn({ + columnId: 'lastUpdated', + }), + createColumn({ + columnId: 'lastUpdate', + }), + ], + [], + ); + + const { + getRows, + selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, + } = useTableFeatures( + { + columns, + items, + }, + [ + useTableSelection({ + selectionMode: 'multiselect', + defaultSelectedItems: new Set([0, 1]), + }), + ], + ); + + const rows = getRows(row => { + const selected = isRowSelected(row.rowId); + return { + ...row, + onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === ' ') { + e.preventDefault(); + toggleRow(e, row.rowId); + } + }, + selected, + appearance: selected ? ('brand' as const) : ('none' as const), + }; + }); + + const toggleAllKeydown = React.useCallback( + (e: React.KeyboardEvent) => { + if (e.key === ' ') { + toggleAllRows(e); + e.preventDefault(); + } + }, + [toggleAllRows], + ); + + return ( + + + + + File + Author + Last updated + Last update + + + + {rows.map(({ item, selected, onClick, onKeyDown, appearance }) => ( + + + + {item.file.label} + + + {item.author.label} + + {item.lastUpdated.label} + + {item.lastUpdate.label} + + + ))} + +
+ ); +}; + +MultipleSelect.decorator = (props: { children: React.ReactNode }) => ( + {props.children} +); +export default MultipleSelect; diff --git a/apps/perf-test/src/scenarios/DetailsListDataGrid.tsx b/apps/perf-test/src/scenarios/DetailsListDataGrid.tsx new file mode 100644 index 0000000000000..92249f5918491 --- /dev/null +++ b/apps/perf-test/src/scenarios/DetailsListDataGrid.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { DetailsList, DetailsListLayoutMode, Selection, IColumn } from '@fluentui/react/lib/DetailsList'; + +export interface IDetailsListBasicExampleItem { + file: string; + author: string; + lastUpdated: string; + lastUpdate: string; +} + +export interface IDetailsListBasicExampleState { + items: IDetailsListBasicExampleItem[]; +} + +export default class DetailsListSelectionExample extends React.Component<{}, IDetailsListBasicExampleState> { + private _selection: Selection; + private _allItems: IDetailsListBasicExampleItem[]; + private _columns: IColumn[]; + + constructor(props: {}) { + super(props); + + this._selection = new Selection({}); + const items = [ + { + file: 'Meeting notes', + author: 'Max Mustermann', + lastUpdated: '7h ago', + lastUpdate: 'You edited this', + }, + { + file: 'Thursday presentation', + author: 'Erika Mustermann', + lastUpdated: 'Yesterday at 1:45 PM', + lastUpdate: 'You recently opened this', + }, + { + file: 'Training recording', + author: 'John Doe', + lastUpdated: 'Yesterday at 1:45 PM', + lastUpdate: 'You recently opened this', + }, + { + file: 'Purchase order', + author: 'Jane Doe', + lastUpdated: 'Tue at 9:30 AM', + lastUpdate: 'You shared this in a Teams chat', + }, + ]; + + // Populate with items for demos. + this._allItems = new Array(15).fill(0).map((_, i) => items[i % items.length]); + + this._columns = [ + { key: 'file', name: 'File', fieldName: 'file', minWidth: 150, maxWidth: 200 }, + { key: 'author', name: 'Author', fieldName: 'author', minWidth: 150, maxWidth: 200 }, + { key: 'lastUpdated', name: 'Last updated', fieldName: 'lastUpdated', minWidth: 150, maxWidth: 200 }, + { key: 'lastUpdate', name: 'Last update', fieldName: 'lastUpdate', minWidth: 150, maxWidth: 200 }, + ]; + + this.state = { + items: this._allItems, + }; + } + + public render(): JSX.Element { + const { items } = this.state; + + return ( + + ); + } +} diff --git a/apps/stress-test/src/renderers/v8/detailsList.tsx b/apps/stress-test/src/renderers/v8/detailsList.tsx index 06b59505de132..3b7bb2febbfad 100644 --- a/apps/stress-test/src/renderers/v8/detailsList.tsx +++ b/apps/stress-test/src/renderers/v8/detailsList.tsx @@ -50,7 +50,7 @@ class DetailsListDataGridExample extends React.Component<{}, DetailsListBasicExa ]; // Populate with items for demos. - this._allItems = new Array(1).fill(0).map((_, i) => items[i % items.length]); + this._allItems = new Array(15).fill(0).map((_, i) => items[i % items.length]); this._columns = [ { key: 'file', name: 'File', fieldName: 'file', minWidth: 150, maxWidth: 200 }, diff --git a/apps/stress-test/src/shared/react/TestMount.tsx b/apps/stress-test/src/shared/react/TestMount.tsx index 94490901b69a0..2060d00cf0525 100644 --- a/apps/stress-test/src/shared/react/TestMount.tsx +++ b/apps/stress-test/src/shared/react/TestMount.tsx @@ -8,7 +8,11 @@ type DebouncedOnRender = () => React.ProfilerOnRenderCallback; const debouncedOnRender: DebouncedOnRender = () => { let start: number; let timeoutId: number; - return (_profilerId, _mode, _actualTime, _baseTime, startTime, commitTime) => { + return (_profilerId, mode, _actualTime, _baseTime, startTime, commitTime) => { + if (mode !== 'mount') { + return; + } + if (!start) { start = startTime; } diff --git a/packages/fluentui/docs/package.json b/packages/fluentui/docs/package.json index 8936e0af12612..1583785276dc4 100644 --- a/packages/fluentui/docs/package.json +++ b/packages/fluentui/docs/package.json @@ -5,6 +5,7 @@ "license": "MIT", "dependencies": { "@charlietango/use-script": "^2.1.1", + "@fluentui/react-components": "*", "@fluentui/ability-attributes": "^0.65.0", "@fluentui/accessibility": "^0.65.0", "@fluentui/code-sandbox": "^0.65.0", diff --git a/packages/fluentui/docs/src/examples/components/Avatar/Performance/AvatarMinimal.perf.tsx b/packages/fluentui/docs/src/examples/components/Avatar/Performance/AvatarMinimal.perf.tsx deleted file mode 100644 index eabfeb984eb98..0000000000000 --- a/packages/fluentui/docs/src/examples/components/Avatar/Performance/AvatarMinimal.perf.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Avatar } from '@fluentui/react-northstar'; -import * as React from 'react'; - -const AvatarMinimalPerf = () => ; - -AvatarMinimalPerf.iterations = 1000; -AvatarMinimalPerf.filename = 'AvatarMinimal.perf.tsx'; - -export default AvatarMinimalPerf; diff --git a/packages/fluentui/docs/src/examples/components/Avatar/Performance/DetailsList.perf.tsx b/packages/fluentui/docs/src/examples/components/Avatar/Performance/DetailsList.perf.tsx new file mode 100644 index 0000000000000..1a69a2c5552c2 --- /dev/null +++ b/packages/fluentui/docs/src/examples/components/Avatar/Performance/DetailsList.perf.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { DetailsList, DetailsListLayoutMode, Selection, IColumn } from '@fluentui/react/lib/DetailsList'; + +interface DetailsListBasicExampleItem { + file: string; + author: string; + lastUpdated: string; + lastUpdate: string; +} + +interface DetailsListBasicExampleState { + items: DetailsListBasicExampleItem[]; +} + +class DetailsListDataGridExample extends React.Component<{}, DetailsListBasicExampleState> { + _selection: Selection; + _allItems: DetailsListBasicExampleItem[]; + _columns: IColumn[]; + + constructor(props: {}) { + super(props); + + this._selection = new Selection({}); + const items = [ + { + file: 'Meeting notes', + author: 'Max Mustermann', + lastUpdated: '7h ago', + lastUpdate: 'You edited this', + }, + { + file: 'Thursday presentation', + author: 'Erika Mustermann', + lastUpdated: 'Yesterday at 1:45 PM', + lastUpdate: 'You recently opened this', + }, + { + file: 'Training recording', + author: 'John Doe', + lastUpdated: 'Yesterday at 1:45 PM', + lastUpdate: 'You recently opened this', + }, + { + file: 'Purchase order', + author: 'Jane Doe', + lastUpdated: 'Tue at 9:30 AM', + lastUpdate: 'You shared this in a Teams chat', + }, + ]; + + // Populate with items for demos. + this._allItems = new Array(15).fill(0).map((_, i) => items[i % items.length]); + + this._columns = [ + { key: 'file', name: 'File', fieldName: 'file', minWidth: 150, maxWidth: 200 }, + { key: 'author', name: 'Author', fieldName: 'author', minWidth: 150, maxWidth: 200 }, + { key: 'lastUpdated', name: 'Last updated', fieldName: 'lastUpdated', minWidth: 150, maxWidth: 200 }, + { key: 'lastUpdate', name: 'Last update', fieldName: 'lastUpdate', minWidth: 150, maxWidth: 200 }, + ]; + + this.state = { + items: this._allItems, + }; + } + + render(): JSX.Element { + const { items } = this.state; + + return ( + + ); + } +} + +export default DetailsListDataGridExample; diff --git a/packages/fluentui/docs/src/examples/components/Avatar/Performance/TableAsDataGrid.perf.tsx b/packages/fluentui/docs/src/examples/components/Avatar/Performance/TableAsDataGrid.perf.tsx new file mode 100644 index 0000000000000..6c3585be02864 --- /dev/null +++ b/packages/fluentui/docs/src/examples/components/Avatar/Performance/TableAsDataGrid.perf.tsx @@ -0,0 +1,212 @@ +import * as React from 'react'; +import { useArrowNavigationGroup } from '@fluentui/react-components'; +import { + TableBody, + TableCell, + TableRow, + Table, + TableHeader, + TableHeaderCell, + TableSelectionCell, + TableCellLayout, + useTableFeatures, + ColumnDefinition, + useTableSelection, + useTableSort, + createColumn, + ColumnId, +} from '@fluentui/react-components/unstable'; + +type FileCell = { + label: string; +}; + +type LastUpdatedCell = { + label: string; + timestamp: number; +}; + +type LastUpdateCell = { + label: string; +}; + +type AuthorCell = { + label: string; +}; + +type Item = { + file: FileCell; + author: AuthorCell; + lastUpdated: LastUpdatedCell; + lastUpdate: LastUpdateCell; +}; + +const DataGrid = () => { + const items = React.useMemo(() => { + const baseItems: Item[] = [ + { + file: { label: 'Meeting notes' }, + author: { label: 'Max Mustermann' }, + lastUpdated: { label: '7h ago', timestamp: 3 }, + lastUpdate: { + label: 'You edited this', + }, + }, + { + file: { label: 'Thursday presentation' }, + author: { label: 'Erika Mustermann' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Training recording' }, + author: { label: 'John Doe' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Purchase order' }, + author: { label: 'Jane Doe' }, + lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 1 }, + lastUpdate: { + label: 'You shared this in a Teams chat', + }, + }, + ]; + + return new Array(15).fill(0).map((_, i) => baseItems[i % baseItems.length]); + }, []); + + const columns: ColumnDefinition[] = React.useMemo( + () => [ + createColumn({ + columnId: 'file', + compare: (a, b) => { + return a.file.label.localeCompare(b.file.label); + }, + }), + createColumn({ + columnId: 'author', + compare: (a, b) => { + return a.author.label.localeCompare(b.author.label); + }, + }), + createColumn({ + columnId: 'lastUpdated', + compare: (a, b) => { + return a.lastUpdated.timestamp - b.lastUpdated.timestamp; + }, + }), + createColumn({ + columnId: 'lastUpdate', + compare: (a, b) => { + return a.lastUpdate.label.localeCompare(b.lastUpdate.label); + }, + }), + ], + [], + ); + + const { + getRows, + selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, + sort: { getSortDirection, toggleColumnSort, sort }, + } = useTableFeatures( + { + columns, + items, + }, + [ + useTableSelection({ + selectionMode: 'multiselect', + defaultSelectedItems: new Set([0, 1]), + }), + useTableSort({ defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }), + ], + ); + + const rows = sort( + getRows(row => { + const selected = isRowSelected(row.rowId); + return { + ...row, + onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === ' ') { + e.preventDefault(); + toggleRow(e, row.rowId); + } + }, + selected, + appearance: selected ? ('brand' as const) : ('none' as const), + }; + }), + ); + + const headerSortProps = (columnId: ColumnId) => ({ + onClick: (e: React.MouseEvent) => { + toggleColumnSort(e, columnId); + }, + sortDirection: getSortDirection(columnId), + }); + + const keyboardNavAttr = useArrowNavigationGroup({ axis: 'grid' }); + + return ( + + + + + File + Author + Last updated + Last update + + + + {rows.map(({ item, selected, onClick, onKeyDown, appearance }) => ( + + + + {item.file.label} + + + {item.author.label} + + + {item.lastUpdated.label} + + + {item.lastUpdate.label} + + + ))} + +
+ ); +}; + +DataGrid.iterations = 1000; + +export default DataGrid; diff --git a/packages/fluentui/perf/src/index.tsx b/packages/fluentui/perf/src/index.tsx index 11903058dd424..774ca6ace47e9 100644 --- a/packages/fluentui/perf/src/index.tsx +++ b/packages/fluentui/perf/src/index.tsx @@ -5,11 +5,17 @@ import * as _ from 'lodash'; import * as minimatch from 'minimatch'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { FluentProvider, webLightTheme } from '@fluentui/react-components'; +import { ThemeProvider } from '@fluentui/react'; import { ProfilerMeasure, ProfilerMeasureCycle } from '../types'; const mountNode = document.querySelector('#root'); -const performanceExamplesContext = require.context('@fluentui/docs/src/examples/', true, /.perf.tsx$/); +const performanceExamplesContext = require.context( + '@fluentui/docs/src/examples/components/Avatar/Performance', + true, + /.perf.tsx$/, +); // Heads up! // We want to randomize examples to avoid any notable issues with always first example @@ -33,32 +39,37 @@ const renderCycle = async ( await asyncRender( - { - const renderComponentTelemetry = _.reduce( - _.values(telemetryRef.current.performance), - (acc, next) => { - return { - componentCount: acc.componentCount + next.instances, - renderComponentTime: acc.renderComponentTime + next.msTotal, + + + { + const renderComponentTelemetry = _.reduce( + _.values(telemetryRef.current.performance), + (acc, next) => { + return { + componentCount: acc.componentCount + next.instances, + renderComponentTime: acc.renderComponentTime + next.msTotal, + }; + }, + { componentCount: 0, renderComponentTime: 0 }, + ); + + profilerMeasure = { + actualTime, + exampleIndex, + phase, + commitTime, + startTime, + ...renderComponentTelemetry, }; - }, - { componentCount: 0, renderComponentTime: 0 }, - ); - - profilerMeasure = { - actualTime, - exampleIndex, - phase, - commitTime, - startTime, - ...renderComponentTelemetry, - }; - }} - > - - + }} + > + + + + + ; , mountNode, ); diff --git a/packages/react-components/react-table/stories/Table/DataGrid.stories.tsx b/packages/react-components/react-table/stories/Table/DataGrid.stories.tsx index 345e3ca3962dd..525317bf6c62e 100644 --- a/packages/react-components/react-table/stories/Table/DataGrid.stories.tsx +++ b/packages/react-components/react-table/stories/Table/DataGrid.stories.tsx @@ -1,14 +1,5 @@ import * as React from 'react'; -import { - FolderRegular, - EditRegular, - OpenRegular, - DocumentRegular, - PeopleRegular, - DocumentPdfRegular, - VideoRegular, -} from '@fluentui/react-icons'; -import { PresenceBadgeStatus, Avatar, useArrowNavigationGroup } from '@fluentui/react-components'; +import { useArrowNavigationGroup } from '@fluentui/react-components'; import { TableBody, TableCell, @@ -28,7 +19,6 @@ import { type FileCell = { label: string; - icon: JSX.Element; }; type LastUpdatedCell = { @@ -38,12 +28,10 @@ type LastUpdatedCell = { type LastUpdateCell = { label: string; - icon: JSX.Element; }; type AuthorCell = { label: string; - status: PresenceBadgeStatus; }; type Item = { @@ -53,46 +41,46 @@ type Item = { lastUpdate: LastUpdateCell; }; -const items: Item[] = [ - { - file: { label: 'Meeting notes', icon: }, - author: { label: 'Max Mustermann', status: 'available' }, - lastUpdated: { label: '7h ago', timestamp: 3 }, - lastUpdate: { - label: 'You edited this', - icon: , - }, - }, - { - file: { label: 'Thursday presentation', icon: }, - author: { label: 'Erika Mustermann', status: 'busy' }, - lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, - lastUpdate: { - label: 'You recently opened this', - icon: , - }, - }, - { - file: { label: 'Training recording', icon: }, - author: { label: 'John Doe', status: 'away' }, - lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, - lastUpdate: { - label: 'You recently opened this', - icon: , - }, - }, - { - file: { label: 'Purchase order', icon: }, - author: { label: 'Jane Doe', status: 'offline' }, - lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 1 }, - lastUpdate: { - label: 'You shared this in a Teams chat', - icon: , - }, - }, -]; - export const DataGrid = () => { + const items = React.useMemo(() => { + const baseItems: Item[] = [ + { + file: { label: 'Meeting notes' }, + author: { label: 'Max Mustermann' }, + lastUpdated: { label: '7h ago', timestamp: 3 }, + lastUpdate: { + label: 'You edited this', + }, + }, + { + file: { label: 'Thursday presentation' }, + author: { label: 'Erika Mustermann' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Training recording' }, + author: { label: 'John Doe' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Purchase order' }, + author: { label: 'Jane Doe' }, + lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 1 }, + lastUpdate: { + label: 'You shared this in a Teams chat', + }, + }, + ]; + + return new Array(15).fill(0).map((_, i) => baseItems[i % baseItems.length]); + }, []); + const columns: ColumnDefinition[] = React.useMemo( () => [ createColumn({ @@ -201,26 +189,16 @@ export const DataGrid = () => { checkboxIndicator={{ 'aria-label': 'Select row' }} /> - {item.file.label} + {item.file.label} - - } - > - {item.author.label} - + {item.author.label} {item.lastUpdated.label} - {item.lastUpdate.label} + {item.lastUpdate.label} ))} diff --git a/packages/react-components/react-table/stories/Table/Default.stories.tsx b/packages/react-components/react-table/stories/Table/Default.stories.tsx index 7899b90031f41..8ed5eb1f789ae 100644 --- a/packages/react-components/react-table/stories/Table/Default.stories.tsx +++ b/packages/react-components/react-table/stories/Table/Default.stories.tsx @@ -1,14 +1,4 @@ import * as React from 'react'; -import { - FolderRegular, - EditRegular, - OpenRegular, - DocumentRegular, - PeopleRegular, - DocumentPdfRegular, - VideoRegular, -} from '@fluentui/react-icons'; -import { PresenceBadgeStatus, Avatar } from '@fluentui/react-components'; import { TableBody, TableCell, @@ -17,46 +7,7 @@ import { TableHeader, TableHeaderCell, TableCellLayout, -} from '@fluentui/react-components/unstable'; - -const items = [ - { - file: { label: 'Meeting notes', icon: }, - author: { label: 'Max Mustermann', status: 'available' }, - lastUpdated: { label: '7h ago', timestamp: 1 }, - lastUpdate: { - label: 'You edited this', - icon: , - }, - }, - { - file: { label: 'Thursday presentation', icon: }, - author: { label: 'Erika Mustermann', status: 'busy' }, - lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, - lastUpdate: { - label: 'You recently opened this', - icon: , - }, - }, - { - file: { label: 'Training recording', icon: }, - author: { label: 'John Doe', status: 'away' }, - lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, - lastUpdate: { - label: 'You recently opened this', - icon: , - }, - }, - { - file: { label: 'Purchase order', icon: }, - author: { label: 'Jane Doe', status: 'offline' }, - lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 3 }, - lastUpdate: { - label: 'You shared this in a Teams chat', - icon: , - }, - }, -]; +} from '@fluentui/react-table'; const columns = [ { columnKey: 'file', label: 'File' }, @@ -66,6 +17,45 @@ const columns = [ ]; export const Default = () => { + const items = React.useMemo(() => { + const baseItems: Item[] = [ + { + file: { label: 'Meeting notes' }, + author: { label: 'Max Mustermann' }, + lastUpdated: { label: '7h ago', timestamp: 3 }, + lastUpdate: { + label: 'You edited this', + }, + }, + { + file: { label: 'Thursday presentation' }, + author: { label: 'Erika Mustermann' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Training recording' }, + author: { label: 'John Doe' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Purchase order' }, + author: { label: 'Jane Doe' }, + lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 1 }, + lastUpdate: { + label: 'You shared this in a Teams chat', + }, + }, + ]; + + return new Array(15).fill(0).map((_, i) => baseItems[i % baseItems.length]); + }, []); + return ( @@ -79,24 +69,14 @@ export const Default = () => { {items.map(item => ( - {item.file.label} + {item.file.label} - - } - > - {item.author.label} - + {item.author.label} {item.lastUpdated.label} - {item.lastUpdate.label} + {item.lastUpdate.label} ))} diff --git a/packages/react-components/react-table/stories/Table/MultipleSelect.stories.tsx b/packages/react-components/react-table/stories/Table/MultipleSelect.stories.tsx index 288dab105fbcd..a13efe7b14a5a 100644 --- a/packages/react-components/react-table/stories/Table/MultipleSelect.stories.tsx +++ b/packages/react-components/react-table/stories/Table/MultipleSelect.stories.tsx @@ -1,14 +1,4 @@ import * as React from 'react'; -import { - FolderRegular, - EditRegular, - OpenRegular, - DocumentRegular, - PeopleRegular, - DocumentPdfRegular, - VideoRegular, -} from '@fluentui/react-icons'; -import { PresenceBadgeStatus, Avatar } from '@fluentui/react-components'; import { TableBody, TableCell, @@ -26,7 +16,6 @@ import { type FileCell = { label: string; - icon: JSX.Element; }; type LastUpdatedCell = { @@ -36,12 +25,10 @@ type LastUpdatedCell = { type LastUpdateCell = { label: string; - icon: JSX.Element; }; type AuthorCell = { label: string; - status: PresenceBadgeStatus; }; type Item = { @@ -51,46 +38,45 @@ type Item = { lastUpdate: LastUpdateCell; }; -const items: Item[] = [ - { - file: { label: 'Meeting notes', icon: }, - author: { label: 'Max Mustermann', status: 'available' }, - lastUpdated: { label: '7h ago', timestamp: 3 }, - lastUpdate: { - label: 'You edited this', - icon: , - }, - }, - { - file: { label: 'Thursday presentation', icon: }, - author: { label: 'Erika Mustermann', status: 'busy' }, - lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, - lastUpdate: { - label: 'You recently opened this', - icon: , - }, - }, - { - file: { label: 'Training recording', icon: }, - author: { label: 'John Doe', status: 'away' }, - lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, - lastUpdate: { - label: 'You recently opened this', - icon: , - }, - }, - { - file: { label: 'Purchase order', icon: }, - author: { label: 'Jane Doe', status: 'offline' }, - lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 1 }, - lastUpdate: { - label: 'You shared this in a Teams chat', - icon: , - }, - }, -]; - export const MultipleSelect = () => { + const items = React.useMemo(() => { + const baseItems: Item[] = [ + { + file: { label: 'Meeting notes' }, + author: { label: 'Max Mustermann' }, + lastUpdated: { label: '7h ago', timestamp: 3 }, + lastUpdate: { + label: 'You edited this', + }, + }, + { + file: { label: 'Thursday presentation' }, + author: { label: 'Erika Mustermann' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Training recording' }, + author: { label: 'John Doe' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + }, + }, + { + file: { label: 'Purchase order' }, + author: { label: 'Jane Doe' }, + lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 1 }, + lastUpdate: { + label: 'You shared this in a Teams chat', + }, + }, + ]; + + return new Array(15).fill(0).map((_, i) => baseItems[i % baseItems.length]); + }, []); const columns: ColumnDefinition[] = React.useMemo( () => [ createColumn({ @@ -178,16 +164,14 @@ export const MultipleSelect = () => { > - {item.file.label} + {item.file.label} - }> - {item.author.label} - + {item.author.label} {item.lastUpdated.label} - {item.lastUpdate.label} + {item.lastUpdate.label} ))} diff --git a/packages/react-components/react-toolbar/src/index.ts b/packages/react-components/react-toolbar/src/index.ts index 1f32cbc1bce6a..0f6b9c50547a9 100644 --- a/packages/react-components/react-toolbar/src/index.ts +++ b/packages/react-components/react-toolbar/src/index.ts @@ -6,13 +6,8 @@ export { useToolbar_unstable, } from './Toolbar'; export type { ToolbarContextValue, ToolbarContextValues, ToolbarProps, ToolbarSlots, ToolbarState } from './Toolbar'; -export { ToolbarButton } from './ToolbarButton'; -export type { - ToolbarButtonProps, - ToolbarButtonState, - useToolbarButtonStyles_unstable, - useToolbarButton_unstable, -} from './ToolbarButton'; +export { ToolbarButton, useToolbarButtonStyles_unstable, useToolbarButton_unstable } from './ToolbarButton'; +export type { ToolbarButtonProps, ToolbarButtonState } from './ToolbarButton'; export { ToolbarDivider, useToolbarDividerStyles_unstable, useToolbarDivider_unstable } from './ToolbarDivider'; export type { ToolbarDividerProps, ToolbarDividerState } from './ToolbarDivider'; export { diff --git a/packages/react-examples/src/react/DetailsList/DetailsListDataGrid.Example.tsx b/packages/react-examples/src/react/DetailsList/DetailsListDataGrid.Example.tsx new file mode 100644 index 0000000000000..480ed926d3175 --- /dev/null +++ b/packages/react-examples/src/react/DetailsList/DetailsListDataGrid.Example.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { DetailsList, DetailsListLayoutMode, Selection, IColumn } from '@fluentui/react/lib/DetailsList'; + +export interface IDetailsListBasicExampleItem { + file: string; + author: string; + lastUpdated: string; + lastUpdate: string; +} + +export interface IDetailsListBasicExampleState { + items: IDetailsListBasicExampleItem[]; +} + +export class DetailsListSelectionExample extends React.Component<{}, IDetailsListBasicExampleState> { + private _selection: Selection; + private _allItems: IDetailsListBasicExampleItem[]; + private _columns: IColumn[]; + + constructor(props: {}) { + super(props); + + this._selection = new Selection({}); + const items = [ + { + file: 'Meeting notes', + author: 'Max Mustermann', + lastUpdated: '7h ago', + lastUpdate: 'You edited this', + }, + { + file: 'Thursday presentation', + author: 'Erika Mustermann', + lastUpdated: 'Yesterday at 1:45 PM', + lastUpdate: 'You recently opened this', + }, + { + file: 'Training recording', + author: 'John Doe', + lastUpdated: 'Yesterday at 1:45 PM', + lastUpdate: 'You recently opened this', + }, + { + file: 'Purchase order', + author: 'Jane Doe', + lastUpdated: 'Tue at 9:30 AM', + lastUpdate: 'You shared this in a Teams chat', + }, + ]; + + // Populate with items for demos. + this._allItems = items; + + this._columns = [ + { key: 'file', name: 'File', fieldName: 'file', minWidth: 150, maxWidth: 200 }, + { key: 'author', name: 'Author', fieldName: 'author', minWidth: 150, maxWidth: 200 }, + { key: 'lastUpdated', name: 'Last updated', fieldName: 'lastUpdated', minWidth: 150, maxWidth: 200 }, + { key: 'lastUpdate', name: 'Last update', fieldName: 'lastUpdate', minWidth: 150, maxWidth: 200 }, + ]; + + this.state = { + items: this._allItems, + }; + } + + public render(): JSX.Element { + const { items } = this.state; + + return ( + + ); + } +} diff --git a/scripts/gulp/src/webpack/webpack.config.perf.ts b/scripts/gulp/src/webpack/webpack.config.perf.ts index db9d71bd6f0b9..0d3b960b7cb09 100644 --- a/scripts/gulp/src/webpack/webpack.config.perf.ts +++ b/scripts/gulp/src/webpack/webpack.config.perf.ts @@ -9,6 +9,15 @@ import config from '../config'; const { paths } = config; +const aliases = config.lernaAliases({ type: 'webpack' }); +const finalAliases: Record = {}; +for (const alias of Object.entries(aliases)) { + const [packageName, path] = alias; + if (!path.includes('react-components') && !path.includes('packages/react/')) { + finalAliases[packageName] = path; + } +} + const webpackConfig: webpack.Configuration = { name: 'client', target: 'web', @@ -62,7 +71,7 @@ const webpackConfig: webpack.Configuration = { }, extensions: ['.ts', '.tsx', '.js', '.json'], alias: { - ...config.lernaAliases({ type: 'webpack' }), + ...finalAliases, src: paths.packageSrc('react-northstar'), // We are using React in production mode with tracing.