Skip to content

Commit

Permalink
feat(carousel): add RTL gesture support;
Browse files Browse the repository at this point in the history
- change properties' names;
- add interaction test;
  • Loading branch information
RivaIvanova committed Sep 19, 2024
1 parent 61a0387 commit 05d5e86
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 79 deletions.
72 changes: 58 additions & 14 deletions src/components/carousel/carousel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
nextFrame,
} from '@open-wc/testing';

import { spy } from 'sinon';
import { type SinonFakeTimers, spy, useFakeTimers } from 'sinon';
import type IgcButtonComponent from '../button/button.js';
import {
arrowLeft,
Expand Down Expand Up @@ -70,6 +70,7 @@ describe('Carousel', () => {
let nextButton: IgcButtonComponent;
let prevButton: IgcButtonComponent;
let defaultIndicators: IgcCarouselIndicatorComponent[];
let clock: SinonFakeTimers;

beforeEach(async () => {
carousel = await fixture<IgcCarouselComponent>(createCarouselComponent());
Expand All @@ -85,6 +86,11 @@ describe('Carousel', () => {
defaultIndicators = carousel.shadowRoot?.querySelectorAll(
'igc-carousel-indicator'
) as unknown as IgcCarouselIndicatorComponent[];
clock = useFakeTimers({ toFake: ['setInterval'] });
});

afterEach(() => {
clock.restore();
});

describe('Initialization', () => {
Expand All @@ -94,10 +100,10 @@ describe('Carousel', () => {
});

it('is correctly initialized with its default component state', () => {
expect(carousel.skipLoop).to.be.false;
expect(carousel.skipPauseOnInteraction).to.be.false;
expect(carousel.skipNavigation).to.be.false;
expect(carousel.skipIndicator).to.be.false;
expect(carousel.disableLoop).to.be.false;
expect(carousel.disablePauseOnInteraction).to.be.false;
expect(carousel.hideNavigation).to.be.false;
expect(carousel.hideIndicators).to.be.false;
expect(carousel.vertical).to.be.false;
expect(carousel.indicatorsOrientation).to.equal('end');
expect(carousel.interval).to.be.undefined;
Expand Down Expand Up @@ -240,24 +246,24 @@ describe('Carousel', () => {
);
});

it('should not render indicators if `skipIndicator` is true', async () => {
it('should not render indicators if `hideIndicators` is true', async () => {
let indicators = carousel.shadowRoot?.querySelector(
'div[role="tablist"]'
);
expect(indicators).to.not.be.null;

carousel.skipIndicator = true;
carousel.hideIndicators = true;
await elementUpdated(carousel);

indicators = carousel.shadowRoot?.querySelector('div[role="tablist"]');
expect(indicators).to.be.null;
});

it('should not render navigation if `skipNavigation` is true', async () => {
it('should not render navigation if `hideNavigation` is true', async () => {
let navigation = carousel.shadowRoot?.querySelectorAll('igc-button');
expect(navigation?.length).to.equal(2);

carousel.skipNavigation = true;
carousel.hideNavigation = true;
await elementUpdated(carousel);

navigation = carousel.shadowRoot?.querySelectorAll('igc-button');
Expand All @@ -280,7 +286,7 @@ describe('Carousel', () => {
expect(label?.textContent?.trim()).to.equal('1/3');
});

it('should not render indicators label if `skipIndicator` is true', async () => {
it('should not render indicators label if `hideIndicators` is true', async () => {
let label = carousel.shadowRoot?.querySelector(
'div[part="label indicators"]'
);
Expand All @@ -295,7 +301,7 @@ describe('Carousel', () => {
expect(label).to.not.be.null;
expect(label?.textContent?.trim()).to.equal('1/3');

carousel.skipIndicator = true;
carousel.hideIndicators = true;
await elementUpdated(carousel);

label = carousel.shadowRoot?.querySelector(
Expand Down Expand Up @@ -381,7 +387,7 @@ describe('Carousel', () => {
</igc-carousel>`
);

carousel.skipLoop = true;
carousel.disableLoop = true;
await elementUpdated(carousel);

let animation = await carousel.next();
Expand Down Expand Up @@ -626,6 +632,18 @@ describe('Carousel', () => {
});

describe('Automatic rotation', () => {
it('should automatically change slides', async () => {
expect(carousel.current).to.equal(0);

carousel.interval = 200;
await elementUpdated(carousel);

await clock.tickAsync(200);
await slideChangeComplete(slides[0], slides[1]);

expect(carousel.current).to.equal(1);
});

it('should pause/play on pointerenter/pointerleave', async () => {
const eventSpy = spy(carousel, 'emitEvent');
const divContainer = carousel.shadowRoot?.querySelector(
Expand Down Expand Up @@ -715,7 +733,7 @@ describe('Carousel', () => {
expect(eventSpy.secondCall).calledWith('igcPlaying');
});

it('should not pause on interaction if `skipPauseOnInteraction` is true', async () => {
it('should not pause on interaction if `disablePauseOnInteraction` is true', async () => {
const eventSpy = spy(carousel, 'emitEvent');
const divContainer = carousel.shadowRoot?.querySelector(
'div[aria-live]'
Expand All @@ -724,7 +742,7 @@ describe('Carousel', () => {
expect(divContainer.ariaLive).to.equal('polite');

carousel.interval = 2000;
carousel.skipPauseOnInteraction = true;
carousel.disablePauseOnInteraction = true;
await elementUpdated(carousel);

expect(carousel.isPlaying).to.be.true;
Expand Down Expand Up @@ -767,6 +785,19 @@ describe('Carousel', () => {
expect(carousel.current).to.equal(1);
});

it('should change to previous slide on swipe-left (RTL)', async () => {
carousel.dir = 'rtl';
await elementUpdated(carousel);
expect(carousel.current).to.equal(0);

simulatePointerDown(carouselSlidesContainer);
simulatePointerMove(carouselSlidesContainer, {}, { x: -100 }, 10);
simulateLostPointerCapture(carouselSlidesContainer);
await slideChangeComplete(slides[0], slides[2]);

expect(carousel.current).to.equal(2);
});

it('should change to previous slide on swipe-right', async () => {
expect(carousel.current).to.equal(0);

Expand All @@ -778,6 +809,19 @@ describe('Carousel', () => {
expect(carousel.current).to.equal(2);
});

it('should change to next slide on swipe-right (RTL)', async () => {
carousel.dir = 'rtl';
await elementUpdated(carousel);
expect(carousel.current).to.equal(0);

simulatePointerDown(carouselSlidesContainer);
simulatePointerMove(carouselSlidesContainer, {}, { x: 100 }, 10);
simulateLostPointerCapture(carouselSlidesContainer);
await slideChangeComplete(slides[0], slides[1]);

expect(carousel.current).to.equal(1);
});

it('should not change to next/previous slide on swipe left/right when `vertical` is true', async () => {
carousel.vertical = true;
await elementUpdated(carousel);
Expand Down
48 changes: 27 additions & 21 deletions src/components/carousel/carousel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,35 +173,35 @@ export default class IgcCarouselComponent extends EventEmitterMixin<

/**
* Whether the carousel should skip rotating to the first slide after it reaches the last.
* @attr skip-loop
* @attr disable-loop
*/
@property({ type: Boolean, reflect: true, attribute: 'skip-loop' })
public skipLoop = false;
@property({ type: Boolean, reflect: true, attribute: 'disable-loop' })
public disableLoop = false;

/**
* Whether the carousel should ignore use interactions and not pause on them.
* @attr skip-pause-on-interaction
* @attr disable-pause-on-interaction
*/
@property({
type: Boolean,
reflect: true,
attribute: 'skip-pause-on-interaction',
attribute: 'disable-pause-on-interaction',
})
public skipPauseOnInteraction = false;
public disablePauseOnInteraction = false;

/**
* Whether the carousel should skip rendering of the default navigation buttons.
* @attr skip-navigation
* @attr hide-navigation
*/
@property({ type: Boolean, reflect: true, attribute: 'skip-navigation' })
public skipNavigation = false;
@property({ type: Boolean, reflect: true, attribute: 'hide-navigation' })
public hideNavigation = false;

/**
* Whether the carousel should render the indicator controls (dots).
* @attr skip-indicator
* @attr hide-indicators
*/
@property({ type: Boolean, reflect: true, attribute: 'skip-indicator' })
public skipIndicator = false;
@property({ type: Boolean, reflect: true, attribute: 'hide-indicators' })
public hideIndicators = false;

/**
* Whether the carousel has vertical alignment.
Expand Down Expand Up @@ -398,7 +398,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
}

private handlePauseOnInteraction(): void {
if (!this.interval || this.skipPauseOnInteraction) return;
if (!this.interval || this.disablePauseOnInteraction) return;

if (this.isPlaying) {
this.pause();
Expand Down Expand Up @@ -441,7 +441,13 @@ export default class IgcCarouselComponent extends EventEmitterMixin<

private handleHorizontalSwipe({ data: { direction } }: SwipeEvent) {
if (!this.vertical) {
this.handleInteraction(direction === 'left' ? this.next : this.prev);
this.handleInteraction(async () => {
if (isLTR(this)) {
direction === 'left' ? await this.next() : await this.prev();
} else {
direction === 'left' ? await this.prev() : await this.next();
}
});
}
}

Expand Down Expand Up @@ -585,7 +591,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
* Switches to the next slide running any animations and returns if the operation was a success.
*/
public async next(): Promise<boolean> {
if (this.skipLoop && this.nextIndex === 0) {
if (this.disableLoop && this.nextIndex === 0) {
this.pause();
return false;
}
Expand All @@ -597,7 +603,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
* Switches to the previous slide running any animations and returns if the operation was a success.
*/
public async prev(): Promise<boolean> {
if (this.skipLoop && this.prevIndex === this.total - 1) {
if (this.disableLoop && this.prevIndex === this.total - 1) {
this.pause();
return false;
}
Expand Down Expand Up @@ -632,7 +638,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
part="navigation previous"
aria-label="Previous slide"
aria-controls=${this._carouselId}
?disabled=${this.skipLoop && this.current === 0}
?disabled=${this.disableLoop && this.current === 0}
@click=${this.handleNavigationInteractionPrevious}
>
<slot name="previous-button">
Expand All @@ -649,7 +655,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
part="navigation next"
aria-label="Next slide"
aria-controls=${this._carouselId}
?disabled=${this.skipLoop && this.current === this.total - 1}
?disabled=${this.disableLoop && this.current === this.total - 1}
@click=${this.handleNavigationInteractionNext}
>
<slot name="next-button">
Expand Down Expand Up @@ -731,11 +737,11 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
protected override render() {
return html`
<section @focusin=${this.handleFocusIn} @focusout=${this.handleFocusOut}>
${this.skipNavigation ? nothing : this.navigationTemplate()}
${this.skipIndicator || this.showIndicatorsLabel
${this.hideNavigation ? nothing : this.navigationTemplate()}
${this.hideIndicators || this.showIndicatorsLabel
? nothing
: this.indicatorTemplate()}
${!this.skipIndicator && this.showIndicatorsLabel
${!this.hideIndicators && this.showIndicatorsLabel
? this.labelTemplate()
: nothing}
<div
Expand Down
Loading

0 comments on commit 05d5e86

Please sign in to comment.