Skip to content
Open
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
89 changes: 83 additions & 6 deletions packages/main/cypress/specs/Carousel.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@ import Button from "../../src/Button.js";
import Carousel from "../../src/Carousel.js";
import Card from "../../src/Card.js";
import Input from "../../src/Input.js";
import List from "../../src/List.js";
import CardHeader from "../../src/CardHeader.js";
import Icon from "../../src/Icon.js";
import ListItemGroup from "../../src/ListItemGroup.js";
import Avatar from "../../src/Avatar.js";
import ListItemStandard from "../../src/ListItemStandard.js";

describe("Carousel general interaction", () => {
it("rendering", () => {
Expand Down Expand Up @@ -592,4 +586,87 @@ describe("Carousel general interaction", () => {
.find(".ui5-carousel-item:nth-child(5)")
.should("have.class", "ui5-carousel-item--hidden");
});

it("should render only visible items", () => {
cy.mount(
<Carousel>
<Button />
<Button hidden/>
<Button />
</Carousel>);

cy.get("ui5-carousel")
.shadow()
.find(".ui5-carousel-item")
.should("have.length", 2);
});

it("should update navigation when items become hidden dynamically", () => {
cy.mount(
<Carousel>
<Button />
<Button id="btn2" />
<Button id="btn3" />
<Button id="btn4" />
</Carousel>);

cy.get("ui5-carousel")
.shadow()
.find(".ui5-carousel-item")
.should("have.length", 4);

cy.get("ui5-carousel")
.shadow()
.find(".ui5-carousel-navigation-dot")
.should("have.length", 4);

cy.get("#btn2").invoke("attr", "hidden", "");
cy.get("#btn3").invoke("attr", "hidden", "");

cy.get("ui5-carousel")
.shadow()
.find(".ui5-carousel-item")
.should("have.length", 2);

cy.get("ui5-carousel")
.shadow()
.find(".ui5-carousel-navigation-dot")
.should("have.length", 2);
});

it("should handle filtering with multiple items per page", () => {
cy.mount(
<Carousel itemsPerPage="S2 M2 L2 XL2">
<Button />
<Button hidden />
<Button />
<Button hidden />
<Button />
<Button />
</Carousel>);

cy.get("ui5-carousel")
.shadow()
.find(".ui5-carousel-item")
.should("have.length", 4);
});

it("should update page count correctly with filtered content", () => {
cy.mount(
<Carousel itemsPerPage="S1 M2 L2 XL2">
<Button />
<Button hidden />
<Button />
<Button hidden />
<Button />
<Button />
</Carousel>);

cy.get("ui5-carousel").should("have.prop", "pagesCount", 3);

cy.get("ui5-carousel")
.shadow()
.find(".ui5-carousel-navigation-dot")
.should("have.length", 3);
});
});
74 changes: 67 additions & 7 deletions packages/main/src/Carousel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,13 @@ class Carousel extends UI5Element {
@property({ type: Boolean, noAttribute: true })
_visibleNavigationArrows = false;

/**
* Internal trigger flag that forces component re-rendering when content items change.
* @private
*/
@property({ type: Boolean, noAttribute: true })
_contentUpdateTrigger = false;

_scrollEnablement: ScrollEnablement;
_onResizeBound: ResizeObserverCallback;
_resizing: boolean;
Expand All @@ -297,6 +304,8 @@ class Carousel extends UI5Element {
_pageStep: number = 10;
_visibleItemsIndexes: Array<number>;
_itemIndicator: number = 0;
_contentItemsObserver: MutationObserver;
_observableContent: Array<HTMLElement> = [];

/**
* Defines the content of the component.
Expand All @@ -315,6 +324,13 @@ class Carousel extends UI5Element {
constructor() {
super();

this._contentItemsObserver = new MutationObserver(() => {
this._currentSlideIndex = clamp(this._currentSlideIndex, 0, Math.max(0, this.items.length - this.effectiveItemsPerPage));
this._focusedItemIndex = clamp(this._focusedItemIndex, this._currentSlideIndex, this.items.length - 1);
this._contentUpdateTrigger = !this._contentUpdateTrigger;
this._moveToItem(this._currentSlideIndex);
});

this._scrollEnablement = new ScrollEnablement(this);
this._scrollEnablement.attachEvent("touchend", e => {
this._updateScrolling(e);
Expand All @@ -328,6 +344,8 @@ class Carousel extends UI5Element {
}

onBeforeRendering() {
this._observeContentItems();

if (this.arrowsPlacement === CarouselArrowsPlacement.Navigation || !isDesktop()) {
this._visibleNavigationArrows = true;
}
Expand All @@ -348,6 +366,8 @@ class Carousel extends UI5Element {
}

onExitDOM() {
this._contentItemsObserver.disconnect();
this._observableContent = [];
ResizeHandler.deregister(this, this._onResizeBound);
}

Expand Down Expand Up @@ -424,8 +444,8 @@ class Carousel extends UI5Element {
}

let pageIndex = -1;
for (let i = 0; i < this.content.length; i++) {
if (this.content[i].isEqualNode(target?.querySelector("slot")?.assignedNodes()[0] as HTMLElement)) {
for (let i = 0; i < this._visibleItems.length; i++) {
if (this._visibleItems[i].isEqualNode(target?.querySelector("slot")?.assignedNodes()[0] as HTMLElement)) {
pageIndex = i;
break;
}
Expand Down Expand Up @@ -487,6 +507,33 @@ class Carousel extends UI5Element {
}
}

_observeContentItems() {
if (this.hasMatchingContent) {
return;
}

this.content.forEach(item => {
if (!this._observableContent.includes(item)) {
this._contentItemsObserver.observe(item, {
characterData: false,
childList: false,
subtree: false,
attributes: true,
});
}
});
this._observableContent = this.content;
}

get hasMatchingContent() {
if (this._observableContent.length !== this.content.length) {
return false;
}

const observableContentSet = new WeakSet(this._observableContent);
return this.content.every(item => observableContentSet.has(item));
}

_handleHome(e: KeyboardEvent) {
e.preventDefault();
this.navigateTo(0);
Expand Down Expand Up @@ -673,6 +720,10 @@ class Carousel extends UI5Element {
* @public
*/
navigateTo(itemIndex: number) {
if (!this.isIndexInRange(itemIndex)) {
return;
}

if (this._focusedItemIndex < itemIndex) {
this._itemIndicator = 1;
}
Expand Down Expand Up @@ -705,13 +756,13 @@ class Carousel extends UI5Element {
* @private
*/
get items(): Array<ItemsInfo> {
return this.content.map((item, idx) => {
return this._visibleItems.map((item, idx) => {
return {
id: `${this._id}-carousel-item-${idx + 1}`,
item,
tabIndex: this.isItemInViewport(this._focusedItemIndex) ? 0 : -1,
posinset: idx + 1,
setsize: this.content.length,
setsize: this._visibleItems.length,
visible: this.isItemInViewport(idx),
};
});
Expand Down Expand Up @@ -828,7 +879,7 @@ class Carousel extends UI5Element {
}

get pagesCount() {
const items = this.content.length;
const items = this._visibleItems.length;
return items > this.effectiveItemsPerPage ? items - this.effectiveItemsPerPage + 1 : 1;
}
get isPageTypeDots() {
Expand Down Expand Up @@ -866,7 +917,7 @@ class Carousel extends UI5Element {
}

get hasNext() {
return this.cyclic || (this._focusedItemIndex + 1 <= this.content.length - 1 && this._currentSlideIndex < this.pagesCount - 1);
return this.cyclic || (this._focusedItemIndex + 1 <= this._visibleItems.length - 1 && this._currentSlideIndex < this.pagesCount - 1);
}

get suppressAnimation() {
Expand All @@ -886,7 +937,7 @@ class Carousel extends UI5Element {
}

get ariaActiveDescendant() {
return this.content.length ? `${this._id}-carousel-item-${this._focusedItemIndex + 1}` : undefined;
return this._visibleItems.length ? `${this._id}-carousel-item-${this._focusedItemIndex + 1}` : undefined;
}

get ariaLabelTxt() {
Expand All @@ -905,6 +956,15 @@ class Carousel extends UI5Element {
return Carousel.i18nBundle.getText(CAROUSEL_ARIA_ROLE_DESCRIPTION);
}

/**
* Returns only visible (non-hidden) content items.
* Items with the 'hidden' attribute are automatically excluded from carousel navigation.
* @private
*/
get _visibleItems() {
return this.content.filter(x => !x.hasAttribute("hidden"));
}

carouselItemDomRef(idx: number) : Array<HTMLElement> {
const items = this.getDomRef()?.querySelectorAll(".ui5-carousel-item") || [];
return Array.from(items).filter((item, index) => {
Expand Down
Loading
Loading