diff --git a/docs/data/data-grid/demo/PopularFeaturesDemo.js b/docs/data/data-grid/demo/PopularFeaturesDemo.js
index 4c9494d625db2..a51d18b256900 100644
--- a/docs/data/data-grid/demo/PopularFeaturesDemo.js
+++ b/docs/data/data-grid/demo/PopularFeaturesDemo.js
@@ -382,7 +382,7 @@ export default function PopularFeaturesDemo() {
[`& .${gridClasses.detailPanel}`]: {
background: 'transparent',
},
- [`& .${gridClasses.cell}:focus, & .${gridClasses.cell}:focus-within`]: {
+ [`& .${gridClasses['cell--outlined']}`]: {
outline: 'none',
},
[`& .${gridClasses.columnHeader}:focus, & .${gridClasses.columnHeader}:focus-within`]:
diff --git a/docs/data/data-grid/demo/PopularFeaturesDemo.tsx b/docs/data/data-grid/demo/PopularFeaturesDemo.tsx
index c8d867196b30f..97c07193342e7 100644
--- a/docs/data/data-grid/demo/PopularFeaturesDemo.tsx
+++ b/docs/data/data-grid/demo/PopularFeaturesDemo.tsx
@@ -393,7 +393,7 @@ export default function PopularFeaturesDemo() {
[`& .${gridClasses.detailPanel}`]: {
background: 'transparent',
},
- [`& .${gridClasses.cell}:focus, & .${gridClasses.cell}:focus-within`]: {
+ [`& .${gridClasses['cell--outlined']}`]: {
outline: 'none',
},
[`& .${gridClasses.columnHeader}:focus, & .${gridClasses.columnHeader}:focus-within`]:
diff --git a/docs/data/data-grid/events/events.json b/docs/data/data-grid/events/events.json
index 4a0b4aeab96b4..3586c0b1a3397 100644
--- a/docs/data/data-grid/events/events.json
+++ b/docs/data/data-grid/events/events.json
@@ -6,6 +6,13 @@
"params": "GridAggregationModel",
"event": "MuiEvent<{}>"
},
+ {
+ "projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"],
+ "name": "cellBlur",
+ "description": "Fired when a blur
event happens in a cell.",
+ "params": "GridCellParams",
+ "event": "MuiEvent>"
+ },
{
"projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"],
"name": "cellClick",
@@ -38,6 +45,13 @@
"event": "MuiEvent",
"componentProp": "onCellEditStop"
},
+ {
+ "projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"],
+ "name": "cellFocus",
+ "description": "Fired when a focus
event happens in a cell.",
+ "params": "GridCellParams",
+ "event": "MuiEvent>"
+ },
{
"projects": ["x-data-grid", "x-data-grid-pro", "x-data-grid-premium"],
"name": "cellFocusIn",
diff --git a/docs/data/migration/migration-data-grid-v5/migration-data-grid-v5.md b/docs/data/migration/migration-data-grid-v5/migration-data-grid-v5.md
index a802615f19811..c7fda842d9cf5 100644
--- a/docs/data/migration/migration-data-grid-v5/migration-data-grid-v5.md
+++ b/docs/data/migration/migration-data-grid-v5/migration-data-grid-v5.md
@@ -284,6 +284,16 @@ Most of this breaking change is handled by `preset-safe` codemod but some furthe
### CSS classes
+- To update the outline style of a focused cell, use the `.MuiDataGrid-cell--outlined` class instead of the `:focus-within` selector.
+ ```diff
+ -'.MuiDataGrid-cell:focus-within': {
+ +'.MuiDataGrid-cell--outlined': {
+ ```
+ The new class name is also available in `gridClasses`:
+ ```diff
+ -`.${gridClasses.cell}:focus-within`: {
+ +`.${gridClasses['cell--outlined']}`: {
+ ```
- Some CSS classes were removed or renamed
| MUI X v5 classes | MUI X v6 classes | Note |
diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json
index 01501a218c452..d3fca53cc1d28 100644
--- a/docs/pages/x/api/data-grid/data-grid-premium.json
+++ b/docs/pages/x/api/data-grid/data-grid-premium.json
@@ -71,7 +71,7 @@
"experimentalFeatures": {
"type": {
"name": "shape",
- "description": "{ columnGrouping?: bool, lazyLoading?: bool, rowPinning?: bool, warnIfFocusStateIsNotSynced?: bool }"
+ "description": "{ columnGrouping?: bool, lazyLoading?: bool, rowPinning?: bool }"
}
},
"filterMode": {
@@ -370,6 +370,7 @@
"cell--textLeft",
"cell--textRight",
"cell--withRenderer",
+ "cell--outlined",
"cell--rangeTop",
"cell--rangeBottom",
"cell--rangeLeft",
diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json
index 1edaeff7e5e6e..7f649b8cd3c23 100644
--- a/docs/pages/x/api/data-grid/data-grid-pro.json
+++ b/docs/pages/x/api/data-grid/data-grid-pro.json
@@ -60,7 +60,7 @@
"experimentalFeatures": {
"type": {
"name": "shape",
- "description": "{ columnGrouping?: bool, lazyLoading?: bool, rowPinning?: bool, warnIfFocusStateIsNotSynced?: bool }"
+ "description": "{ columnGrouping?: bool, lazyLoading?: bool, rowPinning?: bool }"
}
},
"filterMode": {
@@ -339,6 +339,7 @@
"cell--textLeft",
"cell--textRight",
"cell--withRenderer",
+ "cell--outlined",
"cell--rangeTop",
"cell--rangeBottom",
"cell--rangeLeft",
diff --git a/docs/pages/x/api/data-grid/data-grid.json b/docs/pages/x/api/data-grid/data-grid.json
index db2101822d412..3fc703422d695 100644
--- a/docs/pages/x/api/data-grid/data-grid.json
+++ b/docs/pages/x/api/data-grid/data-grid.json
@@ -39,10 +39,7 @@
},
"error": { "type": { "name": "any" } },
"experimentalFeatures": {
- "type": {
- "name": "shape",
- "description": "{ columnGrouping?: bool, warnIfFocusStateIsNotSynced?: bool }"
- }
+ "type": { "name": "shape", "description": "{ columnGrouping?: bool }" }
},
"filterMode": {
"type": { "name": "enum", "description": "'client'
| 'server'" },
@@ -276,6 +273,7 @@
"cell--textLeft",
"cell--textRight",
"cell--withRenderer",
+ "cell--outlined",
"cell--rangeTop",
"cell--rangeBottom",
"cell--rangeLeft",
diff --git a/docs/translations/api-docs/data-grid/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium.json
index 42a2ccaebd6ee..930d727f87712 100644
--- a/docs/translations/api-docs/data-grid/data-grid-premium.json
+++ b/docs/translations/api-docs/data-grid/data-grid-premium.json
@@ -217,6 +217,11 @@
"nodeName": "the cell element",
"conditions": "the cell has a custom renderer"
},
+ "cell--outlined": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the cell element",
+ "conditions": "the cell is outlined"
+ },
"cell--rangeTop": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the cell element",
diff --git a/docs/translations/api-docs/data-grid/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro.json
index 7b0529fa3b27d..4c86bb18ed131 100644
--- a/docs/translations/api-docs/data-grid/data-grid-pro.json
+++ b/docs/translations/api-docs/data-grid/data-grid-pro.json
@@ -204,6 +204,11 @@
"nodeName": "the cell element",
"conditions": "the cell has a custom renderer"
},
+ "cell--outlined": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the cell element",
+ "conditions": "the cell is outlined"
+ },
"cell--rangeTop": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the cell element",
diff --git a/docs/translations/api-docs/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid.json
index c7bbaadd74fd1..efde63bb281ae 100644
--- a/docs/translations/api-docs/data-grid/data-grid.json
+++ b/docs/translations/api-docs/data-grid/data-grid.json
@@ -172,6 +172,11 @@
"nodeName": "the cell element",
"conditions": "the cell has a custom renderer"
},
+ "cell--outlined": {
+ "description": "Styles applied to {{nodeName}} if {{conditions}}.",
+ "nodeName": "the cell element",
+ "conditions": "the cell is outlined"
+ },
"cell--rangeTop": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the cell element",
diff --git a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
index 8d3fcb0106ad9..4df87599d6b2e 100644
--- a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
+++ b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
@@ -281,7 +281,6 @@ DataGridPremiumRaw.propTypes = {
columnGrouping: PropTypes.bool,
lazyLoading: PropTypes.bool,
rowPinning: PropTypes.bool,
- warnIfFocusStateIsNotSynced: PropTypes.bool,
}),
/**
* Filtering can be processed on the server or client-side.
diff --git a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
index 6d99056a23a97..1b954df540349 100644
--- a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
+++ b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
@@ -252,7 +252,6 @@ DataGridProRaw.propTypes = {
columnGrouping: PropTypes.bool,
lazyLoading: PropTypes.bool,
rowPinning: PropTypes.bool,
- warnIfFocusStateIsNotSynced: PropTypes.bool,
}),
/**
* Filtering can be processed on the server or client-side.
diff --git a/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx b/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx
index 78b881b94a284..1da3894ce5acf 100644
--- a/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx
+++ b/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx
@@ -191,7 +191,6 @@ DataGridRaw.propTypes = {
*/
experimentalFeatures: PropTypes.shape({
columnGrouping: PropTypes.bool,
- warnIfFocusStateIsNotSynced: PropTypes.bool,
}),
/**
* Filtering can be processed on the server or client-side.
diff --git a/packages/grid/x-data-grid/src/components/GridRow.tsx b/packages/grid/x-data-grid/src/components/GridRow.tsx
index 77f2341c3d2b1..4f9e26681cb27 100644
--- a/packages/grid/x-data-grid/src/components/GridRow.tsx
+++ b/packages/grid/x-data-grid/src/components/GridRow.tsx
@@ -13,7 +13,6 @@ import {
GridEditingState,
GridCellModes,
} from '../models/gridEditRowModel';
-import { useGridApiContext } from '../hooks/utils/useGridApiContext';
import { getDataGridUtilityClass, gridClasses } from '../constants/gridClasses';
import { useGridRootProps } from '../hooks/utils/useGridRootProps';
import { DataGridProcessedProps } from '../models/props/DataGridProps';
@@ -33,6 +32,7 @@ import { gridRowMaximumTreeDepthSelector } from '../hooks/features/rows/gridRows
import { gridColumnGroupsHeaderMaxDepthSelector } from '../hooks/features/columnGrouping/gridColumnGroupsSelector';
import { randomNumberBetween } from '../utils/utils';
import { GridCellProps } from './cell/GridCell';
+import { useGridPrivateApiContext } from '../hooks/utils/useGridPrivateApiContext';
export interface GridRowProps {
rowId: GridRowId;
@@ -122,7 +122,7 @@ const GridRow = React.forwardRef<
onMouseLeave,
...other
} = props;
- const apiRef = useGridApiContext();
+ const apiRef = useGridPrivateApiContext();
const ref = React.useRef(null);
const rootProps = useGridRootProps();
const currentPage = useGridVisibleRows(apiRef, rootProps);
@@ -362,6 +362,7 @@ const GridRow = React.forwardRef<
cellMode={cellParams.cellMode}
colIndex={cellProps.indexRelativeToAllColumns}
isEditable={cellParams.isEditable}
+ isOutlined={apiRef.current.isCellOutlined(rowId, column.field)}
isSelected={isSelected}
hasFocus={hasFocus}
tabIndex={tabIndex}
diff --git a/packages/grid/x-data-grid/src/components/cell/GridCell.tsx b/packages/grid/x-data-grid/src/components/cell/GridCell.tsx
index e212a7b7e6dc5..6dcaeed097ba9 100644
--- a/packages/grid/x-data-grid/src/components/cell/GridCell.tsx
+++ b/packages/grid/x-data-grid/src/components/cell/GridCell.tsx
@@ -18,7 +18,6 @@ import {
import { GridAlignment } from '../../models/colDef/gridColDef';
import { useGridApiContext } from '../../hooks/utils/useGridApiContext';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
-import { gridFocusCellSelector } from '../../hooks/features/focus/gridFocusStateSelector';
import { DataGridProcessedProps } from '../../models/props/DataGridProps';
import { FocusElement } from '../../models/params/gridCellParams';
@@ -32,6 +31,7 @@ export interface GridCellProps {
hasFocus?: boolean;
height: number | 'auto';
isEditable?: boolean;
+ isOutlined?: boolean;
isSelected?: boolean;
showRightBorder?: boolean;
value?: V;
@@ -65,17 +65,21 @@ function doesSupportPreventScroll(): boolean {
return cachedSupportsPreventScroll;
}
-type OwnerState = Pick & {
+type OwnerState = Pick<
+ GridCellProps,
+ 'align' | 'showRightBorder' | 'isEditable' | 'isOutlined' | 'isSelected'
+> & {
classes?: DataGridProcessedProps['classes'];
};
const useUtilityClasses = (ownerState: OwnerState) => {
- const { align, showRightBorder, isEditable, isSelected, classes } = ownerState;
+ const { align, isOutlined, showRightBorder, isEditable, isSelected, classes } = ownerState;
const slots = {
root: [
'cell',
`cell--text${capitalize(align)}`,
+ isOutlined && `cell--outlined`,
isEditable && 'cell--editable',
isSelected && 'selected',
showRightBorder && 'cell--withRightBorder',
@@ -87,8 +91,6 @@ const useUtilityClasses = (ownerState: OwnerState) => {
return composeClasses(slots, getDataGridUtilityClass, classes);
};
-let warnedOnce = false;
-
function GridCell(props: GridCellProps) {
const {
align,
@@ -101,6 +103,7 @@ function GridCell(props: GridCellProps) {
hasFocus,
height,
isEditable,
+ isOutlined,
isSelected,
rowId,
tabIndex,
@@ -118,6 +121,8 @@ function GridCell(props: GridCellProps) {
onMouseUp,
onMouseOver,
onKeyDown,
+ onFocus,
+ onBlur,
onKeyUp,
onDragEnter,
onDragOver,
@@ -130,7 +135,14 @@ function GridCell(props: GridCellProps) {
const apiRef = useGridApiContext();
const rootProps = useGridRootProps();
- const ownerState = { align, showRightBorder, isEditable, classes: rootProps.classes, isSelected };
+ const ownerState = {
+ align,
+ showRightBorder,
+ isEditable,
+ classes: rootProps.classes,
+ isSelected,
+ isOutlined,
+ };
const classes = useUtilityClasses(ownerState);
const publishMouseUp = React.useCallback(
@@ -203,36 +215,6 @@ function GridCell(props: GridCellProps) {
}
}, [hasFocus, cellMode, apiRef]);
- let handleFocus: any = other.onFocus;
-
- if (
- process.env.NODE_ENV === 'test' &&
- rootProps.experimentalFeatures?.warnIfFocusStateIsNotSynced
- ) {
- handleFocus = (event: React.FocusEvent) => {
- const focusedCell = gridFocusCellSelector(apiRef);
- if (focusedCell?.id === rowId && focusedCell.field === field) {
- if (typeof other.onFocus === 'function') {
- other.onFocus(event);
- }
- return;
- }
-
- if (!warnedOnce) {
- console.warn(
- [
- `MUI: The cell with id=${rowId} and field=${field} received focus.`,
- `According to the state, the focus should be at id=${focusedCell?.id}, field=${focusedCell?.field}.`,
- "Not syncing the state may cause unwanted behaviors since the `cellFocusIn` event won't be fired.",
- 'Call `fireEvent.mouseUp` before the `fireEvent.click` to sync the focus with the state.',
- ].join('\n'),
- );
-
- warnedOnce = true;
- }
- };
- }
-
const column = apiRef.current.getColumn(field);
const managesOwnFocus = column.type === 'actions';
@@ -272,10 +254,11 @@ function GridCell(props: GridCellProps) {
onMouseDown={publishMouseDown('cellMouseDown')}
onMouseUp={publishMouseUp('cellMouseUp')}
onKeyDown={publish('cellKeyDown', onKeyDown)}
+ onBlur={publish('cellBlur', onBlur)}
+ onFocus={publish('cellFocus', onFocus)}
onKeyUp={publish('cellKeyUp', onKeyUp)}
{...draggableEventHandlers}
{...other}
- onFocus={handleFocus}
>
{renderChildren()}
@@ -299,6 +282,7 @@ GridCell.propTypes = {
hasFocus: PropTypes.bool,
height: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.number]).isRequired,
isEditable: PropTypes.bool,
+ isOutlined: PropTypes.bool,
isSelected: PropTypes.bool,
onClick: PropTypes.func,
onDoubleClick: PropTypes.func,
diff --git a/packages/grid/x-data-grid/src/components/containers/GridRootStyles.ts b/packages/grid/x-data-grid/src/components/containers/GridRootStyles.ts
index 2fc18aee01aca..78fca7a551df9 100644
--- a/packages/grid/x-data-grid/src/components/containers/GridRootStyles.ts
+++ b/packages/grid/x-data-grid/src/components/containers/GridRootStyles.ts
@@ -124,7 +124,7 @@ export const GridRootStyles = styled('div', {
padding: '0 10px',
boxSizing: 'border-box',
},
- [`& .${gridClasses.columnHeader}:focus-within, & .${gridClasses.cell}:focus-within`]: {
+ [`& .${gridClasses.columnHeader}:focus-within, & .${gridClasses['cell--outlined']}`]: {
outline: `solid ${
theme.vars
? `rgba(${theme.vars.palette.primary.mainChannel} / 0.5)`
@@ -133,7 +133,7 @@ export const GridRootStyles = styled('div', {
outlineWidth: 1,
outlineOffset: -1,
},
- [`& .${gridClasses.columnHeader}:focus, & .${gridClasses.cell}:focus`]: {
+ [`& .${gridClasses.columnHeader}:focus, & .${gridClasses['cell--outlined']}`]: {
outline: `solid ${theme.palette.primary.main} 1px`,
},
[`& .${gridClasses.columnHeaderCheckbox}, & .${gridClasses.cellCheckbox}`]: {
@@ -353,10 +353,6 @@ export const GridRootStyles = styled('div', {
display: 'flex',
boxShadow: theme.shadows[2],
backgroundColor: (theme.vars || theme).palette.background.paper,
- '&:focus-within': {
- outline: `solid ${(theme.vars || theme).palette.primary.main} 1px`,
- outlineOffset: '-1px',
- },
},
[`& .${gridClasses['row--editing']}`]: {
boxShadow: theme.shadows[2],
diff --git a/packages/grid/x-data-grid/src/constants/gridClasses.ts b/packages/grid/x-data-grid/src/constants/gridClasses.ts
index d6394c964e963..290542af19a2b 100644
--- a/packages/grid/x-data-grid/src/constants/gridClasses.ts
+++ b/packages/grid/x-data-grid/src/constants/gridClasses.ts
@@ -60,6 +60,10 @@ export interface GridClasses {
* Styles applied to the cell element if the cell has a custom renderer.
*/
'cell--withRenderer': string;
+ /**
+ * Styles applied to the cell element if the cell is outlined.
+ */
+ 'cell--outlined': string;
/**
* Styles applied to the cell element if it is at the top edge of a cell selection range.
*/
@@ -548,6 +552,7 @@ export const gridClasses = generateUtilityClasses('MuiDataGrid', [
'cell--textLeft',
'cell--textRight',
'cell--withRenderer',
+ 'cell--outlined',
'cell--rangeTop',
'cell--rangeBottom',
'cell--rangeLeft',
diff --git a/packages/grid/x-data-grid/src/hooks/features/focus/gridFocusState.ts b/packages/grid/x-data-grid/src/hooks/features/focus/gridFocusState.ts
index cbbe1ef4714b6..6215dac62b304 100644
--- a/packages/grid/x-data-grid/src/hooks/features/focus/gridFocusState.ts
+++ b/packages/grid/x-data-grid/src/hooks/features/focus/gridFocusState.ts
@@ -1,6 +1,6 @@
import { GridRowId } from '../../../models/gridRows';
-export type GridCellIdentifier = { id: GridRowId; field: string };
+export type GridCellIdentifier = { id: GridRowId; field: string }; // TODO: Reuse GridCellCoordinates
export type GridColumnIdentifier = { field: string };
export type GridColumnGroupIdentifier = { field: string; depth: number };
@@ -10,6 +10,10 @@ export interface GridFocusState {
columnGroupHeader: GridColumnGroupIdentifier | null;
}
+export interface GridOutlineState {
+ cell: GridCellIdentifier | null;
+}
+
export interface GridTabIndexState {
cell: GridCellIdentifier | null;
columnHeader: GridColumnIdentifier | null;
diff --git a/packages/grid/x-data-grid/src/hooks/features/focus/gridFocusStateSelector.ts b/packages/grid/x-data-grid/src/hooks/features/focus/gridFocusStateSelector.ts
index d48d170da81de..02058fb104035 100644
--- a/packages/grid/x-data-grid/src/hooks/features/focus/gridFocusStateSelector.ts
+++ b/packages/grid/x-data-grid/src/hooks/features/focus/gridFocusStateSelector.ts
@@ -1,6 +1,6 @@
import { createSelector } from '../../../utils/createSelector';
import { GridStateCommunity } from '../../../models/gridStateCommunity';
-import { GridFocusState, GridTabIndexState } from './gridFocusState';
+import { GridFocusState, GridOutlineState, GridTabIndexState } from './gridFocusState';
export const gridFocusStateSelector = (state: GridStateCommunity) => state.focus;
@@ -37,3 +37,16 @@ export const unstable_gridTabIndexColumnGroupHeaderSelector = createSelector(
gridTabIndexStateSelector,
(state: GridTabIndexState) => state.columnGroupHeader,
);
+
+/**
+ * @ignore - do not document.
+ */
+export const gridOutlineStateSelector = (state: GridStateCommunity) => state.outline;
+
+/**
+ * @ignore - do not document.
+ */
+export const gridCellOutlineCellSelector = createSelector(
+ gridOutlineStateSelector,
+ (state: GridOutlineState) => state.cell,
+);
diff --git a/packages/grid/x-data-grid/src/hooks/features/focus/useGridFocus.ts b/packages/grid/x-data-grid/src/hooks/features/focus/useGridFocus.ts
index 132d9da218da6..1e2b10825cdf8 100644
--- a/packages/grid/x-data-grid/src/hooks/features/focus/useGridFocus.ts
+++ b/packages/grid/x-data-grid/src/hooks/features/focus/useGridFocus.ts
@@ -1,5 +1,8 @@
import * as React from 'react';
-import { unstable_ownerDocument as ownerDocument } from '@mui/utils';
+import {
+ unstable_ownerDocument as ownerDocument,
+ unstable_useEventCallback as useEventCallback,
+} from '@mui/utils';
import { GridEventListener, GridEventLookup } from '../../../models/events';
import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity';
import { GridFocusApi, GridFocusPrivateApi } from '../../../models/api/gridFocusApi';
@@ -10,6 +13,7 @@ import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler';
import { DataGridProcessedProps } from '../../../models/props/DataGridProps';
import { isNavigationKey } from '../../../utils/keyboardUtils';
import {
+ gridCellOutlineCellSelector,
gridFocusCellSelector,
unstable_gridFocusColumnGroupHeaderSelector,
} from './gridFocusStateSelector';
@@ -23,6 +27,7 @@ export const focusStateInitializer: GridStateInitializer = (state) => ({
...state,
focus: { cell: null, columnHeader: null, columnGroupHeader: null },
tabIndex: { cell: null, columnHeader: null, columnGroupHeader: null },
+ outline: { cell: null },
});
/**
@@ -35,7 +40,7 @@ export const useGridFocus = (
props: Pick,
): void => {
const logger = useGridLogger(apiRef, 'useGridFocus');
-
+ const lastKeydownEvent = React.useRef(null);
const lastClickedCell = React.useRef(null);
const publishCellFocusOut = React.useCallback(
@@ -67,6 +72,7 @@ export const useGridFocus = (
...state,
tabIndex: { cell: { id, field }, columnHeader: null, columnGroupHeader: null },
focus: { cell: { id, field }, columnHeader: null, columnGroupHeader: null },
+ outline: { cell: { id, field } },
};
});
apiRef.current.forceUpdate();
@@ -99,6 +105,7 @@ export const useGridFocus = (
...state,
tabIndex: { columnHeader: { field }, cell: null, columnGroupHeader: null },
focus: { columnHeader: { field }, cell: null, columnGroupHeader: null },
+ outline: { cell: null }, // The column header outline is handled via CSS
};
});
@@ -137,6 +144,14 @@ export const useGridFocus = (
GridFocusPrivateApi['getColumnGroupHeaderFocus']
>(() => unstable_gridFocusColumnGroupHeaderSelector(apiRef), [apiRef]);
+ const isCellOutlined = React.useCallback(
+ (id, field) => {
+ const outlinedCell = gridCellOutlineCellSelector(apiRef);
+ return outlinedCell?.id === id && outlinedCell?.field === field;
+ },
+ [apiRef],
+ );
+
const moveFocusToRelativeCell = React.useCallback(
(id, field, direction) => {
let columnIndexToFocus = apiRef.current.getColumnIndex(field);
@@ -214,6 +229,25 @@ export const useGridFocus = (
[apiRef],
);
+ const handleCellBlur = useEventCallback(() => {
+ if (lastKeydownEvent.current?.key !== 'Tab') {
+ return;
+ }
+ apiRef.current.setState((state) => ({
+ ...state,
+ // The tabIndex is kept to allow the focus to return to this cell if Shift+Tab is pressed
+ focus: { cell: null, columnHeader: null, columnGroupHeader: null },
+ outline: { cell: null },
+ }));
+ apiRef.current.forceUpdate();
+ });
+
+ const handleCellFocus = useEventCallback((params: GridCellParams) => {
+ if (lastKeydownEvent.current?.key === 'Tab') {
+ apiRef.current.setCellFocus(params.id, params.field);
+ }
+ });
+
const handleColumnHeaderFocus = React.useCallback>(
({ field }, event) => {
if (event.target !== event.currentTarget) {
@@ -224,7 +258,7 @@ export const useGridFocus = (
[apiRef],
);
- const focussedColumnGroup = unstable_gridFocusColumnGroupHeaderSelector(apiRef);
+ const focusedColumnGroup = unstable_gridFocusColumnGroupHeaderSelector(apiRef);
const handleColumnGroupHeaderFocus = React.useCallback<
GridEventListener<'columnGroupHeaderFocus'>
@@ -234,19 +268,19 @@ export const useGridFocus = (
return;
}
if (
- focussedColumnGroup !== null &&
- focussedColumnGroup.depth === depth &&
- fields.includes(focussedColumnGroup.field)
+ focusedColumnGroup !== null &&
+ focusedColumnGroup.depth === depth &&
+ fields.includes(focusedColumnGroup.field)
) {
// This group cell has already been focused
return;
}
apiRef.current.setColumnGroupHeaderFocus(fields[0], depth, event);
},
- [apiRef, focussedColumnGroup],
+ [apiRef, focusedColumnGroup],
);
- const handleBlur = React.useCallback>(() => {
+ const handleColumnHeaderBlur = React.useCallback>(() => {
logger.debug(`Clearing focus`);
apiRef.current.setState((state) => ({
...state,
@@ -287,6 +321,7 @@ export const useGridFocus = (
apiRef.current.setState((state) => ({
...state,
focus: { cell: null, columnHeader: null, columnGroupHeader: null },
+ outline: { cell: null },
}));
apiRef.current.forceUpdate();
@@ -298,6 +333,14 @@ export const useGridFocus = (
[apiRef, publishCellFocusOut],
);
+ const handleDocumentKeyDown = useEventCallback<[KeyboardEvent], void>((event) => {
+ lastKeydownEvent.current = event;
+ });
+
+ const handleDocumentKeyUp = useEventCallback(() => {
+ lastKeydownEvent.current = null;
+ });
+
const handleCellModeChange = React.useCallback>(
(params) => {
if (params.cellMode === 'view') {
@@ -319,6 +362,7 @@ export const useGridFocus = (
apiRef.current.setState((state) => ({
...state,
focus: { cell: null, columnHeader: null, columnGroupHeader: null },
+ outline: { cell: null },
}));
}
}, [apiRef]);
@@ -332,6 +376,7 @@ export const useGridFocus = (
moveFocusToRelativeCell,
setColumnGroupHeaderFocus,
getColumnGroupHeaderFocus,
+ isCellOutlined,
};
useGridApiMethod(apiRef, focusApi, 'public');
@@ -340,13 +385,19 @@ export const useGridFocus = (
React.useEffect(() => {
const doc = ownerDocument(apiRef.current.rootElementRef!.current);
doc.addEventListener('click', handleDocumentClick);
+ doc.addEventListener('keydown', handleDocumentKeyDown);
+ doc.addEventListener('keyup', handleDocumentKeyUp);
return () => {
doc.removeEventListener('click', handleDocumentClick);
+ doc.removeEventListener('keydown', handleDocumentKeyDown);
+ doc.removeEventListener('keyup', handleDocumentKeyUp);
};
- }, [apiRef, handleDocumentClick]);
+ }, [apiRef, handleDocumentClick, handleDocumentKeyDown, handleDocumentKeyUp]);
- useGridApiEventHandler(apiRef, 'columnHeaderBlur', handleBlur);
+ useGridApiEventHandler(apiRef, 'cellFocus', handleCellFocus);
+ useGridApiEventHandler(apiRef, 'cellBlur', handleCellBlur);
+ useGridApiEventHandler(apiRef, 'columnHeaderBlur', handleColumnHeaderBlur);
useGridApiEventHandler(apiRef, 'cellDoubleClick', handleCellDoubleClick);
useGridApiEventHandler(apiRef, 'cellMouseDown', handleCellMouseDown);
useGridApiEventHandler(apiRef, 'cellKeyDown', handleCellKeyDown);
diff --git a/packages/grid/x-data-grid/src/models/api/gridFocusApi.ts b/packages/grid/x-data-grid/src/models/api/gridFocusApi.ts
index e2b98e1e008bf..f3dfe31730813 100644
--- a/packages/grid/x-data-grid/src/models/api/gridFocusApi.ts
+++ b/packages/grid/x-data-grid/src/models/api/gridFocusApi.ts
@@ -43,4 +43,11 @@ export interface GridFocusPrivateApi {
field: string,
direction: 'below' | 'right' | 'left',
) => void;
+ /**
+ * Checks if a given cell has outline.
+ * @param {GridRowId} id The row id.
+ * @param {string} field The column field.
+ * @returns {boolean} Whether the cell has outline or not.
+ */
+ isCellOutlined: (id: GridRowId, field: string) => boolean;
}
diff --git a/packages/grid/x-data-grid/src/models/events/gridEventLookup.ts b/packages/grid/x-data-grid/src/models/events/gridEventLookup.ts
index 888f7eab1f382..d395c02f694cf 100644
--- a/packages/grid/x-data-grid/src/models/events/gridEventLookup.ts
+++ b/packages/grid/x-data-grid/src/models/events/gridEventLookup.ts
@@ -259,6 +259,20 @@ export interface GridCellEventLookup {
params: GridCellParams;
event: React.KeyboardEvent;
};
+ /**
+ * Fired when a `focus` event happens in a cell.
+ */
+ cellFocus: {
+ params: GridCellParams;
+ event: React.FocusEvent;
+ };
+ /**
+ * Fired when a `blur` event happens in a cell.
+ */
+ cellBlur: {
+ params: GridCellParams;
+ event: React.FocusEvent;
+ };
/**
* Fired when the dragged cell enters a valid drop target. It's mapped to the `dragend` DOM event.
* @ignore - do not document.
diff --git a/packages/grid/x-data-grid/src/models/gridStateCommunity.ts b/packages/grid/x-data-grid/src/models/gridStateCommunity.ts
index 08f55af3d682d..baed572670b4f 100644
--- a/packages/grid/x-data-grid/src/models/gridStateCommunity.ts
+++ b/packages/grid/x-data-grid/src/models/gridStateCommunity.ts
@@ -15,6 +15,7 @@ import type {
GridSortingInitialState,
GridSortingState,
GridTabIndexState,
+ GridOutlineState,
} from '../hooks';
import type { GridRowsMetaState } from '../hooks/features/rows/gridRowsMetaState';
import type { GridEditingState } from './gridEditRowModel';
@@ -33,6 +34,7 @@ export interface GridStateCommunity {
columnMenu: GridColumnMenuState;
sorting: GridSortingState;
focus: GridFocusState;
+ outline: GridOutlineState;
tabIndex: GridTabIndexState;
rowSelection: GridRowSelectionModel;
filter: GridFilterState;
diff --git a/packages/grid/x-data-grid/src/models/props/DataGridProps.ts b/packages/grid/x-data-grid/src/models/props/DataGridProps.ts
index c7ff97d27f95e..1d50de575cca4 100644
--- a/packages/grid/x-data-grid/src/models/props/DataGridProps.ts
+++ b/packages/grid/x-data-grid/src/models/props/DataGridProps.ts
@@ -36,11 +36,6 @@ export interface GridExperimentalFeatures {
* Enables the column grouping.
*/
columnGrouping: boolean;
- /**
- * Emits a warning if the cell receives focus without also syncing the focus state.
- * Only works if NODE_ENV=test.
- */
- warnIfFocusStateIsNotSynced: boolean;
}
/**
diff --git a/packages/grid/x-data-grid/src/tests/cells.DataGrid.test.tsx b/packages/grid/x-data-grid/src/tests/cells.DataGrid.test.tsx
index fcd468171f4d9..4586a7e355cfd 100644
--- a/packages/grid/x-data-grid/src/tests/cells.DataGrid.test.tsx
+++ b/packages/grid/x-data-grid/src/tests/cells.DataGrid.test.tsx
@@ -167,24 +167,6 @@ describe(' - Cells', () => {
expect(valueFormatter.lastCall.args[0].value).to.equal(true);
});
- it('should throw when focusing cell without updating the state', () => {
- render(
-
-
-
,
- );
-
- userEvent.mousePress(getCell(0, 0));
-
- expect(() => {
- getCell(1, 0).focus();
- }).toWarnDev(['MUI: The cell with id=1 and field=brand received focus.']);
- });
-
// See https://github.com/mui/mui-x/issues/6378
it('should not cause scroll jump when focused cell mounts in the render zone', async function test() {
if (isJSDOM) {
diff --git a/packages/grid/x-data-grid/src/tests/keyboard.DataGrid.test.tsx b/packages/grid/x-data-grid/src/tests/keyboard.DataGrid.test.tsx
index 22c94f1cc36e8..33b9f78b47f97 100644
--- a/packages/grid/x-data-grid/src/tests/keyboard.DataGrid.test.tsx
+++ b/packages/grid/x-data-grid/src/tests/keyboard.DataGrid.test.tsx
@@ -11,7 +11,7 @@ import {
getColumnValues,
getRow,
} from 'test/utils/helperFn';
-import { DataGrid, DataGridProps, GridColDef } from '@mui/x-data-grid';
+import { DataGrid, DataGridProps, GridColDef, gridClasses } from '@mui/x-data-grid';
import { useBasicDemoData, getBasicGridData } from '@mui/x-data-grid-generator';
const isJSDOM = /jsdom/.test(window.navigator.userAgent);
@@ -55,7 +55,7 @@ describe(' - Keyboard', () => {
columnHeaderHeight={HEADER_HEIGHT}
hideFooter
filterModel={{ items: [{ field: 'id', operator: '>', value: 10 }] }}
- experimentalFeatures={{ warnIfFocusStateIsNotSynced: true, columnGrouping: true }}
+ experimentalFeatures={{ columnGrouping: true }}
{...props}
/>
@@ -300,6 +300,57 @@ describe(' - Keyboard', () => {
});
});
+ describe('cell outline', () => {
+ it('should add the outlined class to the cell when it is focused', () => {
+ render();
+ const cell = getCell(8, 1);
+ expect(cell).not.to.have.class(gridClasses['cell--outlined']);
+ userEvent.mousePress(cell);
+ expect(cell).to.have.class(gridClasses['cell--outlined']);
+ });
+
+ it('should remove the outlined class from the cell when Tab is pressed', () => {
+ render();
+ const cell = getCell(8, 1);
+ userEvent.mousePress(cell);
+ expect(cell).to.have.class(gridClasses['cell--outlined']);
+ fireEvent.keyDown(cell, { key: 'Tab' });
+ act(() => cell.blur());
+ fireEvent.keyUp(document.activeElement, { key: 'Tab' });
+ expect(cell).not.to.have.class(gridClasses['cell--outlined']);
+ });
+
+ it('should keep the cell focusable after Tab is pressed', () => {
+ render();
+ const cell = getCell(8, 1);
+ userEvent.mousePress(cell);
+ expect(cell).to.have.attr('tabindex', '0');
+ fireEvent.keyDown(cell, { key: 'Tab' });
+ act(() => cell.blur());
+ fireEvent.keyUp(document.activeElement, { key: 'Tab' });
+ expect(cell).to.have.attr('tabindex', '0');
+ });
+
+ it('should add the outlined class to the cell when it is focused via Tab', () => {
+ render();
+ const cell = getCell(8, 1);
+ userEvent.mousePress(cell);
+ expect(cell).to.have.class(gridClasses['cell--outlined']);
+
+ // Simulates cell losing focus to another element with Tab
+ fireEvent.keyDown(cell, { key: 'Tab' });
+ act(() => cell.blur());
+ fireEvent.keyUp(document.activeElement, { key: 'Tab' });
+ expect(cell).not.to.have.class(gridClasses['cell--outlined']);
+
+ // Simulates cell gaining focus again with Shift+Tab
+ fireEvent.keyDown(document.activeElement, { key: 'Tab', shiftKey: true });
+ act(() => cell.focus());
+ fireEvent.keyUp(cell, { key: 'Tab', shiftKey: true });
+ expect(cell).to.have.class(gridClasses['cell--outlined']);
+ });
+ });
+
describe('column header navigation', () => {
it('should scroll horizontally when navigating between column headers with arrows', function test() {
if (isJSDOM) {
@@ -484,7 +535,7 @@ describe(' - Keyboard', () => {
hideFooter
disableVirtualization
columnGroupingModel={columnGroupingModel}
- experimentalFeatures={{ warnIfFocusStateIsNotSynced: true, columnGrouping: true }}
+ experimentalFeatures={{ columnGrouping: true }}
{...props}
/>
diff --git a/packages/grid/x-data-grid/src/tests/rowSelection.DataGrid.test.tsx b/packages/grid/x-data-grid/src/tests/rowSelection.DataGrid.test.tsx
index 9421bca12d525..5826a4e21119a 100644
--- a/packages/grid/x-data-grid/src/tests/rowSelection.DataGrid.test.tsx
+++ b/packages/grid/x-data-grid/src/tests/rowSelection.DataGrid.test.tsx
@@ -43,15 +43,7 @@ describe(' - Row Selection', () => {
function TestDataGridSelection(props: Partial) {
return (
-
+
);
}
diff --git a/scripts/x-data-grid-premium.exports.json b/scripts/x-data-grid-premium.exports.json
index 314eb2316e9f9..e339d6b0a6604 100644
--- a/scripts/x-data-grid-premium.exports.json
+++ b/scripts/x-data-grid-premium.exports.json
@@ -112,6 +112,7 @@
{ "name": "GridCellMode", "kind": "TypeAlias" },
{ "name": "GridCellModes", "kind": "Enum" },
{ "name": "GridCellModesModel", "kind": "TypeAlias" },
+ { "name": "gridCellOutlineCellSelector", "kind": "Variable" },
{ "name": "GridCellParams", "kind": "Interface" },
{ "name": "GridCellProps", "kind": "Interface" },
{ "name": "GridCellSelectionApi", "kind": "Interface" },
@@ -354,6 +355,8 @@
{ "name": "GridNativeColTypes", "kind": "TypeAlias" },
{ "name": "GridNoRowsOverlay", "kind": "Variable" },
{ "name": "gridNumberComparator", "kind": "Variable" },
+ { "name": "GridOutlineState", "kind": "Interface" },
+ { "name": "gridOutlineStateSelector", "kind": "Variable" },
{ "name": "GridOverlay", "kind": "Variable" },
{ "name": "GridOverlayProps", "kind": "TypeAlias" },
{ "name": "GridOverlays", "kind": "Function" },
diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json
index 229ebbf6bc8c6..bb2b8e0cc17fa 100644
--- a/scripts/x-data-grid-pro.exports.json
+++ b/scripts/x-data-grid-pro.exports.json
@@ -90,6 +90,7 @@
{ "name": "GridCellMode", "kind": "TypeAlias" },
{ "name": "GridCellModes", "kind": "Enum" },
{ "name": "GridCellModesModel", "kind": "TypeAlias" },
+ { "name": "gridCellOutlineCellSelector", "kind": "Variable" },
{ "name": "GridCellParams", "kind": "Interface" },
{ "name": "GridCellProps", "kind": "Interface" },
{ "name": "GridCheckCircleIcon", "kind": "Variable" },
@@ -318,6 +319,8 @@
{ "name": "GridNativeColTypes", "kind": "TypeAlias" },
{ "name": "GridNoRowsOverlay", "kind": "Variable" },
{ "name": "gridNumberComparator", "kind": "Variable" },
+ { "name": "GridOutlineState", "kind": "Interface" },
+ { "name": "gridOutlineStateSelector", "kind": "Variable" },
{ "name": "GridOverlay", "kind": "Variable" },
{ "name": "GridOverlayProps", "kind": "TypeAlias" },
{ "name": "GridOverlays", "kind": "Function" },
diff --git a/scripts/x-data-grid.exports.json b/scripts/x-data-grid.exports.json
index 97b197bf3e0fc..89ce8c000fd0d 100644
--- a/scripts/x-data-grid.exports.json
+++ b/scripts/x-data-grid.exports.json
@@ -83,6 +83,7 @@
{ "name": "GridCellMode", "kind": "TypeAlias" },
{ "name": "GridCellModes", "kind": "Enum" },
{ "name": "GridCellModesModel", "kind": "TypeAlias" },
+ { "name": "gridCellOutlineCellSelector", "kind": "Variable" },
{ "name": "GridCellParams", "kind": "Interface" },
{ "name": "GridCellProps", "kind": "Interface" },
{ "name": "GridCheckCircleIcon", "kind": "Variable" },
@@ -291,6 +292,8 @@
{ "name": "GridNativeColTypes", "kind": "TypeAlias" },
{ "name": "GridNoRowsOverlay", "kind": "Variable" },
{ "name": "gridNumberComparator", "kind": "Variable" },
+ { "name": "GridOutlineState", "kind": "Interface" },
+ { "name": "gridOutlineStateSelector", "kind": "Variable" },
{ "name": "GridOverlay", "kind": "Variable" },
{ "name": "GridOverlayProps", "kind": "TypeAlias" },
{ "name": "GridOverlays", "kind": "Function" },