diff --git a/packages/main/cypress/specs/Carousel.cy.tsx b/packages/main/cypress/specs/Carousel.cy.tsx
index 84a65384b3c6..36641f4f89e7 100644
--- a/packages/main/cypress/specs/Carousel.cy.tsx
+++ b/packages/main/cypress/specs/Carousel.cy.tsx
@@ -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", () => {
@@ -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(
+
+
+
+
+ );
+
+ cy.get("ui5-carousel")
+ .shadow()
+ .find(".ui5-carousel-item")
+ .should("have.length", 2);
+ });
+
+ it("should update navigation when items become hidden dynamically", () => {
+ cy.mount(
+
+
+
+
+
+ );
+
+ 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(
+
+
+
+
+
+
+
+ );
+
+ cy.get("ui5-carousel")
+ .shadow()
+ .find(".ui5-carousel-item")
+ .should("have.length", 4);
+ });
+
+ it("should update page count correctly with filtered content", () => {
+ cy.mount(
+
+
+
+
+
+
+
+ );
+
+ cy.get("ui5-carousel").should("have.prop", "pagesCount", 3);
+
+ cy.get("ui5-carousel")
+ .shadow()
+ .find(".ui5-carousel-navigation-dot")
+ .should("have.length", 3);
+ });
});
\ No newline at end of file
diff --git a/packages/main/src/Carousel.ts b/packages/main/src/Carousel.ts
index a8826f0d1b69..766bb556caa6 100644
--- a/packages/main/src/Carousel.ts
+++ b/packages/main/src/Carousel.ts
@@ -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;
@@ -297,6 +304,8 @@ class Carousel extends UI5Element {
_pageStep: number = 10;
_visibleItemsIndexes: Array;
_itemIndicator: number = 0;
+ _contentItemsObserver: MutationObserver;
+ _observableContent: Array = [];
/**
* Defines the content of the component.
@@ -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);
@@ -328,6 +344,8 @@ class Carousel extends UI5Element {
}
onBeforeRendering() {
+ this._observeContentItems();
+
if (this.arrowsPlacement === CarouselArrowsPlacement.Navigation || !isDesktop()) {
this._visibleNavigationArrows = true;
}
@@ -348,6 +366,8 @@ class Carousel extends UI5Element {
}
onExitDOM() {
+ this._contentItemsObserver.disconnect();
+ this._observableContent = [];
ResizeHandler.deregister(this, this._onResizeBound);
}
@@ -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;
}
@@ -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);
@@ -673,6 +720,10 @@ class Carousel extends UI5Element {
* @public
*/
navigateTo(itemIndex: number) {
+ if (!this.isIndexInRange(itemIndex)) {
+ return;
+ }
+
if (this._focusedItemIndex < itemIndex) {
this._itemIndicator = 1;
}
@@ -705,13 +756,13 @@ class Carousel extends UI5Element {
* @private
*/
get items(): Array {
- 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),
};
});
@@ -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() {
@@ -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() {
@@ -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() {
@@ -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 {
const items = this.getDomRef()?.querySelectorAll(".ui5-carousel-item") || [];
return Array.from(items).filter((item, index) => {
diff --git a/packages/main/test/pages/Carousel.html b/packages/main/test/pages/Carousel.html
index 04a9690082f7..2de8cadae41b 100644
--- a/packages/main/test/pages/Carousel.html
+++ b/packages/main/test/pages/Carousel.html
@@ -792,6 +792,124 @@
+ Testing hidden items
+
+
+
+
+
+
+ Marketing Overview
+ Sales Performance
+ Quarterly Reports
+
+
+
+
+
+
+
+
+ Hidden Analytics
+ Hidden Data Cleanup
+
+
+
+
+
+
+
+ Customer Insights
+ Campaign Performance
+
+
+
+
+
+
+
+ Trend Analysis
+ Customer Segments
+ Action Items
+
+
+
+
+
+
+
+
+ Hidden Task
+ Hidden Entry
+
+
+
+
+
+
+
+ Final Overview
+ Closing Metrics
+
+
+
+
+ Hide Last Item
+ add Item
+
+
+
+
+
+
+ Marketing Overview
+ Segmentation Models
+ Marketing Plans
+
+
+
+
+
+
+
+ Marketing Overview
+ Segmentation Models
+ Marketing Plans
+
+
+
+
+
+
+
+ Marketing Overview
+ Segmentation Models
+ Marketing Plans
+
+
+
+
+
+
+
+ Marketing Overview
+ Segmentation Models
+ Marketing Plans
+
+
+
+