Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement focusMode prop for DataGrid, apply role="grid" correctly #25530

Merged
merged 6 commits into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,8 @@ exports[`DataGridCell renders a default state 1`] = `
<div>
<div
class="fui-DataGridCell fui-TableCell"
role="cell"
role="gridcell"
tabindex="0"
>
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