UNPKG

lucid-ui

Version:

A UI component library from AppNexus.

171 lines (170 loc) 6.76 kB
import React from 'react'; import PropTypes from 'react-peek/prop-types'; import _ from 'lodash'; import { lucidClassNames } from '../../util/style-helpers'; import { omitProps } from '../../util/component-types'; import { getAbsoluteBoundingClientRect } from '../../util/dom-helpers'; const cx = lucidClassNames.bind('&-StickySection'); const { node, number, object, string } = PropTypes; class StickySection extends React.Component { constructor() { super(...arguments); this.scrollContainer = React.createRef(); this.stickySection = React.createRef(); this.stickyFrame = React.createRef(); this.state = { isAboveFold: false, containerRect: { bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, frameLeft: 0, scrollWidth: 0, }, }; this.handleScroll = () => { const { lowerBound, topOffset = 0 } = this.props; const { isAboveFold, containerRect } = this.state; const nextContainerRect = this.getContainerRect(); if (window.pageYOffset + topOffset >= nextContainerRect.top) { if (!isAboveFold) { this.setState({ isAboveFold: true, }); } } else { if (isAboveFold) { this.setState({ isAboveFold: false, }); } } if (_.isNumber(lowerBound) && window.pageYOffset >= lowerBound) { this.setState({ isAboveFold: false, }); } if (containerRect.bottom !== nextContainerRect.bottom || containerRect.height !== nextContainerRect.height || containerRect.left !== nextContainerRect.left || containerRect.right !== nextContainerRect.right || containerRect.top !== nextContainerRect.top || containerRect.width !== nextContainerRect.width || containerRect.scrollWidth !== nextContainerRect.scrollWidth || containerRect.frameLeft !== nextContainerRect.frameLeft) { this.setState({ containerRect: nextContainerRect, }); } }; this.getContainerRect = () => { const containerRect = getAbsoluteBoundingClientRect(this.scrollContainer .current); const stickyRect = this.stickySection .current.getBoundingClientRect(); const frameRect = this.stickyFrame .current.getBoundingClientRect(); return { bottom: containerRect.top + stickyRect.height, height: stickyRect.height, left: containerRect.left, right: containerRect.left + stickyRect.width, top: containerRect.top, scrollWidth: this.stickySection.current.scrollWidth, width: containerRect.width, frameLeft: frameRect.left, }; }; } componentDidMount() { setTimeout(() => { this.setState({ containerRect: this.getContainerRect(), }); this.handleScroll(); }, 1); window.addEventListener('scroll', this.handleScroll, true); } componentWillUnmount() { window.removeEventListener('scroll', this.handleScroll, true); } render() { const { children, className, style, topOffset = 0, viewportWidth, ...passThroughs } = this.props; const { isAboveFold, containerRect } = this.state; return (React.createElement("div", Object.assign({}, omitProps(passThroughs, undefined, Object.keys(StickySection.propTypes)), { className: cx('&', className), style: { ...(isAboveFold ? { height: containerRect.height, visibility: 'hidden', } : {}), ...style, }, ref: this.scrollContainer }), React.createElement("div", { className: cx('&-sticky-frame'), ref: this.stickyFrame, style: { ...(isAboveFold ? { visibility: 'visible', position: 'fixed', top: topOffset, width: _.isNumber(viewportWidth) ? viewportWidth : containerRect.width, height: containerRect.height, overflow: 'hidden', } : {}), ...style, } }, React.createElement("div", { className: cx('&-sticky-section'), ref: this.stickySection, style: { ...(isAboveFold ? { position: 'absolute', top: 0, left: containerRect.left - containerRect.frameLeft || 0, width: containerRect.scrollWidth, height: containerRect.height, } : { position: 'relative', }), ...style, } }, children)))); } } StickySection.displayName = 'StickySection'; StickySection.peek = { description: ` \`StickySection\` can be wrapped around any content to make it _stick_ to the top edge of the screen when a user scrolls beyond its initial location. `, categories: ['helpers'], }; StickySection.propTypes = { children: node ` any valid React children `, className: string ` Appended to the component-specific class names set on the root element. `, style: object ` Styles that are passed through to the root container. `, lowerBound: number ` Pixel value from the top of the document. When scrolled passed, the sticky header is no longer sticky, and renders normally. `, viewportWidth: number ` Width of section when it sticks to the top edge of the screen. When omitted, it defaults to the last width of the section. `, topOffset: number ` Top offset threshold before sticking to the top. The sticky content will display with this offset. `, }; export default StickySection;