import React, { useMemo, useRef, useCallback, useState, useEffect } from 'react' import classNames from 'classnames' import { BsArrowUpShort, BsArrowDownShort } from 'react-icons/bs' import Checkbox from './Checkbox' import { DescriptiveHeading } from './Typography' import Button from './Button' import useNomenclature from '../hooks/useNomenclature' import { useScreen } from '../hooks/useMediaQuery' const defaultHeading = <DescriptiveHeading>Lineages</DescriptiveHeading> const LineageFilter = (props) => { const { allSelected, className, emptyMessage, fixedLayout, heading = defaultHeading, isMobile, sortedLineages, toggleAll, toggleLineage } = props const isScrolling = useMemo(() => { return fixedLayout || sortedLineages.length > (isMobile ? 9 : 10) }, [fixedLayout, sortedLineages, isMobile]) const sections = useMemo(() => { if (!isScrolling) { return [sortedLineages] } const sectionSize = isMobile ? 9 : 8 const _sections = [] const numSections = Math.ceil(sortedLineages.length / sectionSize) || 1 for (let i = 0; i < numSections; i++) { const start = i * sectionSize _sections.push(sortedLineages.slice(start, start + sectionSize)) } return _sections }, [sortedLineages, isMobile]) const { nomenclature } = useNomenclature() const isLarge = useScreen('lg') const gridStyle = useMemo(() => { const style = { scrollSnapAlign: 'start', scrollSnapStop: 'always' } if (isMobile) { return style } const numLineages = sortedLineages.length const maxColumns = isScrolling ? 4 : 5 const numColumns = isLarge && fixedLayout ? maxColumns : Math.max(2, Math.min(Math.ceil(numLineages / 2), maxColumns)) return { ...style, gridTemplateColumns: `repeat(${numColumns}, minmax(0, 1fr))` } }, [sortedLineages, fixedLayout, isScrolling, isMobile, isLarge]) const scrollContainer = useRef() const userScrolled = useRef(false) const doScroll = useCallback((direction) => { const { height } = scrollContainer.current.getBoundingClientRect() scrollContainer.current.scrollBy({ top: height * direction, behavior: 'smooth' }) userScrolled.current = true }, []) const sectionRefs = useRef([]) const [currentSection, setCurrentSection] = useState(null) useEffect(() => { const callback = entries => { entries.forEach(entry => { if (entry.isIntersecting) { const index = (sectionRefs.current).indexOf(entry.target) setCurrentSection(index) } }) } const observer = new IntersectionObserver(callback, { root: scrollContainer.current, threshold: 0.6 }) sectionRefs.current.filter(s => s).forEach(section => { observer.observe(section) }) return function cleanup () { observer.disconnect() } }, [sections]) const scrollUpBtnRef = useRef(null) const scrollDownBtnRef = useRef(null) useEffect(() => { if (isMobile || userScrolled.current === false) { return } if (currentSection === 0) { scrollDownBtnRef.current.focus() } else if (currentSection === sections.length - 1) { scrollUpBtnRef.current.focus() } }, [isMobile, currentSection]) return ( <div className={className}> <header className='flex justify-between space-x-6'> {heading} <div className='flex items-center'> <label htmlFor='lineage_toggle_all' className='pr-2 text-primary text-xs uppercase tracking-wide font-bold leading-5' > toggle all </label> <Checkbox className='text-xs text-primary mx-auto flex-row-reverse' id='lineage_toggle_all' checked={allSelected} onChange={toggleAll} /> </div> </header> <div className='md:flex md:mt-0.5'> <form ref={scrollContainer} className={classNames( 'overflow-auto hide-scrollbars flex-grow -mx-4 md:-mx-2 flex md:flex-col md:h-16', { 'md:-mx-1': fixedLayout } )} style={{ scrollSnapType: isMobile ? 'x mandatory' : 'y mandatory' }} > {sections.map((lineages, i) => ( <section key={`lineages-${i}`} ref={el => { sectionRefs.current[i] = el }} className={classNames( 'w-full h-full flex-shrink-0 flex flex-wrap content-start px-4 md:px-0 md:grid md:gap-0.5 relative', { 'md:px-1': fixedLayout } )} style={gridStyle} > { lineages.length > 0 ? lineages.map(({ lineage, active, colour, title = lineage, primaryText, secondaryText = lineage }) => ( <Checkbox key={lineage} className={classNames( 'w-1/3 my-1 h-7 md:my-0', fixedLayout ? 'md:w-24 md:mx-0.5' : 'md:w-auto md:mx-2', { 'md:mb-1': isScrolling && nomenclature.length === 0 } )} title={title} style={{ color: colour }} id={`lineage_filter_${lineage}`} checked={active} onChange={() => toggleLineage(lineage)} > {primaryText ? <span className={classNames('block text-gray-700 dark:text-gray-100')}>{primaryText}</span> : null} <span className={classNames({ 'text-xs tracking-wide leading-none text-gray-500 dark:text-gray-300': primaryText })}>{secondaryText}</span> </Checkbox> )) : <> <div className={classNames({ 'lg:w-24 lg:mx-0.5': fixedLayout })} /> <div className='absolute inset-0 flex items-center justify-center'> {emptyMessage} </div> </> } </section> ))} </form> {(sections.length > 1 || (!isMobile && fixedLayout)) && ( isMobile ? <ol className='list-none p-1 flex justify-center space-x-2'> { sections.map((_, i) => <li key={`section-indicator-${i}`} className={classNames( 'rounded-full bg-gray-500 dark:bg-gray-300 w-2 h-2 transition-opacity', { 'opacity-50': i !== currentSection || 0 } )} /> )} </ol> : <form onSubmit={e => e.preventDefault()} className={classNames( 'flex flex-col justify-center relative left-1 pb-1 space-y-0.5 md:ml-2', { 'lg:ml-0': fixedLayout } )} > <Button ref={scrollUpBtnRef} title='Previous lineages' className='w-6 h-6 !p-0 flex items-center text-gray-700 dark:text-gray-200 transition-opacity disabled:opacity-50' onClick={() => doScroll(-1)} disabled={currentSection === 0 || null} > <BsArrowUpShort className='fill-current w-6 h-6'/> </Button> <Button ref={scrollDownBtnRef} title='Next lineages' className='w-6 h-6 !p-0 flex items-center text-gray-700 dark:text-gray-200 transition-opacity disabled:opacity-50' onClick={() => doScroll(1)} disabled={currentSection === sections.length - 1} > <BsArrowDownShort className='fill-current w-6 h-6'/> </Button> </form> )} </div> </div> ) } export default LineageFilter