diff --git a/packages/main/cypress/specs/Carousel.cy.tsx b/packages/main/cypress/specs/Carousel.cy.tsx index 7a548c2b96a6..b540e040041f 100644 --- a/packages/main/cypress/specs/Carousel.cy.tsx +++ b/packages/main/cypress/specs/Carousel.cy.tsx @@ -403,34 +403,6 @@ describe("Carousel general interaction", () => { }); - it("navigateTo method and visibleItemsIndices", () => { - cy.mount( - - - - - - - - - - - - ); - - cy.get("#carousel9") - .invoke("prop", "visibleItemsIndices") - .should("deep.equal", [0, 1]); - - cy.get("#carousel9").then(($carousel) => { - $carousel[0].navigateTo(1); - }); - - cy.get("#carousel9") - .invoke("prop", "visibleItemsIndices") - .should("deep.equal", [1, 2]); - }); - it("F7 keyboard navigation", () => { cy.mount( diff --git a/packages/main/src/Carousel.ts b/packages/main/src/Carousel.ts index 99886d58bf44..01d66d2fa640 100644 --- a/packages/main/src/Carousel.ts +++ b/packages/main/src/Carousel.ts @@ -51,7 +51,7 @@ type ItemsInfo = { tabIndex: number, posinset: number, setsize: number, - selected: boolean, + visible: boolean, _individualSlot?: string, } @@ -277,6 +277,10 @@ class Carousel extends UI5Element { _resizing: boolean; _lastFocusedElements: Array; _orderOfLastFocusedPages: Array; + _visibleItemsIndexes: Array; + _calculatedX: number = 0; + _itemIndicator: number = 0; + _currentSlideIndex: number = 0; /** * Defines the content of the component. @@ -304,6 +308,7 @@ class Carousel extends UI5Element { this._lastFocusedElements = []; this._orderOfLastFocusedPages = []; + this._visibleItemsIndexes = []; } onBeforeRendering() { @@ -324,6 +329,9 @@ class Carousel extends UI5Element { if (isDesktop()) { this.setAttribute("desktop", ""); } + this._width = this.offsetWidth; + this._updateVisibleItems(this._selectedIndex); + this._currentSlideIndex = Math.min(this._selectedIndex, this.pagesCount - 1); } onExitDOM() { @@ -345,6 +353,7 @@ class Carousel extends UI5Element { // Change transitively effectiveItemsPerPage by modifying _width this._width = this.offsetWidth; this._itemWidth = Math.floor(this._width / this.effectiveItemsPerPage); + this._updateVisibleItems(this._selectedIndex); // Items per page did not change or the current, // therefore page index does not need to be re-adjusted @@ -352,8 +361,8 @@ class Carousel extends UI5Element { return; } - if (this._selectedIndex > this.pagesCount - 1) { - this._selectedIndex = this.pagesCount - 1; + if (this._selectedIndex > this.items.length - 1) { + this._selectedIndex = this.items.length - 1; this.fireDecoratorEvent("navigate", { selectedIndex: this._selectedIndex }); } } @@ -370,24 +379,16 @@ class Carousel extends UI5Element { } } - async _onkeydown(e: KeyboardEvent) { + _onkeydown(e: KeyboardEvent) { if (isF7(e)) { this._handleF7Key(e); return; } - if (e.target !== this.getDomRef()) { - return; - } - if (isLeft(e) || isDown(e)) { this.navigateLeft(); - await renderFinished(); - this.getDomRef()!.focus(); } else if (isRight(e) || isUp(e)) { this.navigateRight(); - await renderFinished(); - this.getDomRef()!.focus(); } } @@ -400,7 +401,7 @@ class Carousel extends UI5Element { let pageIndex = -1; for (let i = 0; i < this.content.length; i++) { - if (this.content[i].contains(target)) { + if (this.content[i].isEqualNode(target?.querySelector("slot")?.assignedNodes()[0] as HTMLElement)) { pageIndex = i; break; } @@ -410,6 +411,7 @@ class Carousel extends UI5Element { return; } + this._selectedIndex = pageIndex; // Save reference of the last focused element for each page this._lastFocusedElements[pageIndex] = target; @@ -439,6 +441,7 @@ class Carousel extends UI5Element { if (e.target === this.getDomRef() && lastFocusedElement) { lastFocusedElement.focus(); } else { + //@TODO where shoul the focus go? this.getDomRef()!.focus(); } } @@ -465,14 +468,21 @@ class Carousel extends UI5Element { const previousSelectedIndex = this._selectedIndex; if (this._selectedIndex - 1 < 0) { + // TODO clean up logic, no cyclic for morre than pne page if (this.cyclic) { - this._selectedIndex = this.pagesCount - 1; + // make better + if (this._selectedIndex === 0 && this.effectiveItemsPerPage > 1) { + this._selectedIndex = 0; + } else { + this._selectedIndex = this.items.length - 1; + } } } else { --this._selectedIndex; } if (previousSelectedIndex !== this._selectedIndex) { + this.skipToItem(this._selectedIndex, -1); this.fireDecoratorEvent("navigate", { selectedIndex: this._selectedIndex }); } } @@ -482,9 +492,14 @@ class Carousel extends UI5Element { const previousSelectedIndex = this._selectedIndex; - if (this._selectedIndex + 1 > this.pagesCount - 1) { + if (this._selectedIndex + 1 > this.items.length - 1) { + // TODO clean up logic, no cyclic for mоre than оne page if (this.cyclic) { - this._selectedIndex = 0; + if (this._selectedIndex === this.items.length - 1 && this.effectiveItemsPerPage > 1) { + this._selectedIndex = this.items.length - 1; + } else { + this._selectedIndex = 0; + } } else { return; } @@ -492,11 +507,68 @@ class Carousel extends UI5Element { ++this._selectedIndex; } + //TOOD fix fast clicking of arrow left breaks animation if (previousSelectedIndex !== this._selectedIndex) { + this.skipToItem(this._selectedIndex, 1); this.fireDecoratorEvent("navigate", { selectedIndex: this._selectedIndex }); } } + _calculateItemSlideIndex(currentSlideIndex: number, itemStep: number) { + const itemsPerPage = this.effectiveItemsPerPage; + + let slideIndex; + + if (itemsPerPage > 1) { + if (currentSlideIndex === 0 && itemStep < 0) { + return 0; + } + + if (currentSlideIndex >= this.pagesCount && itemStep > 0) { + return this.pagesCount - 1; + } + + slideIndex = currentSlideIndex + itemStep; + } else { + slideIndex = itemStep > 0 ? currentSlideIndex + 1 : currentSlideIndex - 1; + if (this.cyclic) { + if (currentSlideIndex === 0 && itemStep < 0) { + return this.pagesCount - 1; + } + + if (currentSlideIndex === this.items.length - 1 && itemStep > 0) { + return 0; + } + } + } + return slideIndex; + } + + _moveToItem(slideIndex: number) { + if (this.items.length === 0) { + return; + } + + const itemsInViewportToShow = this.effectiveItemsPerPage, + itemsCount = this.items.length, + cyclic = this.cyclic; + + if (cyclic && itemsInViewportToShow !== 1 && (slideIndex < 0 || slideIndex > itemsCount - 1)) { + return; + } + + if (slideIndex + itemsInViewportToShow > itemsCount - 1) { + slideIndex = itemsCount - itemsInViewportToShow; + } + + this._updateVisibleItems(slideIndex); + this._currentSlideIndex = slideIndex; + } + + focusItem() { + this.carouselItemDomRef(this._selectedIndex)[0].focus(); + } + _navButtonClick(e: UI5CustomEvent) { const button = e.target as Button; if (button.hasAttribute("data-ui5-arrow-forward")) { @@ -504,8 +576,6 @@ class Carousel extends UI5Element { } else { this.navigateLeft(); } - - this.focus(); } /** @@ -514,29 +584,41 @@ class Carousel extends UI5Element { * @since 1.0.0-rc.15 * @public */ - navigateTo(itemIndex: number) : void { + navigateTo(itemIndex: number) { + //TODO test this and fix this._resizing = false; + //TODO check if _SelectedIndex is grater or smaller than itemIndex for skipToItemOffset this._selectedIndex = itemIndex; + this._currentSlideIndex = itemIndex; + + this.skipToItem(this._selectedIndex, 1); } + async skipToItem(focusIndex: number, offset: number) { + if (!this.isItemInViewport(focusIndex)) { + const slideIndex = this._calculateItemSlideIndex(this._currentSlideIndex, offset); + console.log(" SLIDE INDEX ", slideIndex); + this._moveToItem(slideIndex); + } + await renderFinished(); + this.focusItem(); + } /** * Assuming that all items have the same width * @private */ get items(): Array { return this.content.map((item, idx) => { - const visible = this.isItemInViewport(idx); return { id: `${this._id}-carousel-item-${idx + 1}`, item, - tabIndex: visible ? 0 : -1, + tabIndex: this._selectedIndex === idx ? 0 : -1, posinset: idx + 1, setsize: this.content.length, - selected: visible, + visible: this.isItemInViewport(idx), }; }); } - get effectiveItemsPerPage(): number { const itemsPerPageArray = this.itemsPerPage.split(" "); let itemsPerPageSizeS = 1, @@ -576,11 +658,33 @@ class Carousel extends UI5Element { } isItemInViewport(index: number): boolean { - return index >= this._selectedIndex && index <= this._selectedIndex + this.effectiveItemsPerPage - 1; + return this._visibleItemsIndexes.includes(index); + } + + _updateVisibleItems(index:number) { + let newItemIndex = index; + const effectiveItemsPerPage: number = this.effectiveItemsPerPage; + const items = this.items; + + if (!items.length) { + return; + } + + if (newItemIndex > items.length - effectiveItemsPerPage) { + newItemIndex = items.length - effectiveItemsPerPage; + } + const lastItemIndex = newItemIndex + effectiveItemsPerPage; + + this._visibleItemsIndexes = []; + + for (let i = newItemIndex; i < lastItemIndex; i++) { + this._visibleItemsIndexes.push(i); + } + console.log(" VISIBLE ", this._visibleItemsIndexes); } isIndexInRange(index: number): boolean { - return index >= 0 && index <= this.pagesCount - 1; + return index >= 0 && index <= this.items.length - 1; } /** @@ -631,7 +735,6 @@ class Carousel extends UI5Element { const items = this.content.length; return items > this.effectiveItemsPerPage ? items - this.effectiveItemsPerPage + 1 : 1; } - get isPageTypeDots() { if (this.pageIndicatorType === CarouselPageIndicatorType.Numeric) { return false; @@ -646,7 +749,7 @@ class Carousel extends UI5Element { for (let index = 0; index < pages; index++) { dots.push({ - active: index === this._selectedIndex, + active: index === this._currentSlideIndex, ariaLabel: Carousel.i18nBundle.getText(CAROUSEL_DOT_TEXT, index + 1, pages), }); } @@ -663,11 +766,11 @@ class Carousel extends UI5Element { } get hasPrev() { - return this.cyclic || this._selectedIndex - 1 >= 0; + return (this.cyclic && this._selectedIndex - 1 >= 0) || this.effectiveItemsPerPage === 1; } get hasNext() { - return this.cyclic || this._selectedIndex + 1 <= this.pagesCount - 1; + return (this.cyclic && this._selectedIndex + 1 <= this.content.length - 1) || this.effectiveItemsPerPage === 1; } get suppressAnimation() { @@ -679,7 +782,7 @@ class Carousel extends UI5Element { } get selectedIndexToShow() { - return this._isRTL ? this.pagesCount - (this.pagesCount - this._selectedIndex) + 1 : this._selectedIndex + 1; + return this._isRTL ? this.items.length - (this.items.length - this._selectedIndex) + 1 : this._selectedIndex + 1; } get ofText() { @@ -706,22 +809,11 @@ class Carousel extends UI5Element { return Carousel.i18nBundle.getText(CAROUSEL_ARIA_ROLE_DESCRIPTION); } - /** - * The indices of the currently visible items of the component. - * @public - * @since 1.0.0-rc.15 - * @default [] - */ - get visibleItemsIndices() : Array { - const visibleItemsIndices: Array = []; - - this.items.forEach((item, index) => { - if (this.isItemInViewport(index)) { - visibleItemsIndices.push(index); - } - }); - - return visibleItemsIndices; + carouselItemDomRef(idx: number) : Array { + const items = this.getDomRef()?.querySelectorAll(".ui5-carousel-item") || []; + return Array.from(items).filter((item, index) => { + return index === idx; + }) as Array; } } diff --git a/packages/main/src/CarouselTemplate.tsx b/packages/main/src/CarouselTemplate.tsx index 714a7eeddbc5..a4e160deb3ef 100644 --- a/packages/main/src/CarouselTemplate.tsx +++ b/packages/main/src/CarouselTemplate.tsx @@ -10,7 +10,6 @@ export default function CarouselTemplate(this: Carousel) { "ui5-carousel-root": true, [`ui5-carousel-background-${this._backgroundDesign}`]: true, }} - tabindex={0} role="list" aria-label={this.ariaLabelTxt} aria-roledescription={this._roleDescription} @@ -21,20 +20,22 @@ export default function CarouselTemplate(this: Carousel) { onMouseOver={this._onmouseover} >
-
+
{this.items.map(itemInfo =>
@@ -105,5 +106,5 @@ function navIndicator(this: Carousel) { ; + >{this._currentSlideIndex + 1} {this.ofText} {this.pagesCount}
; } diff --git a/packages/main/src/themes/Carousel.css b/packages/main/src/themes/Carousel.css index 9f87a8d65aa8..ec9b5fdcd90d 100644 --- a/packages/main/src/themes/Carousel.css +++ b/packages/main/src/themes/Carousel.css @@ -8,8 +8,8 @@ height: 100%; } -:host([desktop]) .ui5-carousel-root:focus, -.ui5-carousel-root:focus-visible { +:host([desktop]) .ui5-carousel-item:focus, +.ui5-carousel-item:focus-visible { outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor); } diff --git a/packages/main/test/pages/Carousel.html b/packages/main/test/pages/Carousel.html index 9bfb1a10a37e..41f31caccf7b 100644 --- a/packages/main/test/pages/Carousel.html +++ b/packages/main/test/pages/Carousel.html @@ -155,7 +155,7 @@ - + Template Based Segmentation @@ -164,7 +164,7 @@ - @@ -177,7 +177,7 @@ - + @@ -187,7 +187,7 @@ - + Template Based Segmentation @@ -196,7 +196,7 @@ - @@ -209,7 +209,7 @@ - + @@ -219,7 +219,7 @@ - + Template Based Segmentation