diff --git a/packages/main/cypress/specs/Carousel.cy.tsx b/packages/main/cypress/specs/Carousel.cy.tsx index 7a548c2b96a6..84a65384b3c6 100644 --- a/packages/main/cypress/specs/Carousel.cy.tsx +++ b/packages/main/cypress/specs/Carousel.cy.tsx @@ -42,12 +42,12 @@ describe("Carousel general interaction", () => { ); cy.get("#carousel1") - .realHover() + .trigger("mouseover") .shadow() - .find(".ui5-carousel-navigation-arrows .ui5-carousel-navigation-button:not(.ui5-carousel-navigation-button--hidden)") + .find(".ui5-carousel-navigation-arrows .ui5-carousel-navigation-button:not(.ui5-carousel-navigation-button--hidden)").first() .realClick(); - cy.get("#carousel1").should("have.prop", "_selectedIndex", 2); + cy.get("#carousel1").should("have.prop", "_focusedItemIndex", 2); }); it("Carousel navigates right", () => { @@ -57,14 +57,15 @@ describe("Carousel general interaction", () => { ); + cy.get("#carousel1").should("have.prop", "_focusedItemIndex", 0); cy.get("#carousel1") - .realHover() + .trigger("mouseover") .shadow() - .find(".ui5-carousel-navigation-arrows .ui5-carousel-navigation-button:not(.ui5-carousel-navigation-button--hidden)") + .find(".ui5-carousel-navigation-arrows .ui5-carousel-navigation-button:not(.ui5-carousel-navigation-button--hidden)").last() .realClick(); - cy.get("#carousel1").should("have.prop", "_selectedIndex", 1); + cy.get("#carousel1").should("have.prop", "_focusedItemIndex", 1); }); it("Navigation is rendered for carousel with less than 9 elements", () => { @@ -117,19 +118,19 @@ describe("Carousel general interaction", () => { ); cy.get("#carousel2") - .realHover() + .trigger('mouseover') .shadow() .find(".ui5-carousel-navigation-arrows .ui5-carousel-navigation-button:not(.ui5-carousel-navigation-button--hidden)") .should("have.length", 1); cy.get("#carousel2") - .realHover() + .trigger('mouseover') .shadow() .find(".ui5-carousel-navigation-arrows .ui5-carousel-navigation-button:not(.ui5-carousel-navigation-button--hidden)") .realClick(); cy.get("#carousel2") - .realHover() + .trigger('mouseover') .shadow() .find(".ui5-carousel-navigation-arrows .ui5-carousel-navigation-button:not(.ui5-carousel-navigation-button--hidden)") .should("have.length", 2); @@ -216,8 +217,12 @@ describe("Carousel general interaction", () => { cy.get("#carousel5") .shadow() - .find(".ui5-carousel-item:first-child") - .should("have.attr", "aria-selected", "true"); + .find(".ui5-carousel-content").find(":first-child") + .realClick(); + cy.get("#carousel5") + .shadow() + .find(".ui5-carousel-content").find(":first-child") + .should("have.attr", "aria-hidden"); cy.get("#carousel5") .shadow() @@ -231,30 +236,6 @@ describe("Carousel general interaction", () => { .should("have.attr", "aria-posinset", CAROUSEL_ITEM4_POS) .and("have.attr", "aria-setsize", SETSIZE); - cy.get('#carousel5') - .then(($carousel) => { - const el = $carousel[0]; - - cy.get('#carousel5') - .shadow() - .find('.ui5-carousel-root') - .should('have.attr', 'aria-activedescendant', `${el._id}-carousel-item-1`); - }); - - cy.get("#carousel5") - .shadow() - .find(".ui5-carousel-navigation-button:nth-child(2)") - .realClick(); - - cy.get('#carousel5') - .then(($carousel) => { - const el = $carousel[0]; - cy.get('#carousel5') - .shadow() - .find('.ui5-carousel-root') - .should('have.attr', 'aria-activedescendant', `${el._id}-carousel-item-2`); - }); - cy.get("#carouselAccName") .shadow() .find(".ui5-carousel-root") @@ -268,6 +249,11 @@ describe("Carousel general interaction", () => { cy.get("#carousel5") .shadow() .find(".ui5-carousel-root") + .should("have.attr", "role", "region"); + + cy.get("#carousel5") + .shadow() + .find(".ui5-carousel-content") .should("have.attr", "role", "list"); cy.get("#carousel5") @@ -328,7 +314,7 @@ describe("Carousel general interaction", () => { it("Event navigate fired when pressing navigation arrows", () => { const navigateEventStub = cy.stub().as("myStub"); cy.mount( - + @@ -340,29 +326,33 @@ describe("Carousel general interaction", () => { ); cy.get("#carousel8") + .trigger("mouseover") .shadow() - .find("ui5-button[data-ui5-arrow-forward]") + .find(".ui5-carousel-navigation-button[data-ui5-arrow-forward]") .should("exist") .realClick(); cy.get("@myStub").should("have.been.calledOnce"); cy.get("#carousel8") + .trigger("mouseover") .shadow() - .find("ui5-button[data-ui5-arrow-forward]") + .find(".ui5-carousel-navigation-button[data-ui5-arrow-forward]") .should("exist") .realClick(); cy.get("@myStub").should("have.been.calledTwice"); cy.get("#carousel8") + .trigger("mouseover") .shadow() - .find("ui5-button[data-ui5-arrow-back]") + .find(".ui5-carousel-navigation-button[data-ui5-arrow-back]") .should("exist") .realClick(); cy.get("@myStub").should("have.been.calledThrice"); cy.get("#carousel8") + .trigger("mouseover") .shadow() - .find("ui5-button[data-ui5-arrow-back]") + .find(".ui5-carousel-navigation-button[data-ui5-arrow-back]") .should("exist") .realClick(); cy.get("@myStub").should("have.callCount", 4); @@ -403,43 +393,15 @@ 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(
Page 1
- +
- +
@@ -497,31 +459,83 @@ describe("Carousel general interaction", () => {
); cy.get(".myCard").should("be.visible"); + cy.get("#carouselF7").shadow().find(".ui5-carousel-content").find(".ui5-carousel-item").first().focus(); + + cy.realPress("F7"); + cy.wait(100) - cy.get("#carouselF7Button").realClick(); cy.get("#carouselF7Button").should('be.focused'); cy.realPress("F7"); - cy.focused().should("have.class", "ui5-carousel-root"); + cy.wait(100) - cy.realPress("F7"); - cy.focused().should("have.class", "ui5-button-root"); + cy.get("#carouselF7").shadow().find(".ui5-carousel-content").find(":first-child").should("be.focused"); + }); - cy.get("#carouselF7Input").realClick(); - cy.realPress("F7"); - cy.focused().should("have.class", "ui5-carousel-root"); - cy.realPress("F7"); - cy.focused().should("have.class", "ui5-input-inner"); + it("'Home' and 'End' button press", () => { + cy.mount( + + + + + + + + + + + + ); - cy.get("#carouselF7Button").realClick(); - cy.realPress("F7"); + cy.get("#firstButton").realClick(); + cy.realPress("End"); + cy.get("#testHomeAndEnd").should("have.prop", "_focusedItemIndex", 9); + cy.realPress("Home"); + cy.get("#testHomeAndEnd").should("have.prop", "_focusedItemIndex", 0); + }); - cy.get("#carouselF7").then(($carousel) => { - $carousel[0].navigateTo(1); - }); - cy.realPress("F7"); - cy.focused().should("have.class", "ui5-input-inner"); + it("'PageUp' and 'PageDown' button press", () => { + cy.mount( + + + + + + + + + + + + + + + + + + + + + + + + ); + + cy.get("#firstButton").realClick(); + cy.get("#testPageUpDown").should("have.prop", "_focusedItemIndex", 0); + cy.realPress("PageUp"); + cy.get("#testPageUpDown").should("have.prop", "_focusedItemIndex", 10); + cy.realPress("PageUp"); + cy.get("#testPageUpDown").should("have.prop", "_focusedItemIndex", 20); + cy.realPress("PageUp"); + cy.get("#testPageUpDown").should("have.prop", "_focusedItemIndex", 21); + cy.realPress("PageDown"); + cy.get("#testPageUpDown").should("have.prop", "_focusedItemIndex", 11); + cy.realPress("PageDown"); + cy.get("#testPageUpDown").should("have.prop", "_focusedItemIndex", 1); + cy.realPress("PageDown"); + cy.get("#testPageUpDown").should("have.prop", "_focusedItemIndex", 0); }); it("Items per page", () => { diff --git a/packages/main/src/Carousel.ts b/packages/main/src/Carousel.ts index 99886d58bf44..a8826f0d1b69 100644 --- a/packages/main/src/Carousel.ts +++ b/packages/main/src/Carousel.ts @@ -4,12 +4,17 @@ import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; +import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js"; import { isLeft, isRight, isDown, isUp, isF7, + isHome, + isEnd, + isPageDown, + isPageUp, } from "@ui5/webcomponents-base/dist/Keys.js"; import type { UI5CustomEvent } from "@ui5/webcomponents-base"; import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; @@ -18,7 +23,6 @@ import ScrollEnablement from "@ui5/webcomponents-base/dist/delegate/ScrollEnable import type { ScrollEnablementEventListenerParam } from "@ui5/webcomponents-base/dist/delegate/ScrollEnablement.js"; import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; -import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js"; import { isDesktop } from "@ui5/webcomponents-base/dist/Device.js"; import AnimationMode from "@ui5/webcomponents-base/dist/types/AnimationMode.js"; import { getAnimationMode } from "@ui5/webcomponents-base/dist/config/AnimationMode.js"; @@ -35,8 +39,9 @@ import CarouselPageIndicatorType from "./types/CarouselPageIndicatorType.js"; import type BackgroundDesign from "./types/BackgroundDesign.js"; import type BorderDesign from "./types/BorderDesign.js"; import CarouselTemplate from "./CarouselTemplate.js"; - -import type Button from "./Button.js"; +import type Icon from "./Icon.js"; +import { getFirstFocusableElement } from "@ui5/webcomponents-base/dist/util/FocusableElements.js"; +import clamp from "@ui5/webcomponents-base/dist/util/clamp.js"; // Styles import CarouselCss from "./generated/themes/Carousel.css.js"; @@ -51,7 +56,7 @@ type ItemsInfo = { tabIndex: number, posinset: number, setsize: number, - selected: boolean, + visible: boolean, _individualSlot?: string, } @@ -179,6 +184,17 @@ class Carousel extends UI5Element { @property({ type: Boolean }) hideNavigationArrows = false; + /** + * Defines the current first visible item in the viewport. + * Default value is 0, which means the first item in the viewport. + * + * @since 1.0.0-rc.15 + * @default 0 + * @public + */ + @property({ type: Number, noAttribute: true }) + _currentSlideIndex: number = 0; + /** * Defines the visibility of the page indicator. * If set to true the page indicator will be hidden. @@ -234,8 +250,8 @@ class Carousel extends UI5Element { * @default 0 * @private */ - @property({ type: Number }) - _selectedIndex = 0; + @property({ type: Number, noAttribute: true }) + _focusedItemIndex = 0; /** * Defines the position of arrows. @@ -277,6 +293,10 @@ class Carousel extends UI5Element { _resizing: boolean; _lastFocusedElements: Array; _orderOfLastFocusedPages: Array; + _lastInnerFocusedElement?: HTMLElement; + _pageStep: number = 10; + _visibleItemsIndexes: Array; + _itemIndicator: number = 0; /** * Defines the content of the component. @@ -304,6 +324,7 @@ class Carousel extends UI5Element { this._lastFocusedElements = []; this._orderOfLastFocusedPages = []; + this._visibleItemsIndexes = []; } onBeforeRendering() { @@ -331,8 +352,8 @@ class Carousel extends UI5Element { } validateSelectedIndex() { - if (!this.isIndexInRange(this._selectedIndex)) { - this._selectedIndex = 0; + if (!this.isIndexInRange(this._focusedItemIndex)) { + this._focusedItemIndex = 0; } } @@ -345,6 +366,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._currentSlideIndex); // Items per page did not change or the current, // therefore page index does not need to be re-adjusted @@ -352,10 +374,7 @@ class Carousel extends UI5Element { return; } - if (this._selectedIndex > this.pagesCount - 1) { - this._selectedIndex = this.pagesCount - 1; - this.fireDecoratorEvent("navigate", { selectedIndex: this._selectedIndex }); - } + this._focusedItemIndex = clamp(this._focusedItemIndex, this._currentSlideIndex, this.items.length - this.effectiveItemsPerPage); } _updateScrolling(e: ScrollEnablementEventListenerParam) { @@ -370,24 +389,30 @@ 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 (isHome(e)) { + this._handleHome(e); + } + if (isEnd(e)) { + this._handleEnd(e); + } + if (isPageUp(e)) { + this._handlePageUp(e); + } + if (isPageDown(e)) { + this._handlePageDown(e); } - if (isLeft(e) || isDown(e)) { + if (isLeft(e) || isUp(e)) { + e.preventDefault(); this.navigateLeft(); - await renderFinished(); - this.getDomRef()!.focus(); - } else if (isRight(e) || isUp(e)) { + } else if (isRight(e) || isDown(e)) { + e.preventDefault(); this.navigateRight(); - await renderFinished(); - this.getDomRef()!.focus(); } } @@ -400,7 +425,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 +435,7 @@ class Carousel extends UI5Element { return; } + this._focusedItemIndex = pageIndex; // Save reference of the last focused element for each page this._lastFocusedElements[pageIndex] = target; @@ -433,16 +459,54 @@ class Carousel extends UI5Element { } } - _handleF7Key(e: KeyboardEvent) { - const lastFocusedElement = this._lastFocusedElements[this._getLastFocusedActivePageIndex]; + _ontouchstart(e: TouchEvent) { + const target = e.target as HTMLElement; + if (target.hasAttribute("data-ui5-arrow-forward") || target.hasAttribute("data-ui5-arrow-back")) { + e.preventDefault(); + } + } + + _onmousedown(e: MouseEvent) { + const target = e.target as HTMLElement; + if (target.hasAttribute("data-ui5-arrow-forward") || target.hasAttribute("data-ui5-arrow-back")) { + e.preventDefault(); + } + } - if (e.target === this.getDomRef() && lastFocusedElement) { + async _handleF7Key(e: KeyboardEvent) { + const lastFocusedElement = this._lastFocusedElements[this._getLastFocusedActivePageIndex]; + if (!this._lastInnerFocusedElement) { + const firstFocusable = await getFirstFocusableElement(this.items[this._focusedItemIndex].item); + firstFocusable?.focus(); + this._lastInnerFocusedElement = firstFocusable || undefined; + } else if (this.carouselItemDomRef(this._focusedItemIndex)[0] === lastFocusedElement && lastFocusedElement !== e.target) { lastFocusedElement.focus(); - } else { - this.getDomRef()!.focus(); + this._lastInnerFocusedElement = e.target as HTMLElement; + } else if (this._lastInnerFocusedElement) { + this._lastInnerFocusedElement.focus(); } } + _handleHome(e: KeyboardEvent) { + e.preventDefault(); + this.navigateTo(0); + } + + _handleEnd(e: KeyboardEvent) { + e.preventDefault(); + this.navigateTo(this.items.length - 1); + } + + _handlePageUp(e: KeyboardEvent) { + e.preventDefault(); + this.navigateTo(this._focusedItemIndex + this._pageStep < this.items.length ? this._focusedItemIndex + this._pageStep : this.items.length - 1); + } + + _handlePageDown(e: KeyboardEvent) { + e.preventDefault(); + this.navigateTo(this._focusedItemIndex - this._pageStep > 0 ? this._focusedItemIndex - this._pageStep : 0); + } + get _backgroundDesign() { return this.backgroundDesign.toLowerCase(); } @@ -456,56 +520,150 @@ class Carousel extends UI5Element { } } - return this._selectedIndex; + return this._focusedItemIndex; } navigateLeft() { this._resizing = false; - const previousSelectedIndex = this._selectedIndex; + const previousSelectedIndex = this._focusedItemIndex; - if (this._selectedIndex - 1 < 0) { - if (this.cyclic) { - this._selectedIndex = this.pagesCount - 1; + if (this._focusedItemIndex - 1 < 0) { + if (this.cyclic && this._visibleItemsIndexes.length >= 1) { + if (this._focusedItemIndex === 0 && this.effectiveItemsPerPage > 1) { + this._focusedItemIndex = 0; + } else { + this._focusedItemIndex = this.items.length - 1; + } } } else { - --this._selectedIndex; + --this._focusedItemIndex; } - if (previousSelectedIndex !== this._selectedIndex) { - this.fireDecoratorEvent("navigate", { selectedIndex: this._selectedIndex }); + if (previousSelectedIndex !== this._focusedItemIndex) { + this.skipToItem(this._focusedItemIndex, -1); + this.fireDecoratorEvent("navigate", { selectedIndex: this._focusedItemIndex }); } } navigateRight() { this._resizing = false; - const previousSelectedIndex = this._selectedIndex; + const previousSelectedIndex = this._focusedItemIndex; - if (this._selectedIndex + 1 > this.pagesCount - 1) { + if (this._focusedItemIndex + 1 > this.items.length - 1) { if (this.cyclic) { - this._selectedIndex = 0; + if (this._focusedItemIndex === this.items.length - 1 && this.effectiveItemsPerPage > 1) { + this._focusedItemIndex = this.items.length - 1; + } else { + this._focusedItemIndex = 0; + } } else { return; } } else { - ++this._selectedIndex; + ++this._focusedItemIndex; } - if (previousSelectedIndex !== this._selectedIndex) { - this.fireDecoratorEvent("navigate", { selectedIndex: this._selectedIndex }); + if (previousSelectedIndex !== this._focusedItemIndex) { + this.skipToItem(this._focusedItemIndex, 1); + this.fireDecoratorEvent("navigate", { selectedIndex: this._focusedItemIndex }); } } - _navButtonClick(e: UI5CustomEvent) { - const button = e.target as Button; - if (button.hasAttribute("data-ui5-arrow-forward")) { - this.navigateRight(); + navigateArrowRight() { + if (this._focusedItemIndex === this._visibleItemsIndexes[0]) { + this.navigateTo(this._focusedItemIndex + 1); + this._moveToItem(this._currentSlideIndex + 1); } else { - this.navigateLeft(); + this._moveToItem(this._currentSlideIndex + 1); + this.navigateTo(this._focusedItemIndex); + } + } + + navigateArrowLeft() { + if (this._focusedItemIndex > 0 && this._focusedItemIndex === this._visibleItemsIndexes[this._visibleItemsIndexes.length - 1]) { + this.navigateTo(this._focusedItemIndex - 1); + this._moveToItem(this._currentSlideIndex - 1); + } else { + this._moveToItem(this._currentSlideIndex === 0 ? this.pagesCount - 1 : this._currentSlideIndex - 1); + this.navigateTo(this._focusedItemIndex === 0 ? this.items.length - 1 : this._focusedItemIndex); + } + } + + _calculateItemSlideIndex(currentSlideIndex: number, itemStep: number) { + if (this.isItemInViewport(this._focusedItemIndex)) { + return 0; + } + 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.focus(); + this._updateVisibleItems(slideIndex); + this._currentSlideIndex = slideIndex; + } + + focusItem() { + this.carouselItemDomRef(this._focusedItemIndex)[0].focus({ preventScroll: true }); + } + + _navButtonClick(e: UI5CustomEvent) { + const target = e.target as Icon; + if (this._visibleItemsIndexes.length > 1) { + if (target.hasAttribute("data-ui5-arrow-forward")) { + this.navigateArrowRight(); + } else { + this.navigateArrowLeft(); + } + } else if (this._visibleItemsIndexes.length <= 1) { + if (target.hasAttribute("data-ui5-arrow-forward")) { + this.navigateRight(); + } else { + this.navigateLeft(); + } + } } /** @@ -514,9 +672,32 @@ class Carousel extends UI5Element { * @since 1.0.0-rc.15 * @public */ - navigateTo(itemIndex: number) : void { - this._resizing = false; - this._selectedIndex = itemIndex; + navigateTo(itemIndex: number) { + if (this._focusedItemIndex < itemIndex) { + this._itemIndicator = 1; + } + this._focusedItemIndex = itemIndex; + this._currentSlideIndex = itemIndex - this._itemIndicator; + if (this.isItemInViewport(itemIndex)) { + this._currentSlideIndex = this._visibleItemsIndexes[0]; + this.focusItem(); + return; + } + this.skipToItem(this._focusedItemIndex, 1); + } + + async skipToItem(focusIndex: number, offset: number) { + if (!this.isItemInViewport(focusIndex)) { + let slideIndex = this._calculateItemSlideIndex(this._currentSlideIndex, offset); + if (focusIndex === 0) { + slideIndex = 0; + } + this._moveToItem(slideIndex); + } + + await renderFinished(); + + this.focusItem(); } /** @@ -525,18 +706,16 @@ class Carousel extends UI5Element { */ 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.isItemInViewport(this._focusedItemIndex) ? 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 +755,32 @@ 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); + } } isIndexInRange(index: number): boolean { - return index >= 0 && index <= this.pagesCount - 1; + return index >= 0 && index <= this.items.length - 1; } /** @@ -631,7 +831,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 +845,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 +862,11 @@ class Carousel extends UI5Element { } get hasPrev() { - return this.cyclic || this._selectedIndex - 1 >= 0; + return this.cyclic || (this._focusedItemIndex - 1 >= 0 && this._currentSlideIndex !== 0); } get hasNext() { - return this.cyclic || this._selectedIndex + 1 <= this.pagesCount - 1; + return this.cyclic || (this._focusedItemIndex + 1 <= this.content.length - 1 && this._currentSlideIndex < this.pagesCount - 1); } get suppressAnimation() { @@ -679,7 +878,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._focusedItemIndex) + 1 : this._focusedItemIndex + 1; } get ofText() { @@ -687,7 +886,7 @@ class Carousel extends UI5Element { } get ariaActiveDescendant() { - return this.content.length ? `${this._id}-carousel-item-${this._selectedIndex + 1}` : undefined; + return this.content.length ? `${this._id}-carousel-item-${this._focusedItemIndex + 1}` : undefined; } get ariaLabelTxt() { @@ -706,22 +905,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..c518f2d6e8f1 100644 --- a/packages/main/src/CarouselTemplate.tsx +++ b/packages/main/src/CarouselTemplate.tsx @@ -1,5 +1,5 @@ import type Carousel from "./Carousel.js"; -import Button from "./Button.js"; +import Icon from "./Icon.js"; import slimArrowLeft from "@ui5/webcomponents-icons/dist/slim-arrow-left.js"; import slimArrowRight from "@ui5/webcomponents-icons/dist/slim-arrow-right.js"; @@ -10,31 +10,33 @@ export default function CarouselTemplate(this: Carousel) { "ui5-carousel-root": true, [`ui5-carousel-background-${this._backgroundDesign}`]: true, }} - tabindex={0} - role="list" + role="region" aria-label={this.ariaLabelTxt} aria-roledescription={this._roleDescription} - aria-activedescendant={this.ariaActiveDescendant} onFocusIn={this._onfocusin} onKeyDown={this._onkeydown} onMouseOut={this._onmouseout} onMouseOver={this._onmouseover} + onTouchStart={this._ontouchstart} + onMouseDown={this._onmousedown} >
-
+
{this.items.map(itemInfo =>
@@ -51,11 +53,9 @@ export default function CarouselTemplate(this: Carousel) { {this.renderNavigation &&
{this.showArrows.navigation && arrowBack.call(this)} - - {this.showArrows.navigation && arrowForward.call(this)}
} @@ -64,29 +64,29 @@ export default function CarouselTemplate(this: Carousel) { } function arrowBack(this: Carousel) { - return
; } diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties index 9e2b48e6f5b1..bd7ae6c73d53 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -104,7 +104,7 @@ CAL_LEGEND_ROLE_DESCRIPTION=Calendar Legend CAROUSEL_OF_TEXT=of #Carousel dots text -CAROUSEL_DOT_TEXT=Item {0} of {1} displayed +CAROUSEL_DOT_TEXT=Page {0} of {1} # Carousel Previous Page text CAROUSEL_PREVIOUS_ARROW_TEXT=Previous Page diff --git a/packages/main/src/themes/Carousel.css b/packages/main/src/themes/Carousel.css index 9f87a8d65aa8..a287989e270b 100644 --- a/packages/main/src/themes/Carousel.css +++ b/packages/main/src/themes/Carousel.css @@ -8,9 +8,10 @@ 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); + outline-offset: -1px; } .ui5-carousel-root { @@ -70,14 +71,14 @@ display: flex; align-items: center; justify-content: center; - padding: 0 0.75rem; + padding: 0.75rem; box-sizing: border-box; - transition: opacity 0.5s linear; - will-change: opacity; } .ui5-carousel-item--hidden { - opacity: 0; + transition: visibility 0.5s linear; + will-change: visibility; + visibility: hidden; } .ui5-carousel-navigation-arrows { @@ -127,13 +128,32 @@ } .ui5-carousel-navigation-button { - width: var(--ui5_carousel_button_size); - height: var(--ui5_carousel_button_size); - border-radius: 50%; - box-shadow: none; - cursor: pointer; - outline-offset: .1rem; - --_ui5_button_focused_border_radius: 50%; + width: var(--ui5_carousel_button_size); + height: var(--ui5_carousel_button_size); + border-radius: 50%; + box-sizing: border-box; + align-items: center; + justify-content: center; + display: flex; + padding: 0 0.5rem; + border: 1px solid var(--sapButton_Hover_BorderColor); + background: var(--sapButton_Background); + pointer-events: all; + color: var(--sapButton_TextColor); +} + +.ui5-carousel-navigation-button:hover { + background: var(--sapButton_Hover_Background); + border: 1px solid var(--sapButton_Hover_BorderColor); + color: var(--sapButton_Hover_TextColor); +} + +.ui5-carousel-navigation-button:active { + background-color: var(--sapButton_Active_Background); + border-color: var(--sapButton_Active_BorderColor); + outline-offset: 1px; + color: var(--sapButton_Active_TextColor); + box-shadow: none; } .ui5-carousel-navigation-arrows .ui5-carousel-navigation-button { diff --git a/packages/main/src/themes/base/Carousel-parameters.css b/packages/main/src/themes/base/Carousel-parameters.css index 4c6a3a9c2e4a..164ba83ddfce 100644 --- a/packages/main/src/themes/base/Carousel-parameters.css +++ b/packages/main/src/themes/base/Carousel-parameters.css @@ -1,7 +1,7 @@ :root { --ui5_carousel_background_color_solid: var(--sapGroup_ContentBackground); --ui5_carousel_background_color_translucent: var(--sapBackgroundColor); - --ui5_carousel_button_size: 2.5rem; + --ui5_carousel_button_size: 2.25rem; --ui5_carousel_inactive_dot_size: 0.25rem; --ui5_carousel_inactive_dot_margin: 0 0.375rem; --ui5_carousel_inactive_dot_border: 1px solid var(--sapContent_ForegroundBorderColor); diff --git a/packages/main/src/themes/sap_fiori_3_hcb/Carousel-parameters.css b/packages/main/src/themes/sap_fiori_3_hcb/Carousel-parameters.css index 8ac76606d1d8..3091ab0854bf 100644 --- a/packages/main/src/themes/sap_fiori_3_hcb/Carousel-parameters.css +++ b/packages/main/src/themes/sap_fiori_3_hcb/Carousel-parameters.css @@ -1,5 +1,5 @@ :root { - --ui5_carousel_button_size: 2.5rem; + --ui5_carousel_button_size: 2.25rem; --ui5_carousel_inactive_dot_size: 0.5rem; --ui5_carousel_inactive_dot_margin: 0 0.25rem; --ui5_carousel_inactive_dot_border: 1px solid var(--sapContent_ForegroundBorderColor); diff --git a/packages/main/src/themes/sap_fiori_3_hcw/Carousel-parameters.css b/packages/main/src/themes/sap_fiori_3_hcw/Carousel-parameters.css index 8ac76606d1d8..3091ab0854bf 100644 --- a/packages/main/src/themes/sap_fiori_3_hcw/Carousel-parameters.css +++ b/packages/main/src/themes/sap_fiori_3_hcw/Carousel-parameters.css @@ -1,5 +1,5 @@ :root { - --ui5_carousel_button_size: 2.5rem; + --ui5_carousel_button_size: 2.25rem; --ui5_carousel_inactive_dot_size: 0.5rem; --ui5_carousel_inactive_dot_margin: 0 0.25rem; --ui5_carousel_inactive_dot_border: 1px solid var(--sapContent_ForegroundBorderColor); diff --git a/packages/main/src/themes/sap_horizon_hcb/Carousel-parameters.css b/packages/main/src/themes/sap_horizon_hcb/Carousel-parameters.css index 71d68de9aeb5..4aa8dac41480 100644 --- a/packages/main/src/themes/sap_horizon_hcb/Carousel-parameters.css +++ b/packages/main/src/themes/sap_horizon_hcb/Carousel-parameters.css @@ -1,5 +1,5 @@ :root { - --ui5_carousel_button_size: 2.5rem; + --ui5_carousel_button_size: 2.25rem; --ui5_carousel_inactive_dot_size: 0.5rem; --ui5_carousel_inactive_dot_margin: 0 0.25rem; --ui5_carousel_inactive_dot_border: 1px solid var(--sapContent_ForegroundBorderColor); diff --git a/packages/main/src/themes/sap_horizon_hcw/Carousel-parameters.css b/packages/main/src/themes/sap_horizon_hcw/Carousel-parameters.css index 8ac76606d1d8..3091ab0854bf 100644 --- a/packages/main/src/themes/sap_horizon_hcw/Carousel-parameters.css +++ b/packages/main/src/themes/sap_horizon_hcw/Carousel-parameters.css @@ -1,5 +1,5 @@ :root { - --ui5_carousel_button_size: 2.5rem; + --ui5_carousel_button_size: 2.25rem; --ui5_carousel_inactive_dot_size: 0.5rem; --ui5_carousel_inactive_dot_margin: 0 0.25rem; --ui5_carousel_inactive_dot_border: 1px solid var(--sapContent_ForegroundBorderColor); diff --git a/packages/main/test/pages/Carousel.html b/packages/main/test/pages/Carousel.html index 9bfb1a10a37e..04a9690082f7 100644 --- a/packages/main/test/pages/Carousel.html +++ b/packages/main/test/pages/Carousel.html @@ -10,9 +10,72 @@ - + + - + + + Template Based Segmentation + Segmentation Models + Marketing plans + + + + + + + + + + Template Based Segmentation + Segmentation Models + + + + + + + Template Based Segmentation + Segmentation Models + + + + + + + Template Based Segmentation + Segmentation Models + Marketing plans + + + + + + + + + + Template Based Segmentation + Segmentation Models + + + + + + + Template Based Segmentation + Segmentation Models + + + + @@ -23,9 +86,43 @@ + + + + + + Template Based Segmentation + Segmentation Models + + + + + + + Template Based Segmentation + Segmentation Models + + + + + + + + Template Based Segmentation + Segmentation Models + + + + + + + Template Based Segmentation + Segmentation Models + + - + @@ -35,7 +132,7 @@ - + Template Based Segmentation @@ -44,7 +141,7 @@ - @@ -57,7 +154,7 @@ - + @@ -67,7 +164,7 @@ - + Template Based Segmentation @@ -76,7 +173,7 @@ - @@ -89,7 +186,7 @@ - + @@ -99,7 +196,7 @@ - + Template Based Segmentation @@ -108,7 +205,7 @@ - + @@ -118,13 +215,29 @@ - + Template Based Segmentation Segmentation Models + + + + + Event Planning + Team Management + + + + + + + Documentation + Tutorials + + @@ -155,7 +268,7 @@ - + Template Based Segmentation @@ -164,7 +277,7 @@ - @@ -177,7 +290,7 @@ - + @@ -187,7 +300,7 @@ - + Template Based Segmentation @@ -196,7 +309,7 @@ - @@ -209,7 +322,7 @@ - + @@ -219,7 +332,7 @@ - + Template Based Segmentation @@ -548,7 +661,7 @@ Content Part - used to align the content items - @@ -560,7 +673,7 @@ - +