reactjstypescriptmaterial-uiaccordion

how to close the previous accordions when scrolling the page and reach to new accordion?


I am using accordion material-mui. at first all of accordions are opened. when I scroll the page and I reach to the second accordion, I want to close the first accordion and when I reach to the third accordion , I want to close the previous ones and so on. the log of ref.current.offsetTop is always bigger than newScrollYPosition and when I change the if to > and it removes accordions[0].id but it did not close the accordion. How can I do that? my simple code is in the below link.

https://stackblitz.com/edit/vitejs-vite-nzp98t?file=src%2FApp.tsx,src%2FApp.css&terminal=dev

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

Solution

  • export const Accordion = ({ className, accordions }: AccordionProps) => {
    
      const accordionRefs = useRef<(React.RefObject<HTMLDivElement> | null)[]>(
        accordions.map(() => React.createRef())
      )
      const scrollableContainerRef = useRef<HTMLDivElement>(null)
    
      const [expandedIds, setExpandedIds] = useState<string[]>(
        accordions.map(({ id }) => id)
      )
    
      const handleScroll = () => {
        const newScrollYPosition = scrollableContainerRef.current?.scrollTop || 0
    
        const observer = new IntersectionObserver(
          (entries) => {
            entries.forEach((entry) => {
              if (entry.isIntersecting) {
                const accordionId = entry.target.getAttribute('data-id')
                if (
                  accordionId &&
                  newScrollYPosition > entry.boundingClientRect.top
                ) {
                  setExpandedIds((prevExpandedIds) =>
                    prevExpandedIds.filter(
                      (expandedId) => expandedId !== accordionId
                    )
                  )
                }
              }
            })
          },
          {
            threshold: 0.5,
            root: document.querySelector('.accordion'),
          }
        )
        accordionRefs.current.forEach((ref, index) => {
          if (ref && ref.current) {
            observer.observe(ref.current)
            ref.current.setAttribute('data-id', accordions[index].id) // Add a unique identifier to each accordion
          }
        })
    
        return () => observer.disconnect()
      }
      //
      const onChange = (id: string) => {
        setExpandedIds((prevIds) => {
          const isExpanded = prevIds.includes(id)
          return isExpanded
            ? prevIds.filter((expandedId) => expandedId !== id)
            : [...prevIds, id]
        })
      }
    
      const accordionsList = useMemo(() => {
        return accordions.map(({ id, title, content, disabled }, index) => (
          <MuiAccordion
            key={id}
            disabled={disabled}
            expanded={expandedIds.includes(id)}
            ref={accordionRefs.current[index]}
            onChange={() => onChange(id)}
          >
            <AccordionSummary expandIcon={<ExpandMoreIcon />} id={id}>
              {title}
            </AccordionSummary>
            <AccordionDetails>{content}</AccordionDetails>
          </MuiAccordion>
        ))
      }, [accordions, expandedIds])
    
      useEffect(() => {
        if (scrollableContainerRef.current) {
          scrollableContainerRef.current.addEventListener('scroll', handleScroll)
        }
        return () => {
          if (scrollableContainerRef.current) {
            scrollableContainerRef.current.removeEventListener(
              'scroll',
              handleScroll
            )
          }
        }
      }, [scrollableContainerRef])
    
      return (
        <div
          className={classNames('accordion', className)}
          ref={scrollableContainerRef}
        >
          {accordionsList}
        </div>
      )
    }