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

滚动至新Material-UI手风琴时关闭前置手风琴的实现问询

问题

使用Material-UI的Accordion组件,初始状态下所有手风琴均为展开状态。需求为:

  • 滚动页面到达第二个手风琴时,关闭第一个手风琴
  • 到达第三个手风琴时,关闭之前所有手风琴,以此类推

目前遇到两个问题:

  1. ref.current.offsetTop始终大于滚动位置newScrollYPosition
  2. 将判断条件改为>后,虽然从expandedIds中移除了第一个手风琴的id,但手风琴并未实际关闭

代码片段
import { Accordion as MuiAccordion } from '@mui/material'

export const Accordion = ({
  className,
  accordions,
  scrollableContainerRef,
}: AccordionProps) => {
  const [expandedIds, setExpandedIds] = useState<string[]>(
    accordions.map(({ id }) => id)
  )
  const accordionRefs = useRef<(React.RefObject<HTMLDivElement> | null)[]>(
    accordions.map(() => React.createRef())
  )

  const handleScroll = () => {
    const newScrollYPosition = scrollableContainerRef.current?.scrollTop || 0

    accordionRefs.current.forEach((ref) => {
      if (ref.current) {
        if (ref.current.offsetTop < newScrollYPosition) {
          setExpandedIds((prevExpandedIds) =>
            prevExpandedIds.filter((id) => id !== accordions[0].id)
          )
        }
      }
    })
  }

  useEffect(() => {
    if (scrollableContainerRef.current) {
      scrollableContainerRef.current.addEventListener('scroll', handleScroll)
    }
    return () => {
      if (scrollableContainerRef.current) {
        scrollableContainerRef.current.removeEventListener(
          'scroll',
          handleScroll
        )
      }
    }
  }, [scrollableContainerRef])

  const accordionsList = useMemo(() => {
    return accordions.map(({ id, title, content, disabled }, index) => (
      <MuiAccordion
        key={id}
        disabled={disabled}
        defaultExpanded={expandedIds.includes(id)}
        ref={accordionRefs.current[index]}
      >
        <AccordionSummary expandIcon={<ExpandMoreIcon />} id={id}>
          {title}
        </AccordionSummary>
        <AccordionDetails>{content}</AccordionDetails>
      </MuiAccordion>
    ))
  }, [accordions, expandedIds])

  return (
    <div className={classNames('accordion', className)}>{accordionsList}</div>
  )
}

解决方案

1. 修复手风琴不关闭的核心问题

你使用了defaultExpanded属性,这个属性仅在组件初始化时生效,后续expandedIds变化不会触发手风琴状态更新。需要替换为expanded属性,实时绑定状态:

<MuiAccordion
  key={id}
  disabled={disabled}
  expanded={expandedIds.includes(id)} // 替换defaultExpanded
  ref={(el) => (accordionRefs.current[index] = el)}
>

2. 重构滚动判断逻辑

原逻辑存在两个缺陷:遍历所有ref时仅移除第一个手风琴ID,不符合“关闭之前所有”的需求;仅判断offsetTop未结合手风琴高度,导致判断不准确。修改后的逻辑:

const handleScroll = useCallback(() => {
  const scrollTop = scrollableContainerRef.current?.scrollTop || 0;
  const closedIndices: number[] = [];

  accordionRefs.current.forEach((ref, index) => {
    if (ref) {
      // 计算手风琴底部相对于容器的位置
      const accordionBottom = ref.offsetTop + ref.offsetHeight;
      // 若手风琴底部已滚出容器可视区域顶部,标记为需要关闭
      if (accordionBottom < scrollTop) {
        closedIndices.push(index);
      }
    }
  });

  if (closedIndices.length > 0) {
    // 保留最大关闭索引之后的所有手风琴ID(即当前可视及下方的)
    const lastClosedIndex = Math.max(...closedIndices);
    setExpandedIds(accordions.slice(lastClosedIndex + 1).map(item => item.id));
  } else {
    // 未滚动过任何手风琴,保持全部展开
    setExpandedIds(accordions.map(item => item.id));
  }
}, [accordions]);

3. 修正Ref初始化方式

原Ref数组初始化方式可能导致DOM元素无法正确绑定,改为直接维护DOM元素数组:

// 初始化空数组
const accordionRefs = useRef<(HTMLDivElement | null)[]>([]);

// 在手风琴列表变化时重置Ref数组长度
useEffect(() => {
  accordionRefs.current = new Array(accordions.length).fill(null);
}, [accordions]);

4. 优化事件监听依赖

用useCallback包裹handleScroll,避免每次渲染创建新函数,同时完善useEffect的依赖项:

useEffect(() => {
  const container = scrollableContainerRef.current;
  if (container) {
    container.addEventListener('scroll', handleScroll);
    return () => container.removeEventListener('scroll', handleScroll);
  }
}, [scrollableContainerRef, handleScroll]);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 21:40:20