Skip to content

Commit

Permalink
feat(carousel): stay paused after calling pause() until cycle() is ca…
Browse files Browse the repository at this point in the history
…lled (#3225)

After setting the carousel on pause through its `pause()` method and clicking
on navigation arrows, the carousel no longer automatically restarts.
Calling the `cycle()` method is needed to undo the effect of the `pause()`
method.
When `pauseOnHover` is true, hovering on the carousel no longer uses the
`pause()` and `cycle()` methods. The hover status is stored separately, so that
calling pause() and removing the mouse from the carousel no longer restarts
it automatically.

In order for the user to have more control, we also introduce:

+ The slide event now has a "source" property allowing to know what triggered
the event: "timer", "arrowLeft", "arrowRight" or "indicator".
+ The "paused" boolean status is now also included in the slide event.

Closes #3188
  • Loading branch information
divdavem authored and Benoit Charbonnier committed Jul 17, 2019
1 parent 5b6cc69 commit 2602154
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 48 deletions.
11 changes: 10 additions & 1 deletion demo/src/app/components/carousel/carousel.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { NgbdCarouselConfig } from './demos/config/carousel-config';
import { NgbdCarouselConfigModule } from './demos/config/carousel-config.module';
import { NgbdCarouselNavigation } from './demos/navigation/carousel-navigation';
import { NgbdCarouselNavigationModule } from './demos/navigation/carousel-navigation.module';
import { NgbdCarouselPause } from './demos/pause/carousel-pause';
import { NgbdCarouselPauseModule } from './demos/pause/carousel-pause.module';

const DEMOS = {
basic: {
Expand All @@ -25,6 +27,12 @@ const DEMOS = {
code: require('!!raw-loader!./demos/navigation/carousel-navigation'),
markup: require('!!raw-loader!./demos/navigation/carousel-navigation.html')
},
pause: {
title: 'Pause/cycle',
type: NgbdCarouselPause,
code: require('!!raw-loader!./demos/pause/carousel-pause'),
markup: require('!!raw-loader!./demos/pause/carousel-pause.html')
},
config: {
title: 'Global configuration of carousels',
type: NgbdCarouselConfig,
Expand All @@ -51,7 +59,8 @@ export const ROUTES = [
NgbdComponentsSharedModule,
NgbdCarouselBasicModule,
NgbdCarouselConfigModule,
NgbdCarouselNavigationModule
NgbdCarouselNavigationModule,
NgbdCarouselPauseModule
]
})
export class NgbdCarouselModule {
Expand Down
30 changes: 30 additions & 0 deletions demo/src/app/components/carousel/demos/pause/carousel-pause.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<ngb-carousel #carousel interval="1000" [pauseOnHover]="pauseOnHover" (slide)="onSlide($event)">
<ng-template ngbSlide *ngFor="let img of images; index as i">
<div class="carousel-caption">
<h3>My slide {{i + 1}} title</h3>
</div>
<a href="https://www.google.fr/?q=Number+{{i+1}}" target="_blank" rel="nofollow noopener noreferrer">
<div class="picsum-img-wrapper">
<img [src]="img" alt="My image {{i + 1}} description">
</div>
</a>
</ng-template>
</ngb-carousel>

<hr>

<div class="form-check">
<input type="checkbox" class="form-check-input" id="pauseOnHover" [(ngModel)]="pauseOnHover">
<label class="form-check-label" for="pauseOnHover">Pause on hover</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="unpauseOnArrow" [(ngModel)]="unpauseOnArrow">
<label class="form-check-label" for="unpauseOnArrow">Unpause when clicking on arrows</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="pauseOnIndicator" [(ngModel)]="pauseOnIndicator">
<label class="form-check-label" for="pauseOnIndicator">Pause when clicking on navigation indicator</label>
</div>
<button type="button" (click)="togglePaused()" class="btn btn-outline-dark btn-sm">
{{paused ? 'Cycle' : 'Pause' }}
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';

import { NgbdCarouselPause } from './carousel-pause';

@NgModule({
imports: [BrowserModule, FormsModule, NgbModule],
declarations: [NgbdCarouselPause],
exports: [NgbdCarouselPause],
bootstrap: [NgbdCarouselPause]
})
export class NgbdCarouselPauseModule {}
34 changes: 34 additions & 0 deletions demo/src/app/components/carousel/demos/pause/carousel-pause.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Component, ViewChild } from '@angular/core';
import { NgbCarousel, NgbSlideEvent, NgbSlideEventSource } from '@ng-bootstrap/ng-bootstrap';


@Component({selector: 'ngbd-carousel-pause', templateUrl: './carousel-pause.html'})
export class NgbdCarouselPause {
images = [1, 2, 3, 4, 5, 6, 7].map(() => `https://picsum.photos/900/500?random&t=${Math.random()}`);

paused = false;
unpauseOnArrow = false;
pauseOnIndicator = false;
pauseOnHover = true;

@ViewChild('carousel', {static : true}) carousel: NgbCarousel;

togglePaused() {
if (this.paused) {
this.carousel.cycle();
} else {
this.carousel.pause();
}
this.paused = !this.paused;
}

onSlide(slideEvent: NgbSlideEvent) {
if (this.unpauseOnArrow && slideEvent.paused &&
(slideEvent.source === NgbSlideEventSource.ARROW_LEFT || slideEvent.source === NgbSlideEventSource.ARROW_RIGHT)) {
this.togglePaused();
}
if (this.pauseOnIndicator && !slideEvent.paused && slideEvent.source === NgbSlideEventSource.INDICATOR) {
this.togglePaused();
}
}
}
2 changes: 1 addition & 1 deletion src/carousel/carousel.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {CommonModule} from '@angular/common';

import {NGB_CAROUSEL_DIRECTIVES} from './carousel';

export {NgbCarousel, NgbSlide, NgbSlideEvent} from './carousel';
export {NgbCarousel, NgbSlide, NgbSlideEvent, NgbSlideEventDirection, NgbSlideEventSource} from './carousel';
export {NgbCarouselConfig} from './carousel-config';

@NgModule({declarations: NGB_CAROUSEL_DIRECTIVES, exports: NGB_CAROUSEL_DIRECTIVES, imports: [CommonModule]})
Expand Down
151 changes: 142 additions & 9 deletions src/carousel/carousel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {By} from '@angular/platform-browser';
import {ChangeDetectionStrategy, Component} from '@angular/core';

import {NgbCarouselModule} from './carousel.module';
import {NgbCarousel, NgbSlideEvent, NgbSlideEventDirection} from './carousel';
import {NgbCarousel, NgbSlideEvent, NgbSlideEventDirection, NgbSlideEventSource} from './carousel';
import {NgbCarouselConfig} from './carousel-config';

const createTestComponent = (html: string) =>
Expand Down Expand Up @@ -193,6 +193,106 @@ describe('ngb-carousel', () => {
discardPeriodicTasks();
}));

it('should not resume without call to cycle()', fakeAsync(() => {
const html = `
<ngb-carousel #c [interval]="1000" (slide)="carouselSlideCallBack($event)">
<ng-template ngbSlide>foo</ng-template>
<ng-template ngbSlide>bar</ng-template>
<ng-template ngbSlide>third</ng-template>
</ngb-carousel>
<button id="next" (click)="c.next()">Next</button>
<button id="pause" (click)="c.pause()">Pause</button>
<button id="cycle" (click)="c.cycle()">Cycle</button>
`;

const fixture = createTestComponent(html);
const spyCallBack = spyOn(fixture.componentInstance, 'carouselSlideCallBack');
const carouselDebugEl = fixture.debugElement.query(By.directive(NgbCarousel));
const indicatorElms = fixture.nativeElement.querySelectorAll('ol.carousel-indicators > li');
const prevControlElm = fixture.nativeElement.querySelector('.carousel-control-prev');
const nextControlElm = fixture.nativeElement.querySelector('.carousel-control-next');
const next = fixture.nativeElement.querySelector('#next');
const pause = fixture.nativeElement.querySelector('#pause');
const cycle = fixture.nativeElement.querySelector('#cycle');

expectActiveSlides(fixture.nativeElement, [true, false, false]);

tick(1000);
fixture.detectChanges();
expect(spyCallBack)
.toHaveBeenCalledWith(jasmine.objectContaining({paused: false, source: NgbSlideEventSource.TIMER}));
spyCallBack.calls.reset();
expectActiveSlides(fixture.nativeElement, [false, true, false]);

pause.click();
tick(1000);
fixture.detectChanges();
expect(spyCallBack).not.toHaveBeenCalled();
expectActiveSlides(fixture.nativeElement, [false, true, false]);

indicatorElms[0].click();
fixture.detectChanges();
expect(spyCallBack)
.toHaveBeenCalledWith(jasmine.objectContaining({paused: true, source: NgbSlideEventSource.INDICATOR}));
spyCallBack.calls.reset();
expectActiveSlides(fixture.nativeElement, [true, false, false]);
tick(1000);
fixture.detectChanges();
expect(spyCallBack).not.toHaveBeenCalled();
expectActiveSlides(fixture.nativeElement, [true, false, false]);

nextControlElm.click();
fixture.detectChanges();
expect(spyCallBack)
.toHaveBeenCalledWith(jasmine.objectContaining({paused: true, source: NgbSlideEventSource.ARROW_RIGHT}));
spyCallBack.calls.reset();
expectActiveSlides(fixture.nativeElement, [false, true, false]);
tick(1000);
fixture.detectChanges();
expect(spyCallBack).not.toHaveBeenCalled();
expectActiveSlides(fixture.nativeElement, [false, true, false]);

prevControlElm.click();
fixture.detectChanges();
expect(spyCallBack)
.toHaveBeenCalledWith(jasmine.objectContaining({paused: true, source: NgbSlideEventSource.ARROW_LEFT}));
spyCallBack.calls.reset();
expectActiveSlides(fixture.nativeElement, [true, false, false]);
tick(1000);
fixture.detectChanges();
expect(spyCallBack).not.toHaveBeenCalled();
expectActiveSlides(fixture.nativeElement, [true, false, false]);

next.click();
fixture.detectChanges();
expect(spyCallBack).toHaveBeenCalledWith(jasmine.objectContaining({paused: true}));
spyCallBack.calls.reset();
expectActiveSlides(fixture.nativeElement, [false, true, false]);
tick(1000);
fixture.detectChanges();
expect(spyCallBack).not.toHaveBeenCalled();
expectActiveSlides(fixture.nativeElement, [false, true, false]);

carouselDebugEl.triggerEventHandler('mouseenter', {});
fixture.detectChanges();
carouselDebugEl.triggerEventHandler('mouseleave', {});
fixture.detectChanges();
tick(1000);
fixture.detectChanges();
expect(spyCallBack).not.toHaveBeenCalled();
expectActiveSlides(fixture.nativeElement, [false, true, false]);

cycle.click();
tick(1000);
fixture.detectChanges();
expect(spyCallBack)
.toHaveBeenCalledWith(jasmine.objectContaining({paused: false, source: NgbSlideEventSource.TIMER}));
expectActiveSlides(fixture.nativeElement, [false, false, true]);

discardPeriodicTasks();
}));


it('should mark component for check for API calls', () => {
const html = `
<ngb-carousel #c [interval]="0">
Expand Down Expand Up @@ -254,7 +354,7 @@ describe('ngb-carousel', () => {
discardPeriodicTasks();
}));

it('should fire a slide event with correct direction on indicator click', fakeAsync(() => {
it('should fire a slide event with correct direction and source on indicator click', fakeAsync(() => {
const html = `
<ngb-carousel (slide)="carouselSlideCallBack($event)">
<ng-template ngbSlide>foo</ng-template>
Expand All @@ -270,21 +370,24 @@ describe('ngb-carousel', () => {
indicatorElms[1].click();
fixture.detectChanges();
expect(fixture.componentInstance.carouselSlideCallBack).toHaveBeenCalledWith(jasmine.objectContaining({
direction: NgbSlideEventDirection.LEFT
direction: NgbSlideEventDirection.LEFT,
source: NgbSlideEventSource.INDICATOR
}));

spyCallBack.calls.reset();
indicatorElms[0].click();
fixture.detectChanges();
expect(fixture.componentInstance.carouselSlideCallBack).toHaveBeenCalledWith(jasmine.objectContaining({
direction: NgbSlideEventDirection.RIGHT
direction: NgbSlideEventDirection.RIGHT,
source: NgbSlideEventSource.INDICATOR
}));

spyCallBack.calls.reset();
indicatorElms[2].click();
fixture.detectChanges();
expect(fixture.componentInstance.carouselSlideCallBack).toHaveBeenCalledWith(jasmine.objectContaining({
direction: NgbSlideEventDirection.LEFT
direction: NgbSlideEventDirection.LEFT,
source: NgbSlideEventSource.INDICATOR
}));

discardPeriodicTasks();
Expand Down Expand Up @@ -316,7 +419,7 @@ describe('ngb-carousel', () => {
discardPeriodicTasks();
}));

it('should fire a slide event with correct direction on carousel control click', fakeAsync(() => {
it('should fire a slide event with correct direction and source on carousel control click', fakeAsync(() => {
const html = `
<ngb-carousel (slide)="carouselSlideCallBack($event)">
<ng-template ngbSlide>foo</ng-template>
Expand All @@ -332,20 +435,23 @@ describe('ngb-carousel', () => {
prevControlElm.click();
fixture.detectChanges();
expect(fixture.componentInstance.carouselSlideCallBack).toHaveBeenCalledWith(jasmine.objectContaining({
direction: NgbSlideEventDirection.RIGHT
direction: NgbSlideEventDirection.RIGHT,
source: NgbSlideEventSource.ARROW_LEFT
}));
spyCallBack.calls.reset();
nextControlElm.click();
fixture.detectChanges();
expect(fixture.componentInstance.carouselSlideCallBack).toHaveBeenCalledWith(jasmine.objectContaining({
direction: NgbSlideEventDirection.LEFT
direction: NgbSlideEventDirection.LEFT,
source: NgbSlideEventSource.ARROW_RIGHT
}));

spyCallBack.calls.reset();
prevControlElm.click();
fixture.detectChanges();
expect(fixture.componentInstance.carouselSlideCallBack).toHaveBeenCalledWith(jasmine.objectContaining({
direction: NgbSlideEventDirection.RIGHT
direction: NgbSlideEventDirection.RIGHT,
source: NgbSlideEventSource.ARROW_LEFT
}));

discardPeriodicTasks();
Expand All @@ -370,6 +476,33 @@ describe('ngb-carousel', () => {
discardPeriodicTasks();
}));

it('should fire a slide event with correct direction and source on time passage', fakeAsync(() => {
const html = `
<ngb-carousel [interval]="2000" (slide)="carouselSlideCallBack($event)">
<ng-template ngbSlide>foo</ng-template>
<ng-template ngbSlide>bar</ng-template>
</ngb-carousel>
`;

const fixture = createTestComponent(html);
const spyCallBack = spyOn(fixture.componentInstance, 'carouselSlideCallBack');

tick(1999);
fixture.detectChanges();
expectActiveSlides(fixture.nativeElement, [true, false]);
expect(spyCallBack).not.toHaveBeenCalled();

tick(1);
fixture.detectChanges();
expectActiveSlides(fixture.nativeElement, [false, true]);
expect(spyCallBack).toHaveBeenCalledWith(jasmine.objectContaining({
direction: NgbSlideEventDirection.LEFT,
source: NgbSlideEventSource.TIMER
}));

discardPeriodicTasks();
}));

it('should change slide on time passage in OnPush component (default interval value)', fakeAsync(() => {
const fixture = createTestComponent('<test-cmp-on-push></test-cmp-on-push>');

Expand Down

0 comments on commit 2602154

Please sign in to comment.