Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(carousel): animate items with the same direction #9325

Merged
merged 2 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -930,4 +930,55 @@ describe("calcite-carousel", () => {
expect(selectedItem.id).toEqual("two");
});
});

it("item slide animation finishes between paging/selection", async () => {
const page = await newE2EPage();
await page.setContent(
html`<calcite-carousel label="carousel">
<calcite-carousel-item label="item 1" selected><p>first</p></calcite-carousel-item>
<calcite-carousel-item label="item 2"><p>second</p></calcite-carousel-item>
<calcite-carousel-item label="item 3"><p>third</p></calcite-carousel-item>
</calcite-carousel>`,
);

const container = await page.find(`calcite-carousel >>> .${CSS.container}`);
const animationStartSpy = await container.spyOnEvent("animationstart");
const animationEndSpy = await container.spyOnEvent("animationend");
const nextButton = await page.find(`calcite-carousel >>> .${CSS.pageNext}`);

await nextButton.click();
await page.waitForChanges();
await nextButton.click();
await page.waitForChanges();

expect(animationStartSpy).toHaveReceivedEventTimes(2);
expect(animationEndSpy).toHaveReceivedEventTimes(2);

const previousButton = await page.find(`calcite-carousel >>> .${CSS.pagePrevious}`);
await previousButton.click();
await page.waitForChanges();
await previousButton.click();
await page.waitForChanges();

expect(animationStartSpy).toHaveReceivedEventTimes(4);
expect(animationEndSpy).toHaveReceivedEventTimes(4);

const [item1, item2, item3] = await page.findAll(`calcite-carousel >>> .${CSS.paginationItemIndividual}`);

await item2.click();
await page.waitForChanges();
await item3.click();
await page.waitForChanges();

expect(animationStartSpy).toHaveReceivedEventTimes(6);
expect(animationEndSpy).toHaveReceivedEventTimes(6);

await item2.click();
await page.waitForChanges();
await item1.click();
await page.waitForChanges();

expect(animationStartSpy).toHaveReceivedEventTimes(8);
expect(animationEndSpy).toHaveReceivedEventTimes(8);
});
});
36 changes: 33 additions & 3 deletions packages/calcite-components/src/components/carousel/carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getElementDir,
slotChangeGetAssignedElements,
toAriaBoolean,
whenAnimationDone,
} from "../../utils/dom";
import { connectLocalized, disconnectLocalized, LocalizedComponent } from "../../utils/locale";
import { guid } from "../../utils/guid";
Expand Down Expand Up @@ -215,7 +216,20 @@ export class Carousel

@State() items: HTMLCalciteCarouselItemElement[] = [];

@State() direction: "forward" | "backward";
@State() direction: "forward" | "backward" | "standby" = "standby";

@Watch("direction")
async directionWatcher(direction: string): Promise<void> {
if (direction === "standby") {
return;
}

await whenAnimationDone(
this.itemContainer,
direction === "forward" ? "item-forward" : "item-backward",
);
this.direction = "standby";
}

@State() defaultMessages: CarouselMessages;

Expand Down Expand Up @@ -394,18 +408,26 @@ export class Carousel
private handleArrowClick = (event: MouseEvent): void => {
const direction = (event.target as HTMLDivElement).dataset.direction;
if (direction === "next") {
this.direction = "forward";
this.nextItem(true);
} else if (direction === "previous") {
this.direction = "backward";
this.previousItem();
}
};

private handleItemSelection = (event: MouseEvent): void => {
const item = event.target as HTMLCalciteActionElement;
const requestedPosition = parseInt(item.dataset.index);

if (requestedPosition === this.selectedIndex) {
return;
}

if (this.playing) {
this.handlePause(true);
}
const item = event.target as HTMLCalciteActionElement;
const requestedPosition = parseInt(item.dataset.index);

this.direction = requestedPosition > this.selectedIndex ? "forward" : "backward";
this.setSelectedItem(requestedPosition, true);
};
Expand Down Expand Up @@ -528,6 +550,12 @@ export class Carousel
this.container = el;
};

private itemContainer: HTMLDivElement;

private storeItemContainerRef = (el: HTMLDivElement): void => {
this.itemContainer = el;
};

// --------------------------------------------------------------------------
//
// Render Methods
Expand Down Expand Up @@ -653,6 +681,8 @@ export class Carousel
[CSS.itemContainerBackward]: direction === "backward",
}}
id={this.containerId}
// eslint-disable-next-line react/jsx-sort-props -- auto-generated by @esri/calcite-components/enforce-ref-last-prop
ref={this.storeItemContainerRef}
>
<slot onSlotchange={this.handleSlotChange} />
</section>
Expand Down
58 changes: 58 additions & 0 deletions packages/calcite-components/src/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,3 +653,61 @@ export function isBefore(a: HTMLElement, b: HTMLElement): boolean {
const children = Array.from(a.parentNode.children);
return children.indexOf(a) < children.indexOf(b);
}

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎈

* This util helps determine when an animation has completed.
*
* @param targetEl The element to watch for the animation to complete.
* @param animationName The name of the animation to watch for completion.
*/
export async function whenAnimationDone(targetEl: HTMLElement, animationName: string): Promise<void> {
const { animationDuration: allDurations, animationName: allNames } = getComputedStyle(targetEl);

const allDurationsArray = allDurations.split(",");
const allPropsArray = allNames.split(",");
const propIndex = allPropsArray.indexOf(animationName);
const duration =
allDurationsArray[propIndex] ??
/* Safari will have a single duration value for the shorthand prop when multiple, separate names/props are defined,
so we fall back to it if there's no matching prop duration */
allDurationsArray[0];

if (duration === "0s") {
return Promise.resolve();
}

const startEvent = "animationstart";
const endEvent = "animationend";
const cancelEvent = "animationcancel";

return new Promise<void>((resolve) => {
const fallbackTimeoutId = setTimeout(
(): void => {
targetEl.removeEventListener(startEvent, onStart);
targetEl.removeEventListener(endEvent, onEndOrCancel);
targetEl.removeEventListener(cancelEvent, onEndOrCancel);
resolve();
},
parseFloat(duration) * 1000,
);

targetEl.addEventListener(startEvent, onStart);
targetEl.addEventListener(endEvent, onEndOrCancel);
targetEl.addEventListener(cancelEvent, onEndOrCancel);

function onStart(event: AnimationEvent): void {
if (event.animationName === animationName && event.target === targetEl) {
clearTimeout(fallbackTimeoutId);
targetEl.removeEventListener(startEvent, onStart);
}
}

function onEndOrCancel(event: AnimationEvent): void {
if (event.animationName === animationName && event.target === targetEl) {
targetEl.removeEventListener(endEvent, onEndOrCancel);
targetEl.removeEventListener(cancelEvent, onEndOrCancel);
resolve();
}
}
});
}
Loading