0

When I hover over any row in the Datagrid, I want the "Analyze" button to change from variant outlined to contained. I cannot get any event to trigger when the row is hovered over, nor can I find any information on how to update/re-render a cell within that row when the mouse is within that row.

"@mui/x-data-grid": "^5.17.25", "@mui/x-data-grid-generator": "^6.0.0", "@mui/x-data-grid-pro": "^6.0.0",

import React, { useRef, useState, useEffect } from "react";
import { DataGrid, GridRowsProp, GridColDef } from "@mui/x-data-grid";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import { useTheme } from "@mui/system";
import Link from "next/link";
import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import SearchIcon from "@mui/icons-material/Search";
import AddIcon from "@mui/icons-material/Add";
import CircularProgress from "@mui/material/CircularProgress";
import { alpha, styled, lighten } from "@mui/material/styles";

export default function PropertiesList({ newProperties }) {
  const theme = useTheme();
  const boxRef = useRef(null);
  const [searchText, setSearchText] = useState("");
  const columns = getColumns(theme);

  function getColumns(theme) {
    // commented because irrelevant
    return [
      {
        field: "id",
        headerName: "Actions",
        width: 150,
        renderCell: (params) => {
          return (
            <Box
              sx={{
                display: "flex",
                justifyContent: "space-between",
                width: "100%"
              }}
            >
              <Link
                href="/properties/[id]"
                as={`/properties/${params.row.original_doc || params.row.id}`}
              >
                <Button
                  size="small"
                  variant="outlined"
                  startIcon={<CalculateIcon />}
                  sx={{
                    backgroundColor:
                      hoveredRowId === params.id
                        ? theme.palette.success.main
                        : ""
                  }}
                >
                  Analyze
                </Button>
              </Link>
            </Box>
          );
        }
      }
    ];
  }

  useEffect(() => {
    if (!boxRef.current) return;
    const screenHeight = window.innerHeight;
    boxRef.current.style.height = `${screenHeight - 120}px`;
  }, []);

  const handleRowOver = (params) => {
    // change the analyze button from "outlined" to "contained" when hovered.
    // The below console.log does not trigger.
    console.log(`Row ${params.id} is being hovered over`);
  };

  return (
    <Box ref={boxRef}>
      {!newProperties && (
        <Box
          sx={{
            height: "calc(100vh - 160px)",
            display: "flex",
            justifyContent: "center",
            alignItems: "center"
          }}
        >
          <CircularProgress size={32} />
        </Box>
      )}
      {newProperties && (
        <>
          <Box
            sx={{
              display: "flex",
              justifyContent: "space-between",
              alignItems: "center",
              background: theme.palette.background.background2,
              marginTop: 3,
              // marginBottom: 1,
              padding: 2,
              border: "1px solid " + theme.palette.contrast.contrast1,
              borderTopLeftRadius: 8,
              borderTopRightRadius: 8
            }}
          >
            <TextField
              label="Search for property"
              placeholder=""
              sx={{ marginTop: 1, marginBottom: 1 }}
              onChange={(event) => setSearchText(event.target.value)}
              InputProps={{
                startAdornment: (
                  <InputAdornment position="start">
                    <SearchIcon />
                  </InputAdornment>
                )
              }}
            />

            <Link href="/properties/add">
              <Button
                size="medium"
                variant="contained"
                sx={{ height: 50 }}
                startIcon={<AddIcon />}
              >
                Add Property
              </Button>
            </Link>
          </Box>

          <DataGrid
            rowMouseEnter={handleRowOver}
            sx={{
              border: "1px solid " + theme.palette.contrast.contrast1,
              height: "calc(100vh - 280px)",
              background: theme.palette.background.background1,
              "& .MuiDataGrid-virtualScroller::-webkit-scrollbar": {
                height: "0.4em",
                width: "0.4em"
              },
              "& .MuiDataGrid-virtualScroller::-webkit-scrollbar-track": {
                background: theme.palette.contrast.contrast1
              },
              "& .MuiDataGrid-virtualScroller::-webkit-scrollbar-thumb": {
                backgroundColor: theme.palette.contrast.contrast2
              },
              "& .MuiDataGrid-virtualScroller::-webkit-scrollbar-thumb:hover": {
                background: theme.palette.contrast.default
              },
              borderTopLeftRadius: 0,
              borderTopRightRadius: 0,
              borderBottomLeftRadius: 8,
              borderBottomRightRadius: 8
            }}
            rows={newProperties.filter(
              (row) =>
                (row.address &&
                  row.address
                    .toLowerCase()
                    .includes(searchText.toLowerCase())) ||
                (row.city &&
                  row.city.toLowerCase().includes(searchText.toLowerCase())) ||
                (row.state &&
                  row.state.toLowerCase().includes(searchText.toLowerCase())) ||
                (row.zip &&
                  row.zip
                    .toString()
                    .toLowerCase()
                    .includes(searchText.toLowerCase()))
            )}
            columns={columns}
            pageSize={13}
            disableColumnFilter
            disableSelectionOnClick
            disableColumnSelector
          />
        </>
      )}
    </Box>
  );
}
Steve
  • 11,596
  • 7
  • 39
  • 53
Evan Hessler
  • 297
  • 3
  • 19

2 Answers2

1

As @VonC mentioned in his answer, you can use slotProps to pass props to a row element, in particular onMouseEnter and onMouseLeave. Using the technique described here, I was able to reproduce the behavior you are trying to achieve in a fairly concise manner.

The main idea is to fire an event inside onMouseEnter and onMouseLeave that we will subscribe to in our custom button component.

In order to achieve isolation of events between different rows, we will include the row id in the event name.

It was difficult to run your component without a context, so to demonstrate the principle, I built a minimal DataGrid myself.

You can see a live example here:

Edit MUI Datagrid change cell props on row hover

Code:

import React, { FC, useState, useEffect } from "react";
import Button from "@mui/material/Button";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import CalculateIcon from "@mui/icons-material/Calculate";

const CustomButtonElement: FC<{ rowId: number | string }> = ({ rowId }) => {
  const [rowHovered, setRowHovered] = useState(false);
  useEffect(() => {
    const handleCustomEvent = (e) => setRowHovered(e.detail.hovered);
    document.addEventListener(`row${rowId}HoverChange`, handleCustomEvent);
    // cleanup listener
    return () =>
      document.removeEventListener(`row${rowId}HoverChange`, handleCustomEvent);
  }, [rowId]);

  return (
    <Button variant={rowHovered ? "outlined" : "contained"}>
      <CalculateIcon />
    </Button>
  );
};

export default function DataGridDemo() {
  const rows = [
    { id: 1, lastName: "Snow", firstName: "Jon", age: 35 },
    { id: 2, lastName: "Lannister", firstName: "Cersei", age: 42 },
    { id: 3, lastName: "Lannister", firstName: "Jaime", age: 45 },
    { id: 4, lastName: "Stark", firstName: "Arya", age: 16 },
    { id: 5, lastName: "Targaryen", firstName: "Daenerys", age: null }
  ];

  const columns: GridColDef[] = [
    { field: "id", headerName: "ID", width: 90 },
    {
      field: "",
      headerName: "Action",
      renderCell: (params) => <CustomButtonElement rowId={params.id} />
    },
    { field: "firstName", headerName: "First Name", width: 90 },
    { field: "lastName", headerName: "Last Name", width: 90 }
  ];

  const handleRowHovered = (event: React.MouseEvent<HTMLElement>) => {
    const rowId = event.currentTarget?.dataset?.id;
    document.dispatchEvent(
      new CustomEvent(`row${rowId}HoverChange`, { detail: { hovered: true } })
    );
  };

  const handleRowLeaved = (event: React.MouseEvent<HTMLElement>) => {
    const rowId = event.currentTarget?.dataset?.id;
    document.dispatchEvent(
      new CustomEvent(`row${rowId}HoverChange`, { detail: { hovered: false } })
    );
  };

  return (
    <DataGrid
      rows={rows}
      columns={columns}
      slotProps={{
        row: {
          onMouseEnter: handleRowHovered,
          onMouseLeave: handleRowLeaved
        }
      }}
    />
  );
}

UPDATE

To address the issue of state loss when the component goes out of view (due to unmount ), I added a useEffect which will run on every button mount and check if the mouse is hovering over the button row element. To do this, I use the matches and the apiRef object for more native access to the DataGrid row element through its context.

As it turned out, thanks to the same apiRef and the useGridApiEventHandler hook, you can subscribe to events in a more native way (withou creating custom ones), so the code is even more concise and expressive.

Updated Code (the above Codesandbox is also updated):

import React, { FC, useState, useEffect } from "react";
import Button from "@mui/material/Button";
import {
  DataGrid,
  GridColDef,
  GridEventListener,
  useGridApiContext,
  useGridApiEventHandler
} from "@mui/x-data-grid";
import CalculateIcon from "@mui/icons-material/Calculate";

const CustomButtonElement: FC<{ rowId: number | string }> = ({ rowId }) => {
  const [rowHovered, setRowHovered] = useState(false);
  const apiRef = useGridApiContext();

  // runs only "onComponentMount"
  useEffect(() => {
    if (apiRef.current.getRowElement(rowId).matches(":hover"))
      setRowHovered(true);
  }, []);

  const handleRowEnter: GridEventListener<"rowMouseEnter"> = ({ id }) =>
    id === rowId && setRowHovered(true);
  const handleRowLeave: GridEventListener<"rowMouseLeave"> = ({ id }) =>
    id === rowId && setRowHovered(false);

  useGridApiEventHandler(apiRef, "rowMouseEnter", handleRowEnter);
  useGridApiEventHandler(apiRef, "rowMouseLeave", handleRowLeave);

  return (
    <Button variant={rowHovered ? "outlined" : "contained"}>
      <CalculateIcon />
    </Button>
  );
};

export default function DataGridDemo() {
  const rows = [
    { id: 1, lastName: "Snow", firstName: "Jon", age: 35 },
    { id: 2, lastName: "Lannister", firstName: "Cersei", age: 42 },
    { id: 3, lastName: "Lannister", firstName: "Jaime", age: 45 },
    { id: 4, lastName: "Stark", firstName: "Arya", age: 16 },
    { id: 5, lastName: "Targaryen", firstName: "Daenerys", age: null }
  ];

  const columns: GridColDef[] = [
    { field: "id", headerName: "ID", width: 90 },
    {
      field: "",
      headerName: "Action",
      renderCell: (params) => <CustomButtonElement rowId={params.id} />
    },
    { field: "firstName", headerName: "First Name", width: 90 },
    { field: "lastName", headerName: "Last Name", width: 90 },
    { field: "age", headerName: "Age", width: 90 }
  ];

  return <DataGrid rows={rows} columns={columns} />;
}
  • This seems more detailed than my answer (and with a working example). Upvoted. – VonC May 11 '23 at 06:59
  • You're a boss! Thank you! The only thing I had to do to get it working on mine was change it from slotProps to componentsProps. In case that helps anyone reading this. – Evan Hessler May 11 '23 at 15:56
  • Only other bug I noticed is if I scroll all the way to the right on a table with a lot of columns, when I scroll back left the row I am hovered over does not have the hovered styles. – Evan Hessler May 11 '23 at 22:06
  • @EvanHessler Indeed... I didn't consider the case when the button is remounted while the cursor neither leaves the row nor enters it (because in our case there is no direct check "whether the mouse is over the row"). I'll update my answer to take this into account. – Олексій Холостенко May 12 '23 at 03:10
0

The MUI DataGrid does not include any event function regarding hovering.
(You only have functions like onCellClick(), onCellEditStart(), onCellKeyDown(), ...)

I thought about event Listener like onMouseEnter and onMouseLeave, but, as mentioned here, that it will cause so many renders.

Maybe using the sx prop would help to style those blocks with :hover:

Something like:

function getColumns(theme) {
  return [
    {
      field: "id",
      headerName: "Actions",
      width: 150,
      renderCell: (params) => {
        return (
          <Box
            sx={{
              display: "flex",
              justifyContent: "space-between",
              width: "100%",
              '&:hover button': {
                backgroundColor: theme.palette.success.main,
                border: '1px solid transparent',
                color: theme.palette.common.white,
              },
            }}
          >
            <Link
              href="/properties/[id]"
              as={`/properties/${params.row.original_doc || params.row.id}`}
            >
              <Button
                size="small"
                variant="outlined"
                startIcon={<CalculateIcon />}
                sx={{
                  borderColor: theme.palette.success.main,
                  '&:hover': {
                    backgroundColor: theme.palette.success.main,
                    borderColor: 'transparent',
                    color: theme.palette.common.white,
                  },
                }}
              >
                Analyze
              </Button>
            </Link>
          </Box>
        );
      },
    },
  ];
}

Associated with

<DataGrid
  onRowMouseEnter={handleRowOver}
  // Other props
/>

(not rowMouseEnter)

Using a similar idea as the one show in DataGrid row:

<DataGrid
  // Other props
  slotProps={{
    row: {
      onMouseEnter: (event) => handleRowOver(Number(event.currentTarget.getAttribute('data-id'))),
      onMouseLeave: () => console.log('Mouse left the row'),
    },
  }}
/>

with:

const handleRowOver = (rowId) => {
  console.log(`Row ${rowId} is being hovered over`);
};

When you hover over a row in the DataGrid, the handleRowOver function will be called, and you can see the log in the browser console.
When the mouse leaves the row, the anonymous function will print a message. The hover effect for the "Analyze" button will still be handled by the :hover pseudo-class in the sx prop.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250