UNPKG

lucid-ui

Version:

A UI component library from Xandr.

169 lines 6.67 kB
import _, { omit } from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; import { lucidClassNames } from '../../util/style-helpers'; 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", { ...omit(passThroughs, [ 'lowerBound', 'topOffset', 'initialState', 'callbackId', ]), 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 = { /** any valid React children */ children: node, /** Appended to the component-specific class names set on the root element. */ className: string, /** Styles that are passed through to the root container. */ style: object, /** Pixel value from the top of the document. When scrolled passed, the sticky header is no longer sticky, and renders normally. */ lowerBound: number, /** Top offset threshold before sticking to the top. The sticky content will display with this offset. */ topOffset: number, }; export default StickySection; //# sourceMappingURL=StickySection.js.map