Skip to content

Commit 9fe8ddb

Browse files
author
Sindhu Balakrishnan
authored
New features - arrow click callback and custom arrow (#41)
* New features - arrow click callback and custom arrow option * lint * lint * prop types * prop types * Review comments fixes * Linting * remove spacing * remove spacing * Change callback name * Unit test for auto play
1 parent cdda231 commit 9fe8ddb

File tree

4 files changed

+198
-27
lines changed

4 files changed

+198
-27
lines changed

src/carousel.less

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,23 @@
1616
text-align: left;
1717
}
1818

19-
.carousel-arrow {
20-
position: absolute;
21-
z-index: 1;
22-
color: white;
23-
border: 3px solid;
19+
.carousel-arrow-default {
20+
border: 3px solid !important;
2421
border-radius: 50%;
25-
bottom: 23px;
22+
color: rgba(255, 255, 255, 0.9);
2623
height: 32px;
2724
width: 32px;
2825
font-weight: 900;
2926
background: rgba(0, 0, 0, 0.15);
27+
}
28+
29+
.carousel-arrow {
30+
position: absolute;
31+
z-index: 1;
32+
bottom: 23px;
3033
padding: 0;
3134
cursor: pointer;
35+
border: none;
3236

3337
&:focus {
3438
outline: none;
@@ -48,17 +52,27 @@
4852

4953
.carousel-left-arrow {
5054
left: 23px;
51-
&:before {
52-
content: '<';
53-
padding-right: 2px;
54-
}
5555
}
5656

5757
.carousel-right-arrow {
5858
right: 23px;
59-
&:before {
60-
content: '>';
61-
padding-left: 2px;
59+
}
60+
61+
.carousel-left-arrow {
62+
&.carousel-arrow-default {
63+
&:before {
64+
content: '<';
65+
padding-right: 2px;
66+
}
67+
}
68+
}
69+
70+
.carousel-right-arrow {
71+
&.carousel-arrow-default {
72+
&:before {
73+
content: '>';
74+
padding-left: 2px;
75+
}
6276
}
6377
}
6478

src/controls/arrow.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@ export default class Arrow extends Component {
1313
infinite: PropTypes.bool.isRequired,
1414
prevSlide: PropTypes.func.isRequired,
1515
nextSlide: PropTypes.func.isRequired,
16-
direction: PropTypes.oneOf(['left', 'right']).isRequired
16+
direction: PropTypes.oneOf(['left', 'right']).isRequired,
17+
arrows: PropTypes.oneOfType([
18+
PropTypes.bool,
19+
PropTypes.shape({
20+
left: PropTypes.node.isRequired,
21+
right: PropTypes.node.isRequired,
22+
className: PropTypes.string
23+
})
24+
])
1725
};
1826
}
1927

@@ -27,14 +35,23 @@ export default class Arrow extends Component {
2735
}
2836

2937
render() {
30-
const { prevSlide, nextSlide, direction } = this.props;
38+
const { prevSlide, nextSlide, direction, arrows } = this.props;
39+
let arrowComponent = null;
40+
let buttonClass = 'carousel-arrow-default';
41+
42+
if (arrows.left) {
43+
buttonClass = arrows.className ? arrows.className : '';
44+
arrowComponent = direction === 'left' ? arrows.left : arrows.right;
45+
}
3146

3247
return (
3348
<button
49+
type='button'
3450
disabled={ !this.hasNext() }
3551
onClick={ direction === 'left' ? prevSlide : nextSlide }
36-
className={ `carousel-arrow carousel-${direction}-arrow` }
37-
/>
52+
className={ `carousel-arrow carousel-${direction}-arrow ${buttonClass}` }>
53+
{ arrowComponent }
54+
</button>
3855
);
3956
}
4057
}

src/index.js

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ export default class Carousel extends Component {
2525
className: PropTypes.string,
2626
transition: PropTypes.oneOf(['slide', 'fade']),
2727
dots: PropTypes.bool,
28-
arrows: PropTypes.bool,
28+
arrows: PropTypes.oneOfType([
29+
PropTypes.bool,
30+
PropTypes.shape({
31+
left: PropTypes.node.isRequired,
32+
right: PropTypes.node.isRequired,
33+
className: PropTypes.string
34+
})
35+
]),
2936
infinite: PropTypes.bool,
3037
children: PropTypes.any,
3138
viewportWidth: PropTypes.string,
@@ -52,6 +59,7 @@ export default class Carousel extends Component {
5259
pauseOnHover: PropTypes.bool,
5360
clickToNavigate: PropTypes.bool,
5461
dragThreshold: PropTypes.number,
62+
onSlideTransitioned: PropTypes.func,
5563
easing: PropTypes.oneOf([
5664
'ease',
5765
'linear',
@@ -270,9 +278,19 @@ export default class Carousel extends Component {
270278
*
271279
* @param {Number} index - The slide index to move to.
272280
* @param {String} direction - The direction to transition, should be 'right' or 'left'.
281+
* @param {Boolean} autoSlide - The source of slide transition, should be true for autoPlay and false for user click.
273282
*/
274-
goToSlide(index, direction) {
275-
const { beforeChange, transitionDuration, transition } = this.props;
283+
goToSlide(index, direction, autoSlide = false) {
284+
const { beforeChange, transitionDuration, transition, onSlideTransitioned } = this.props;
285+
286+
if(onSlideTransitioned) {
287+
onSlideTransitioned({
288+
autoPlay: autoSlide,
289+
index,
290+
direction
291+
});
292+
}
293+
276294
const { currentSlide } = this.state;
277295
if (currentSlide === index) {
278296
return;
@@ -303,12 +321,13 @@ export default class Carousel extends Component {
303321

304322
/**
305323
* Transitions to the next slide moving from left to right.
324+
* @param {Object} e - The event that calls nextSlide, will be undefined for autoPlay.
306325
*/
307-
nextSlide() {
326+
nextSlide(e) {
308327
const { children } = this.props;
309328
const { currentSlide } = this.state;
310329
const newIndex = currentSlide < Children.count(children) - 1 ? currentSlide + 1 : 0;
311-
this.goToSlide(newIndex, 'right');
330+
this.goToSlide(newIndex, 'right', typeof e !== 'object');
312331
}
313332

314333
/**
@@ -379,7 +398,7 @@ export default class Carousel extends Component {
379398
*/
380399
render() {
381400
const { className, viewportWidth, viewportHeight, width, height, dots, infinite,
382-
children, slideHeight, transition, style, draggable, easing } = this.props;
401+
children, slideHeight, transition, style, draggable, easing, arrows } = this.props;
383402
const { loading, transitionDuration, dragOffset, currentSlide, leftOffset } = this.state;
384403
const numSlides = Children.count(children);
385404
const classes = classnames('carousel', className, {
@@ -456,6 +475,7 @@ export default class Carousel extends Component {
456475
nextSlide={ this.nextSlide }
457476
prevSlide={ this.prevSlide }
458477
goToSlide={ this.goToSlide }
478+
arrows={ arrows }
459479
infinite={ infinite } />
460480
))
461481
}

test/unit/carousel.tests.js

Lines changed: 124 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import React, { Fragment } from 'react';
22
import { render } from 'react-dom';
3-
import { expect } from 'chai';
3+
import chai, { expect } from 'chai';
4+
import sinon from 'sinon';
5+
import sinonChai from 'sinon-chai';
46
import { Simulate } from 'react-dom/test-utils';
57
import Carousel from '../../src/index';
68

79
function renderToJsdom(component) {
810
return render(component, window.document.querySelector('#root'));
911
}
1012

13+
chai.use(sinonChai);
1114
let imagesFetched;
1215

1316
global.Image = class MyImage {
@@ -57,6 +60,7 @@ describe('Carousel', () => {
5760
expect(dots.length).to.equal(3);
5861
expect(dots[0].className).to.contain('selected');
5962
const nextButton = document.querySelector('.carousel-right-arrow');
63+
expect(nextButton.className).to.contain('carousel-arrow-default');
6064
Simulate.click(nextButton);
6165
expect(dots[0].className).to.not.contain('selected');
6266
expect(dots[1].className).to.contain('selected');
@@ -65,8 +69,14 @@ describe('Carousel', () => {
6569
});
6670

6771
it('should navigate to the previous slide when the button is clicked', done => {
72+
const onSlideTransitionedStub = sinon.stub();
73+
6874
renderToJsdom(
69-
<Carousel initialSlide={ 1 } slideWidth='300px' viewportWidth='300px' infinite={ false }>
75+
<Carousel initialSlide={ 1 }
76+
slideWidth='300px'
77+
viewportWidth='300px'
78+
infinite={ false }
79+
onSlideTransitioned={ onSlideTransitionedStub }>
7080
<div id='slide1'/>
7181
<div id='slide2'/>
7282
<div id='slide3'/>
@@ -78,16 +88,28 @@ describe('Carousel', () => {
7888
expect(dots.length).to.equal(3);
7989
expect(dots[1].className).to.contain('selected');
8090
const prevButton = document.querySelector('.carousel-left-arrow');
91+
expect(prevButton.className).to.contain('carousel-arrow-default');
8192
Simulate.click(prevButton);
8293
expect(dots[1].className).to.not.contain('selected');
8394
expect(dots[0].className).to.contain('selected');
95+
expect(onSlideTransitionedStub).to.have.been.calledWith({
96+
autoPlay: false,
97+
index: 0,
98+
direction: 'left'
99+
});
84100
done();
85101
});
86102
});
87103

88104
it('should wrap around from the last to first slide if infinite is true and next is clicked', done => {
105+
const onSlideTransitionedStub = sinon.stub();
106+
89107
renderToJsdom(
90-
<Carousel initialSlide={ 2 } slideWidth='300px' viewportWidth='300px' infinite={ true }>
108+
<Carousel initialSlide={ 2 }
109+
slideWidth='300px'
110+
viewportWidth='300px'
111+
infinite={ true }
112+
onSlideTransitioned={ onSlideTransitionedStub }>
91113
<div id='slide1'/>
92114
<div id='slide2'/>
93115
<div id='slide3'/>
@@ -102,6 +124,11 @@ describe('Carousel', () => {
102124
Simulate.click(nextButton);
103125
expect(dots[2].className).to.not.contain('selected');
104126
expect(dots[0].className).to.contain('selected');
127+
expect(onSlideTransitionedStub).to.have.been.calledWith({
128+
autoPlay: false,
129+
index: 0,
130+
direction: 'right'
131+
});
105132
done();
106133
});
107134
});
@@ -128,8 +155,13 @@ describe('Carousel', () => {
128155
});
129156

130157
it('should jump directly to a slide when the dot is clicked', done => {
158+
const onSlideTransitionedStub = sinon.stub();
159+
131160
renderToJsdom(
132-
<Carousel slideWidth='300px' viewportWidth='300px' infinite={ false }>
161+
<Carousel slideWidth='300px'
162+
viewportWidth='300px'
163+
infinite={ false }
164+
onSlideTransitioned={ onSlideTransitionedStub }>
133165
<div id='slide1'/>
134166
<div id='slide2'/>
135167
<div id='slide3'/>
@@ -143,6 +175,7 @@ describe('Carousel', () => {
143175
Simulate.click(dots[2]);
144176
expect(dots[0].className).to.not.contain('selected');
145177
expect(dots[2].className).to.contain('selected');
178+
expect(onSlideTransitionedStub).to.have.been.calledOnce;
146179
done();
147180
});
148181
});
@@ -451,4 +484,91 @@ describe('Carousel', () => {
451484
expect(document.getElementById('slide10')).to.exist;
452485
});
453486
});
487+
488+
it('should render custom arrow', done => {
489+
const arrows = {
490+
className: 'test-custom-arrow',
491+
left: <span id='custom-left'>Left</span>,
492+
right: <span id='custom-right'>Right</span>
493+
};
494+
495+
renderToJsdom(
496+
<Carousel slideWidth='300px'
497+
viewportWidth='300px'
498+
infinite={ false }
499+
arrows={ arrows }>
500+
<div id='slide1'/>
501+
<div id='slide2'/>
502+
<div id='slide3'/>
503+
</Carousel>
504+
);
505+
506+
setImmediate(() => {
507+
const prevButton = document.querySelector('.carousel-left-arrow');
508+
const nextButton = document.querySelector('.carousel-right-arrow');
509+
expect(prevButton.className).to.contain('test-custom-arrow');
510+
expect(nextButton.className).to.contain('test-custom-arrow');
511+
expect(document.getElementById('custom-left')).to.exist;
512+
expect(document.getElementById('custom-right')).to.exist;
513+
done();
514+
});
515+
});
516+
517+
it('should render custom arrow without className', done => {
518+
const arrows = {
519+
left: <span id='custom-left'>Left</span>,
520+
right: <span id='custom-right'>Right</span>
521+
};
522+
523+
renderToJsdom(
524+
<Carousel slideWidth='300px'
525+
viewportWidth='300px'
526+
infinite={ false }
527+
arrows={ arrows }>
528+
<div id='slide1'/>
529+
<div id='slide2'/>
530+
<div id='slide3'/>
531+
</Carousel>
532+
);
533+
534+
setImmediate(() => {
535+
const prevButton = document.querySelector('.carousel-left-arrow');
536+
const nextButton = document.querySelector('.carousel-right-arrow');
537+
expect(prevButton.className).to.not.contain('carousel-arrow-default');
538+
expect(nextButton.className).to.not.contain('carousel-arrow-default');
539+
expect(document.getElementById('custom-left')).to.exist;
540+
expect(document.getElementById('custom-right')).to.exist;
541+
done();
542+
});
543+
});
544+
545+
it('should call onSlideTransitioned with autoPlay true', done => {
546+
const onSlideTransitionedStub = sinon.stub();
547+
const carousel = renderToJsdom(
548+
<Carousel slideWidth='300px'
549+
viewportWidth='300px'
550+
infinite={ false }
551+
autoplay={ true }
552+
pauseOnHover={ true }
553+
onSlideTransitioned={ onSlideTransitionedStub }>
554+
<div id='slide1' />
555+
<div id='slide2' />
556+
<div id='slide3' />
557+
</Carousel>
558+
);
559+
560+
setImmediate(() => {
561+
const track = document.querySelector('.carousel-viewport');
562+
const setHoverState = (bool) => {
563+
expect(bool).to.be.true;
564+
done();
565+
};
566+
carousel.setHoverState = setHoverState;
567+
carousel.handleMovement(track);
568+
expect(onSlideTransitionedStub).to.have.been.calledWith({
569+
autoPlay: true,
570+
direction: 'right'
571+
});
572+
});
573+
});
454574
});

0 commit comments

Comments
 (0)