@uppy/utils
Version:
Shared utility functions for Uppy Core and plugins maintained by the Uppy team.
159 lines (136 loc) • 4.95 kB
JSX
/**
* 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