@render-props/scrollable
Version:
A state container which provides an interface for listening to the scroll event of its child component and providing valuable data about direction, distance, and more. It also provides convenience functions for scrollTo with optional animation.
228 lines (210 loc) • 5.17 kB
JavaScript
import _extends from '@babel/runtime/helpers/esm/extends'
import React from 'react'
import PropTypes from 'prop-types'
import Events from '@render-props/events'
import Throttle from '@render-props/throttle'
import {callIfExists} from '@render-props/utils'
import {scrollTo, getDistance, getDirection} from './utils'
/**
import Scrollable from '@render-props/scrollable'
const ScrollableBox = props => (
<Scrollable>
{
({
scrollRef,
scrollToX,
scrollToY,
scrollTo,
scrollHeight,
scrollWidth,
scrollY,
scrollX,
clientHeight,
clientWidth,
max,
direction,
distance,
}) => (
<div
ref={scrollRef}
style={{
width: 300,
height: 300,
overflow: 'auto',
background: '#000'
}}
>
<div style={{width: 600, height: 16000, position: 'relative'}}>
<pre style={{
position: 'absolute',
top: scrollY + 10,
left: scrollX + 10
}}>
{JSON.stringify({
scrollRef,
scrollToX,
scrollToY,
scrollTo,
scrollHeight,
scrollWidth,
scrollY,
scrollX,
clientHeight,
clientWidth,
max,
direction,
distance,
}, null, 2)}
</pre>
</div>
</div>
)
}
</Scrollable>
)
*/
const initialState = {
scrollHeight: 0,
scrollWidth: 0,
scrollY: 0,
scrollX: 0,
clientHeight: 0,
clientWidth: 0,
max: {
x: 0,
y: 0,
},
direction: {
x: 0,
y: 0,
},
distance: {
x: 0,
y: 0,
},
}
export class Scrollable_ extends React.Component {
constructor(props) {
super(props)
this.scrollable = null
this.scrollRef = e => {
if (e === null) {
return
}
const scrollableChanged = this.scrollable !== e
if (this.scrollable !== e) {
if (this.scrollable !== null) {
this.props.removeAllEvents(this.scrollable)
}
this.scrollable = e
this.props.addEvent(e, 'scroll', this.onScroll)
this.onScroll()
}
}
this.onScroll = e => {
this.props.throttleState(prevState => {
const {
scrollWidth,
scrollHeight,
scrollLeft,
scrollTop,
clientWidth,
clientHeight,
} = this.scrollable
const nextScroll = {
scrollX: scrollLeft,
scrollY: scrollTop,
}
const direction = getDirection(prevState, nextScroll)
const distance = getDistance(prevState, nextScroll)
return {
scrollWidth: scrollWidth,
scrollHeight: scrollHeight,
scrollX: scrollLeft,
scrollY: scrollTop,
clientWidth,
clientHeight,
max: {
x: scrollWidth - clientWidth,
y: scrollHeight - clientHeight,
},
direction,
distance,
}
})
}
this.scrollTo = (posX, posY, opt) => {
if (typeof opt !== 'object' && opt !== null) {
if (posY !== void 0 && posX !== null) {
this.scrollable.scrollTop = posY
}
if (posX !== void 0 && posY !== null) {
this.scrollable.scrollLeft = posX
}
} else {
scrollTo(
this.scrollable,
{
x: this.props.state.scrollX,
y: this.props.state.scrollY,
},
{
x: posX,
y: posY,
},
opt
)
}
}
this.scrollToX = (posX, opt) => this.scrollTo(posX, void 0, opt)
this.scrollToY = (posY, opt) => this.scrollTo(void 0, posY, opt)
this.scrollableContext = {
scrollRef: this.scrollRef,
scrollToX: this.scrollToX,
scrollToY: this.scrollToY,
scrollTo: this.scrollTo,
}
}
componentDidMount() {
const {initialX, initialY} = this.props
if (initialX || initialY) {
this.scrollTo(initialX, initialY)
}
}
componentDidUpdate({state}) {
if (
state.scrollX !== this.props.state.scrollX ||
state.scrollY !== this.props.state.scrollY
) {
callIfExists(this.props.onScroll, this.props.state)
}
}
render() {
return this.props.children(
_extends(this.scrollableContext, this.props.state)
)
}
}
Scrollable_.displayName = 'Scrollable'
Scrollable_.propTypes = {
children: PropTypes.func.isRequired,
onScroll: PropTypes.func,
initialX: PropTypes.number,
initialY: PropTypes.number,
}
export default function Scrollable(props) {
return React.createElement(Events, null, function(eventContext) {
return React.createElement(
Throttle,
{
initialState: initialState,
},
function(throttleContext) {
return React.createElement(
Scrollable_,
_extends({}, eventContext, throttleContext, props)
)
}
)
})
}