From ca1ac111bcfa5bd45826fc5894cf542a1b118213 Mon Sep 17 00:00:00 2001 From: Ignacio Becerra Date: Fri, 12 Feb 2021 08:31:00 -0800 Subject: [PATCH] fix(carousel): improved tab page navigation (#5035) ### Related Ticket(s) #4799 ### Description Carousel now moves pages automatically when using tab navigation and focusing on the next card in a different page. Works going forward or backwards. ### Changelog **New** - pages now change dynamically depending on the focused card index --- .../src/components/carousel/carousel.ts | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/web-components/src/components/carousel/carousel.ts b/packages/web-components/src/components/carousel/carousel.ts index 5c22683e645..7376cc5ec11 100644 --- a/packages/web-components/src/components/carousel/carousel.ts +++ b/packages/web-components/src/components/carousel/carousel.ts @@ -13,6 +13,9 @@ import ddsSettings from '@carbon/ibmdotcom-utilities/es/utilities/settings/setti import ifNonNull from 'carbon-web-components/es/globals/directives/if-non-null.js'; import CaretLeft20 from 'carbon-web-components/es/icons/caret--left/20.js'; import CaretRight20 from 'carbon-web-components/es/icons/caret--right/20.js'; +import HostListener from 'carbon-web-components/es/globals/decorators/host-listener'; +import HostListenerMixin from 'carbon-web-components/es/globals/mixins/host-listener'; +import DDSCard from '../card/card'; import styles from './carousel.scss'; const { prefix } = settings; @@ -26,7 +29,7 @@ const { stablePrefix: ddsPrefix } = ddsSettings; * @csspart next-button The button to go to the next page. */ @customElement(`${ddsPrefix}-carousel`) -class DDSCarousel extends LitElement { +class DDSCarousel extends HostListenerMixin(LitElement) { /** * The scrolling contents node. */ @@ -111,6 +114,55 @@ class DDSCarousel extends LitElement { } } + /** + * Stops the container from scrolling when focusing on a card outside of the viewport. + * + * @param event The event. + */ + // eslint-disable-next-line class-methods-use-this + private _handleScrollFocus({ target }: Event) { + (target as HTMLElement).scrollTo(0, 0); + } + + /** + * Handles card focus throughout pages. + * + * @param event The event. + */ + @HostListener('shadowRoot:focusin') + // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to + private _handleFocus = async ({ target, relatedTarget }: FocusEvent) => { + const currentContains = target !== this && this.contains(target as DDSCard); + const oldNotContains = target !== this && !this.contains(relatedTarget as DDSCard); + const currentCardIndex = Array.from(this.children).indexOf(target as HTMLElement); + + // Confirmed by design team; ensures the carousel remains on the current page when focusing back on the component. + // This conforms to the natural flow of the current content on the screen. + // The first card on the page should be focused if tabbing from the top, + // The last card on the page should be focused if tabbing from the bottom. + if (currentContains && oldNotContains) { + // focus coming from the top + if (currentCardIndex === 0) { + (this.children[this.start] as HTMLElement).focus(); + + // focus coming from bottom + } else if (currentCardIndex === this._total - 1) { + (this.children[this.start + this.pageSize - 1] as HTMLElement).focus(); + } + return; + } + + // Calculates proper page to display if focus is outside the current page + if (currentContains && (currentCardIndex < this.start || currentCardIndex >= this.start + this.pageSize)) { + // The `currentIndex` floored by `pageSize` + const nextStart = Math.floor(currentCardIndex / this.pageSize) * this.pageSize; + const pageOffset = this.start % this.pageSize; + + // Ensures the page moves by `this.pageSize` in either direction + this.start = nextStart + pageOffset; + } + }; + /** * Handles `click` event on the next button. */ @@ -236,6 +288,7 @@ class DDSCarousel extends LitElement { _total: total, _handleClickNextButton: handleClickNextButton, _handleClickPrevButton: handleClickPrevButton, + _handleScrollFocus: handleScrollFocus, _handleSlotChange: handleSlotChange, } = this; // Copes with the condition where `start % pageSize` is non-zero @@ -245,6 +298,7 @@ class DDSCarousel extends LitElement { return html`