/** * Adapted from preact-virtual-list: https://github.com/developit/preact-virtual-list * * © 2016 Jason Miller * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * Adaptations: * - Added role=presentation to helper elements * - Tweaked styles for Uppy's Dashboard use case */ import type { ComponentChild, h } from 'preact' import { useCallback, useEffect, useRef, useState } from 'preact/hooks' const STYLE_INNER = { position: 'relative', // Disabled for our use case: the wrapper elements around FileList already deal with overflow, // and this additional property would hide things that we want to show. // // overflow: 'hidden', width: '100%', minHeight: '100%', } const STYLE_CONTENT = { position: 'absolute', top: 0, left: 0, // Because the `top` value gets set to some offset, this `height` being 100% would make the scrollbar // stretch far beyond the content. For our use case, the content div actually can get its height from // the elements inside it, so we don't need to specify a `height` property at all. // // height: '100%', width: '100%', overflow: 'visible', } export default function VirtualList({ data, rowHeight, renderRow, overscanCount = 10, padding = 4, ...props }: { data: T[] rowHeight: number padding?: number // last visible + overscan row index + padding to allow keyboard focus to travel past the visible area renderRow: (item: T) => ComponentChild overscanCount?: number } & h.JSX.HTMLAttributes) { const scrollerRef = useRef(null) const [offset, setOffset] = useState(0) const [height, setHeight] = useState(0) useEffect(() => { function resize() { if ( scrollerRef.current != null && height !== scrollerRef.current.offsetHeight ) { setHeight(scrollerRef.current.offsetHeight) } } resize() window.addEventListener('resize', resize) return () => { window.removeEventListener('resize', resize) } }, [height]) const handleScroll = useCallback(() => { if (scrollerRef.current) setOffset(scrollerRef.current.scrollTop) }, []) // first visible row index let start = Math.floor(offset / rowHeight) // actual number of visible rows (without overscan) let visibleRowCount = Math.floor(height / rowHeight) // Overscan: render blocks of rows modulo an overscan row count // This dramatically reduces DOM writes during scrolling if (overscanCount) { start = Math.max(0, start - (start % overscanCount)) visibleRowCount += overscanCount } const end = start + visibleRowCount + padding // data slice currently in viewport plus overscan items const selection = data.slice(start, end) const styleInner = { ...STYLE_INNER, height: data.length * rowHeight } const styleContent = { ...STYLE_CONTENT, top: start * rowHeight } // The `role="presentation"` attributes ensure that these wrapper elements are not treated as list // items by accessibility and outline tools. return (
{selection.map(renderRow)}
) }