Skip to content

Commit

Permalink
Merge pull request #2552 from exadel-inc/feat/esl-carousel-move-reworked
Browse files Browse the repository at this point in the history
fix(esl-carousel): multiple carousel touch behavior fixes
  • Loading branch information
ala-n authored Jul 30, 2024
2 parents ae9d283 + 4c6cb5c commit a35928e
Show file tree
Hide file tree
Showing 9 changed files with 76 additions and 50 deletions.
7 changes: 5 additions & 2 deletions site/views/examples/carousel/centered-siblings.sample.njk
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ tags: carousel-sample
</button>

<esl-carousel demo-options-target
style="padding-inline: 60px; margin-inline: 0;"
style="padding-inline: 60px; margin-inline: 0"
esl-carousel-touch
type="centered"
media="all|@+MD"
Expand All @@ -20,10 +20,13 @@ tags: carousel-sample
<ul esl-carousel-slides>
{% for i in range(0, 6) -%}
<li esl-carousel-slide style="max-width: 400px">
<div class="card">
<div class="card img-container">
<esl-image mode="cover" lazy
data-alt="Alt Text Test"
data-src="{{ '/assets/carousel/' + loop.index + '-sm.jpg' | url }}"></esl-image>
<button type="button" class="close close-icon close-remove inverse"
title="Remove" aria-label="Remove"
onclick="this.closest('li').remove()"></button>
</div>
</li>
{%- endfor %}
Expand Down
5 changes: 4 additions & 1 deletion site/views/examples/carousel/siblings.sample.njk
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ tags: carousel-sample
</button>

<esl-carousel demo-options-target
style="padding-inline: 60px; margin-inline: 0;"
style="padding-inline: 60px; margin-inline: 0"
esl-carousel-touch
count="2"
loop="true">
Expand All @@ -20,6 +20,9 @@ tags: carousel-sample
<esl-image mode="cover" lazy
data-alt="Alt Text Test"
data-src="{{ '/assets/carousel/' + loop.index + '-sm.jpg' | url }}"></esl-image>
<button type="button" class="close close-icon close-remove inverse"
title="Remove" aria-label="Remove"
onclick="this.closest('li').remove()"></button>
</div>
</li>
{%- endfor %}
Expand Down
13 changes: 8 additions & 5 deletions src/modules/esl-carousel/core/esl-carousel.less
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ esl-carousel {
overflow: clip;
/* stylelint-enable */

&.esl-carousel-vertical [esl-carousel-slides] {
flex-direction: column;
&.esl-carousel-vertical {
[esl-carousel-slides] {
flex-direction: column;
}
touch-action: pan-x;
}
&.esl-carousel-horizontal [esl-carousel-slides] {
flex-direction: row;
&.esl-carousel-horizontal {
[esl-carousel-slides] {
flex-direction: row;
}
touch-action: pan-y;
}

Expand All @@ -37,7 +41,6 @@ esl-carousel {

&[dragging] [esl-carousel-slides],
&[animating] [esl-carousel-slides] {
touch-action: none;
user-select: none;
pointer-events: none;
}
Expand Down
16 changes: 12 additions & 4 deletions src/modules/esl-carousel/core/esl-carousel.renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,18 @@ export abstract class ESLCarouselRenderer implements ESLCarouselConfig {
/** Post-processing animation action. */
public async onAfterAnimate(index: number, direction: ESLCarouselDirection): Promise<void> {}

/** Handles the slides transition. */
public abstract onMove(offset: number): void;
/** Ends current transition and makes permanent all changes performed in the transition. */
public abstract commit(offset?: number): void;
/**
* Moves slide by the passed offset in px.
* @param offset - offset in px
* @param from - start index (default: current active index)
*/
public abstract move(offset: number, from?: number): void;
/**
* Normalizes move offset to the "nearest stable" slide position.
* @param offset - offset in px
* @param from - start index (default: current active index)
*/
public abstract commit(offset?: number, from?: number): void;

/** Sets active slides from passed index **/
public setActive(current: number, event?: Partial<ESLCarouselSlideEventInit>): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin<ESLCarouselTouchCon
swipeTimeout: 400
};

/** Start index of the drag action */
protected startIndex: number;
/** Start pointer event to detect action */
protected startEvent?: PointerEvent;
/** Initial scroll offsets, filled on touch action start */
Expand Down Expand Up @@ -110,6 +112,7 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin<ESLCarouselTouchCon
if (this.isDisabled) return;

this.startEvent = event;
this.startIndex = this.$host.activeIndex;
this.startScrollOffsets = getParentScrollOffsets(event.target as Element, this.$host);

this.$$on({group: 'pointer'});
Expand All @@ -130,7 +133,7 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin<ESLCarouselTouchCon

this.$host.setPointerCapture(event.pointerId);

if (this.isDragMode) this.$host.renderer.onMove(offset);
if (this.isDragMode) this.$host.renderer.move(offset, this.startIndex);
}

/** Processes `mouseup` and `touchend` events. */
Expand All @@ -147,7 +150,7 @@ export class ESLCarouselTouchMixin extends ESLCarouselPlugin<ESLCarouselTouchCon

const offset = this.getOffset(event);
// Commit drag offset (should be commited to 0 if the event is canceled)
if (this.isDragMode) this.$host.renderer.commit(offset);
if (this.isDragMode) this.$host.renderer.commit(offset, this.startIndex);
// Swipe final check
if (this.isSwipeMode && offset && !this.isPrevented && this.isSwipeAccepted(event)) {
const target = `${this.config.swipeType}:${offset < 0 ? 'next' : 'prev'}`;
Expand Down
40 changes: 27 additions & 13 deletions src/modules/esl-carousel/renderers/esl-carousel.default.renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,21 @@ export class ESLDefaultCarouselRenderer extends ESLCarouselRenderer {

/** Slides gap size */
protected gap: number = 0;
/** Slide size cached value */
protected slideSize: number = 0;
/** First index of active slides. */
protected currentIndex: number = 0;

/** Multiplier for the index move on the slide move */
protected get INDEX_MOVE_MULTIPLIER(): number {
return 1;
}

/** Actual slide size (uses average) */
protected get slideSize(): number {
return this.$slides.reduce((size, $slide) => {
return size + (this.vertical ? $slide.offsetHeight : $slide.offsetWidth);
}, 0) / this.$slides.length;
}

/**
* Processes binding of defined renderer to the carousel {@link ESLCarousel}.
* Prepare to renderer animation.
Expand Down Expand Up @@ -121,17 +131,13 @@ export class ESLDefaultCarouselRenderer extends ESLCarouselRenderer {
this.$carousel.$$attr('animating', false);
}

protected indexByOffset(count: number): number {
return this.$carousel.activeIndex + count;
}

/** Handles the slides transition. */
public onMove(offset: number): void {
public move(offset: number, from = this.$carousel.activeIndex): void {
this.$carousel.toggleAttribute('active', true);

const slideSize = this.slideSize + this.gap;
const count = Math.floor(Math.abs(offset) / slideSize);
const index = this.indexByOffset(count * (offset < 0 ? 1 : -1));
const index = from + count * this.INDEX_MOVE_MULTIPLIER * (offset < 0 ? 1 : -1);

// check left border of non-loop state
if (!this.loop && offset > 0 && index <= 0) return;
Expand All @@ -143,17 +149,22 @@ export class ESLDefaultCarouselRenderer extends ESLCarouselRenderer {

const stageOffset = this.getOffset(this.currentIndex) - (offset % slideSize);
this.setTransformOffset(-stageOffset);

if (this.currentIndex !== this.$carousel.activeIndex) {
console.log('Apply active index %d (before %d)', this.currentIndex, this.$carousel.activeIndex);
this.setActive(this.currentIndex, {direction: offset < 0 ? 'next' : 'prev'});
}
}

/** Ends current transition and make permanent all changes performed in the transition. */
// eslint-disable-next-line sonarjs/cognitive-complexity
public async commit(offset: number): Promise<void> {
public async commit(offset: number, from = this.$carousel.activeIndex): Promise<void> {
const slideSize = this.slideSize + this.gap;

const amount = Math.abs(offset) / slideSize;
const tolerance = ESLDefaultCarouselRenderer.NEXT_SLIDE_TOLERANCE;
const count = (amount - Math.floor(amount)) > tolerance ? Math.ceil(amount) : Math.floor(amount);
const index = this.indexByOffset(count * (offset < 0 ? 1 : -1));
const index = from + count * this.INDEX_MOVE_MULTIPLIER * (offset < 0 ? 1 : -1);

this.currentIndex = normalizeIndex(index, this);

Expand All @@ -168,8 +179,11 @@ export class ESLDefaultCarouselRenderer extends ESLCarouselRenderer {

this.reorder();
this.setTransformOffset(-this.getOffset(this.currentIndex));
this.setActive(this.currentIndex, {direction: offset < 0 ? 'next' : 'prev'});
this.$carousel.$$attr('active', false);

if (this.currentIndex !== this.$carousel.activeIndex) {
this.setActive(this.currentIndex, {direction: offset < 0 ? 'next' : 'prev'});
}
}

/**
Expand Down Expand Up @@ -210,7 +224,7 @@ export class ESLDefaultCarouselRenderer extends ESLCarouselRenderer {

this.gap = parseFloat(this.vertical ? areaStyles.rowGap : areaStyles.columnGap);
const areaSize = parseFloat(this.vertical ? areaStyles.height : areaStyles.width);
this.slideSize = Math.floor((areaSize - this.gap * (this.count - 1)) / this.count);
this.$area.style.setProperty(ESLDefaultCarouselRenderer.SIZE_PROP, this.slideSize + 'px');
const slideSize = Math.floor((areaSize - this.gap * (this.count - 1)) / this.count);
this.$area.style.setProperty(ESLDefaultCarouselRenderer.SIZE_PROP, slideSize + 'px');
}
}
17 changes: 9 additions & 8 deletions src/modules/esl-carousel/renderers/esl-carousel.grid.renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ export class ESLGridCarouselRenderer extends ESLDefaultCarouselRenderer {
@prop(2, {readonly: true})
public readonly ROWS: number;

/** @returns count of fake slides to fill the last "row" or incomplete carousel state */
/** Multiplier for the index move on the slide move */
protected override get INDEX_MOVE_MULTIPLIER(): number {
return this.ROWS;
}

/** Count of fake slides to fill the last "row" or incomplete carousel state */
public get fakeSlidesCount(): number {
if (this.$carousel.$slides.length < this.count) {
return this.count - this.$carousel.$slides.length;
Expand All @@ -41,7 +46,7 @@ export class ESLGridCarouselRenderer extends ESLDefaultCarouselRenderer {
return Array.from({length}, this.buildFakeSlide.bind(this));
}

/** @returns all slides including {@link ESLGridCarouselRenderer.$fakeSlides} slides created in grid mode */
/** All slides including {@link ESLGridCarouselRenderer.$fakeSlides} slides created in grid mode */
public override get $slides(): HTMLElement[] {
return (this.$carousel.$slides || []).concat(this.$fakeSlides);
}
Expand Down Expand Up @@ -89,10 +94,6 @@ export class ESLGridCarouselRenderer extends ESLDefaultCarouselRenderer {
while (this.currentIndex !== nextIndex) await this.onStepAnimate(step);
}

protected override indexByOffset(offset: number): number {
return super.indexByOffset(offset * this.ROWS);
}

/**
* @returns count of slides to be rendered (reserved) before the first slide does not include fake slides
*/
Expand All @@ -109,7 +110,7 @@ export class ESLGridCarouselRenderer extends ESLDefaultCarouselRenderer {
this.gap = parseFloat(this.vertical ? areaStyles.rowGap : areaStyles.columnGap);
const areaSize = parseFloat(this.vertical ? areaStyles.height : areaStyles.width);
const count = Math.floor(this.count / this.ROWS);
this.slideSize = Math.floor((areaSize - this.gap * (count - 1)) / count);
this.$area.style.setProperty(ESLDefaultCarouselRenderer.SIZE_PROP, this.slideSize + 'px');
const slideSize = Math.floor((areaSize - this.gap * (count - 1)) / count);
this.$area.style.setProperty(ESLDefaultCarouselRenderer.SIZE_PROP, slideSize + 'px');
}
}
18 changes: 5 additions & 13 deletions src/modules/esl-carousel/renderers/esl-carousel.none.renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,12 @@ export class ESLNoneCarouselRenderer extends ESLCarouselRenderer {
this.setActive(0);
}

public override onUnbind(): void {
// this.$carousel.scrollTop = this.$carousel.scrollLeft = 0;
}
public override onUnbind(): void {}

/** Processes animation. */
public async onAnimate(nextIndex: number, direction: ESLCarouselDirection): Promise<void> {
}
public async onAnimate(nextIndex: number, direction: ESLCarouselDirection): Promise<void> {}

/** Handles the slides transition. */
public onMove(offset: number): void {
// TODO: implement if scroll behaviour requested
// const property = this.vertical ? 'scrollTop' : 'scrollLeft';
// this.$carousel[property] = -offset;
}
public commit(offset?: number): void {
}
/* Handles the slide move actions */
public move(offset: number, from?: number): void {}
public commit(offset: number, from?: number): void {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export class ESLCarouselDummyRenderer extends ESLCarouselRenderer {
public override onBeforeAnimate = jest.fn();
public override onAfterAnimate = jest.fn();

public override onMove = jest.fn();

public override move = jest.fn();
public override commit = jest.fn();
}

0 comments on commit a35928e

Please sign in to comment.