Search code examples
reactjsmaterial-uiscrollspy

Change selected button in a list on MUI LIst Scroll - React


I have a sticky MUI List. I also have another list with just the headers which when clicked on will scroll the sticky list and take me to the corresponding category. However, I am not able to make it work when I scroll through the sticky list and the corresponding list header on the other list should be selected. I am using @makotot/ghostui to achieve scrollspy but its not working. Please advice.

This is my code.

import * as React from "react";
import Grid from "@mui/material/Grid";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemText from "@mui/material/ListItemText";
import ListSubheader from "@mui/material/ListSubheader";
import ListItemButton from "@mui/material/ListItemButton";
import Divider from "@mui/material/Divider";
import { map } from "lodash";
import { Scrollspy } from "@makotot/ghostui";

const categories = [0, 1, 2, 3, 4];

export default function PinnedSubheaderList() {
  const sectionRefs = React.useMemo(
    () => categories.map((i) => React.createRef<HTMLUListElement>()),
    []
  );

  const [selected, setSelected] = React.useState<number>(0);

  const handleChangeCategory = (category: number): void => {
    let categoryItem = document.getElementById(category.toString());
    categoryItem &&
      categoryItem.scrollIntoView({ behavior: "smooth", block: "start" });
    setSelected(category);
  };

  return (
    <Grid container spacing={2}>
      <Scrollspy sectionRefs={sectionRefs} offset={-100}>
        {({ currentElementIndexInViewport }) => (
          <>
            {console.log(
              "currentElementIndexInViewport",
              currentElementIndexInViewport
            )}
            <Grid item md={6} xs={4}>
              <List
                sx={{ border: `1px solid grey`, my: 2 }}
                dense
                disablePadding
                data-cy="nav-wrapper"
              >
                {map(categories, (category, index) => (
                  <React.Fragment key={category}>
                    <ListItem disablePadding data-cy={`nav-item`}>
                      <ListItemButton
                        selected={currentElementIndexInViewport === index}
                        onClick={() => handleChangeCategory(category)}
                      >
                        <ListItemText primary={category} />
                      </ListItemButton>
                    </ListItem>
                    <Divider />
                  </React.Fragment>
                ))}
              </List>
            </Grid>
            <Grid item md={6} xs={4}>
              <List
                sx={{
                  width: "100%",
                  bgcolor: "background.paper",
                  position: "relative",
                  overflow: "auto",
                  maxHeight: 300,
                  "& ul": { padding: 0 }
                }}
                subheader={<li />}
                data-cy="section-wrapper"
              >
                {categories.map((sectionId, sectionIndex) => (
                  <li key={`section-${sectionId}`} id={sectionId.toString()}>
                    <ul ref={sectionRefs[sectionIndex]}>
                      <ListSubheader>{`I'm sticky ${sectionId}`}</ListSubheader>
                      {[0, 1, 2].map((item) => (
                        <ListItem key={`item-${sectionId}-${item}`}>
                          <ListItemText primary={`Item ${item}`} />
                        </ListItem>
                      ))}
                    </ul>
                  </li>
                ))}
              </List>
            </Grid>
          </>
        )}
      </Scrollspy>
    </Grid>
  );
}

Stackblitz link: Link


Solution

  • You need to pass rootSelector prop to your Scrollspy component.

    In order to do this, first, you can set a unique id to your List component like this:

    <List
        sx={{
          width: "100%",
          bgcolor: "background.paper",
          position: "relative",
          overflow: "auto",
          maxHeight: 300,
          "& ul": { padding: 0 }
        }}
        subheader={<li />}
        data-cy="section-wrapper"
        id="my-list-root"
    >
    

    And then pass its querySelector string ("#my-list-root") to the Scrollspy like this:

    <Scrollspy
        sectionRefs={sectionRefs}
        offset={-100}
        rootSelector="#my-list-root"
    >
    

    You can take a look at this sandbox for a live working example of this solution.