diff --git a/docs/data/data-grid/filtering-recipes/UseNonNativeSelect.js b/docs/data/data-grid/filtering-recipes/UseNonNativeSelect.js deleted file mode 100644 index 53ee4312c7d4..000000000000 --- a/docs/data/data-grid/filtering-recipes/UseNonNativeSelect.js +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import { DataGrid } from '@mui/x-data-grid'; -import { useDemoData } from '@mui/x-data-grid-generator'; - -const VISIBLE_FIELDS = ['name', 'rating', 'country', 'dateCreated', 'isAdmin']; - -export default function UseNonNativeSelect() { - const { data } = useDemoData({ - dataSet: 'Employee', - visibleFields: VISIBLE_FIELDS, - rowLength: 100, - }); - - return ( -
- -
- ); -} diff --git a/docs/data/data-grid/filtering-recipes/UseNonNativeSelect.tsx b/docs/data/data-grid/filtering-recipes/UseNonNativeSelect.tsx deleted file mode 100644 index 53ee4312c7d4..000000000000 --- a/docs/data/data-grid/filtering-recipes/UseNonNativeSelect.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import { DataGrid } from '@mui/x-data-grid'; -import { useDemoData } from '@mui/x-data-grid-generator'; - -const VISIBLE_FIELDS = ['name', 'rating', 'country', 'dateCreated', 'isAdmin']; - -export default function UseNonNativeSelect() { - const { data } = useDemoData({ - dataSet: 'Employee', - visibleFields: VISIBLE_FIELDS, - rowLength: 100, - }); - - return ( -
- -
- ); -} diff --git a/docs/data/data-grid/filtering-recipes/UseNonNativeSelect.tsx.preview b/docs/data/data-grid/filtering-recipes/UseNonNativeSelect.tsx.preview deleted file mode 100644 index f042b7c7db38..000000000000 --- a/docs/data/data-grid/filtering-recipes/UseNonNativeSelect.tsx.preview +++ /dev/null @@ -1,8 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/data-grid/filtering-recipes/filtering-recipes.md b/docs/data/data-grid/filtering-recipes/filtering-recipes.md index 6ff6d103ae59..02e7115a12b7 100644 --- a/docs/data/data-grid/filtering-recipes/filtering-recipes.md +++ b/docs/data/data-grid/filtering-recipes/filtering-recipes.md @@ -13,9 +13,3 @@ Currently if you want to use the [Quick filter](/x/react-data-grid/filtering/qui A common use case is to have certain components positioned outside of the grid. Because of the way the grid context works this might not be a straightforward thing to do. The example below illustrates how this use case can be achieved. {{"demo": "QuickFilterOutsideOfGrid.js", "bg": "inline", "defaultCodeOpen": false}} - -## Use non-native select in filter panel - -If you do not want to use the native select in the filtering panel you can switch it to the `@mui/material/Select` component by using the `slotProps` property. - -{{"demo": "UseNonNativeSelect.js", "bg": "inline", "defaultCodeOpen": false}} diff --git a/docs/data/migration/migration-data-grid-v6/migration-data-grid-v6.md b/docs/data/migration/migration-data-grid-v6/migration-data-grid-v6.md index fd5f2221b27f..f0c60e7c976a 100644 --- a/docs/data/migration/migration-data-grid-v6/migration-data-grid-v6.md +++ b/docs/data/migration/migration-data-grid-v6/migration-data-grid-v6.md @@ -77,6 +77,26 @@ Below are described the steps you need to make to migrate from v6 to v7. - The deprecated props `components` and `componentsProps` have been removed. Use `slots` and `slotProps` instead. See [components section](/x/react-data-grid/components/) for more details. - The `slots.preferencesPanel` slot and the `slotProps.preferencesPanel` prop were removed. Use `slots.panel` and `slotProps.panel` instead. +- The `getOptionValue` and `getOptionLabel` props were removed from the following components: + + - `GridEditSingleSelectCell` + - `GridFilterInputSingleSelect` + - `GridFilterInputMultipleSingleSelect` + + Use the `getOptionValue` and `getOptionLabel` properties on the `singleSelect` column definition instead: + + ```tsx + const column: GridColDef = { + type: 'singleSelect', + field: 'country', + valueOptions: [ + { code: 'BR', name: 'Brazil' }, + { code: 'FR', name: 'France' }, + ], + getOptionValue: (value: any) => value.code, + getOptionLabel: (value: any) => value.name, + }; + ``` ### State access @@ -208,6 +228,8 @@ Below are described the steps you need to make to migrate from v6 to v7. | `unstable_gridHeaderFilteringStateSelector` | `gridHeaderFilteringStateSelector` | | `unstable_gridTabIndexColumnHeaderFilterSelector` | `gridTabIndexColumnHeaderFilterSelector` | +- The filter panel no longer uses the native version of the [`Select`](https://mui.com/material-ui/react-select/) component for all components. + diff --git a/packages/grid/x-data-grid-premium/src/tests/rowGrouping.DataGridPremium.test.tsx b/packages/grid/x-data-grid-premium/src/tests/rowGrouping.DataGridPremium.test.tsx index 60fb45c1802e..0075fab62b38 100644 --- a/packages/grid/x-data-grid-premium/src/tests/rowGrouping.DataGridPremium.test.tsx +++ b/packages/grid/x-data-grid-premium/src/tests/rowGrouping.DataGridPremium.test.tsx @@ -13,6 +13,7 @@ import { getColumnHeadersTextContent, getColumnValues, getCell, + getSelectByName, } from 'test/utils/helperFn'; import { expect } from 'chai'; import { @@ -2121,7 +2122,7 @@ describe(' - Row grouping', () => { />, ); - fireEvent.change(screen.getByRole('combobox', { name: 'Operator' }), { + fireEvent.change(getSelectByName('Operator'), { target: { value: '>' }, }); fireEvent.change(screen.getByRole('spinbutton', { name: 'Value' }), { @@ -2147,7 +2148,7 @@ describe(' - Row grouping', () => { />, ); - fireEvent.change(screen.getByRole('combobox', { name: 'Operator' }), { + fireEvent.change(getSelectByName('Operator'), { target: { value: '>' }, }); fireEvent.change(screen.getByRole('spinbutton', { name: 'Value' }), { @@ -2375,7 +2376,7 @@ describe(' - Row grouping', () => { />, ); - fireEvent.change(screen.getByRole('combobox', { name: 'Operator' }), { + fireEvent.change(getSelectByName('Operator'), { target: { value: '>' }, }); fireEvent.change(screen.getByRole('spinbutton', { name: 'Value' }), { @@ -2402,7 +2403,7 @@ describe(' - Row grouping', () => { />, ); - fireEvent.change(screen.getByRole('combobox', { name: 'Operator' }), { + fireEvent.change(getSelectByName('Operator'), { target: { value: '>' }, }); fireEvent.change(screen.getByRole('spinbutton', { name: 'Value' }), { diff --git a/packages/grid/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx b/packages/grid/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx index 5cb84a07d210..09fad0bdbba0 100644 --- a/packages/grid/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx +++ b/packages/grid/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx @@ -141,8 +141,10 @@ const GridHeaderFilterCell = React.forwardRef { if (!hasFocus) { - if (inputRef.current && inputRef.current.contains(event.target as HTMLElement)) { + if (inputRef.current?.contains?.(event.target as HTMLElement)) { inputRef.current.focus(); } apiRef.current.setColumnHeaderFilterFocus(colDef.field, event); diff --git a/packages/grid/x-data-grid-pro/src/tests/editComponents.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/editComponents.DataGridPro.test.tsx index 15f9d1928c5a..abeab01c4098 100644 --- a/packages/grid/x-data-grid-pro/src/tests/editComponents.DataGridPro.test.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/editComponents.DataGridPro.test.tsx @@ -531,22 +531,6 @@ describe(' - Edit components', () => { }); }); - it('should apply getOptionLabel to the options provided', () => { - defaultData.columns[0].renderEditCell = (params) => { - return renderEditSingleSelectCell({ - ...params, - getOptionLabel: (value) => (value as string).toLowerCase(), - }); - }; - render(); - - const cell = getCell(0, 0); - fireEvent.doubleClick(cell); - - expect(screen.queryAllByRole('option')[0]).to.have.text('nike'); - expect(screen.queryAllByRole('option')[1]).to.have.text('adidas'); - }); - it('should pass the value prop to the select', async () => { render(); diff --git a/packages/grid/x-data-grid-pro/src/tests/filtering.DataGridPro.test.tsx b/packages/grid/x-data-grid-pro/src/tests/filtering.DataGridPro.test.tsx index 0e837228980e..6427e742118f 100644 --- a/packages/grid/x-data-grid-pro/src/tests/filtering.DataGridPro.test.tsx +++ b/packages/grid/x-data-grid-pro/src/tests/filtering.DataGridPro.test.tsx @@ -19,7 +19,7 @@ import { createRenderer, fireEvent, screen, act, within } from '@mui-internal/te import { expect } from 'chai'; import * as React from 'react'; import { spy } from 'sinon'; -import { getColumnHeaderCell, getColumnValues } from 'test/utils/helperFn'; +import { getColumnHeaderCell, getColumnValues, getSelectInput } from 'test/utils/helperFn'; const SUBMIT_FILTER_STROKE_TIME = DATA_GRID_PRO_PROPS_DEFAULT_VALUES.filterDebounceMs; @@ -170,10 +170,10 @@ describe(' - Filter', () => { />, ); - const selectListOfColumns = document.querySelectorAll( - '.MuiDataGrid-filterFormColumnInput', - )[0]; - const availableColumns = within(selectListOfColumns).getAllByRole('option'); + const select = screen.getByRole('combobox', { name: 'Columns' }); + fireEvent.mouseDown(select); + const listbox = screen.getByRole('listbox', { name: 'Columns' }); + const availableColumns = within(listbox).getAllByRole('option'); expect(availableColumns.length).to.equal(1); }); @@ -437,10 +437,12 @@ describe(' - Filter', () => { // The first combo is hidden and we include hidden elements to make the query faster // https://github.com/testing-library/dom-testing-library/issues/820#issuecomment-726936225 - const select = screen.queryAllByRole('combobox', { name: 'Logic operator', hidden: true })[ - isJSDOM ? 1 : 0 // https://github.com/testing-library/dom-testing-library/issues/846 - ]; - fireEvent.change(select, { target: { value: 'or' } }); + const input = getSelectInput( + screen.queryAllByRole('combobox', { name: 'Logic operator', hidden: true })[ + isJSDOM ? 1 : 0 // https://github.com/testing-library/dom-testing-library/issues/846 + ], + ); + fireEvent.change(input!, { target: { value: 'or' } }); expect(onFilterModelChange.callCount).to.equal(1); expect(onFilterModelChange.lastCall.args[1].reason).to.equal('changeLogicOperator'); expect(getColumnValues(0)).to.deep.equal([]); diff --git a/packages/grid/x-data-grid/src/colDef/gridSingleSelectColDef.tsx b/packages/grid/x-data-grid/src/colDef/gridSingleSelectColDef.tsx index 0a4514541b3b..44f13cc77952 100644 --- a/packages/grid/x-data-grid/src/colDef/gridSingleSelectColDef.tsx +++ b/packages/grid/x-data-grid/src/colDef/gridSingleSelectColDef.tsx @@ -2,7 +2,10 @@ import { GRID_STRING_COL_DEF } from './gridStringColDef'; import { GridSingleSelectColDef, ValueOptions } from '../models/colDef/gridColDef'; import { renderEditSingleSelectCell } from '../components/cell/GridEditSingleSelectCell'; import { getGridSingleSelectOperators } from './gridSingleSelectOperators'; -import { isSingleSelectColDef } from '../components/panel/filterPanel/filterPanelUtils'; +import { + getValueOptions, + isSingleSelectColDef, +} from '../components/panel/filterPanel/filterPanelUtils'; import { isObject } from '../utils/utils'; const isArrayOfObjects = (options: any): options is Array> => { @@ -30,13 +33,7 @@ export const GRID_SINGLE_SELECT_COL_DEF: Omit = return ''; } - let valueOptions: Array; - if (typeof colDef.valueOptions === 'function') { - valueOptions = colDef.valueOptions!({ id, row: id ? api.getRow(id) : null, field }); - } else { - valueOptions = colDef.valueOptions!; - } - + const valueOptions = getValueOptions(colDef, { id, row: id ? api.getRow(id) : null }); if (value == null) { return ''; } @@ -56,12 +53,8 @@ export const GRID_SINGLE_SELECT_COL_DEF: Omit = filterOperators: getGridSingleSelectOperators(), // @ts-ignore pastedValueParser: (value, params) => { - const colDef = params.colDef; - const colDefValueOptions = (colDef as GridSingleSelectColDef).valueOptions; - const valueOptions = - typeof colDefValueOptions === 'function' - ? colDefValueOptions({ field: colDef.field }) - : colDefValueOptions || []; + const colDef = params.colDef as GridSingleSelectColDef; + const valueOptions = getValueOptions(colDef) || []; const getOptionValue = (colDef as GridSingleSelectColDef).getOptionValue!; const valueOption = valueOptions.find((option) => { if (getOptionValue(option) === value) { diff --git a/packages/grid/x-data-grid/src/components/cell/GridEditSingleSelectCell.tsx b/packages/grid/x-data-grid/src/components/cell/GridEditSingleSelectCell.tsx index dd4fe8940fbb..0f0ac6412031 100644 --- a/packages/grid/x-data-grid/src/components/cell/GridEditSingleSelectCell.tsx +++ b/packages/grid/x-data-grid/src/components/cell/GridEditSingleSelectCell.tsx @@ -7,17 +7,16 @@ import { GridRenderEditCellParams } from '../../models/params/gridCellParams'; import { isEscapeKey } from '../../utils/keyboardUtils'; import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; import { GridEditModes } from '../../models/gridEditRowModel'; -import { GridSingleSelectColDef, ValueOptions } from '../../models/colDef/gridColDef'; import { getValueFromValueOptions, + getValueOptions, isSingleSelectColDef, } from '../panel/filterPanel/filterPanelUtils'; import { useGridApiContext } from '../../hooks/utils/useGridApiContext'; export interface GridEditSingleSelectCellProps extends GridRenderEditCellParams, - Omit, - Pick { + Omit { /** * Callback called when the value is changed by the user. * @param {SelectChangeEvent} event The event source of the callback. @@ -56,8 +55,6 @@ function GridEditSingleSelectCell(props: GridEditSingleSelectCellProps) { error, onValueChange, initialOpen = rootProps.editMode === GridEditModes.Cell, - getOptionLabel: getOptionLabelProp, - getOptionValue: getOptionValueProp, ...other } = props; @@ -80,19 +77,13 @@ function GridEditSingleSelectCell(props: GridEditSingleSelectCellProps) { return null; } - let valueOptions: Array | undefined; - if (typeof colDef?.valueOptions === 'function') { - valueOptions = colDef?.valueOptions({ id, row, field }); - } else { - valueOptions = colDef?.valueOptions; - } - + const valueOptions = getValueOptions(colDef, { id, row }); if (!valueOptions) { return null; } - const getOptionValue = getOptionValueProp || colDef.getOptionValue!; - const getOptionLabel = getOptionLabelProp || colDef.getOptionLabel!; + const getOptionValue = colDef.getOptionValue!; + const getOptionLabel = colDef.getOptionLabel!; const handleChange: SelectProps['onChange'] = async (event) => { if (!isSingleSelectColDef(colDef) || !valueOptions) { @@ -204,18 +195,6 @@ GridEditSingleSelectCell.propTypes = { * The cell value formatted with the column valueFormatter. */ formattedValue: PropTypes.any, - /** - * Used to determine the label displayed for a given value option. - * @param {ValueOptions} value The current value option. - * @returns {string} The text to be displayed. - */ - getOptionLabel: PropTypes.func, - /** - * Used to determine the value used for a value option. - * @param {ValueOptions} value The current value option. - * @returns {string} The value to be used. - */ - getOptionValue: PropTypes.func, /** * If true, the cell is the active element. */ diff --git a/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterForm.tsx b/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterForm.tsx index ce527a1affac..691e3946aa1c 100644 --- a/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterForm.tsx +++ b/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterForm.tsx @@ -16,7 +16,12 @@ import { useGridApiContext } from '../../../hooks/utils/useGridApiContext'; import { useGridRootProps } from '../../../hooks/utils/useGridRootProps'; import type { DataGridProcessedProps } from '../../../models/props/DataGridProps'; import { getDataGridUtilityClass } from '../../../constants/gridClasses'; -import { GridColDef, GridStateColDef } from '../../../models/colDef/gridColDef'; +import { + GridColDef, + GridSingleSelectColDef, + GridStateColDef, +} from '../../../models/colDef/gridColDef'; +import { getValueFromValueOptions, getValueOptions } from './filterPanelUtils'; export interface FilterColumnsArgs { field: GridColDef['field']; @@ -233,16 +238,17 @@ const GridFilterForm = React.forwardRef( const baseFormControlProps = rootProps.slotProps?.baseFormControl || {}; const baseSelectProps = rootProps.slotProps?.baseSelect || {}; - const isBaseSelectNative = baseSelectProps.native ?? true; + const isBaseSelectNative = baseSelectProps.native ?? false; const baseInputLabelProps = rootProps.slotProps?.baseInputLabel || {}; const baseSelectOptionProps = rootProps.slotProps?.baseSelectOption || {}; const { InputComponentProps, ...valueInputPropsOther } = valueInputProps; - const filteredColumns = React.useMemo(() => { + const { filteredColumns, selectedField } = React.useMemo(() => { + let itemField: string | undefined = item.field; if (filterColumns === undefined || typeof filterColumns !== 'function') { - return filterableColumns; + return { filteredColumns: filterableColumns, selectedField: itemField }; } const filteredFields = filterColumns({ @@ -251,7 +257,16 @@ const GridFilterForm = React.forwardRef( currentFilters: filterModel?.items || [], }); - return filterableColumns.filter((column) => filteredFields.includes(column.field)); + return { + filteredColumns: filterableColumns.filter((column) => { + const isFieldIncluded = filteredFields.includes(column.field); + if (column.field === item.field && !isFieldIncluded) { + itemField = undefined; + } + return isFieldIncluded; + }), + selectedField: itemField, + }; }, [filterColumns, filterModel?.items, filterableColumns, item.field]); const sortedFilteredColumns = React.useMemo(() => { @@ -297,16 +312,38 @@ const GridFilterForm = React.forwardRef( column.filterOperators![0]; // Erase filter value if the input component or filtered column type is modified - const eraseItemValue = + const eraseFilterValue = !newOperator.InputComponent || newOperator.InputComponent !== currentOperator?.InputComponent || column.type !== currentColumn!.type; + let filterValue = eraseFilterValue ? undefined : item.value; + + // Check filter value against the new valueOptions + if (column.type === 'singleSelect' && filterValue !== undefined) { + const colDef = column as GridSingleSelectColDef; + const valueOptions = getValueOptions(colDef); + if (Array.isArray(filterValue)) { + filterValue = filterValue.filter((val) => { + return ( + // Only keep values that are in the new value options + getValueFromValueOptions(val, valueOptions, colDef?.getOptionValue!) !== undefined + ); + }); + } else if ( + getValueFromValueOptions(item.value, valueOptions, colDef?.getOptionValue!) === + undefined + ) { + // Reset the filter value if it is not in the new value options + filterValue = undefined; + } + } + applyFilterChanges({ ...item, field, operator: newOperator.value, - value: eraseItemValue ? undefined : item.value, + value: filterValue, }); }, [apiRef, applyFilterChanges, item, currentColumn, currentOperator], @@ -421,7 +458,7 @@ const GridFilterForm = React.forwardRef( inputProps={{ 'aria-label': apiRef.current.getLocaleText('filterPanelLogicOperator'), }} - value={multiFilterOperator} + value={multiFilterOperator ?? ''} onChange={changeLogicOperator} disabled={!!disableMultiFilterOperator || logicOperators.length === 1} native={isBaseSelectNative} @@ -462,7 +499,7 @@ const GridFilterForm = React.forwardRef( labelId={columnSelectLabelId} id={columnSelectId} label={apiRef.current.getLocaleText('filterPanelColumns')} - value={item.field || ''} + value={selectedField ?? ''} onChange={changeColumn} native={isBaseSelectNative} {...rootProps.slotProps?.baseSelect} @@ -541,6 +578,7 @@ const GridFilterForm = React.forwardRef( item={item} applyValue={applyFilterChanges} focusElementRef={valueRef} + key={item.field} {...currentOperator.InputComponentProps} {...InputComponentProps} /> diff --git a/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputBoolean.tsx b/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputBoolean.tsx index 46f9585b38e2..32c79a69e6b7 100644 --- a/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputBoolean.tsx +++ b/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputBoolean.tsx @@ -45,7 +45,7 @@ function GridFilterInputBoolean(props: GridFilterInputBooleanProps) { const selectId = useId(); const baseSelectProps = rootProps.slotProps?.baseSelect || {}; - const isSelectNative = baseSelectProps.native ?? true; + const isSelectNative = baseSelectProps.native ?? false; const baseSelectOptionProps = rootProps.slotProps?.baseSelectOption || {}; diff --git a/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputMultipleSingleSelect.tsx b/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputMultipleSingleSelect.tsx index c18299a319b3..94048d5b06a6 100644 --- a/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputMultipleSingleSelect.tsx +++ b/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputMultipleSingleSelect.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import Autocomplete, { AutocompleteProps, createFilterOptions } from '@mui/material/Autocomplete'; import { unstable_useId as useId } from '@mui/utils'; -import { isSingleSelectColDef } from './filterPanelUtils'; +import { getValueOptions, isSingleSelectColDef } from './filterPanelUtils'; import { useGridRootProps } from '../../../hooks/utils/useGridRootProps'; import { GridFilterInputValueProps } from './GridFilterInputValueProps'; import type { GridSingleSelectColDef, ValueOptions } from '../../../models/colDef/gridColDef'; @@ -21,7 +21,6 @@ export interface GridFilterInputMultipleSingleSelectProps | 'color' | 'getOptionLabel' >, - Pick, GridFilterInputValueProps { type?: 'singleSelect'; } @@ -40,8 +39,6 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl helperText, size, variant = 'standard', - getOptionLabel: getOptionLabelProp, - getOptionValue: getOptionValueProp, ...other } = props; const TextFieldProps = { @@ -63,8 +60,8 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl } } - const getOptionValue = getOptionValueProp || resolvedColumn?.getOptionValue!; - const getOptionLabel = getOptionLabelProp || resolvedColumn?.getOptionLabel!; + const getOptionValue = resolvedColumn?.getOptionValue!; + const getOptionLabel = resolvedColumn?.getOptionLabel!; const isOptionEqualToValue = React.useCallback( (option: ValueOptions, value: ValueOptions) => getOptionValue(option) === getOptionValue(value), @@ -72,48 +69,17 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl ); const resolvedValueOptions = React.useMemo(() => { - if (!resolvedColumn?.valueOptions) { - return []; - } - - if (typeof resolvedColumn.valueOptions === 'function') { - return resolvedColumn.valueOptions({ field: resolvedColumn.field }); - } - - return resolvedColumn.valueOptions; + return getValueOptions(resolvedColumn!) || []; }, [resolvedColumn]); - const resolvedFormattedValueOptions = React.useMemo(() => { - return resolvedValueOptions?.map(getOptionValue); - }, [resolvedValueOptions, getOptionValue]); - // The value is computed from the item.value and used directly // If it was done by a useEffect/useState, the Autocomplete could receive incoherent value and options const filteredValues = React.useMemo(() => { if (!Array.isArray(item.value)) { return []; } - if (resolvedValueOptions !== undefined) { - const itemValueIndexes = item.value.map((element) => { - // Gets the index matching between values and valueOptions - return resolvedFormattedValueOptions?.findIndex( - (formattedOption) => formattedOption === element, - ); - }); - - return itemValueIndexes - .filter((index) => index >= 0) - .map((index: number) => resolvedValueOptions[index]); - } return item.value; - }, [item.value, resolvedValueOptions, resolvedFormattedValueOptions]); - - React.useEffect(() => { - if (!Array.isArray(item.value) || filteredValues.length !== item.value.length) { - // Updates the state if the filter value has been cleaned by the component - applyValue({ ...item, value: filteredValues.map(getOptionValue) }); - } - }, [item, filteredValues, applyValue, getOptionValue]); + }, [item.value]); const handleChange = React.useCallback< NonNullable['onChange']> @@ -177,18 +143,6 @@ GridFilterInputMultipleSingleSelect.propTypes = { PropTypes.func, PropTypes.object, ]), - /** - * Used to determine the label displayed for a given value option. - * @param {ValueOptions} value The current value option. - * @returns {string} The text to be displayed. - */ - getOptionLabel: PropTypes.func, - /** - * Used to determine the value used for a value option. - * @param {ValueOptions} value The current value option. - * @returns {string} The value to be used. - */ - getOptionValue: PropTypes.func, item: PropTypes.shape({ field: PropTypes.string.isRequired, id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), diff --git a/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputSingleSelect.tsx b/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputSingleSelect.tsx index 73d8da458e7b..43994aa5409f 100644 --- a/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputSingleSelect.tsx +++ b/packages/grid/x-data-grid/src/components/panel/filterPanel/GridFilterInputSingleSelect.tsx @@ -7,11 +7,15 @@ import { styled } from '@mui/material/styles'; import { GridFilterInputValueProps } from './GridFilterInputValueProps'; import { GridSingleSelectColDef } from '../../../models/colDef/gridColDef'; import { useGridRootProps } from '../../../hooks/utils/useGridRootProps'; -import { getValueFromValueOptions, isSingleSelectColDef } from './filterPanelUtils'; +import { + getValueFromValueOptions, + getValueOptions, + isSingleSelectColDef, +} from './filterPanelUtils'; import type { GridSlotsComponentsProps } from '../../../models/gridSlotsComponentsProps'; const renderSingleSelectOptions = ({ - column: { valueOptions, field }, + column, OptionComponent, getOptionLabel, getOptionValue, @@ -25,14 +29,14 @@ const renderSingleSelectOptions = ({ isSelectNative: boolean; baseSelectOptionProps: GridSlotsComponentsProps['baseSelectOption']; }) => { - const iterableColumnValues = - typeof valueOptions === 'function' - ? ['', ...valueOptions({ field })] - : ['', ...(valueOptions || [])]; + const iterableColumnValues = ['', ...(getValueOptions(column) || [])]; return iterableColumnValues.map((option) => { const value = getOptionValue(option); - const label = getOptionLabel(option); + let label = getOptionLabel(option); + if (label === '') { + label = ' '; // To force the height of the empty option + } return ( @@ -52,8 +56,7 @@ const SingleSelectOperatorContainer = styled('div')({ }); export type GridFilterInputSingleSelectProps = GridFilterInputValueProps & - TextFieldProps & - Pick & { + TextFieldProps & { clearButton?: React.ReactNode | null; /** * It is `true` if the filter either has a value or an operator with no value @@ -70,8 +73,6 @@ function GridFilterInputSingleSelect(props: GridFilterInputSingleSelectProps) { type, apiRef, focusElementRef, - getOptionLabel: getOptionLabelProp, - getOptionValue: getOptionValueProp, placeholder, tabIndex, label: labelProp, @@ -80,12 +81,12 @@ function GridFilterInputSingleSelect(props: GridFilterInputSingleSelectProps) { InputLabelProps, ...others } = props; - const [filterValueState, setFilterValueState] = React.useState(item.value ?? ''); + const filterValue = item.value ?? ''; const id = useId(); const labelId = useId(); const rootProps = useGridRootProps(); - const isSelectNative = rootProps.slotProps?.baseSelect?.native ?? true; + const isSelectNative = rootProps.slotProps?.baseSelect?.native ?? false; let resolvedColumn: GridSingleSelectColDef | null = null; if (item.field) { @@ -95,16 +96,11 @@ function GridFilterInputSingleSelect(props: GridFilterInputSingleSelectProps) { } } - const getOptionValue = getOptionValueProp || resolvedColumn?.getOptionValue!; - const getOptionLabel = getOptionLabelProp || resolvedColumn?.getOptionLabel!; + const getOptionValue = resolvedColumn?.getOptionValue!; + const getOptionLabel = resolvedColumn?.getOptionLabel!; const currentValueOptions = React.useMemo(() => { - if (!resolvedColumn) { - return undefined; - } - return typeof resolvedColumn.valueOptions === 'function' - ? resolvedColumn.valueOptions({ field: resolvedColumn.field }) - : resolvedColumn.valueOptions; + return getValueOptions(resolvedColumn!); }, [resolvedColumn]); const onFilterChange = React.useCallback( @@ -113,36 +109,11 @@ function GridFilterInputSingleSelect(props: GridFilterInputSingleSelectProps) { // NativeSelect casts the value to a string. value = getValueFromValueOptions(value, currentValueOptions, getOptionValue); - - setFilterValueState(String(value)); applyValue({ ...item, value }); }, [currentValueOptions, getOptionValue, applyValue, item], ); - React.useEffect(() => { - let itemValue; - - if (currentValueOptions !== undefined) { - // sanitize if valueOptions are provided - itemValue = getValueFromValueOptions(item.value, currentValueOptions, getOptionValue); - if (itemValue !== item.value) { - applyValue({ ...item, value: itemValue }); - return; - } - } else { - itemValue = item.value; - } - - itemValue = itemValue ?? ''; - - setFilterValueState(String(itemValue)); - }, [item, currentValueOptions, applyValue, getOptionValue]); - - if (!isSingleSelectColDef(resolvedColumn)) { - return null; - } - if (!isSingleSelectColDef(resolvedColumn)) { return null; } @@ -151,7 +122,7 @@ function GridFilterInputSingleSelect(props: GridFilterInputSingleSelectProps) { return ( - + , +) { + if (!column) { + return undefined; + } + return typeof column.valueOptions === 'function' + ? column.valueOptions({ field: column.field, ...additionalParams }) + : column.valueOptions; +} + export function getValueFromValueOptions( value: string, valueOptions: any[] | undefined, @@ -22,8 +31,3 @@ export function getValueFromValueOptions( }); return getOptionValue(result); } - -export const getLabelFromValueOption = (valueOption: ValueOptions) => { - const label = typeof valueOption === 'object' ? valueOption.label : valueOption; - return label != null ? String(label) : ''; -}; diff --git a/packages/grid/x-data-grid/src/tests/filterPanel.DataGrid.test.tsx b/packages/grid/x-data-grid/src/tests/filterPanel.DataGrid.test.tsx index b1c09140fc95..20e3b581f83e 100644 --- a/packages/grid/x-data-grid/src/tests/filterPanel.DataGrid.test.tsx +++ b/packages/grid/x-data-grid/src/tests/filterPanel.DataGrid.test.tsx @@ -10,16 +10,16 @@ import { GridPreferencePanelsValue, } from '@mui/x-data-grid'; import { createRenderer, fireEvent, screen } from '@mui-internal/test-utils'; -import { getColumnHeaderCell, getColumnValues } from 'test/utils/helperFn'; +import { getColumnHeaderCell, getColumnValues, getSelectByName } from 'test/utils/helperFn'; function setColumnValue(columnValue: string) { - fireEvent.change(screen.getByRole('combobox', { name: 'Columns' }), { + fireEvent.change(getSelectByName('Columns'), { target: { value: columnValue }, }); } function setOperatorValue(operator: string) { - fireEvent.change(screen.getByRole('combobox', { name: 'Operator' }), { + fireEvent.change(getSelectByName('Operator'), { target: { value: operator }, }); } @@ -175,17 +175,13 @@ describe(' - Filter panel', () => { />, ); expect(screen.getByRole('textbox', { name: 'Value' }).value).to.equal('Puma'); - expect(screen.getByRole('combobox', { name: 'Operator' }).value).to.equal( - 'equals', - ); + expect(getSelectByName('Operator').value).to.equal('equals'); expect(getColumnValues(0)).to.deep.equal(['Puma']); setColumnValue('slogan'); expect(getColumnValues(0)).to.deep.equal([]); - expect(screen.getByRole('combobox', { name: 'Operator' }).value).to.equal( - 'equals', - ); + expect(getSelectByName('Operator').value).to.equal('equals'); expect(screen.getByRole('textbox', { name: 'Value' }).value).to.equal('Puma'); }); @@ -212,17 +208,13 @@ describe(' - Filter panel', () => { />, ); expect(screen.getByRole('textbox', { name: 'Value' }).value).to.equal('Pu'); - expect(screen.getByRole('combobox', { name: 'Operator' }).value).to.equal( - 'contains', - ); + expect(getSelectByName('Operator').value).to.equal('contains'); expect(getColumnValues(0)).to.deep.equal(['Puma']); setColumnValue('slogan'); expect(getColumnValues(0)).to.deep.equal([]); - expect(screen.getByRole('combobox', { name: 'Operator' }).value).to.equal( - 'from', - ); + expect(getSelectByName('Operator').value).to.equal('from'); expect(screen.getByTestId('customInput').value).to.equal(''); }); @@ -252,9 +244,7 @@ describe(' - Filter panel', () => { />, ); expect(screen.getByRole('textbox', { name: 'Value' }).value).to.equal('Pu'); - expect(screen.getByRole('combobox', { name: 'Operator' }).value).to.equal( - 'contains', - ); + expect(getSelectByName('Operator').value).to.equal('contains'); expect(getColumnValues(0)).to.deep.equal(['Puma']); expect(onFilterModelChange.callCount).to.equal(0); @@ -264,9 +254,7 @@ describe(' - Filter panel', () => { expect(onFilterModelChange.callCount).to.equal(1); expect(onFilterModelChange.lastCall.args[0].items[0].value).to.equal(undefined); - expect(screen.getByRole('combobox', { name: 'Operator' }).value).to.equal( - 'isEmpty', - ); + expect(getSelectByName('Operator').value).to.equal('isEmpty'); }); it('should reset filter value if not available in the new valueOptions', () => { @@ -508,12 +496,8 @@ describe(' - Filter panel', () => { fireEvent.click(screen.getByRole('menuitem', { name: 'Filter' })); // check that the filter is still in the model - expect(screen.getByRole('combobox', { name: 'Columns' }).value).to.equal( - 'brand', - ); - expect(screen.getByRole('combobox', { name: 'Operator' }).value).to.equal( - 'isEmpty', - ); + expect(getSelectByName('Columns').value).to.equal('brand'); + expect(getSelectByName('Operator').value).to.equal('isEmpty'); }); // See https://github.com/mui/mui-x/issues/7901#issuecomment-1427058922 @@ -537,11 +521,7 @@ describe(' - Filter panel', () => { fireEvent.click(screen.getByRole('menuitem', { name: 'Filter' })); // check that the filter is changed to default one (`is`) - expect(screen.getByRole('combobox', { name: 'Columns' }).value).to.equal( - 'country', - ); - expect(screen.getByRole('combobox', { name: 'Operator' }).value).to.equal( - 'is', - ); + expect(getSelectByName('Columns').value).to.equal('country'); + expect(getSelectByName('Operator').value).to.equal('is'); }); }); diff --git a/test/utils/helperFn.ts b/test/utils/helperFn.ts index 0c72d6fcc421..ca0258f2ee0f 100644 --- a/test/utils/helperFn.ts +++ b/test/utils/helperFn.ts @@ -1,5 +1,5 @@ import { spy } from 'sinon'; -import { act } from '@mui-internal/test-utils'; +import { act, screen } from '@mui-internal/test-utils'; import { unwrapPrivateAPI } from '@mui/x-data-grid/internals'; import type { GridApiCommon } from '@mui/x-data-grid/models/api/gridApiCommon'; @@ -137,3 +137,22 @@ export function getRow(rowIndex: number): HTMLElement { } return row; } + +/** + * Returns the hidden `input` element of the Material UI Select component + */ +export const getSelectInput = (combobox: Element) => { + if (!combobox) { + return null; + } + const comboboxParent = combobox.parentElement; + if (!comboboxParent) { + return null; + } + const input = comboboxParent.querySelector('input'); + return input; +}; + +export function getSelectByName(name: string) { + return getSelectInput(screen.getByRole('combobox', { name }))!; +}