滚动至新Material-UI手风琴时关闭前置手风琴的实现问询
问题
使用Material-UI的Accordion组件,初始状态下所有手风琴均为展开状态。需求为:
- 滚动页面到达第二个手风琴时,关闭第一个手风琴
- 到达第三个手风琴时,关闭之前所有手风琴,以此类推
目前遇到两个问题:
ref.current.offsetTop始终大于滚动位置newScrollYPosition- 将判断条件改为
>后,虽然从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
相关产品推荐
相关产品推荐

