lucid-ui
Version:
A UI component library from Xandr.
178 lines • 7.11 kB
JavaScript
import _, { omit } from 'lodash';
import React from 'react';
import PropTypes from '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 } 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';
const nonPassThroughs = [
'className',
'children',
'Slide',
'slidesToShow',
'offset',
'isAnimated',
'isLooped',
'onSwipe',
'initialState',
'callbackId',
];
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", { ...omit(passThroughs, nonPassThroughs), 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", { ...omit(passThroughs, nonPassThroughs), 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", { 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 = {
/**
Appended to the component-specific class names set on the root element.
*/
className: string,
/**
SlidePanel.Slide elements are passed in as children.
*/
children: node,
/**
This is the child component that will be displayed inside the SlidePanel.
*/
Slide: any,
/**
Max number of viewable slides to show simultaneously.
*/
slidesToShow: number,
/**
The offset of the left-most rendered slide.
*/
offset: number,
/**
Animate slides transitions from changes in \`offset\`.
*/
isAnimated: 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.
*/
isLooped: bool,
/**
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 }) => {}\`
*/
onSwipe: func,
};
SlidePanel.Slide = SlidePanelSlide;
SlidePanel.defaultProps = {
slidesToShow: 1,
offset: 0,
isAnimated: true,
onSwipe: _.noop,
isLooped: false,
};
export default SlidePanel;
//# sourceMappingURL=SlidePanel.js.map