Skip to content

Commit

Permalink
feat: Implement focusMode prop for DataGrid, apply role="grid" corr…
Browse files Browse the repository at this point in the history
…ectly (#25530)

* feat: DataGrid cells navigable by default, apply role="grid" correctly

* update snapshots

* changefile

* imlement focusMode prop

* update changefile

* update snapshot
  • Loading branch information
ling1726 authored Nov 7, 2022
1 parent 5de3909 commit 3088a39
Show file tree
Hide file tree
Showing 16 changed files with 245 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "feat: Implement `focusMode` prop for DataGrid, apply role=\"grid\" correctly",
"packageName": "@fluentui/react-table",
"email": "[email protected]",
"dependentChangeType": "patch"
}
11 changes: 8 additions & 3 deletions packages/react-components/react-table/etc/react-table.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ export type DataGridCellState = TableCellState;
export const dataGridClassNames: SlotClassNames<DataGridSlots>;

// @public (undocumented)
export type DataGridContextValue = HeadlessTableState<any>;
export type DataGridContextValue = HeadlessTableState<any> & {
focusMode: FocusMode;
};

// @public (undocumented)
export type DataGridContextValues = TableContextValues & {
Expand Down Expand Up @@ -128,7 +130,7 @@ export type DataGridHeaderSlots = TableHeaderSlots;
export type DataGridHeaderState = TableHeaderState;

// @public
export type DataGridProps = TableProps & Pick<DataGridContextValue, 'items' | 'columns'>;
export type DataGridProps = TableProps & Pick<DataGridContextValue, 'items' | 'columns'> & Pick<Partial<DataGridContextValue>, 'focusMode'>;

// @public
export const DataGridRow: ForwardRefComponent<DataGridRowProps>;
Expand Down Expand Up @@ -168,7 +170,10 @@ export type DataGridSlots = TableSlots;
// @public
export type DataGridState = TableState & {
tableState: HeadlessTableState<unknown>;
};
} & Pick<DataGridContextValue, 'focusMode'>;

// @public (undocumented)
export type FocusMode = 'none' | 'cell';

// @public (undocumented)
export interface HeadlessTableState<TItem> extends Pick<UseTableOptions<TItem>, 'items' | 'getRowId'> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,69 @@ describe('DataGrid', () => {
);
expect(result.container).toMatchSnapshot();
});

it('should render tabster attributes when `focusMode` has value `cell`', () => {
const result = render(
<DataGrid items={testItems} columns={testColumns} focusMode="cell">
<DataGridHeader>
<DataGridRow>
{({ renderHeaderCell, columnId }) => <DataGridCell key={columnId}>{renderHeaderCell()}</DataGridCell>}
</DataGridRow>
</DataGridHeader>
<DataGridBody>
{({ item, rowId }: RowState<Item>) => (
<DataGridRow key={rowId}>
{({ renderCell, columnId }) => <DataGridCell key={columnId}>{renderCell(item)}</DataGridCell>}
</DataGridRow>
)}
</DataGridBody>
</DataGrid>,
);

expect(result.getByRole('grid').getAttribute('data-tabster')).toMatchInlineSnapshot(
`"{\\"mover\\":{\\"cyclic\\":false,\\"direction\\":3}}"`,
);
});

it('should not render tabster attributes when `focusMode` has value `none`', () => {
const result = render(
<DataGrid items={testItems} columns={testColumns} focusMode="none">
<DataGridHeader>
<DataGridRow>
{({ renderHeaderCell, columnId }) => <DataGridCell key={columnId}>{renderHeaderCell()}</DataGridCell>}
</DataGridRow>
</DataGridHeader>
<DataGridBody>
{({ item, rowId }: RowState<Item>) => (
<DataGridRow key={rowId}>
{({ renderCell, columnId }) => <DataGridCell key={columnId}>{renderCell(item)}</DataGridCell>}
</DataGridRow>
)}
</DataGridBody>
</DataGrid>,
);

expect(result.getByRole('grid').hasAttribute('data-tabster')).toBe(false);
});

it('should not render tabster attributes when `focusMode` prop is not set', () => {
const result = render(
<DataGrid items={testItems} columns={testColumns}>
<DataGridHeader>
<DataGridRow>
{({ renderHeaderCell, columnId }) => <DataGridCell key={columnId}>{renderHeaderCell()}</DataGridCell>}
</DataGridRow>
</DataGridHeader>
<DataGridBody>
{({ item, rowId }: RowState<Item>) => (
<DataGridRow key={rowId}>
{({ renderCell, columnId }) => <DataGridCell key={columnId}>{renderCell(item)}</DataGridCell>}
</DataGridRow>
)}
</DataGridBody>
</DataGrid>,
);

expect(result.getByRole('grid').hasAttribute('data-tabster')).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,34 @@ import { TableState as HeadlessTableState } from '../../hooks';

export type DataGridSlots = TableSlots;

export type FocusMode = 'none' | 'cell';

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<any>;
export type DataGridContextValue = HeadlessTableState<any> & {
/**
* How focus navigation will work in the datagrid
* @default none
*/
focusMode: FocusMode;
};

/**
* DataGrid Props
*/
export type DataGridProps = TableProps & Pick<DataGridContextValue, 'items' | 'columns'>;
export type DataGridProps = TableProps &
Pick<DataGridContextValue, 'items' | 'columns'> &
Pick<Partial<DataGridContextValue>, 'focusMode'>;

/**
* State used in rendering DataGrid
*/
export type DataGridState = TableState & { tableState: HeadlessTableState<unknown> };
export type DataGridState = TableState & { tableState: HeadlessTableState<unknown> } & Pick<
DataGridContextValue,
'focusMode'
>;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`DataGrid renders a default state 1`] = `
<div>
<div
class="fui-DataGrid fui-Table"
role="table"
role="grid"
>
<div
class="fui-DataGridHeader fui-TableHeader"
Expand All @@ -16,19 +16,19 @@ exports[`DataGrid renders a default state 1`] = `
>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
first
</div>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
second
</div>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
third
</div>
Expand All @@ -44,19 +44,19 @@ exports[`DataGrid renders a default state 1`] = `
>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
first
</div>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
second
</div>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
third
</div>
Expand All @@ -67,19 +67,19 @@ exports[`DataGrid renders a default state 1`] = `
>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
first
</div>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
second
</div>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
third
</div>
Expand All @@ -90,19 +90,19 @@ exports[`DataGrid renders a default state 1`] = `
>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
first
</div>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
second
</div>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
third
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { useArrowNavigationGroup } from '@fluentui/react-tabster';
import type { DataGridProps, DataGridState } from './DataGrid.types';
import { useTable_unstable } from '../Table/useTable';
import { useTable } from '../../hooks/useTable';
Expand All @@ -13,12 +14,18 @@ import { useTable } from '../../hooks/useTable';
* @param ref - reference to root HTMLElement of DataGrid
*/
export const useDataGrid_unstable = (props: DataGridProps, ref: React.Ref<HTMLElement>): DataGridState => {
const { items, columns } = props;
const { items, columns, focusMode = 'none' } = props;
const navigable = focusMode !== 'none';
const keyboardNavAttr = useArrowNavigationGroup({ axis: 'grid' });
const tableState = useTable({ items, columns }, []);
const baseTableState = useTable_unstable({ ...props, as: 'div' }, ref);
const baseTableState = useTable_unstable(
{ role: 'grid', as: 'div', ...(navigable && keyboardNavAttr), ...props },
ref,
);

return {
...baseTableState,
focusMode,
tableState,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export function useDataGridContextValues_unstable(state: DataGridState): DataGri
...tableContextValues,
dataGrid: {
...state.tableState,
focusMode: state.focusMode,
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,40 @@ import { render } from '@testing-library/react';
import { DataGridCell } from './DataGridCell';
import { isConformant } from '../../testing/isConformant';
import { DataGridCellProps } from './DataGridCell.types';
import { DataGridContextProvider } from '../../contexts/dataGridContext';
import { mockDataGridContext } from '../../testing/mockDataGridContext';

describe('DataGridCell', () => {
isConformant<DataGridCellProps>({
Component: DataGridCell,
displayName: 'DataGridCell',
});

// TODO add more tests here, and create visual regression tests in /apps/vr-tests

it('renders a default state', () => {
const result = render(<DataGridCell>Default DataGridCell</DataGridCell>);
expect(result.container).toMatchSnapshot();
});

it('should set tabindex="0" when focusMode is cell', () => {
const context = mockDataGridContext({ focusMode: 'cell' });
const result = render(
<DataGridContextProvider value={context}>
<DataGridCell>Default DataGridCell</DataGridCell>
</DataGridContextProvider>,
);

expect(result.getByRole('gridcell').tabIndex).toBe(0);
});

it('should not set tabindex when focusMode is none', () => {
const context = mockDataGridContext({ focusMode: 'none' });
const result = render(
<DataGridContextProvider value={context}>
<DataGridCell>Default DataGridCell</DataGridCell>
</DataGridContextProvider>,
);

expect(result.getByRole('gridcell').tabIndex).toBe(-1);
expect(result.getByRole('gridcell').hasAttribute('tabindex')).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`DataGridCell renders a default state 1`] = `
<div>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
>
Default DataGridCell
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import type { DataGridCellProps, DataGridCellState } from './DataGridCell.types';
import { useTableCell_unstable } from '../TableCell/useTableCell';
import { useDataGridContext_unstable } from '../../contexts/dataGridContext';

/**
* Create the state required to render DataGridCell.
Expand All @@ -12,5 +13,6 @@ import { useTableCell_unstable } from '../TableCell/useTableCell';
* @param ref - reference to root HTMLElement of DataGridCell
*/
export const useDataGridCell_unstable = (props: DataGridCellProps, ref: React.Ref<HTMLElement>): DataGridCellState => {
return useTableCell_unstable({ ...props, as: 'div' }, ref);
const tabbable = useDataGridContext_unstable(ctx => ctx.focusMode !== 'none');
return useTableCell_unstable({ as: 'div', role: 'gridcell', tabIndex: tabbable ? 0 : undefined, ...props }, ref);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`DataGridSelectionCell renders a default state 1`] = `
<div>
<div
class="fui-DataGridSelectionCell fui-TableSelectionCell"
role="cell"
role="gridcell"
>
<span
class="fui-Checkbox fui-DataGridSelectionCell__checkboxIndicator fui-TableSelectionCell__checkboxIndicator"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ export const useDataGridSelectionCell_unstable = (
props: DataGridSelectionCellProps,
ref: React.Ref<HTMLElement>,
): DataGridSelectionCellState => {
return useTableSelectionCell_unstable({ ...props, as: 'div' }, ref);
return useTableSelectionCell_unstable({ as: 'div', role: 'gridcell', ...props }, ref);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const dataGridContext = createContext<DataGridContextValue | undefined>(undefine

const dataGridContextDefaultValue: DataGridContextValue = {
...defaultTableState,
focusMode: 'none',
};

export const DataGridContextProvider = dataGridContext.Provider;
Expand Down
1 change: 1 addition & 0 deletions packages/react-components/react-table/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export type {
DataGridState,
DataGridContextValues,
DataGridContextValue,
FocusMode,
} from './DataGrid';

export {
Expand Down
Loading

0 comments on commit 3088a39

Please sign in to comment.