Skip to content

Commit 627a73e

Browse files
committed
feat(UncontrolledCarousel): add UncontrolledCarousel
1 parent 085181c commit 627a73e

10 files changed

Lines changed: 409 additions & 67 deletions

File tree

docs/lib/Components/CarouselPage.js

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import React from 'react';
33
import { PrismCode } from 'react-prism';
44
import Helmet from 'react-helmet';
55
import CarouselExample from '../examples/Carousel';
6-
const CarouslExampleSource = require('!!raw!../examples/Carousel');
6+
const CarouselExampleSource = require('!!raw!../examples/Carousel');
7+
import CarouselUncontrolledExample from '../examples/CarouselUncontrolled';
8+
const CarouselUncontrolledExampleSource = require('!!raw!../examples/CarouselUncontrolled');
79

810
export default class CarouselPage extends React.Component {
911
render() {
@@ -17,7 +19,7 @@ export default class CarouselPage extends React.Component {
1719
</div>
1820
<pre>
1921
<PrismCode className="language-jsx">
20-
{CarouslExampleSource}
22+
{CarouselExampleSource}
2123
</PrismCode>
2224
</pre>
2325

@@ -33,22 +35,55 @@ export default class CarouselPage extends React.Component {
3335
previous: PropTypes.func.isRequired,
3436
// controls if the left and right arrow keys should control the carousel
3537
keyboard: PropTypes.bool,
36-
// controls if the carousel should not automatically cycle (default: false)
37-
paused: PropTypes.bool,
38-
// the interval at which the carousel automatically cycles (default: 5000)
38+
/* If set to "hover", pauses the cycling of the carousel on mouseenter and resumes the cycling of the carousel on
39+
* mouseleave. If set to false, hovering over the carousel won't pause it. (default: "hover")
40+
*/
41+
pause: PropTypes.oneOf(['hover', false]),
42+
// Autoplays the carousel after the user manually cycles the first item. If "carousel", autoplays the carousel on load.
43+
// This is how bootstrap defines it... I would prefer a bool named autoplay or something...
44+
ride: PropTypes.oneOf(['carousel']),
45+
// the interval at which the carousel automatically cycles (default: 5000)
3946
interval: PropTypes.oneOfType([
4047
PropTypes.number,
4148
PropTypes.string,
4249
PropTypes.bool,
4350
]),
4451
children: PropTypes.array,
4552
// called when the mouse enters the Carousel
46-
hoverStart: PropTypes.func,
53+
mouseEnter: PropTypes.func,
4754
// called when the mouse exits the Carousel
48-
hoverEnd: PropTypes.func,
55+
mouseLeave: PropTypes.func,
4956
// controls whether the slide animation on the Carousel works or not
5057
slide: PropTypes.bool,
5158
cssModule: PropTypes.object,
59+
};`}
60+
</PrismCode>
61+
</pre>
62+
63+
<h3>Uncontrolled Carousel</h3>
64+
<p>
65+
For the most basic use-case an uncontrolled component can provide the functionality wanted without the need to manage/control the state of the component. <code>UncontrolledCarousel</code> does not require <code>previous</code>, <code>next</code> nor <code>activeIndex</code> props to work.
66+
Anything provided to a normal <code>Carousel</code> can also be provided to <code>UncontrolledCarousel</code>, overriding the control <code>UncontrolledCarousel</code> provides. Additionally, you can hide the controls by passing <code>false</code> to the <code>controls</code> prop
67+
and you can disable the indicators by passing <code>false</code> to the <code>indicators</code> prop; both are visible by default. Autoplay (<code>ride="carousel"</code>) is enabled by default, you can disable it by passing <code>false</code> to the <code>autoPlay</code> prop.
68+
</p>
69+
<div className="docs-example">
70+
<CarouselUncontrolledExample />
71+
</div>
72+
<pre>
73+
<PrismCode className="language-jsx">
74+
{CarouselUncontrolledExampleSource}
75+
</PrismCode>
76+
</pre>
77+
78+
<h3>Uncontrolled Carousel Properties</h3>
79+
<p>Same as Carousel (except children) can be overridden plus the following</p>
80+
<pre>
81+
<PrismCode className="language-jsx">
82+
{`UncontrolledCarousel.propTypes = {
83+
items: PropTypes.array,isRequired,
84+
indicators: PropTypes.bool, // default: true
85+
controls: PropTypes.bool, // default: true
86+
autoPlay: PropTypes.bool, // default: true
5287
};`}
5388
</PrismCode>
5489
</pre>

docs/lib/examples/Carousel.js

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,43 +28,48 @@ const items = [
2828
class Example extends Component {
2929
constructor(props) {
3030
super(props);
31-
this.state = { activeIndex: 0, paused: false };
31+
this.state = { activeIndex: 0 };
3232
this.next = this.next.bind(this);
3333
this.previous = this.previous.bind(this);
3434
this.goToIndex = this.goToIndex.bind(this);
35-
this.pause = this.pause.bind(this);
36-
this.cycle = this.cycle.bind(this);
35+
this.onExiting = this.onExiting.bind(this);
36+
this.onExited = this.onExited.bind(this);
37+
}
38+
39+
onExiting() {
40+
this.animating = true;
41+
}
42+
43+
onExited() {
44+
this.animating = false;
3745
}
3846

3947
next() {
48+
if (this.animating) return;
4049
const nextIndex = this.state.activeIndex === items.length - 1 ? 0 : this.state.activeIndex + 1;
4150
this.setState({ activeIndex: nextIndex });
4251
}
4352

4453
previous() {
54+
if (this.animating) return;
4555
const nextIndex = this.state.activeIndex === 0 ? items.length - 1 : this.state.activeIndex - 1;
4656
this.setState({ activeIndex: nextIndex });
4757
}
4858

4959
goToIndex(newIndex) {
60+
if (this.animating) return;
5061
this.setState({ activeIndex: newIndex });
5162
}
5263

53-
pause() {
54-
this.setState({ paused: true });
55-
}
56-
57-
cycle() {
58-
this.setState({ paused: false });
59-
}
60-
6164
render() {
6265
const { activeIndex } = this.state;
6366

64-
const slides = items.map((item, idx) => {
67+
const slides = items.map((item) => {
6568
return (
6669
<CarouselItem
67-
key={idx}
70+
onExiting={this.onExiting}
71+
onExited={this.onExited}
72+
key={item.src}
6873
src={item.src}
6974
altText={item.altText}
7075
>
@@ -75,14 +80,11 @@ class Example extends Component {
7580

7681
return (
7782
<Carousel
78-
activeIndex={this.state.activeIndex}
83+
activeIndex={activeIndex}
7984
next={this.next}
8085
previous={this.previous}
81-
paused={this.state.paused}
82-
hoverStart={this.pause}
83-
hoverEnd={this.cycle}
8486
>
85-
<CarouselIndicators items={slides} activeIndex={activeIndex} onClickHandler={this.goToIndex} />
87+
<CarouselIndicators items={items} activeIndex={activeIndex} onClickHandler={this.goToIndex} />
8688
{slides}
8789
<CarouselControl direction="prev" directionText="Previous" onClickHandler={this.previous} />
8890
<CarouselControl direction="next" directionText="Next" onClickHandler={this.next} />
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import { UncontrolledCarousel } from 'reactstrap';
3+
4+
const items = [
5+
{
6+
src: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa1d%20text%20%7B%20fill%3A%23555%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa1d%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23777%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22285.921875%22%20y%3D%22218.3%22%3EFirst%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E',
7+
altText: 'Slide 1',
8+
caption: 'Slide 1'
9+
},
10+
{
11+
src: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa20%20text%20%7B%20fill%3A%23444%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa20%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23666%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22247.3203125%22%20y%3D%22218.3%22%3ESecond%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E',
12+
altText: 'Slide 2',
13+
caption: 'Slide 2'
14+
},
15+
{
16+
src: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa21%20text%20%7B%20fill%3A%23333%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa21%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23555%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22277%22%20y%3D%22218.3%22%3EThird%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E',
17+
altText: 'Slide 3',
18+
caption: 'Slide 3'
19+
}
20+
];
21+
22+
const Example = () => <UncontrolledCarousel items={items} />;
23+
24+
export default Example;

src/Carousel.js

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import classNames from 'classnames';
4+
import CarouselItem from './CarouselItem';
45
import { mapToCssModules } from './utils';
56

67
class Carousel extends React.Component {
78
constructor(props) {
89
super(props);
910
this.handleKeyPress = this.handleKeyPress.bind(this);
1011
this.renderItems = this.renderItems.bind(this);
12+
this.hoverStart = this.hoverStart.bind(this);
13+
this.hoverEnd = this.hoverEnd.bind(this);
1114
this.state = { direction: 'right' };
1215
}
1316

@@ -17,42 +20,72 @@ class Carousel extends React.Component {
1720

1821
componentDidMount() {
1922
// Set up the cycle
20-
if (this.props.interval) {
21-
this.cycleInterval = setInterval(() => {
22-
if (!this.props.paused) {
23-
this.props.next();
24-
}
25-
}, parseInt(this.props.interval, 10));
23+
if (this.props.ride === 'carousel') {
24+
this.setInterval();
2625
}
2726

28-
if (this.props.keyboard) {
29-
document.addEventListener('keyup', this.handleKeyPress);
30-
}
27+
// TODO: move this to the specific carousel like bootstrap. Currently it will trigger ALL carousels on the page.
28+
document.addEventListener('keyup', this.handleKeyPress);
3129
}
3230

3331
componentWillReceiveProps(nextProps) {
32+
this.setInterval(nextProps);
3433
// Calculate the direction to turn
3534
if (this.props.activeIndex + 1 === nextProps.activeIndex) {
3635
this.setState({ direction: 'right' });
3736
} else if (this.props.activeIndex - 1 === nextProps.activeIndex) {
3837
this.setState({ direction: 'left' });
3938
} else if (this.props.activeIndex > nextProps.activeIndex) {
4039
this.setState({ direction: 'right' });
41-
} else {
40+
} else if (this.props.activeIndex !== nextProps.activeIndex) {
4241
this.setState({ direction: 'left' });
4342
}
4443
}
4544

4645
componentWillUnmount() {
46+
this.clearInterval();
47+
document.removeEventListener('keyup', this.handleKeyPress);
48+
}
49+
50+
setInterval(props = this.props) {
51+
// make sure not to have multiple intervals going...
52+
this.clearInterval();
53+
if (props.interval) {
54+
this.cycleInterval = setInterval(() => {
55+
props.next();
56+
}, parseInt(props.interval, 10));
57+
}
58+
}
59+
60+
clearInterval() {
4761
clearInterval(this.cycleInterval);
48-
document.removeEventListener('key', this.handleKeyPress);
62+
}
63+
64+
hoverStart(...args) {
65+
if (this.props.pause === 'hover') {
66+
this.clearInterval();
67+
}
68+
if (this.props.mouseEnter) {
69+
this.props.mouseEnter(...args);
70+
}
71+
}
72+
73+
hoverEnd(...args) {
74+
if (this.props.pause === 'hover') {
75+
this.setInterval();
76+
}
77+
if (this.props.mouseLeave) {
78+
this.props.mouseLeave(...args);
79+
}
4980
}
5081

5182
handleKeyPress(evt) {
52-
if (this.props.keyboard && evt.keyCode === 37) {
53-
this.props.previous();
54-
} else if (this.props.keyboard && evt.keyCode === 39) {
55-
this.props.next();
83+
if (this.props.keyboard) {
84+
if (evt.keyCode === 37) {
85+
this.props.previous();
86+
} else if (evt.keyCode === 39) {
87+
this.props.next();
88+
}
5689
}
5790
}
5891

@@ -72,7 +105,7 @@ class Carousel extends React.Component {
72105
}
73106

74107
render() {
75-
const { children, cssModule, hoverStart, hoverEnd, slide } = this.props;
108+
const { children, cssModule, slide } = this.props;
76109
const outerClasses = mapToCssModules(classNames(
77110
'carousel',
78111
slide && 'slide',
@@ -83,14 +116,12 @@ class Carousel extends React.Component {
83116
), cssModule);
84117

85118

86-
const slidesOnly = children.every((child) => {
87-
return child.type && child.type.name === 'CarouselItem';
88-
});
119+
const slidesOnly = children.every(child => child.type === CarouselItem);
89120

90121
// Rendering only slides
91122
if (slidesOnly) {
92123
return (
93-
<div className={outerClasses} onMouseEnter={hoverStart} onMouseLeave={hoverEnd}>
124+
<div className={outerClasses} onMouseEnter={this.hoverStart} onMouseLeave={this.hoverEnd}>
94125
{this.renderItems(children, innerClasses)}
95126
</div>
96127
);
@@ -103,7 +134,7 @@ class Carousel extends React.Component {
103134
const controlRight = children[2];
104135

105136
return (
106-
<div className={outerClasses} onMouseEnter={hoverStart} onMouseLeave={hoverEnd}>
137+
<div className={outerClasses} onMouseEnter={this.hoverStart} onMouseLeave={this.hoverEnd}>
107138
{this.renderItems(carouselItems, innerClasses)}
108139
{controlLeft}
109140
{controlRight}
@@ -118,14 +149,7 @@ class Carousel extends React.Component {
118149
const controlRight = children[3];
119150

120151
return (
121-
<div
122-
ref={(carousel) => {
123-
this.carousel = carousel;
124-
}}
125-
className={outerClasses}
126-
onMouseEnter={hoverStart}
127-
onMouseLeave={hoverEnd}
128-
>
152+
<div className={outerClasses} onMouseEnter={this.hoverStart} onMouseLeave={this.hoverEnd}>
129153
{indicators}
130154
{this.renderItems(carouselItems, innerClasses)}
131155
{controlLeft}
@@ -144,8 +168,13 @@ Carousel.propTypes = {
144168
previous: PropTypes.func.isRequired,
145169
// controls if the left and right arrow keys should control the carousel
146170
keyboard: PropTypes.bool,
147-
// controls if the carousel should not automatically cycle (default: false)
148-
paused: PropTypes.bool,
171+
/* If set to "hover", pauses the cycling of the carousel on mouseenter and resumes the cycling of the carousel on
172+
* mouseleave. If set to false, hovering over the carousel won't pause it. (default: "hover")
173+
*/
174+
pause: PropTypes.oneOf(['hover', false]),
175+
// Autoplays the carousel after the user manually cycles the first item. If "carousel", autoplays the carousel on load.
176+
// This is how bootstrap defines it... I would prefer a bool named autoplay or something...
177+
ride: PropTypes.oneOf(['carousel']),
149178
// the interval at which the carousel automatically cycles (default: 5000)
150179
interval: PropTypes.oneOfType([
151180
PropTypes.number,
@@ -154,18 +183,17 @@ Carousel.propTypes = {
154183
]),
155184
children: PropTypes.array,
156185
// called when the mouse enters the Carousel
157-
hoverStart: PropTypes.func,
186+
mouseEnter: PropTypes.func,
158187
// called when the mouse exits the Carousel
159-
hoverEnd: PropTypes.func,
188+
mouseLeave: PropTypes.func,
160189
// controls whether the slide animation on the Carousel works or not
161190
slide: PropTypes.bool,
162191
cssModule: PropTypes.object,
163192
};
164193

165194
Carousel.defaultProps = {
166195
interval: 5000,
167-
hover: false,
168-
paused: false,
196+
pause: 'hover',
169197
keyboard: true,
170198
slide: true,
171199
};

src/CarouselIndicators.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const CarouselIndicators = (props) => {
1313
), cssModule);
1414
return (
1515
<li
16-
key={`${item.src}${item.caption}${item.altText}`}
16+
key={`${item.key || item.src}${item.caption}${item.altText}`}
1717
onClick={(e) => {
1818
e.preventDefault();
1919
onClickHandler(idx);

src/CarouselItem.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ class CarouselItem extends React.Component {
2727

2828
onEntering(node, isAppearing) {
2929
// getting this variable triggers a reflow
30-
const _unused = node.offsetHeight; // eslint-disable-line no-unused-vars
30+
const offsetHeight = node.offsetHeight;
3131
this.setState({ startAnimation: true });
3232
this.props.onEntering(node, isAppearing);
33+
return offsetHeight;
3334
}
3435

3536
onExit(node) {

0 commit comments

Comments
 (0)