UNPKG

react-virtualized

Version:

React components for efficiently rendering large, scrollable lists and tabular data

278 lines (232 loc) 7.34 kB
// @flow import * as React from 'react'; import { registerScrollListener, unregisterScrollListener, } from './utils/onScroll'; import { getDimensions, getPositionOffset, getScrollOffset, } from './utils/dimensions'; import createDetectElementResize from '../vendor/detectElementResize'; type Props = { /** * Function responsible for rendering children. * This function should implement the following signature: * ({ height, isScrolling, scrollLeft, scrollTop, width }) => PropTypes.element */ children: ({ onChildScroll: ({scrollTop: number}) => void, registerChild: (?Element) => void, height: number, isScrolling: boolean, scrollLeft: number, scrollTop: number, width: number, }) => React.Node, /** Callback to be invoked on-resize: ({ height, width }) */ onResize: ({height: number, width: number}) => void, /** Callback to be invoked on-scroll: ({ scrollLeft, scrollTop }) */ onScroll: ({scrollLeft: number, scrollTop: number}) => void, /** Element to attach scroll event listeners. Defaults to window. */ scrollElement: ?(typeof window | Element), /** * Wait this amount of time after the last scroll event before resetting child `pointer-events`. */ scrollingResetTimeInterval: number, /** Height used for server-side rendering */ serverHeight: number, /** Width used for server-side rendering */ serverWidth: number, /** Force scrollTop updates when .updatePosition is called, fixing forced header height change updates */ updateScrollTopOnUpdatePosition?: boolean, }; type State = { height: number, width: number, isScrolling: boolean, scrollLeft: number, scrollTop: number, }; type ResizeHandler = (element: Element, onResize: () => void) => void; type DetectElementResize = { addResizeListener: ResizeHandler, removeResizeListener: ResizeHandler, }; /** * Specifies the number of milliseconds during which to disable pointer events while a scroll is in progress. * This improves performance and makes scrolling smoother. */ export const IS_SCROLLING_TIMEOUT = 150; const getWindow = () => (typeof window !== 'undefined' ? window : undefined); export default class WindowScroller extends React.PureComponent<Props, State> { static defaultProps = { onResize: () => {}, onScroll: () => {}, scrollingResetTimeInterval: IS_SCROLLING_TIMEOUT, scrollElement: getWindow(), serverHeight: 0, serverWidth: 0, }; _window = getWindow(); _isMounted = false; _positionFromTop = 0; _positionFromLeft = 0; _detectElementResize: DetectElementResize; _child: ?Element; _windowScrollerRef: {current: HTMLElement | null} = React.createRef(); state = { ...getDimensions(this.props.scrollElement, this.props), isScrolling: false, scrollLeft: 0, scrollTop: 0, }; updatePosition(scrollElement: ?Element = this.props.scrollElement) { const {onResize} = this.props; const {height, width} = this.state; const thisNode = this._child || this._windowScrollerRef.current; if (thisNode instanceof Element && scrollElement) { const offset = getPositionOffset(thisNode, scrollElement); this._positionFromTop = offset.top; this._positionFromLeft = offset.left; } const dimensions = getDimensions(scrollElement, this.props); if (height !== dimensions.height || width !== dimensions.width) { this.setState({ height: dimensions.height, width: dimensions.width, }); onResize({ height: dimensions.height, width: dimensions.width, }); } if (this.props.updateScrollTopOnUpdatePosition === true) { this.__handleWindowScrollEvent(); this.__resetIsScrolling(); } } componentDidMount() { const scrollElement = this.props.scrollElement; this._detectElementResize = createDetectElementResize(); this.updatePosition(scrollElement); if (scrollElement) { registerScrollListener(this, scrollElement); this._registerResizeListener(scrollElement); } this._isMounted = true; } componentDidUpdate(prevProps: Props, prevState: State) { const {scrollElement} = this.props; const {scrollElement: prevScrollElement} = prevProps; if ( prevScrollElement !== scrollElement && prevScrollElement != null && scrollElement != null ) { this.updatePosition(scrollElement); unregisterScrollListener(this, prevScrollElement); registerScrollListener(this, scrollElement); this._unregisterResizeListener(prevScrollElement); this._registerResizeListener(scrollElement); } } componentWillUnmount() { const scrollElement = this.props.scrollElement; if (scrollElement) { unregisterScrollListener(this, scrollElement); this._unregisterResizeListener(scrollElement); } this._isMounted = false; } render() { const {children} = this.props; const {isScrolling, scrollTop, scrollLeft, height, width} = this.state; return React.createElement( 'div', { ref: this._windowScrollerRef, }, children({ onChildScroll: this._onChildScroll, registerChild: this._registerChild, height, isScrolling, scrollLeft, scrollTop, width, }), ); } _registerChild = element => { if (element && !(element instanceof Element)) { console.warn( 'WindowScroller registerChild expects to be passed Element or null', ); } this._child = element; this.updatePosition(); }; _onChildScroll = ({scrollTop}) => { if (this.state.scrollTop === scrollTop) { return; } const scrollElement = this.props.scrollElement; if (scrollElement) { if (typeof scrollElement.scrollTo === 'function') { scrollElement.scrollTo(0, scrollTop + this._positionFromTop); } else { scrollElement.scrollTop = scrollTop + this._positionFromTop; } } }; _registerResizeListener = element => { if (element === window) { window.addEventListener('resize', this._onResize, false); } else { this._detectElementResize.addResizeListener(element, this._onResize); } }; _unregisterResizeListener = element => { if (element === window) { window.removeEventListener('resize', this._onResize, false); } else if (element) { this._detectElementResize.removeResizeListener(element, this._onResize); } }; _onResize = () => { this.updatePosition(); }; // Referenced by utils/onScroll __handleWindowScrollEvent = () => { if (!this._isMounted) { return; } const {onScroll} = this.props; const scrollElement = this.props.scrollElement; if (scrollElement) { const scrollOffset = getScrollOffset(scrollElement); const scrollLeft = Math.max( 0, scrollOffset.left - this._positionFromLeft, ); const scrollTop = Math.max(0, scrollOffset.top - this._positionFromTop); this.setState({ isScrolling: true, scrollLeft, scrollTop, }); onScroll({ scrollLeft, scrollTop, }); } }; // Referenced by utils/onScroll __resetIsScrolling = () => { this.setState({ isScrolling: false, }); }; }