UNPKG

@uppy/utils

Version:

Shared utility functions for Uppy Core and plugins maintained by the Uppy team.

159 lines (136 loc) 4.95 kB
/** * 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 { h, Component } from 'preact' 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', } class VirtualList extends Component { constructor (props) { super(props) // The currently focused node, used to retain focus when the visible rows change. // To avoid update loops, this should not cause state updates, so it's kept as a plain property. this.focusElement = null this.state = { offset: 0, height: 0, } } componentDidMount () { this.resize() window.addEventListener('resize', this.handleResize) } // TODO: refactor to stable lifecycle method // eslint-disable-next-line componentWillUpdate () { if (this.base.contains(document.activeElement)) { this.focusElement = document.activeElement } } componentDidUpdate () { // Maintain focus when rows are added and removed. if (this.focusElement && this.focusElement.parentNode && document.activeElement !== this.focusElement) { this.focusElement.focus() } this.focusElement = null this.resize() } componentWillUnmount () { window.removeEventListener('resize', this.handleResize) } handleScroll = () => { this.setState({ offset: this.base.scrollTop }) } handleResize = () => { this.resize() } resize () { const { height } = this.state if (height !== this.base.offsetHeight) { this.setState({ height: this.base.offsetHeight, }) } } render ({ data, rowHeight, renderRow, overscanCount = 10, ...props }) { const { offset, height } = this.state // 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 } // last visible + overscan row index + padding to allow keyboard focus to travel past the visible area const end = start + visibleRowCount + 4 // 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 ( // eslint-disable-next-line react/jsx-props-no-spreading <div onScroll={this.handleScroll} {...props}> <div role="presentation" style={styleInner}> <div role="presentation" style={styleContent}> {selection.map(renderRow)} </div> </div> </div> ) } } export default VirtualList