lucid-ui
Version:
A UI component library from AppNexus.
162 lines (161 loc) • 6.88 kB
JavaScript
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;