Search code examples
reactjsmaterial-uidatagrid

React Material UI Datagrid rowMouseEnter not triggering


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>
  );
}

Solution

  • 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} />;
    }