diff --git a/docs/pages/api-docs/data-grid/data-grid.md b/docs/pages/api-docs/data-grid/data-grid.md index 4dcb3d089dd5..302382efbf16 100644 --- a/docs/pages/api-docs/data-grid/data-grid.md +++ b/docs/pages/api-docs/data-grid/data-grid.md @@ -82,7 +82,6 @@ import { DataGrid } from '@material-ui/data-grid'; | onRowOut | (param: GridRowParams, event: React.MouseEvent) => void | | Callback fired when a mouse out comes from a row container element. | | onRowEnter | (param: GridRowParams, event: React.MouseEvent) => void | | Callback fired when a mouse enter comes from a row container element. | | onRowLeave | (param: GridRowParams, event: React.MouseEvent) => void | | Callback fired when a mouse leave event comes from a row container element. | -| onRowSelected | (param: GridRowSelectedParams) => void | | Callback fired when one row is selected. | | onSelectionModelChange | (param: GridSelectionModelChangeParams) => void | | Callback fired when the selection state of one or multiple rows changes. | | onSortModelChange | (param: GridSortModelParams) => void | | Callback fired when the sort model changes before a column is sorted. | | page | number | 1 | Set the current page. | diff --git a/docs/pages/api-docs/data-grid/x-grid.md b/docs/pages/api-docs/data-grid/x-grid.md index 6c76ccad289f..d701450049cb 100644 --- a/docs/pages/api-docs/data-grid/x-grid.md +++ b/docs/pages/api-docs/data-grid/x-grid.md @@ -87,7 +87,6 @@ import { XGrid } from '@material-ui/x-grid'; | onRowOut | (param: GridRowParams, event: React.MouseEvent) => void | | Callback fired when a mouse out comes from a row container element. | | onRowEnter | (param: GridRowParams, event: React.MouseEvent) => void | | Callback fired when a mouse enter comes from a row container element. | | onRowLeave | (param: GridRowParams, event: React.MouseEvent) => void | | Callback fired when a mouse leave event comes from a row container element. | -| onRowSelected | (param: GridRowSelectedParams) => void | | Callback fired when one row is selected. | | onRowsScrollEnd | (param: GridRowScrollEndParams) => void | | Callback fired when scrolling to the bottom of the grid viewport. | | onSelectionModelChange | (param: GridSelectionModelChangeParams) => void | | Callback fired when the selection state of one or multiple rows changes. | | onSortModelChange | (param: GridSortModelParams) => void | | Callback fired when the sort model changes before a column is sorted. | diff --git a/docs/src/pages/components/data-grid/events/events.json b/docs/src/pages/components/data-grid/events/events.json index 88f428ffaa6c..95ede2d26296 100644 --- a/docs/src/pages/components/data-grid/events/events.json +++ b/docs/src/pages/components/data-grid/events/events.json @@ -109,10 +109,6 @@ "name": "editRowModelChange", "description": "Fired when the row editing model changes. Called with a GridEditRowModelParams object." }, - { - "name": "rowSelected", - "description": "Fired when a row is selected or unselected. Called with a GridRowSelectedParams object." - }, { "name": "columnHeaderKeyDown", "description": "Fired when a key is pressed in a column header. It's mapped do the keydown DOM event.\nCalled with a GridColumnHeaderParams object." diff --git a/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.js b/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.js index ec9f126d0efc..b00cf2f8357f 100644 --- a/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.js +++ b/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.js @@ -15,8 +15,8 @@ export default function ControlledSelectionGrid() {
{ - setSelectionModel(newSelection.selectionModel); + onSelectionModelChange={(newSelectionModel) => { + setSelectionModel(newSelectionModel); }} selectionModel={selectionModel} {...data} diff --git a/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.tsx b/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.tsx index 4594a7004a68..a2be7dc406fd 100644 --- a/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.tsx +++ b/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.tsx @@ -15,8 +15,8 @@ export default function ControlledSelectionGrid() {
{ - setSelectionModel(newSelection.selectionModel); + onSelectionModelChange={(newSelectionModel) => { + setSelectionModel(newSelectionModel); }} selectionModel={selectionModel} {...data} diff --git a/packages/grid/_modules_/grid/components/GridViewport.tsx b/packages/grid/_modules_/grid/components/GridViewport.tsx index 0ed7f2e839a6..e8ddabf3d845 100644 --- a/packages/grid/_modules_/grid/components/GridViewport.tsx +++ b/packages/grid/_modules_/grid/components/GridViewport.tsx @@ -8,7 +8,7 @@ import { gridTabIndexCellSelector, } from '../hooks/features/focus/gridFocusStateSelector'; import { gridEditRowsStateSelector } from '../hooks/features/rows/gridEditRowsSelector'; -import { gridSelectionStateSelector } from '../hooks/features/selection/gridSelectionSelector'; +import { selectedIdsLookupSelector } from '../hooks/features/selection/gridSelectionSelector'; import { renderStateSelector } from '../hooks/features/virtualization/renderingStateSelector'; import { optionsSelector } from '../hooks/utils/optionsSelector'; import { useGridApiContext } from '../hooks/root/useGridApiContext'; @@ -38,7 +38,7 @@ export const GridViewport: ViewportType = React.forwardRef( const renderState = useGridSelector(apiRef, renderStateSelector); const cellFocus = useGridSelector(apiRef, gridFocusCellSelector); const cellTabIndex = useGridSelector(apiRef, gridTabIndexCellSelector); - const selectionState = useGridSelector(apiRef, gridSelectionStateSelector); + const selectionLookup = useGridSelector(apiRef, selectedIdsLookupSelector); const rows = useGridSelector(apiRef, visibleSortedGridRowsAsArraySelector); const rowHeight = useGridSelector(apiRef, gridDensityRowHeightSelector); const editRowsState = useGridSelector(apiRef, gridEditRowsStateSelector); @@ -59,7 +59,7 @@ export const GridViewport: ViewportType = React.forwardRef( } key={id} id={id} - selected={selectionState[id] !== undefined} + selected={selectionLookup[id] !== undefined} rowIndex={renderState.renderContext!.firstRowIdx! + idx} > @@ -77,7 +77,7 @@ export const GridViewport: ViewportType = React.forwardRef( rowIndex={renderState.renderContext!.firstRowIdx! + idx} cellFocus={cellFocus} cellTabIndex={cellTabIndex} - isSelected={selectionState[id] !== undefined} + isSelected={selectionLookup[id] !== undefined} editRowState={editRowsState[id]} cellClassName={options.classes?.cell} getCellClassName={options.getCellClassName} diff --git a/packages/grid/_modules_/grid/constants/eventsConstants.ts b/packages/grid/_modules_/grid/constants/eventsConstants.ts index 9d7bedc4f5d0..675bab2623c3 100644 --- a/packages/grid/_modules_/grid/constants/eventsConstants.ts +++ b/packages/grid/_modules_/grid/constants/eventsConstants.ts @@ -225,12 +225,6 @@ export const GRID_ROW_LEAVE = 'rowLeave'; */ export const GRID_ROW_EDIT_MODEL_CHANGE = 'editRowModelChange'; -/** - * Fired when a row is selected or unselected. Called with a [[GridRowSelectedParams]] object. - * @event - */ -export const GRID_ROW_SELECTED = 'rowSelected'; - /** * Fired when a column header loses focus. Called with a [[GridColumnHeaderParams]] object. * @ignore - do not document. diff --git a/packages/grid/_modules_/grid/hooks/features/clipboard/useGridClipboard.ts b/packages/grid/_modules_/grid/hooks/features/clipboard/useGridClipboard.ts index dc49737ab5df..3f7afe034033 100644 --- a/packages/grid/_modules_/grid/hooks/features/clipboard/useGridClipboard.ts +++ b/packages/grid/_modules_/grid/hooks/features/clipboard/useGridClipboard.ts @@ -48,6 +48,7 @@ export const useGridClipboard = (apiRef: GridApiRef): void => { const data = buildCSV({ columns: visibleColumns, rows: selectedRows, + selectedRowIds: [], includeHeaders, getCellParams: apiRef.current.getCellParams, delimiterCharacter: '\t', diff --git a/packages/grid/_modules_/grid/hooks/features/core/gridState.ts b/packages/grid/_modules_/grid/hooks/features/core/gridState.ts index 4bb5d512c797..1474d1683502 100644 --- a/packages/grid/_modules_/grid/hooks/features/core/gridState.ts +++ b/packages/grid/_modules_/grid/hooks/features/core/gridState.ts @@ -79,7 +79,7 @@ export const getInitialGridState: () => GridState = () => ({ sorting: getInitialGridSortingState(), focus: { cell: null, columnHeader: null }, tabIndex: { cell: null, columnHeader: null }, - selection: {}, + selection: [], filter: getInitialGridFilterState(), columnMenu: { open: false }, preferencePanel: { open: false }, diff --git a/packages/grid/_modules_/grid/hooks/features/core/index.ts b/packages/grid/_modules_/grid/hooks/features/core/index.ts index 6a14d3632bbc..367a02e51bbe 100644 --- a/packages/grid/_modules_/grid/hooks/features/core/index.ts +++ b/packages/grid/_modules_/grid/hooks/features/core/index.ts @@ -1,5 +1,6 @@ export * from './gridState'; export * from './useGridApi'; +export * from './useGridControlState'; export * from './useGridReducer'; export * from './useGridSelector'; export * from './useGridState'; diff --git a/packages/grid/_modules_/grid/hooks/features/core/useGridControlState.ts b/packages/grid/_modules_/grid/hooks/features/core/useGridControlState.ts new file mode 100644 index 000000000000..6fe8396dcd0b --- /dev/null +++ b/packages/grid/_modules_/grid/hooks/features/core/useGridControlState.ts @@ -0,0 +1,78 @@ +import React from 'react'; +import { GridApiRef } from '../../../models/api/gridApiRef'; +import { GridControlStateApi } from '../../../models/api/gridControlStateApi'; +import { ControlStateItem } from '../../../models/controlStateItem'; +import { useGridApiMethod } from '../../root/useGridApiMethod'; + +export function useGridControlState(apiRef: GridApiRef) { + const controlStateMapRef = React.useRef>>({}); + + const updateControlState = React.useCallback((controlStateItem: ControlStateItem) => { + const { stateId, stateSelector, ...others } = controlStateItem; + + controlStateMapRef.current[stateId] = { + ...others, + stateId, + stateSelector: !stateSelector ? (state) => state[stateId] : stateSelector, + }; + }, []); + + const applyControlStateConstraint = React.useCallback( + (newState) => { + let shouldUpdate = true; + const updatedStateIds: string[] = []; + const controlStateMap = controlStateMapRef.current!; + + Object.keys(controlStateMap).forEach((stateId) => { + const controlState = controlStateMap[stateId]; + const oldState = controlState.stateSelector(apiRef.current.state); + const newSubState = controlState.stateSelector(newState); + const hasSubStateChanged = oldState !== newSubState; + + if (updatedStateIds.length >= 1 && hasSubStateChanged) { + // Each hook modify its own state and it should not leak + // Events are here to forward to other hooks and apply changes. + // You are trying to update several states in a no isolated way. + throw new Error( + `You're not allowed to update several sub-state in one transaction. You already updated ${updatedStateIds[0]}, therefore, you're not allowed to update ${controlState.stateId} in the same transaction.`, + ); + } + + if (hasSubStateChanged) { + if (controlState.propOnChange) { + const newModel = newSubState; + if (controlState.propModel !== newModel) { + controlState.propOnChange(newModel); + } + shouldUpdate = + controlState.propModel === undefined || controlState.propModel === newModel; + } else if (controlState.propModel !== undefined) { + shouldUpdate = oldState !== controlState.propModel; + } + if (shouldUpdate) { + updatedStateIds.push(controlState.stateId); + } + } + }); + + return { + shouldUpdate, + postUpdate: () => { + updatedStateIds.forEach((stateId) => { + if (controlStateMap[stateId].onChangeCallback) { + const model = controlStateMap[stateId].stateSelector(newState); + controlStateMap[stateId].onChangeCallback!(model); + } + }); + }, + }; + }, + [apiRef], + ); + + const controlStateApi: GridControlStateApi = { + updateControlState, + applyControlStateConstraint, + }; + useGridApiMethod(apiRef, controlStateApi, 'controlStateApi'); +} diff --git a/packages/grid/_modules_/grid/hooks/features/core/useGridState.ts b/packages/grid/_modules_/grid/hooks/features/core/useGridState.ts index aca1d7efcb79..57aecabcce99 100644 --- a/packages/grid/_modules_/grid/hooks/features/core/useGridState.ts +++ b/packages/grid/_modules_/grid/hooks/features/core/useGridState.ts @@ -9,23 +9,37 @@ export const useGridState = ( apiRef: GridApiRef, ): [GridState, (stateUpdaterFn: (oldState: GridState) => GridState) => boolean, () => void] => { useGridApi(apiRef); + const forceUpdate = React.useCallback( () => apiRef.current.forceUpdate(() => apiRef.current.state), [apiRef], ); + const setGridState = React.useCallback( (stateUpdaterFn: (oldState: GridState) => GridState) => { const newState = stateUpdaterFn(apiRef.current.state); const hasChanged = apiRef.current.state !== newState; + if (!hasChanged) { + // We always assign it as we mutate rows for perf reason. + apiRef.current.state = newState; + return false; + } + + const { shouldUpdate, postUpdate } = apiRef.current.applyControlStateConstraint(newState); + if (!shouldUpdate) { + return false; + } // We always assign it as we mutate rows for perf reason. apiRef.current.state = newState; - // TODO deepFreeze(newState); if (hasChanged && apiRef.current.publishEvent) { const params: GridStateChangeParams = { api: apiRef.current, state: newState }; apiRef.current.publishEvent(GRID_STATE_CHANGE, params); + + postUpdate(); } + return hasChanged; }, [apiRef], diff --git a/packages/grid/_modules_/grid/hooks/features/export/serializers/csvSerializer.ts b/packages/grid/_modules_/grid/hooks/features/export/serializers/csvSerializer.ts index 7cc108784475..01a9daec97f1 100644 --- a/packages/grid/_modules_/grid/hooks/features/export/serializers/csvSerializer.ts +++ b/packages/grid/_modules_/grid/hooks/features/export/serializers/csvSerializer.ts @@ -36,7 +36,7 @@ export function serialiseRow( interface BuildCSVOptions { columns: GridColumns; rows: Map; - selectedRows?: Record; + selectedRowIds: GridRowId[]; getCellParams: (id: GridRowId, field: string) => GridCellParams; delimiterCharacter: GridExportCsvDelimiter; includeHeaders?: boolean; @@ -46,18 +46,15 @@ export function buildCSV(options: BuildCSVOptions): string { const { columns, rows, - selectedRows, + selectedRowIds, getCellParams, delimiterCharacter, includeHeaders = true, } = options; let rowIds = [...rows.keys()]; - if (selectedRows) { - const selectedRowIds = Object.keys(selectedRows); - if (selectedRowIds.length) { - rowIds = rowIds.filter((id) => selectedRowIds.includes(`${id}`)); - } + if (selectedRowIds.length) { + rowIds = rowIds.filter((id) => selectedRowIds.includes(`${id}`)); } const CSVBody = rowIds diff --git a/packages/grid/_modules_/grid/hooks/features/export/useGridCsvExport.tsx b/packages/grid/_modules_/grid/hooks/features/export/useGridCsvExport.tsx index 8e69ef4ad191..c4f3c619d88e 100644 --- a/packages/grid/_modules_/grid/hooks/features/export/useGridCsvExport.tsx +++ b/packages/grid/_modules_/grid/hooks/features/export/useGridCsvExport.tsx @@ -24,7 +24,7 @@ export const useGridCsvExport = (apiRef: GridApiRef): void => { return buildCSV({ columns: visibleColumns, rows: visibleSortedRows, - selectedRows: selection, + selectedRowIds: selection, getCellParams: apiRef.current.getCellParams, delimiterCharacter: options?.delimiter || ',', }); diff --git a/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionSelector.ts b/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionSelector.ts index bea005bc383f..a75ec4de19d7 100644 --- a/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionSelector.ts +++ b/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionSelector.ts @@ -11,7 +11,7 @@ export const selectedGridRowsCountSelector: OutputSelector< (res: GridSelectionState) => number > = createSelector( gridSelectionStateSelector, - (selection) => Object.keys(selection).length, + (selection) => selection.length, ); export const selectedGridRowsSelector = createSelector< @@ -22,6 +22,18 @@ export const selectedGridRowsSelector = createSelector< >( gridSelectionStateSelector, gridRowsLookupSelector, - (selectedRows, rowsLookup) => - new Map(Object.values(selectedRows).map((id) => [id, rowsLookup[id]])), + (selectedRows, rowsLookup) => new Map(selectedRows.map((id) => [id, rowsLookup[id]])), +); + +export const selectedIdsLookupSelector: OutputSelector< + GridState, + Record, + (res: GridSelectionState) => Record +> = createSelector>( + gridSelectionStateSelector, + (selection) => + selection.reduce((lookup, rowId) => { + lookup[rowId] = rowId; + return lookup; + }, {}), ); diff --git a/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionState.ts b/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionState.ts index 02e7d98206e0..28fd25e8c7bb 100644 --- a/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionState.ts +++ b/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionState.ts @@ -1,3 +1,3 @@ import { GridRowId } from '../../../models/gridRows'; -export type GridSelectionState = Record; +export type GridSelectionState = GridRowId[]; diff --git a/packages/grid/_modules_/grid/hooks/features/selection/useGridSelection.ts b/packages/grid/_modules_/grid/hooks/features/selection/useGridSelection.ts index 437d0ece8f6c..62cecbb4b681 100644 --- a/packages/grid/_modules_/grid/hooks/features/selection/useGridSelection.ts +++ b/packages/grid/_modules_/grid/hooks/features/selection/useGridSelection.ts @@ -1,42 +1,32 @@ import * as React from 'react'; -import { - GRID_ROW_CLICK, - GRID_ROW_SELECTED, - GRID_SELECTION_CHANGE, -} from '../../../constants/eventsConstants'; +import { GRID_ROW_CLICK, GRID_SELECTION_CHANGE } from '../../../constants/eventsConstants'; +import { GridComponentProps } from '../../../GridComponentProps'; import { GridApiRef } from '../../../models/api/gridApiRef'; import { GridSelectionApi } from '../../../models/api/gridSelectionApi'; import { GridRowParams } from '../../../models/params/gridRowParams'; -import { GridRowSelectedParams } from '../../../models/params/gridRowSelectedParams'; -import { GridSelectionModelChangeParams } from '../../../models/params/gridSelectionModelChangeParams'; import { GridRowId, GridRowModel } from '../../../models/gridRows'; import { GridSelectionModel } from '../../../models/gridSelectionModel'; -import { isDeepEqual } from '../../../utils/utils'; -import { useGridApiEventHandler, useGridApiOptionHandler } from '../../root/useGridApiEventHandler'; +import { useGridApiEventHandler } from '../../root/useGridApiEventHandler'; import { useGridApiMethod } from '../../root/useGridApiMethod'; import { optionsSelector } from '../../utils/optionsSelector'; import { useLogger } from '../../utils/useLogger'; import { useGridSelector } from '../core/useGridSelector'; import { useGridState } from '../core/useGridState'; import { gridRowsLookupSelector } from '../rows/gridRowsSelector'; -import { GridSelectionState } from './gridSelectionState'; -import { selectedGridRowsSelector } from './gridSelectionSelector'; +import { + gridSelectionStateSelector, + selectedGridRowsSelector, + selectedIdsLookupSelector, +} from './gridSelectionSelector'; -export const useGridSelection = (apiRef: GridApiRef): void => { +export const useGridSelection = (apiRef: GridApiRef, props: GridComponentProps): void => { const logger = useLogger('useGridSelection'); const [, setGridState, forceUpdate] = useGridState(apiRef); const options = useGridSelector(apiRef, optionsSelector); const rowsLookup = useGridSelector(apiRef, gridRowsLookupSelector); - const { - checkboxSelection, - disableMultipleSelection, - disableSelectionOnClick, - selectionModel, - isRowSelectable, - onRowSelected, - onSelectionModelChange, - } = options; + const { checkboxSelection, disableMultipleSelection, disableSelectionOnClick, isRowSelectable } = + options; const getSelectedRows = React.useCallback( () => selectedGridRowsSelector(apiRef.current.getState()), @@ -53,7 +43,7 @@ export const useGridSelection = (apiRef: GridApiRef): void => { const selectRowModel = React.useCallback( (rowModelParams: RowModelParams) => { - const { id, row, allowMultipleOverride, isSelected, isMultipleKey } = rowModelParams; + const { id, allowMultipleOverride, isSelected, isMultipleKey } = rowModelParams; if (isRowSelectable && !isRowSelectable(apiRef.current.getRowParams(id))) { return; @@ -62,43 +52,30 @@ export const useGridSelection = (apiRef: GridApiRef): void => { logger.debug(`Selecting row ${id}`); setGridState((state) => { - let selectionState: GridSelectionState = { ...state.selection }; + let selectionLookup = selectedIdsLookupSelector(state); const allowMultiSelect = allowMultipleOverride || (!disableMultipleSelection && isMultipleKey) || checkboxSelection; if (allowMultiSelect) { - const isRowSelected = isSelected == null ? selectionState[id] === undefined : isSelected; + const isRowSelected = isSelected == null ? selectionLookup[id] === undefined : isSelected; if (isRowSelected) { - selectionState[id] = id; + selectionLookup[id] = id; } else { - delete selectionState[id]; + delete selectionLookup[id]; } } else { const isRowSelected = - isSelected == null ? !isMultipleKey || selectionState[id] === undefined : isSelected; - selectionState = {}; + isSelected == null ? !isMultipleKey || selectionLookup[id] === undefined : isSelected; + selectionLookup = {}; if (isRowSelected) { - selectionState[id] = id; + selectionLookup[id] = id; } } - return { ...state, selection: selectionState }; + return { ...state, selection: Object.values(selectionLookup) }; }); forceUpdate(); - - const selectionState = apiRef!.current!.getState('selection'); - - const rowSelectedParam: GridRowSelectedParams = { - api: apiRef, - data: row, - isSelected: selectionState[id] !== undefined, - }; - const selectionChangeParam: GridSelectionModelChangeParams = { - selectionModel: Object.values(selectionState), - }; - apiRef.current.publishEvent(GRID_ROW_SELECTED, rowSelectedParam); - apiRef.current.publishEvent(GRID_SELECTION_CHANGE, selectionChangeParam); }, [ isRowSelectable, @@ -134,24 +111,18 @@ export const useGridSelection = (apiRef: GridApiRef): void => { } setGridState((state) => { - const selectionState: GridSelectionState = deSelectOthers ? {} : { ...state.selection }; + const selectionLookup = deSelectOthers ? {} : selectedIdsLookupSelector(state); selectableIds.forEach((id) => { if (isSelected) { - selectionState[id] = id; - } else if (selectionState[id] !== undefined) { - delete selectionState[id]; + selectionLookup[id] = id; + } else if (selectionLookup[id] !== undefined) { + delete selectionLookup[id]; } }); - return { ...state, selection: selectionState }; + return { ...state, selection: Object.values(selectionLookup) }; }); forceUpdate(); - - const params: GridSelectionModelChangeParams = { - selectionModel: Object.values(apiRef!.current!.getState('selection')), - }; - // We don't emit GRID_ROW_SELECTED on each row as it would be too consuming for large set of data. - apiRef.current.publishEvent(GRID_SELECTION_CHANGE, params); }, [ isRowSelectable, @@ -184,8 +155,6 @@ export const useGridSelection = (apiRef: GridApiRef): void => { ); useGridApiEventHandler(apiRef, GRID_ROW_CLICK, handleRowClick); - useGridApiOptionHandler(apiRef, GRID_ROW_SELECTED, onRowSelected); - useGridApiOptionHandler(apiRef, GRID_SELECTION_CHANGE, onSelectionModelChange); // TODO handle Cell Click/range selection? const selectionApi: GridSelectionApi = { @@ -197,17 +166,32 @@ export const useGridSelection = (apiRef: GridApiRef): void => { useGridApiMethod(apiRef, selectionApi, 'GridSelectionApi'); React.useEffect(() => { + apiRef.current.updateControlState({ + stateId: 'selection', + propModel: props.selectionModel, + propOnChange: props.onSelectionModelChange, + stateSelector: gridSelectionStateSelector, + onChangeCallback: (model: GridSelectionModel) => { + apiRef.current.publishEvent(GRID_SELECTION_CHANGE, model); + }, + }); + }, [apiRef, props.onSelectionModelChange, props.selectionModel]); + + React.useEffect(() => { + // Rows changed setGridState((state) => { - const newSelectionState = { ...state.selection }; + const newSelectionState = [...state.selection]; + const selectionLookup = selectedIdsLookupSelector(state); + let hasChanged = false; - Object.keys(newSelectionState).forEach((id: GridRowId) => { + newSelectionState.forEach((id: GridRowId) => { if (!rowsLookup[id]) { - delete newSelectionState[id]; + delete selectionLookup[id]; hasChanged = true; } }); if (hasChanged) { - return { ...state, selection: newSelectionState }; + return { ...state, selection: Object.values(selectionLookup) }; } return state; }); @@ -215,25 +199,31 @@ export const useGridSelection = (apiRef: GridApiRef): void => { }, [rowsLookup, apiRef, setGridState, forceUpdate]); React.useEffect(() => { - const currentModel = Object.values(apiRef.current.getState().selection); - if (!isDeepEqual(currentModel, selectionModel)) { - apiRef.current.setSelectionModel(selectionModel || []); + // prop selectionModel changed + if (props.selectionModel === undefined) { + return; + } + const currentModel = apiRef.current.getState().selection; + if (currentModel !== props.selectionModel) { + setGridState((state) => ({ ...state, selection: props.selectionModel || [] })); } - }, [apiRef, selectionModel]); + }, [apiRef, props.selectionModel, setGridState]); React.useEffect(() => { + // isRowSelectable changed setGridState((state) => { - const newSelectionState = { ...state.selection }; + const newSelectionState = [...state.selection]; + const selectionLookup = selectedIdsLookupSelector(state); let hasChanged = false; - Object.keys(newSelectionState).forEach((id: GridRowId) => { + newSelectionState.forEach((id: GridRowId) => { const isSelectable = !isRowSelectable || isRowSelectable(apiRef.current.getRowParams(id)); if (!isSelectable) { - delete newSelectionState[id]; + delete selectionLookup[id]; hasChanged = true; } }); if (hasChanged) { - return { ...state, selection: newSelectionState }; + return { ...state, selection: Object.values(selectionLookup) }; } return state; }); diff --git a/packages/grid/_modules_/grid/hooks/root/useGridApiMethod.ts b/packages/grid/_modules_/grid/hooks/root/useGridApiMethod.ts index 946667a04612..cca797fc9a0f 100644 --- a/packages/grid/_modules_/grid/hooks/root/useGridApiMethod.ts +++ b/packages/grid/_modules_/grid/hooks/root/useGridApiMethod.ts @@ -12,11 +12,10 @@ export function useGridApiMethod>( const apiMethodsRef = React.useRef(apiMethods); const [apiMethodsNames] = React.useState(Object.keys(apiMethods)); - React.useEffect(() => { - apiMethodsRef.current = apiMethods; - }, [apiMethods]); - - React.useEffect(() => { + const installMethods = React.useCallback(() => { + if (!apiRef.current) { + return; + } apiMethodsNames.forEach((methodName) => { if (!apiRef.current.hasOwnProperty(methodName)) { logger.debug(`Adding ${apiName}.${methodName} to apiRef`); @@ -24,4 +23,14 @@ export function useGridApiMethod>( } }); }, [apiMethodsNames, apiName, apiRef, logger]); + + React.useEffect(() => { + apiMethodsRef.current = apiMethods; + }, [apiMethods]); + + React.useEffect(() => { + installMethods(); + }, [installMethods]); + + installMethods(); } diff --git a/packages/grid/_modules_/grid/models/api/gridApi.ts b/packages/grid/_modules_/grid/models/api/gridApi.ts index 48fc530901d0..5b549803495c 100644 --- a/packages/grid/_modules_/grid/models/api/gridApi.ts +++ b/packages/grid/_modules_/grid/models/api/gridApi.ts @@ -1,23 +1,24 @@ +import { GridColumnApi } from './gridColumnApi'; import { GridColumnMenuApi } from './gridColumnMenuApi'; -import { GridFocusApi } from './gridFocusApi'; -import { GridParamsApi } from './gridParamsApi'; import { GridComponentsApi } from './gridComponentsApi'; -import { GridFilterApi } from './gridFilterApi'; +import { GridControlStateApi } from './gridControlStateApi'; +import { GridCoreApi } from './gridCoreApi'; +import { GridClipboardApi } from './gridClipboardApi'; +import { GridCsvExportApi } from './gridCsvExportApi'; +import { GridDensityApi } from './gridDensityApi'; import { GridEditRowApi } from './gridEditRowApi'; +import { GridEventsApi } from './gridEventsApi'; +import { GridFilterApi } from './gridFilterApi'; +import { GridFocusApi } from './gridFocusApi'; +import { GridLocaleTextApi } from './gridLocaleTextApi'; +import { GridPaginationApi } from './gridPaginationApi'; +import { GridParamsApi } from './gridParamsApi'; import { GridPreferencesPanelApi } from './gridPreferencesPanelApi'; import { GridRowApi } from './gridRowApi'; -import { GridColumnApi } from './gridColumnApi'; import { GridSelectionApi } from './gridSelectionApi'; import { GridSortApi } from './gridSortApi'; -import { GridPaginationApi } from './gridPaginationApi'; import { GridStateApi } from './gridStateApi'; import { GridVirtualizationApi } from './gridVirtualizationApi'; -import { GridCoreApi } from './gridCoreApi'; -import { GridEventsApi } from './gridEventsApi'; -import { GridDensityApi } from './gridDensityApi'; -import { GridLocaleTextApi } from './gridLocaleTextApi'; -import { GridCsvExportApi } from './gridCsvExportApi'; -import { GridClipboardApi } from './gridClipboardApi'; /** * The full grid API. @@ -42,4 +43,5 @@ export interface GridApi GridColumnMenuApi, GridPreferencesPanelApi, GridLocaleTextApi, + GridControlStateApi, GridClipboardApi {} diff --git a/packages/grid/_modules_/grid/models/api/gridControlStateApi.ts b/packages/grid/_modules_/grid/models/api/gridControlStateApi.ts new file mode 100644 index 000000000000..9eb2a12aa70b --- /dev/null +++ b/packages/grid/_modules_/grid/models/api/gridControlStateApi.ts @@ -0,0 +1,24 @@ +import { GridState } from '../../hooks/features/core/gridState'; +import { ControlStateItem } from '../controlStateItem'; + +/** + * The control state API interface that is available in the grid `apiRef`. + */ +export interface GridControlStateApi { + /** + * Updates a control state that binds the model, the onChange prop, and the grid state together. + * @param {ControlStateItem} controlState The [[ControlStateItem]] to be registered. + * @ignore - do not document. + */ + updateControlState: (controlState: ControlStateItem) => void; + /** + * Allows the internal grid state to apply the registered control state constraint. + * @param {GridState} state The new modified state that would be the next if the state is not controlled. + * @returns {shouldUpdate: boolean, postUpdate: () => void}, shouldUpdate let the state know if it should update, and postUpdate is a callback function triggered if the state has updated. + * @ignore - do not document. + */ + applyControlStateConstraint: (state: GridState) => { + shouldUpdate: boolean; + postUpdate: () => void; + }; +} diff --git a/packages/grid/_modules_/grid/models/api/index.ts b/packages/grid/_modules_/grid/models/api/index.ts index 7599251c833d..3b51def9aa47 100644 --- a/packages/grid/_modules_/grid/models/api/index.ts +++ b/packages/grid/_modules_/grid/models/api/index.ts @@ -19,4 +19,5 @@ export * from './gridFocusApi'; export * from './gridFilterApi'; export * from './gridColumnMenuApi'; export * from './gridPreferencesPanelApi'; +export * from './gridControlStateApi'; export * from './gridClipboardApi'; diff --git a/packages/grid/_modules_/grid/models/colDef/gridCheckboxSelection.tsx b/packages/grid/_modules_/grid/models/colDef/gridCheckboxSelection.tsx index 8c6efbbfebcc..0e7005d0de0d 100644 --- a/packages/grid/_modules_/grid/models/colDef/gridCheckboxSelection.tsx +++ b/packages/grid/_modules_/grid/models/colDef/gridCheckboxSelection.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { GridCellCheckboxRenderer } from '../../components/columnSelection/GridCellCheckboxRenderer'; import { GridHeaderCheckbox } from '../../components/columnSelection/GridHeaderCheckbox'; +import { selectedIdsLookupSelector } from '../../hooks/features/selection/gridSelectionSelector'; import { GridColDef } from './gridColDef'; import { GRID_BOOLEAN_COL_DEF } from './gridBooleanColDef'; @@ -13,7 +14,10 @@ export const gridCheckboxSelectionColDef: GridColDef = { sortable: false, filterable: false, disableColumnMenu: true, - valueGetter: (params) => params.api.getState().selection[params.id] !== undefined, + valueGetter: (params) => { + const selectionLookup = selectedIdsLookupSelector(params.api.getState()); + return selectionLookup[params.id] !== undefined; + }, renderHeader: (params) => , renderCell: (params) => , cellClassName: 'MuiDataGrid-cellCheckbox', diff --git a/packages/grid/_modules_/grid/models/controlStateItem.ts b/packages/grid/_modules_/grid/models/controlStateItem.ts new file mode 100644 index 000000000000..a79f00d618ac --- /dev/null +++ b/packages/grid/_modules_/grid/models/controlStateItem.ts @@ -0,0 +1,9 @@ +import { GridState } from '../hooks/features/core/gridState'; + +export interface ControlStateItem { + stateId: string; + propModel?: any; + stateSelector: (state: GridState) => TModel; + propOnChange?: (model: TModel) => void; + onChangeCallback?: (model: TModel) => void; +} diff --git a/packages/grid/_modules_/grid/models/gridOptions.tsx b/packages/grid/_modules_/grid/models/gridOptions.tsx index aa118a90b099..c199f43f45d0 100644 --- a/packages/grid/_modules_/grid/models/gridOptions.tsx +++ b/packages/grid/_modules_/grid/models/gridOptions.tsx @@ -7,14 +7,13 @@ import { getGridDefaultColumnTypes } from './colDef/gridDefaultColumnTypes'; import { GridDensity, GridDensityTypes } from './gridDensity'; import { GridEditRowsModel } from './gridEditRowModel'; import { GridFeatureMode, GridFeatureModeConstant } from './gridFeatureMode'; +import { GridRowId } from './gridRows'; import { Logger } from './logger'; import { GridCellParams } from './params/gridCellParams'; import { GridColumnHeaderParams } from './params/gridColumnHeaderParams'; import { GridFilterModelParams } from './params/gridFilterModelParams'; import { GridPageChangeParams } from './params/gridPageChangeParams'; import { GridRowParams } from './params/gridRowParams'; -import { GridRowSelectedParams } from './params/gridRowSelectedParams'; -import { GridSelectionModelChangeParams } from './params/gridSelectionModelChangeParams'; import { GridSortModelParams } from './params/gridSortModelParams'; import { GridSelectionModel } from './gridSelectionModel'; import { GridSortDirection, GridSortModel } from './gridSortModel'; @@ -407,11 +406,6 @@ export interface GridOptions { * @param event [[React.MouseEvent]]. */ onRowLeave?: (param: GridRowParams, event: React.MouseEvent) => void; - /** - * Callback fired when one row is selected. - * @param param With all properties from [[GridRowSelectedParams]]. - */ - onRowSelected?: (param: GridRowSelectedParams) => void; /** * Callback fired when the grid is resized. * @param param With all properties from [[GridResizeParams]]. @@ -419,9 +413,9 @@ export interface GridOptions { onResize?: (param: GridResizeParams) => void; /** * Callback fired when the selection state of one or multiple rows changes. - * @param param With all properties from [[SelectionChangeParams]]. + * @param selectionModel With all the row ids [[GridRowId]][]. */ - onSelectionModelChange?: (param: GridSelectionModelChangeParams) => void; + onSelectionModelChange?: (selectionModel: GridRowId[]) => void; /** * Callback fired when the sort model changes before a column is sorted. * @param param With all properties from [[GridSortModelParams]]. diff --git a/packages/grid/_modules_/grid/models/index.ts b/packages/grid/_modules_/grid/models/index.ts index 29988490a06f..640b081ce39f 100644 --- a/packages/grid/_modules_/grid/models/index.ts +++ b/packages/grid/_modules_/grid/models/index.ts @@ -11,6 +11,7 @@ export * from './gridRootContainerRef'; export * from './gridRenderContextProps'; export * from './gridRows'; export * from './gridSortModel'; +export * from './gridSelectionModel'; export * from './params'; export * from './gridCellClass'; export * from './gridCell'; diff --git a/packages/grid/_modules_/grid/models/params/gridRowSelectedParams.ts b/packages/grid/_modules_/grid/models/params/gridRowSelectedParams.ts deleted file mode 100644 index 265faf565005..000000000000 --- a/packages/grid/_modules_/grid/models/params/gridRowSelectedParams.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { GridRowModel } from '../gridRows'; - -/** - * Object passed as parameter as the row selected event handler. - */ -export interface GridRowSelectedParams { - /** - * The row data of the row that triggers the event. - */ - data: GridRowModel; - /** - * The selected state of the row that triggers the event. - */ - isSelected: boolean; - /** - * GridApiRef that let you manipulate the grid. - */ - api: any; -} diff --git a/packages/grid/_modules_/grid/models/params/index.ts b/packages/grid/_modules_/grid/models/params/index.ts index 1a46417fabdd..f22e87144c99 100644 --- a/packages/grid/_modules_/grid/models/params/index.ts +++ b/packages/grid/_modules_/grid/models/params/index.ts @@ -7,7 +7,6 @@ export * from './gridSlotComponentProps'; export * from './gridFilterModelParams'; export * from './gridPageChangeParams'; export * from './gridRowParams'; -export * from './gridRowSelectedParams'; export * from './gridScrollParams'; export * from './gridSelectionModelChangeParams'; export * from './gridSortModelParams'; diff --git a/packages/grid/_modules_/grid/useGridComponent.tsx b/packages/grid/_modules_/grid/useGridComponent.tsx index e6bf70b7de11..efdebb59a26f 100644 --- a/packages/grid/_modules_/grid/useGridComponent.tsx +++ b/packages/grid/_modules_/grid/useGridComponent.tsx @@ -3,6 +3,7 @@ import { useGridColumnMenu } from './hooks/features/columnMenu/useGridColumnMenu import { useGridColumnReorder } from './hooks/features/columnReorder/useGridColumnReorder'; import { useGridColumnResize } from './hooks/features/columnResize/useGridColumnResize'; import { useGridColumns } from './hooks/features/columns/useGridColumns'; +import { useGridControlState } from './hooks/features/core/useGridControlState'; import { useGridDensity } from './hooks/features/density/useGridDensity'; import { useGridCsvExport } from './hooks/features/export/useGridCsvExport'; import { useGridFilter } from './hooks/features/filter/useGridFilter'; @@ -38,6 +39,7 @@ export const useGridComponent = (apiRef: GridApiRef, props: GridComponentProps) useLoggerFactory(apiRef, props); useApi(apiRef); useErrorHandler(apiRef, props); + useGridControlState(apiRef); useGridScrollbarSizeDetector(apiRef, props); useOptionsProp(apiRef, props); useEvents(apiRef); @@ -51,7 +53,7 @@ export const useGridComponent = (apiRef: GridApiRef, props: GridComponentProps) useGridFocus(apiRef, props); useGridKeyboard(apiRef); useGridKeyboardNavigation(apiRef); - useGridSelection(apiRef); + useGridSelection(apiRef, props); useGridSorting(apiRef, props); useGridColumnMenu(apiRef); useGridPreferencesPanel(apiRef); diff --git a/packages/grid/data-grid/src/DataGrid.tsx b/packages/grid/data-grid/src/DataGrid.tsx index 682d6a7ff85a..98a64d79f75d 100644 --- a/packages/grid/data-grid/src/DataGrid.tsx +++ b/packages/grid/data-grid/src/DataGrid.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { chainPropTypes } from '@material-ui/utils'; -import { GridComponent, GridComponentProps, useThemeProps } from '../../_modules_/grid'; +import { GridComponent, GridComponentProps, GridRowId, useThemeProps } from '../../_modules_/grid'; const FORCED_PROPS: Partial = { disableColumnResize: true, @@ -29,6 +29,7 @@ export type DataGridProps = Omit< | 'onRowsScrollEnd' | 'pagination' | 'scrollEndThreshold' + | 'selectionModel' > & { apiRef?: undefined; checkboxSelectionVisibleOnly?: false; @@ -38,6 +39,7 @@ export type DataGridProps = Omit< disableMultipleColumnsSorting?: true; disableMultipleSelection?: true; onRowsScrollEnd?: undefined; + selectionModel?: GridRowId | GridRowId[]; pagination?: true; }; @@ -48,19 +50,25 @@ const DataGridRaw = React.forwardRef(function Dat ref, ) { const props = useThemeProps({ props: inProps, name: 'MuiDataGrid' }); - const { pageSize: pageSizeProp, ...other } = props; + const { pageSize: pageSizeProp, selectionModel: dataGridSelectionModel, ...other } = props; let pageSize = pageSizeProp; if (pageSize && pageSize > MAX_PAGE_SIZE) { pageSize = MAX_PAGE_SIZE; } + const selectionModel = + dataGridSelectionModel !== undefined && !Array.isArray(dataGridSelectionModel) + ? [dataGridSelectionModel] + : dataGridSelectionModel; + return ( ); @@ -227,4 +235,22 @@ DataGrid.propTypes = { } return null; }), + selectionModel: chainPropTypes( + PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.array]), + (props: any) => { + if (Array.isArray(props.selectionModel) && props.selectionModel.length > 1) { + return new Error( + [ + `Material-UI: \`\` is not a valid prop.`, + 'selectionModel can only be of 1 item in DataGrid.', + '', + 'You need to upgrade to the XGrid component to unlock multiple selection.', + ].join('\n'), + ); + } + return null; + }, + ), } as any; diff --git a/packages/grid/data-grid/src/tests/selection.DataGrid.test.tsx b/packages/grid/data-grid/src/tests/selection.DataGrid.test.tsx index 850f4c79572e..4860fd22e06a 100644 --- a/packages/grid/data-grid/src/tests/selection.DataGrid.test.tsx +++ b/packages/grid/data-grid/src/tests/selection.DataGrid.test.tsx @@ -195,7 +195,7 @@ describe(' - Selection', () => { }, ]} columns={[{ field: 'brand', width: 100 }]} - selectionModel={[1]} + selectionModel={1} />
, ); @@ -234,7 +234,7 @@ describe(' - Selection', () => { const row0 = getRow(0); expect(row0).to.have.class('Mui-selected'); - setProps({ selectionModel: [1] }); + setProps({ selectionModel: 1 }); // TODO fix this assertion. The model is forced from the outside, hence shouldn't change. // https://github.com/mui-org/material-ui-x/issues/190 expect(row0).not.to.have.class('Mui-selected'); @@ -265,15 +265,13 @@ describe(' - Selection', () => {
); } - const { setProps } = render(); - expect(onSelectionModelChange.callCount).to.equal(1); - expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equals([0]); - setProps({ selectionModel: [0, 1] }); - expect(onSelectionModelChange.callCount).to.equal(2); - expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equals([0, 1]); - setProps({ selectionModel: [0, 1] }); - expect(onSelectionModelChange.callCount).to.equal(2); - expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equals([0, 1]); + const { setProps } = render(); + expect(onSelectionModelChange.callCount).to.equal(0); + const firstRow = getRow(0); + expect(firstRow).to.have.class('Mui-selected'); + setProps({ selectionModel: 0 }); + expect(onSelectionModelChange.callCount).to.equal(0); + expect(getRow(0)).to.have.class('Mui-selected'); }); it('should filter out unselectable rows when the selectionModel prop changes', () => { @@ -290,7 +288,7 @@ describe(' - Selection', () => { }, ], columns: [{ field: 'brand', width: 100 }], - selectionModel: [1], + selectionModel: 1, isRowSelectable: (params) => params.id > 0, }; @@ -306,8 +304,8 @@ describe(' - Selection', () => { expect(getRow(0)).not.to.have.class('Mui-selected'); expect(getRow(1)).to.have.class('Mui-selected'); - setProps({ selectionModel: [0] }); - expect(getRow(0)).not.to.have.class('Mui-selected'); + setProps({ selectionModel: 0 }); + expect(getRow(0)).to.have.class('Mui-selected'); expect(getRow(1)).not.to.have.class('Mui-selected'); }); }); @@ -327,7 +325,6 @@ describe(' - Selection', () => { }, ], columns: [{ field: 'brand', width: 100 }], - selectionModel: [0], isRowSelectable: () => true, }; @@ -340,6 +337,7 @@ describe(' - Selection', () => { } const { setProps } = render(); + fireEvent.click(getRow(0)); expect(getRow(0)).to.have.class('Mui-selected'); expect(getRow(1)).not.to.have.class('Mui-selected'); diff --git a/packages/grid/x-grid/src/tests/events.XGrid.test.tsx b/packages/grid/x-grid/src/tests/events.XGrid.test.tsx index 0cb26c161477..0d25316d7f21 100644 --- a/packages/grid/x-grid/src/tests/events.XGrid.test.tsx +++ b/packages/grid/x-grid/src/tests/events.XGrid.test.tsx @@ -211,7 +211,7 @@ describe(' - Events Params', () => { const stopClick = (params, event) => { event.stopPropagation(); }; - render(); + render(); const cell11 = getCell(1, 1); fireEvent.click(cell11); @@ -219,18 +219,21 @@ describe(' - Events Params', () => { }); it('should select a row by default', () => { - render(); + const handleSelection = spy(); + render(); const cell11 = getCell(1, 1); fireEvent.click(cell11); - expect(eventStack).to.deep.equal(['rowSelected']); + expect(handleSelection.callCount).to.equal(1); + expect(handleSelection.lastCall.firstArg).to.deep.equal([2]); }); it('should not select a row if options.disableSelectionOnClick', () => { - render(); + const handleSelection = spy(); + render(); const cell11 = getCell(1, 1); fireEvent.click(cell11); - expect(eventStack).to.deep.equal([]); + expect(handleSelection.callCount).to.equal(0); }); }); it('publishing GRID_ROWS_SCROLL should call onRowsScrollEnd callback', () => { diff --git a/packages/grid/x-grid/src/tests/filtering.XGrid.test.tsx b/packages/grid/x-grid/src/tests/filtering.XGrid.test.tsx index e8aed2bdf58f..d527e7d5e9d5 100644 --- a/packages/grid/x-grid/src/tests/filtering.XGrid.test.tsx +++ b/packages/grid/x-grid/src/tests/filtering.XGrid.test.tsx @@ -248,7 +248,7 @@ describe(' - Filter', () => { render(); const checkAllCell = getColumnHeaderCell(0).querySelector('input'); fireEvent.click(checkAllCell); - expect(apiRef.current.getState().selection).to.deep.equal({ 1: 1 }); + expect(apiRef.current.getState().selection).to.deep.equal([1]); }); it('should allow to clear filters by passing an empty filter model', () => { diff --git a/packages/grid/x-grid/src/tests/selection.XGrid.test.tsx b/packages/grid/x-grid/src/tests/selection.XGrid.test.tsx index 92492923eeb3..77e56801e973 100644 --- a/packages/grid/x-grid/src/tests/selection.XGrid.test.tsx +++ b/packages/grid/x-grid/src/tests/selection.XGrid.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; -import { getColumnValues, getRow } from 'test/utils/helperFn'; +import { getCell, getColumnValues, getRow } from 'test/utils/helperFn'; import { // @ts-expect-error need to migrate helpers to TypeScript screen, @@ -9,7 +9,13 @@ import { // @ts-expect-error need to migrate helpers to TypeScript fireEvent, } from 'test/utils'; -import { GridApiRef, GridComponentProps, useGridApiRef, XGrid } from '@material-ui/x-grid'; +import { + GridApiRef, + GridComponentProps, + GridSelectionModel, + useGridApiRef, + XGrid, +} from '@material-ui/x-grid'; const isJSDOM = /jsdom/.test(window.navigator.userAgent); @@ -52,17 +58,18 @@ describe(' - Selection', () => { }; describe('getSelectedRows', () => { - it('should return the latest values when called inside onSelectionModelChange', () => { + it('should not change before onSelectionModelChange', () => { render( { - expect(apiRef!.current.getSelectedRows().size).to.equal(1); - expect(apiRef!.current.getSelectedRows().get(1)).to.equal(baselineProps.rows[1]); + onSelectionModelChange={(model) => { + expect(apiRef!.current.getSelectedRows().size).to.equal(0); + expect(model).to.deep.equal([1]); }} />, ); expect(apiRef!.current.getSelectedRows().size).to.equal(0); apiRef!.current.selectRow(1); + expect(apiRef!.current.getSelectedRows().get(1)).to.equal(baselineProps.rows[1]); }); }); @@ -71,14 +78,14 @@ describe(' - Selection', () => { const onSelectionModelChange = spy(); render(); apiRef!.current.selectRow(1); - expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([1]); + expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([1]); apiRef!.current.selectRow(2); - expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([2]); + expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([2]); // Keep old selection apiRef!.current.selectRow(3, true, true); - expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([2, 3]); + expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([2, 3]); apiRef!.current.selectRow(3, false, true); - expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([2]); + expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([2]); }); it('should not call onSelectionModelChange if the row is unselectable', () => { @@ -101,14 +108,14 @@ describe(' - Selection', () => { const onSelectionModelChange = spy(); render(); apiRef!.current.selectRows([1, 2]); - expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([1, 2]); + expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([1, 2]); apiRef!.current.selectRows([3]); - expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([1, 2, 3]); + expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([1, 2, 3]); apiRef!.current.selectRows([1, 2], false); - expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([3]); + expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([3]); // Deselect others apiRef!.current.selectRows([4, 5], true, true); - expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([4, 5]); + expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([4, 5]); }); it('should filter out unselectable rows before calling onSelectionModelChange', () => { @@ -120,12 +127,29 @@ describe(' - Selection', () => { />, ); apiRef!.current.selectRows([0, 1, 2]); - expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([1, 2]); + expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([1, 2]); }); }); it('should clean the selected ids when the rows prop changes', () => { - const { setProps } = render(); + const DemoTest = (props: Partial) => { + apiRef = useGridApiRef(); + const [selectionModelState, setSelectionModelState] = React.useState(props.selectionModel); + const handleSelectionChange = (model) => setSelectionModelState(model); + return ( +
+ +
+ ); + }; + + const { setProps } = render(); expect(getSelectedRows(apiRef)).to.deep.equal([0, 1, 2]); setProps({ rows: [ @@ -191,4 +215,67 @@ describe(' - Selection', () => { fireEvent.click(screen.getByRole('button', { name: /next page/i })); expect(getRow(1)).not.to.have.class('Mui-selected'); }); + + describe('control Selection', () => { + it('should update the selection state when neither the model nor the onChange are set', () => { + render(); + fireEvent.click(getCell(0, 0)); + expect(getRow(0)).to.have.class('Mui-selected'); + }); + + it('should not update the selection model when the selectionModelProp is set', () => { + const selectionModel: GridSelectionModel = [1]; + render(); + + expect(getRow(0)).not.to.have.class('Mui-selected'); + expect(getRow(1)).to.have.class('Mui-selected'); + fireEvent.click(getCell(0, 0)); + expect(getRow(0)).not.to.have.class('Mui-selected'); + }); + + it('should update the selection state when the model is not set, but the onChange is set', () => { + const onModelChange = spy(); + render(); + + fireEvent.click(getCell(0, 0)); + expect(getRow(0)).to.have.class('Mui-selected'); + expect(onModelChange.callCount).to.equal(1); + expect(onModelChange.firstCall.firstArg).to.deep.equal([0]); + }); + + it('should control selection state when the model and the onChange are set', () => { + const ControlCase = (props: Partial) => { + const { rows, columns, ...others } = props; + const [selectionModel, setSelectionModel] = React.useState([0]); + const handleSelectionChange = (newModel) => { + if (newModel.length) { + setSelectionModel([...newModel, 2]); + return; + } + setSelectionModel(newModel); + }; + + return ( +
+ +
+ ); + }; + + render(); + + expect(getRow(0)).to.have.class('Mui-selected'); + fireEvent.click(getCell(1, 0)); + expect(getRow(0)).not.to.have.class('Mui-selected'); + expect(getRow(1)).to.have.class('Mui-selected'); + expect(getRow(2)).to.have.class('Mui-selected'); + }); + }); }); diff --git a/packages/storybook/src/stories/grid-events.stories.tsx b/packages/storybook/src/stories/grid-events.stories.tsx index bba60ff57be9..04c115ae4772 100644 --- a/packages/storybook/src/stories/grid-events.stories.tsx +++ b/packages/storybook/src/stories/grid-events.stories.tsx @@ -23,7 +23,6 @@ export function AllEvents() { onCellOver: (params) => action('onCellOver')(params), onRowOver: (params) => action('onRowOver')(params), onColumnHeaderClick: (params) => action('onColumnHeaderClick')(params), - onRowSelected: (params) => action('onRowSelected')(params), onSelectionModelChange: (params) => action('onSelectionChange', { depth: 1 })(params), onPageChange: (params) => action('onPageChange')(params), onPageSizeChange: (params) => action('onPageSizeChange')(params), diff --git a/packages/storybook/src/stories/grid-selection.stories.tsx b/packages/storybook/src/stories/grid-selection.stories.tsx index 6d401e5f214a..df00c45bb680 100644 --- a/packages/storybook/src/stories/grid-selection.stories.tsx +++ b/packages/storybook/src/stories/grid-selection.stories.tsx @@ -1,13 +1,7 @@ import { useDemoData } from '@material-ui/x-grid-data-generator'; import * as React from 'react'; import { action } from '@storybook/addon-actions'; -import { - XGrid, - GridOptionsProp, - useGridApiRef, - GridSelectionModelChangeParams, - GridRowId, -} from '@material-ui/x-grid'; +import { XGrid, GridOptionsProp, useGridApiRef, GridRowId } from '@material-ui/x-grid'; import { getData, GridData } from '../data/data-service'; import { useData } from '../hooks/useData'; @@ -45,7 +39,6 @@ export const EventsMapped = () => { const options: GridOptionsProp = { onSelectionModelChange: (params) => action('onSelectionChange', { depth: 1 })(params), - onRowSelected: (params) => action('onRowSelected')(params), }; return ; @@ -79,8 +72,8 @@ export function HandleSelection() { const [selectionModel, setSelectionModel] = React.useState([]); const handleSelection = React.useCallback( - (params: GridSelectionModelChangeParams) => { - setSelectionModel(params.selectionModel); + (model) => { + setSelectionModel(model); }, [setSelectionModel], ); @@ -112,3 +105,87 @@ export const UnselectableRows = () => { /> ); }; +export function ControlSelection() { + const [storyState] = React.useState({ + rows: [ + { + id: 0, + brand: 'Nike', + isPublished: false, + }, + { + id: 1, + brand: 'Adidas', + isPublished: true, + }, + { + id: 2, + brand: 'Puma', + isPublished: true, + }, + ], + columns: [{ field: 'brand' }, { field: 'isPublished', type: 'boolean' }], + }); + + const [selectionModel, setSelectionModel] = React.useState([0]); + const handleSelectionChange = React.useCallback((newModel) => { + setSelectionModel(newModel); + }, []); + + return ( +
+ +
+ ); +} +export function NoControlSelection() { + const [storyState] = React.useState({ + rows: [ + { + id: 0, + brand: 'Nike', + isPublished: false, + }, + { + id: 1, + brand: 'Adidas', + isPublished: true, + }, + { + id: 2, + brand: 'Puma', + isPublished: true, + }, + ], + columns: [{ field: 'brand' }, { field: 'isPublished', type: 'boolean' }], + }); + + return ( +
+ +
+ ); +} +export function LargeControlSelection() { + const { data } = useDemoData({ rowLength: 500000, dataSet: 'Commodity', maxColumns: 100 }); + + const [selectionModel, setSelectionModel] = React.useState([]); + const handleSelectionChange = React.useCallback((newModel) => { + setSelectionModel(newModel); + }, []); + + return ( + + ); +} diff --git a/packages/storybook/src/stories/grid-streaming.stories.tsx b/packages/storybook/src/stories/grid-streaming.stories.tsx index 07e2cb7ea78c..d25156b4fd22 100644 --- a/packages/storybook/src/stories/grid-streaming.stories.tsx +++ b/packages/storybook/src/stories/grid-streaming.stories.tsx @@ -20,7 +20,6 @@ export default { export const SlowUpdateGrid = () => { const options: GridOptionsProp = { onSelectionModelChange: (params) => action('onSelectionChange', { depth: 1 })(params), - onRowSelected: (params) => action('onRowSelected')(params), }; const rate = { min: 1000, max: 5000 }; return ( @@ -35,7 +34,6 @@ export const SlowUpdateGrid = () => { export const FastUpdateGrid = () => { const options: GridOptionsProp = { onSelectionModelChange: (params) => action('onSelectionChange', { depth: 1 })(params), - onRowSelected: (params) => action('onRowSelected')(params), }; const rate = { min: 100, max: 500 }; return ( @@ -50,7 +48,6 @@ export const FastUpdateGrid = () => { export const SingleSubscriptionFast = () => { const options: GridOptionsProp = { onSelectionModelChange: (params) => action('onSelectionChange', { depth: 1 })(params), - onRowSelected: (params) => action('onRowSelected')(params), }; const rate = { min: 100, max: 500 }; return (