You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Material UI粘性列表滚动时同步选中导航按钮问题求助

Solution for Scroll-Active Side Navigation with Material UI Sticky List

The core issue with your original implementation is that @makotot/ghostui's Scrollspy listens to window scroll events, but your scrollable content is contained within a Material UI List (with overflow: auto), so scroll events don't bubble up to the window. Here's a custom implementation that fixes this:

Modified 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";

const categories = [0, 1, 2, 3, 4];
const SCROLL_OFFSET = 100; // Adjust this to match your sticky header offset

export default function PinnedSubheaderList() {
  const sectionRefs = React.useMemo(
    () => categories.map(() => React.createRef<HTMLLIElement>()),
    []
  );
  const scrollContainerRef = React.useRef<HTMLUListElement>(null);
  const [selected, setSelected] = React.useState<number>(0);

  const handleChangeCategory = (categoryIndex: number): void => {
    const targetSection = sectionRefs[categoryIndex].current;
    if (targetSection && scrollContainerRef.current) {
      scrollContainerRef.current.scrollTo({
        top: targetSection.offsetTop - SCROLL_OFFSET,
        behavior: "smooth"
      });
      setSelected(categoryIndex);
    }
  };

  React.useEffect(() => {
    const container = scrollContainerRef.current;
    if (!container) return;

    const handleScroll = () => {
      const scrollTop = container.scrollTop;

      // Iterate from bottom to top to find the first visible section
      for (let i = sectionRefs.length - 1; i >= 0; i--) {
        const section = sectionRefs[i].current;
        if (!section) continue;

        const sectionTop = section.offsetTop;
        const sectionBottom = sectionTop + section.offsetHeight;

        // Check if section is within the visible area (adjusted for offset)
        if (sectionTop <= scrollTop + SCROLL_OFFSET && sectionBottom > scrollTop) {
          setSelected(i);
          break;
        }
      }
    };

    container.addEventListener("scroll", handleScroll);
    return () => container.removeEventListener("scroll", handleScroll);
  }, []);

  return (
    <Grid container spacing={2}>
      <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={selected === index}
                  onClick={() => handleChangeCategory(index)}
                >
                  <ListItemText primary={category} />
                </ListItemButton>
              </ListItem>
              <Divider />
            </React.Fragment>
          ))}
        </List>
      </Grid>
      <Grid item md={6} xs={4}>
        <List
          ref={scrollContainerRef}
          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}`} ref={sectionRefs[sectionIndex]}>
              <ul>
                <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>
    </Grid>
  );
}

Key Changes Explained

  1. Custom Scroll Listener: Added a scroll event listener directly to the scrollable List container (instead of the window) to capture scroll events from your sticky list.
  2. Section Ref Adjustment: Updated sectionRefs to target the outer <li> elements (the actual section containers) instead of inner <ul> elements, ensuring accurate visibility checks.
  3. Visibility Detection: Implemented logic to find the section that's within the visible area of the scroll container, adjusted for your offset value.
  4. Smooth Scroll Control: Replaced scrollIntoView with scrollTo on the container for better control over scroll positioning with the offset.

Notes

  • Adjust SCROLL_OFFSET to match the height of your sticky header or desired scroll position offset.
  • The scroll handler iterates from bottom to top to ensure the correct section is selected when multiple sections are partially visible.

内容的提问来源于stack exchange,提问作者arunmmanoharan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 21:01:03