diff --git a/change/@fluentui-react-table-350ae095-7533-4307-a31f-6d9cbb178cb8.json b/change/@fluentui-react-table-350ae095-7533-4307-a31f-6d9cbb178cb8.json new file mode 100644 index 0000000000000..9e7b32197151b --- /dev/null +++ b/change/@fluentui-react-table-350ae095-7533-4307-a31f-6d9cbb178cb8.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat: Implement child render function for DataGrid rows", + "packageName": "@fluentui/react-table", + "email": "lingfangao@hotmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-table/etc/react-table.api.md b/packages/react-components/react-table/etc/react-table.api.md index b6132dd2db27e..166b2ed20920e 100644 --- a/packages/react-components/react-table/etc/react-table.api.md +++ b/packages/react-components/react-table/etc/react-table.api.md @@ -39,7 +39,9 @@ export const DataGridBody: ForwardRefComponent; export const dataGridBodyClassNames: SlotClassNames; // @public -export type DataGridBodyProps = TableBodyProps; +export type DataGridBodyProps = Omit & { + children: RowRenderFunction; +}; // @public (undocumented) export type DataGridBodySlots = TableBodySlots; @@ -66,7 +68,12 @@ export type DataGridCellState = TableCellState; export const dataGridClassNames: SlotClassNames; // @public (undocumented) -export type DataGridContextValues = TableContextValues; +export type DataGridContextValue = HeadlessTableState; + +// @public (undocumented) +export type DataGridContextValues = TableContextValues & { + dataGrid: DataGridContextValue; +}; // @public export const DataGridHeader: ForwardRefComponent; @@ -99,7 +106,7 @@ export type DataGridHeaderSlots = TableHeaderSlots; export type DataGridHeaderState = TableHeaderState; // @public -export type DataGridProps = TableProps; +export type DataGridProps = TableProps & Pick; // @public export const DataGridRow: ForwardRefComponent; @@ -135,7 +142,9 @@ export type DataGridSelectionCellState = TableSelectionCellState; export type DataGridSlots = TableSlots; // @public -export type DataGridState = TableState; +export type DataGridState = TableState & { + tableState: HeadlessTableState; +}; // @public (undocumented) export interface HeadlessTableState extends Pick, 'items' | 'getRowId'> { @@ -196,6 +205,9 @@ export const renderTableSelectionCell_unstable: (state: TableSelectionCellState) // @public (undocumented) export type RowId = string | number; +// @public (undocumented) +export type RowRenderFunction = (row: RowState) => React_2.ReactNode; + // @public (undocumented) export interface RowState { item: TItem; diff --git a/packages/react-components/react-table/package.json b/packages/react-components/react-table/package.json index f8d1db03df024..ee00fa03430f9 100644 --- a/packages/react-components/react-table/package.json +++ b/packages/react-components/react-table/package.json @@ -33,6 +33,7 @@ "@fluentui/react-aria": "^9.3.0", "@fluentui/react-avatar": "^9.2.5", "@fluentui/react-checkbox": "^9.0.11", + "@fluentui/react-context-selector": "^9.1.0", "@fluentui/react-icons": "^2.0.175", "@fluentui/react-radio": "^9.0.10", "@fluentui/react-tabster": "^9.2.1", diff --git a/packages/react-components/react-table/src/components/DataGrid/DataGrid.test.tsx b/packages/react-components/react-table/src/components/DataGrid/DataGrid.test.tsx index bfa11e0876028..ed8516faa8b27 100644 --- a/packages/react-components/react-table/src/components/DataGrid/DataGrid.test.tsx +++ b/packages/react-components/react-table/src/components/DataGrid/DataGrid.test.tsx @@ -3,6 +3,10 @@ import { render } from '@testing-library/react'; import { DataGrid } from './DataGrid'; import { isConformant } from '../../testing/isConformant'; import { DataGridProps } from './DataGrid.types'; +import { ColumnDefinition, RowState } from '../../hooks'; +import { DataGridBody } from '../DataGridBody/DataGridBody'; +import { DataGridRow } from '../DataGridRow/DataGridRow'; +import { DataGridCell } from '../DataGridCell/DataGridCell'; describe('DataGrid', () => { isConformant({ @@ -10,10 +14,33 @@ describe('DataGrid', () => { displayName: 'DataGrid', }); - // TODO add more tests here, and create visual regression tests in /apps/vr-tests + interface Item { + first: string; + second: string; + third: string; + } + + const testColumns: ColumnDefinition[] = [{ columnId: 'first' }, { columnId: 'second' }, { columnId: 'third' }]; + const testItems: Item[] = [ + { first: 'first', second: 'second', third: 'third' }, + { first: 'first', second: 'second', third: 'third' }, + { first: 'first', second: 'second', third: 'third' }, + ]; it('renders a default state', () => { - const result = render(Default DataGrid); + const result = render( + + + {({ item, rowId }: RowState) => ( + + {item.first} + {item.second} + {item.third} + + )} + + , + ); expect(result.container).toMatchSnapshot(); }); }); diff --git a/packages/react-components/react-table/src/components/DataGrid/DataGrid.types.ts b/packages/react-components/react-table/src/components/DataGrid/DataGrid.types.ts index 0932ad03209b8..b9049215b02a1 100644 --- a/packages/react-components/react-table/src/components/DataGrid/DataGrid.types.ts +++ b/packages/react-components/react-table/src/components/DataGrid/DataGrid.types.ts @@ -1,15 +1,23 @@ import { TableContextValues, TableProps, TableSlots, TableState } from '../Table/Table.types'; +import { TableState as HeadlessTableState } from '../../hooks'; export type DataGridSlots = TableSlots; -export type DataGridContextValues = TableContextValues; +export type DataGridContextValues = TableContextValues & { + dataGrid: DataGridContextValue; +}; + +// Use any here since we can't know the user types +// The user is responsible for narrowing the type downstream +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type DataGridContextValue = HeadlessTableState; /** * DataGrid Props */ -export type DataGridProps = TableProps; +export type DataGridProps = TableProps & Pick; /** * State used in rendering DataGrid */ -export type DataGridState = TableState; +export type DataGridState = TableState & { tableState: HeadlessTableState }; diff --git a/packages/react-components/react-table/src/components/DataGrid/__snapshots__/DataGrid.test.tsx.snap b/packages/react-components/react-table/src/components/DataGrid/__snapshots__/DataGrid.test.tsx.snap index e176a36318f37..280a37cda5650 100644 --- a/packages/react-components/react-table/src/components/DataGrid/__snapshots__/DataGrid.test.tsx.snap +++ b/packages/react-components/react-table/src/components/DataGrid/__snapshots__/DataGrid.test.tsx.snap @@ -6,7 +6,80 @@ exports[`DataGrid renders a default state 1`] = ` class="fui-DataGrid fui-Table" role="table" > - Default DataGrid +
+
+
+ first +
+
+ second +
+
+ third +
+
+
+
+ first +
+
+ second +
+
+ third +
+
+
+
+ first +
+
+ second +
+
+ third +
+
+
`; diff --git a/packages/react-components/react-table/src/components/DataGrid/renderDataGrid.tsx b/packages/react-components/react-table/src/components/DataGrid/renderDataGrid.tsx index 50e0065e5e0a8..1eb7eb8daf40e 100644 --- a/packages/react-components/react-table/src/components/DataGrid/renderDataGrid.tsx +++ b/packages/react-components/react-table/src/components/DataGrid/renderDataGrid.tsx @@ -1,9 +1,15 @@ +import * as React from 'react'; import type { DataGridContextValues, DataGridState } from './DataGrid.types'; import { renderTable_unstable } from '../Table/renderTable'; +import { DataGridContextProvider } from '../../contexts/dataGridContext'; /** * Render the final JSX of DataGrid */ export const renderDataGrid_unstable = (state: DataGridState, contextValues: DataGridContextValues) => { - return renderTable_unstable(state, contextValues); + return ( + + {renderTable_unstable(state, contextValues)} + + ); }; diff --git a/packages/react-components/react-table/src/components/DataGrid/useDataGrid.ts b/packages/react-components/react-table/src/components/DataGrid/useDataGrid.ts index a2060dc4d9125..eb2766d7ffdb8 100644 --- a/packages/react-components/react-table/src/components/DataGrid/useDataGrid.ts +++ b/packages/react-components/react-table/src/components/DataGrid/useDataGrid.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import type { DataGridProps, DataGridState } from './DataGrid.types'; import { useTable_unstable } from '../Table/useTable'; +import { useTable } from '../../hooks/useTable'; /** * Create the state required to render DataGrid. @@ -12,5 +13,12 @@ import { useTable_unstable } from '../Table/useTable'; * @param ref - reference to root HTMLElement of DataGrid */ export const useDataGrid_unstable = (props: DataGridProps, ref: React.Ref): DataGridState => { - return useTable_unstable({ ...props, as: 'div' }, ref); + const { items, columns } = props; + const tableState = useTable({ items, columns }, []); + const baseTableState = useTable_unstable({ ...props, as: 'div' }, ref); + + return { + ...baseTableState, + tableState, + }; }; diff --git a/packages/react-components/react-table/src/components/DataGrid/useDataGridContextValues.ts b/packages/react-components/react-table/src/components/DataGrid/useDataGridContextValues.ts index 7e42a057b7ef8..f451ffcd8eb7f 100644 --- a/packages/react-components/react-table/src/components/DataGrid/useDataGridContextValues.ts +++ b/packages/react-components/react-table/src/components/DataGrid/useDataGridContextValues.ts @@ -1,6 +1,12 @@ import { useTableContextValues_unstable } from '../Table/useTableContextValues'; -import { DataGridState } from './DataGrid.types'; +import { DataGridContextValues, DataGridState } from './DataGrid.types'; -export function useDataGridContextValues_unstable(state: DataGridState) { - return useTableContextValues_unstable(state); +export function useDataGridContextValues_unstable(state: DataGridState): DataGridContextValues { + const tableContextValues = useTableContextValues_unstable(state); + return { + ...tableContextValues, + dataGrid: { + ...state.tableState, + }, + }; } diff --git a/packages/react-components/react-table/src/components/DataGridBody/DataGridBody.test.tsx b/packages/react-components/react-table/src/components/DataGridBody/DataGridBody.test.tsx index f27400a2b9b7d..30cd64476019a 100644 --- a/packages/react-components/react-table/src/components/DataGridBody/DataGridBody.test.tsx +++ b/packages/react-components/react-table/src/components/DataGridBody/DataGridBody.test.tsx @@ -10,10 +10,8 @@ describe('DataGridBody', () => { displayName: 'DataGridBody', }); - // TODO add more tests here, and create visual regression tests in /apps/vr-tests - - it('renders a default state', () => { - const result = render(Default DataGridBody); + it('renders items from render function', () => { + const result = render({() => 'foo'}); expect(result.container).toMatchSnapshot(); }); }); diff --git a/packages/react-components/react-table/src/components/DataGridBody/DataGridBody.types.ts b/packages/react-components/react-table/src/components/DataGridBody/DataGridBody.types.ts index 251410f269efc..d5dc7fd6fea42 100644 --- a/packages/react-components/react-table/src/components/DataGridBody/DataGridBody.types.ts +++ b/packages/react-components/react-table/src/components/DataGridBody/DataGridBody.types.ts @@ -1,11 +1,23 @@ -import { TableBodySlots, TableBodyProps, TableBodyState } from '../TableBody/TableBody.types'; +import * as React from 'react'; +import type { RowState } from '../../hooks'; +import type { TableBodySlots, TableBodyProps, TableBodyState } from '../TableBody/TableBody.types'; export type DataGridBodySlots = TableBodySlots; +// Use any here since we can't know the user types +// The user is responsible for narrowing the type downstream +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type RowRenderFunction = (row: RowState) => React.ReactNode; + /** * DataGridBody Props */ -export type DataGridBodyProps = TableBodyProps; +export type DataGridBodyProps = Omit & { + /** + * Render function for rows + */ + children: RowRenderFunction; +}; /** * State used in rendering DataGridBody diff --git a/packages/react-components/react-table/src/components/DataGridBody/__snapshots__/DataGridBody.test.tsx.snap b/packages/react-components/react-table/src/components/DataGridBody/__snapshots__/DataGridBody.test.tsx.snap index 0177f6974d5c8..f879814763581 100644 --- a/packages/react-components/react-table/src/components/DataGridBody/__snapshots__/DataGridBody.test.tsx.snap +++ b/packages/react-components/react-table/src/components/DataGridBody/__snapshots__/DataGridBody.test.tsx.snap @@ -1,12 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DataGridBody renders a default state 1`] = ` +exports[`DataGridBody renders items from render function 1`] = `
- Default DataGridBody -
+ />
`; diff --git a/packages/react-components/react-table/src/components/DataGridBody/useDataGridBody.ts b/packages/react-components/react-table/src/components/DataGridBody/useDataGridBody.ts index 32fb876ae80d0..881904a0428fa 100644 --- a/packages/react-components/react-table/src/components/DataGridBody/useDataGridBody.ts +++ b/packages/react-components/react-table/src/components/DataGridBody/useDataGridBody.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import type { DataGridBodyProps, DataGridBodyState } from './DataGridBody.types'; import { useTableBody_unstable } from '../TableBody/useTableBody'; +import { useDataGridContext_unstable } from '../../contexts/dataGridContext'; /** * Create the state required to render DataGridBody. @@ -12,5 +13,10 @@ import { useTableBody_unstable } from '../TableBody/useTableBody'; * @param ref - reference to root HTMLElement of DataGridBody */ export const useDataGridBody_unstable = (props: DataGridBodyProps, ref: React.Ref): DataGridBodyState => { - return useTableBody_unstable({ ...props, as: 'div' }, ref); + const getRows = useDataGridContext_unstable(ctx => ctx.getRows); + const rows = getRows(); + + const { children: renderRow } = props; + const children = rows.map(row => renderRow(row)); + return useTableBody_unstable({ ...props, children, as: 'div' }, ref); }; diff --git a/packages/react-components/react-table/src/contexts/dataGridContext.ts b/packages/react-components/react-table/src/contexts/dataGridContext.ts new file mode 100644 index 0000000000000..41212883ad189 --- /dev/null +++ b/packages/react-components/react-table/src/contexts/dataGridContext.ts @@ -0,0 +1,15 @@ +import { createContext, useContextSelector } from '@fluentui/react-context-selector'; +import type { ContextSelector } from '@fluentui/react-context-selector'; +import { DataGridContextValue } from '../components/DataGrid/DataGrid.types'; +import { defaultTableState } from '../hooks'; + +const dataGridContext = createContext(undefined); + +const dataGridContextDefaultValue: DataGridContextValue = { + ...defaultTableState, +}; + +export const DataGridContextProvider = dataGridContext.Provider; + +export const useDataGridContext_unstable = (selector: ContextSelector) => + useContextSelector(dataGridContext, (ctx = dataGridContextDefaultValue) => selector(ctx)); diff --git a/packages/react-components/react-table/src/hooks/useTable.ts b/packages/react-components/react-table/src/hooks/useTable.ts index 8874dc44684fe..8347a3b067822 100644 --- a/packages/react-components/react-table/src/hooks/useTable.ts +++ b/packages/react-components/react-table/src/hooks/useTable.ts @@ -4,6 +4,15 @@ import { defaultTableSortState } from './useSort'; const defaultRowEnhancer: RowEnhancer> = row => row; +export const defaultTableState: TableState = { + selection: defaultTableSelectionState, + sort: defaultTableSortState, + getRows: () => [], + getRowId: () => '', + items: [], + columns: [], +}; + export function useTable(options: UseTableOptions, plugins: TableStatePlugin[] = []): TableState { const { items, getRowId, columns } = options; diff --git a/packages/react-components/react-table/src/index.ts b/packages/react-components/react-table/src/index.ts index 37273ec1cf90b..51d1b7e828fcc 100644 --- a/packages/react-components/react-table/src/index.ts +++ b/packages/react-components/react-table/src/index.ts @@ -124,7 +124,7 @@ export { useDataGridBody_unstable, renderDataGridBody_unstable, } from './DataGridBody'; -export type { DataGridBodyProps, DataGridBodyState, DataGridBodySlots } from './DataGridBody'; +export type { DataGridBodyProps, DataGridBodyState, DataGridBodySlots, RowRenderFunction } from './DataGridBody'; export { DataGrid, @@ -133,7 +133,13 @@ export { useDataGrid_unstable, renderDataGrid_unstable, } from './DataGrid'; -export type { DataGridProps, DataGridSlots, DataGridState, DataGridContextValues } from './DataGrid'; +export type { + DataGridProps, + DataGridSlots, + DataGridState, + DataGridContextValues, + DataGridContextValue, +} from './DataGrid'; export { DataGridHeader, diff --git a/packages/react-components/react-table/stories/DataGrid/Default.stories.tsx b/packages/react-components/react-table/stories/DataGrid/Default.stories.tsx new file mode 100644 index 0000000000000..4cc4d27b97328 --- /dev/null +++ b/packages/react-components/react-table/stories/DataGrid/Default.stories.tsx @@ -0,0 +1,132 @@ +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 { TableCellLayout } from '@fluentui/react-components/unstable'; +import { + DataGridBody, + DataGridCell, + DataGridRow, + DataGrid, + DataGridHeader, + DataGridHeaderCell, + ColumnDefinition, + RowState, +} from '@fluentui/react-table'; + +type FileCell = { + label: string; + icon: JSX.Element; +}; + +type LastUpdatedCell = { + label: string; + timestamp: number; +}; + +type LastUpdateCell = { + label: string; + icon: JSX.Element; +}; + +type AuthorCell = { + label: string; + status: PresenceBadgeStatus; +}; + +type Item = { + file: FileCell; + author: AuthorCell; + lastUpdated: LastUpdatedCell; + lastUpdate: LastUpdateCell; +}; + +const items: Item[] = [ + { + 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: , + }, + }, +]; + +const columns: ColumnDefinition[] = [ + { columnId: 'file' }, + { columnId: 'author' }, + { columnId: 'lastUpdated' }, + { columnId: 'lastUpdate' }, +]; + +export const Default = () => { + return ( + + + + File + Author + Last updated + Last update + + + + {({ item, rowId }: RowState) => ( + + + {item.file.label} + + + + } + > + {item.author.label} + + + {item.lastUpdated.label} + + {item.lastUpdate.label} + + + )} + + + ); +}; diff --git a/packages/react-components/react-table/stories/DataGrid/index.stories.tsx b/packages/react-components/react-table/stories/DataGrid/index.stories.tsx new file mode 100644 index 0000000000000..f9a915dec8dfb --- /dev/null +++ b/packages/react-components/react-table/stories/DataGrid/index.stories.tsx @@ -0,0 +1,12 @@ +import { DataGrid } from '../../src/components/DataGrid/DataGrid'; + +export { Default } from './Default.stories'; +export default { + title: 'Preview Components/DataGrid', + component: DataGrid, + parameters: { + docs: { + description: {}, + }, + }, +};