mui / mui-x

MUI X: Build complex and data-rich applications using a growing list of advanced React components, like the Data Grid, Date and Time Pickers, Charts, and more!
https://mui.com/x/
4.07k stars 1.26k forks source link

[data grid] baseCheckbox slot also overrides Column Chooser #14567

Open httpete opened 1 week ago

httpete commented 1 week ago

Steps to reproduce

Link to live example: (required)

Steps: 1. 2. 3.

Current behavior

I have overridden the baseCheckbox slot with a Radio component which works great in the Single Selection Grid mode. But it is completely unepected for baseCheckbox to override the Column chooser checkbox. In my case, I only want the grid selection mechanism to be a radio, not the column chooser one.

image

Expected behavior

Should allow me a way to only override the gridRowSelection checkBox.

Context

No response

Your environment

npx @mui/envinfo ``` Don't forget to mention which browser you used. Output from `npx @mui/envinfo` goes here. ```

Search keywords: mui baseCheckbox Order ID: 79357

k-rajat19 commented 1 week ago

Yeah, this is currently not possible, all checkboxes in the grid are controlled by a single baseCheckbox slot so if you override that it will be overridden everywhere. You can try overriding these properties as a workaround. Also, read this section of docs for more info on how you can implement it.

michelengelen commented 1 week ago

Thanks for the answer @k-rajat19 ... both are valid points. I know this might not be the answer you are looking for, but there is one more thing you can do (although this might be fragile):

const CustomCheckbox = (props) => {
  if (props.inputProps['aria-label'].toLowerCase().includes('select row')) {
    return <RadioButton {...props} />;
  }
  return <Checkbox {...props} />;
};

This is fragile because the aria-label might be different when using a locale.

httpete commented 6 days ago

I overrode the properties, but then the "single select" doesn't take on the radio. It just operates as a checkbox even though it is a radio.

httpete commented 6 days ago

My only workaround is to override the BaseCheckbox but then do this inside there. I found that the checkboxes in the grid rows have a ref, and the ones in the column chooser don't.



import { Checkbox, Radio } from '@mineral/core';
import { forwardRef, useLayoutEffect, useState } from 'react';

export const BaseRadio = forwardRef<HTMLButtonElement | null>((props, ref) => {
  if ('indeterminate' in props) {
    return null;
  }

  const [isSingle, setIsSingle] = useState(false);

  useLayoutEffect(() => {
    if (!ref) {
      return;
    }
    setIsSingle(true);
  }, [ref]);

  return isSingle ? (
    <Radio {...props} ref={ref} />
  ) : (
    <Checkbox {...props} ref={ref} />
  );
});

BaseRadio.displayName = 'BaseCheckbox';

~``
michelengelen commented 5 days ago

Well, in that case I would honestly go for a custom renderCell function in the column definition:

import * as React from 'react';
import {
  DataGrid,
  GRID_CHECKBOX_SELECTION_COL_DEF,
  GridRenderCellParams,
  useGridApiContext,
} from '@mui/x-data-grid';
import { useDemoData } from '@mui/x-data-grid-generator';
import { Radio } from '@mui/material';

const CustomSelectionCell = (params: GridRenderCellParams) => {
  const apiRef = useGridApiContext();
  const isChecked = apiRef.current.getSelectedRows().has(params.row.id);
  const handleClick = React.useCallback(() => {
    if (isChecked) {
      return;
    }
    apiRef.current.setRowSelectionModel([params.row.id]);
  }, [isChecked]);
  return <Radio checked={isChecked} onClick={handleClick} />;
};

export default function DisableClickSelectionGrid() {
  const { data } = useDemoData({
    dataSet: 'Commodity',
    rowLength: 10,
    maxColumns: 6,
  });

  const columns = React.useMemo(
    () => [
      {
        ...GRID_CHECKBOX_SELECTION_COL_DEF,
        renderCell: CustomSelectionCell,
        renderHeader: () => null,
      },
      ...data.columns,
    ],
    [data],
  );

  return (
    <div style={{ height: 400, width: '100%' }}>
      <DataGrid checkboxSelection disableRowSelectionOnClick columns={columns} rows={data.rows} />
    </div>
  );
}
michelengelen commented 5 days ago

if you want to be able to "unselect' a radio you can replace

  const handleClick = React.useCallback(() => {
    if (isChecked) {
-     return;
+     apiRef.current.setRowSelectionModel([]);
+     return;
    }
    apiRef.current.setRowSelectionModel([params.row.id]);
  }, [isChecked]);
httpete commented 5 days ago

So I found subtle differences when I did my own checkbox column in how the selection worked. What you want is clicking anywhere on the row, to highlight and select the radio (just like checkbox). If I clicked the radio directly, it wouldn't work. If I clicked the cell surrounding the radio, it wouldn't work.

The hacky way I ended up with achieved a perfect result. I feel this should be standard behavior, because when you do single selection mode the proper affordance is a radio per row, not a checkbox per row. It does work correctly, but I get hit by our usability experts when it is single select checkbox.

I had to do a little css to hide the "select / deselect all" checkbox in the header when single selection mode.

Can you add this as a feature? Since we support both modes already single or multi, it seems that correcting the control with a radio is the right way.

If I could override the rowSelectionCheckbox slot exclusively that would also fix it.

michelengelen commented 3 days ago

Ok, the requirement that the rowClick should select a row is actually making this a lot easier:

import * as React from 'react';
import {
  DataGrid,
  GRID_CHECKBOX_SELECTION_COL_DEF,
  GridColDef,
  GridRenderCellParams,
  useGridApiContext,
  useGridApiRef,
} from '@mui/x-data-grid';
import { useDemoData } from '@mui/x-data-grid-generator';
import { Radio } from '@mui/material';

const CustomSelectionCell = (params: GridRenderCellParams) => {
  const rowId = params.row.id;
  const apiRef = useGridApiContext();
  const [isChecked, setIsChecked] = React.useState(
    apiRef.current.getSelectedRows().has(params.row.id),
  );

  React.useEffect(() => {
    apiRef.current.subscribeEvent('rowSelectionChange', (newSelectionModel) => {
      setIsChecked(newSelectionModel.includes(rowId));
      console.log('rowClick', params.row.id, newSelectionModel);
    });
  }, [apiRef]);

  return <Radio checked={isChecked} />;
};

export default function DisableClickSelectionGrid() {
  const apiRef = useGridApiRef();
  const { data } = useDemoData({
    dataSet: 'Commodity',
    rowLength: 10,
    maxColumns: 6,
  });

  const columns = React.useMemo(
    () => [
      {
        ...GRID_CHECKBOX_SELECTION_COL_DEF,
        type: 'custom' as GridColDef['type'],
        field: '__radio__',
        renderCell: CustomSelectionCell,
        renderHeader: () => null,
      },
      ...data.columns,
    ],
    [data],
  );

  return (
    <div style={{ height: 400, width: '100%' }}>
      <DataGrid apiRef={apiRef} columns={columns} rows={data.rows} />
    </div>
  );
}

Try this! :D