UNPKG

lucid-ui

Version:

A UI component library from AppNexus.

162 lines (161 loc) 6.88 kB
import _ from 'lodash'; import React from 'react'; import PropTypes from 'react-peek/prop-types'; import { Motion, spring } from 'react-motion'; import { QUICK_SLIDE_MOTION } from '../../constants/motion-spring'; import { lucidClassNames } from '../../util/style-helpers'; import { shiftChildren } from '../../util/dom-helpers'; import { findTypes, omitProps, } from '../../util/component-types'; const cx = lucidClassNames.bind('&-SlidePanel'); const { bool, func, node, number, string, any } = PropTypes; const modulo = (n, a) => a - n * Math.floor(a / n); class SlidePanelSlide extends React.Component { render() { return null; } } SlidePanelSlide.displayName = 'SlidePanel.Slide'; SlidePanelSlide.propName = 'Slide'; class SlidePanel extends React.Component { constructor() { super(...arguments); this.rootHTMLDivElement = React.createRef(); this.slideStrip = React.createRef(); this.offsetTranslate = this.props.isLooped ? Math.floor(_.size(findTypes(this.props, SlidePanel.Slide)) / 2) : 0; this.state = { translateXPixel: 0, startX: 0, isAnimated: this.props.isAnimated, isDragging: false, }; this.handleTouchStart = (event) => { this.setState({ startX: event.touches[0].screenX, isAnimated: false, isDragging: true, }); }; this.handleTouchMove = (event) => { const dX = event.touches[0].screenX - this.state.startX; this.setState({ translateXPixel: dX, }); }; this.handleTouchEnd = (event) => { const dX = event.changedTouches[0].screenX - this.state.startX; const slideWidth = this.rootHTMLDivElement.current.getBoundingClientRect() .width / this.props.slidesToShow; const slidesSwiped = Math.round(dX / slideWidth); if (slidesSwiped !== 0) { this.props.onSwipe(-1 * slidesSwiped, { event, props: this.props }); } this.setState({ translateXPixel: 0, isDragging: false, isAnimated: this.props.isAnimated, }); }; } componentDidMount() { const slides = findTypes(this.props, SlidePanel.Slide); if (this.props.isLooped) { shiftChildren(this.slideStrip.current, Math.floor(_.size(slides) / 2)); } } componentDidUpdate(prevProps, prevState) { const slides = findTypes(this.props, SlidePanel.Slide); const offsetDiff = this.props.offset - prevProps.offset; if (offsetDiff !== 0 && this.props.isLooped) { this.offsetTranslate = modulo(_.size(slides), this.offsetTranslate - offsetDiff); _.delay(() => { shiftChildren(this.slideStrip.current, -offsetDiff); this.setState({ isAnimated: false, }, () => { this.forceUpdate(); this.setState({ isAnimated: this.props.isAnimated, }); }); }, 200); } } render() { const { className, slidesToShow, offset: realOffset, isLooped, ...passThroughs } = this.props; const offset = realOffset + this.offsetTranslate; const slides = findTypes(this.props, SlidePanel.Slide); const translateXPercentage = -1 * (100 / slidesToShow) * (isLooped ? modulo(_.size(slides), offset) : offset); return (React.createElement("div", Object.assign({}, omitProps(passThroughs, undefined, Object.keys(SlidePanel.propTypes)), { ref: this.rootHTMLDivElement, className: cx('&', className) }), React.createElement(Motion, { style: this.state.isAnimated ? { translateXPercentage: spring(translateXPercentage, QUICK_SLIDE_MOTION), translateXPixel: spring(this.state.translateXPixel, QUICK_SLIDE_MOTION), } : { translateXPercentage: translateXPercentage, translateXPixel: this.state.translateXPixel, } }, (tween) => (React.createElement("div", Object.assign({}, omitProps(passThroughs, undefined, Object.keys(SlidePanel.propTypes)), { className: cx('&-slidestrip', className), style: { transform: this.state.isDragging ? `translateX(calc(${tween.translateXPercentage}% + ${this.state.translateXPixel}px))` : `translateX(calc(${tween.translateXPercentage}% + ${tween.translateXPixel}px))`, }, ref: this.slideStrip, onTouchStart: this.handleTouchStart, onTouchMove: this.handleTouchMove, onTouchEnd: this.handleTouchEnd, onTouchCancel: _.noop }), _.map(slides, (slide, offset) => (React.createElement("div", Object.assign({ key: offset }, slide.props, { className: cx('&-Slide', slide.props.className), style: { flexGrow: 1, flexShrink: 0, flexBasis: `${100 / slidesToShow}%`, ...slide.props.style, } }))))))))); } } SlidePanel._isPrivate = true; SlidePanel.displayName = 'SlidePanel'; SlidePanel.peek = { description: ` A container for rendering a set of horizontal slides at at a particular offset. Translation between slides is controlled by passing in a new \`offset\`. Can hook into touch events to update the \`offset\`. `, categories: ['helpers'], }; SlidePanel.propTypes = { className: string ` Appended to the component-specific class names set on the root element. `, children: node ` SlidePanel.Slide elements are passed in as children. `, Slide: any ` This is the child component that will be displayed inside the SlidePanel. `, slidesToShow: number ` Max number of viewable slides to show simultaneously. `, offset: number ` The offset of the left-most rendered slide. `, isAnimated: bool ` Animate slides transitions from changes in \`offset\`. `, isLooped: bool ` Slides are rendered in a continuous loop, where the first slide repeats after the last slide and vice-versa. DOM elements are re-ordered and re-used. `, onSwipe: func ` Called when a user's swipe would change the offset. Callback passes number of slides by the user (positive for forward swipes, negative for backwards swipes). Signature: \`(slidesSwiped, { event, props }) => {}\` `, }; SlidePanel.Slide = SlidePanelSlide; SlidePanel.defaultProps = { slidesToShow: 1, offset: 0, isAnimated: true, onSwipe: _.noop, isLooped: false, }; export default SlidePanel;