From 0018fe607442b1ff4c447422c7d84ce3412f74d0 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 13 Sep 2024 18:46:07 -0700 Subject: [PATCH 01/15] feat(carousel): Improve support for high item counts --- .../src/components/carousel/carousel.scss | 30 ++ .../src/components/carousel/carousel.tsx | 121 ++++++-- .../src/components/carousel/resources.ts | 3 + .../src/demos/carousel.html | 288 +++++++++++++++++- 4 files changed, 412 insertions(+), 30 deletions(-) diff --git a/packages/calcite-components/src/components/carousel/carousel.scss b/packages/calcite-components/src/components/carousel/carousel.scss index 0b5efba059f..ad7a4dc765e 100644 --- a/packages/calcite-components/src/components/carousel/carousel.scss +++ b/packages/calcite-components/src/components/carousel/carousel.scss @@ -219,6 +219,36 @@ calcite-carousel-item:not([selected]) { --calcite-color-foreground-3: var(--calcite-internal-internal-carousel-item-background-color-selected); color: var(--calcite-internal-internal-carousel-item-icon-color-selected); } + &.pagination-item--edge { + opacity: 0.5; + &:hover { + opacity: 1; + } + } +} + +.pagination-item--individual { + inline-size: 0; + padding: 0; + opacity: 0; + pointer-events: none; + visibility: hidden; + transition: + 0.15s ease-in-out inline-size, + 0.15s ease-in-out padding, + 0.25s ease-in-out opacity; + &.visible { + @apply w-8; + pointer-events: all; + visibility: visible; + opacity: 1; + } +} + +.pagination-item--range-edge calcite-icon { + scale: 0.75; + opacity: 0.75; + transition: 0.25s ease-in-out scale; } .container--overlaid .pagination-item { diff --git a/packages/calcite-components/src/components/carousel/carousel.tsx b/packages/calcite-components/src/components/carousel/carousel.tsx index c8d397278f0..da43c900bbe 100644 --- a/packages/calcite-components/src/components/carousel/carousel.tsx +++ b/packages/calcite-components/src/components/carousel/carousel.tsx @@ -38,6 +38,8 @@ import { T9nComponent, updateMessages, } from "../../utils/t9n"; +import { createObserver } from "../../utils/observers"; +import { breakpoints } from "../../utils/responsive"; import { getRoundRobinIndex } from "../../utils/array"; import { CSS, DURATION, ICONS } from "./resources"; import { CarouselMessages } from "./assets/carousel/t9n"; @@ -46,6 +48,15 @@ import { ArrowType, AutoplayType } from "./interfaces"; /** * @slot - A slot for adding `calcite-carousel-item`s. */ + +const maxItemBreakpoints = { + large: 11, + medium: 9, + small: 7, + xsmall: 5, + xxsmall: 3, +}; + @Component({ tag: "calcite-carousel", styleUrl: "carousel.scss", @@ -140,10 +151,12 @@ export class Carousel connectedCallback(): void { connectLocalized(this); connectMessages(this); + this.resizeObserver?.observe(this.el); } componentDidLoad(): void { setComponentLoaded(this); + this.setMaxItemsToBreakpoint(this.el.clientWidth); } componentDidRender(): void { @@ -154,6 +167,7 @@ export class Carousel disconnectLocalized(this); disconnectMessages(this); this.clearIntervals(); + this.resizeObserver?.disconnect(); } async componentWillLoad(): Promise { @@ -263,6 +277,8 @@ export class Carousel @State() slideDurationRemaining = 1; + @State() maxItems = maxItemBreakpoints.xxsmall; + private container: HTMLDivElement; private containerId = `calcite-carousel-container-${guid()}`; @@ -273,6 +289,10 @@ export class Carousel private tabList: HTMLDivElement; + private resizeObserver = createObserver("resize", (entries) => + entries.forEach(this.resizeHandler), + ); + // -------------------------------------------------------------------------- // // Events @@ -300,6 +320,26 @@ export class Carousel // // -------------------------------------------------------------------------- + private setMaxItemsToBreakpoint(width: number): void { + if (!breakpoints || !width) { + return; + } + + const breakpointKeys = ["medium", "small", "xsmall", "xxsmall"]; + for (const key of breakpointKeys) { + if (width >= breakpoints.width[key]) { + this.maxItems = maxItemBreakpoints[key]; + return; + } + } + + this.maxItems = maxItemBreakpoints.xxsmall; + } + + private resizeHandler = ({ contentRect: { width } }: ResizeObserverEntry): void => { + this.setMaxItemsToBreakpoint(width); + }; + private clearIntervals() { clearInterval(this.slideDurationInterval); clearInterval(this.slideInterval); @@ -379,11 +419,13 @@ export class Carousel const requestedSelectedIndex = activeItemIndex > -1 ? activeItemIndex : 0; this.items = items; + this.setSelectedItem(requestedSelectedIndex, false); }; private setSelectedItem = (requestedIndex: number, emit: boolean): void => { const previousSelected = this.selectedIndex; + this.items.forEach((el, index) => { const isMatch = requestedIndex === index; el.selected = isMatch; @@ -501,10 +543,12 @@ export class Carousel } break; case "ArrowRight": + event.preventDefault(); this.direction = "forward"; this.nextItem(true); break; case "ArrowLeft": + event.preventDefault(); this.direction = "backward"; this.previousItem(); break; @@ -609,31 +653,58 @@ export class Carousel ); - renderPaginationItems = (): VNode => ( -
- {this.items.map((item, index) => { - const isMatch = index === this.selectedIndex; - return ( - - ); - })} -
- ); + renderPaginationItems = (): VNode => { + const { selectedIndex, maxItems, items, label, handleItemSelection } = this; + // todo handle arrow navigation for non-visible pagination items when overflowing + // todo handle 3 example + return ( +
+ {items.map((item, index) => { + const isMatch = index === selectedIndex; + const isFirst = index === 0; + const isLast = index === items.length - 1; + const length = items.length; + const halfMaxItems = Math.floor(maxItems / 2); + const sliceLowBound = + selectedIndex < maxItems + ? 0 + : selectedIndex > length - maxItems + ? length - maxItems - 1 + : Math.max(0, selectedIndex - halfMaxItems); + const sliceHighBound = + selectedIndex < maxItems ? maxItems + 1 : Math.min(length, sliceLowBound + maxItems); + const icon = isMatch ? ICONS.active : ICONS.inactive; + const isEdge = + !isFirst && + !isLast && + !isMatch && + (index === sliceLowBound - 1 || index === sliceHighBound); + const isVisible = isMatch || (index <= sliceHighBound && index >= sliceLowBound - 1); + + return ( + + ); + })} +
+ ); + }; renderArrow = (direction: "previous" | "next"): VNode => { const isPrev = direction === "previous"; diff --git a/packages/calcite-components/src/components/carousel/resources.ts b/packages/calcite-components/src/components/carousel/resources.ts index 088997a1a50..b939cc4fdf4 100644 --- a/packages/calcite-components/src/components/carousel/resources.ts +++ b/packages/calcite-components/src/components/carousel/resources.ts @@ -11,7 +11,10 @@ export const CSS = { paginationItems: "pagination-items", paginationItem: "pagination-item", paginationItemIndividual: "pagination-item--individual", + paginationItemVisible: "pagination-item--visible", paginationItemSelected: "pagination-item--selected", + paginationItemEdge: "pagination-item--edge", + paginationItemRangeEdge: "pagination-item--range-edge", pageNext: "page-next", pagePrevious: "page-previous", autoplayControl: "autoplay-control", diff --git a/packages/calcite-components/src/demos/carousel.html b/packages/calcite-components/src/demos/carousel.html index 4fb500a67fc..060262e27f4 100644 --- a/packages/calcite-components/src/demos/carousel.html +++ b/packages/calcite-components/src/demos/carousel.html @@ -116,7 +116,13 @@

Interactive Demo

duration - + play() (if autoplay is present) @@ -156,8 +162,281 @@

Interactive Demo

An unbelievable new feature has arrived in this exciting product category. It's pretty neat.

+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+
+

Max items overlap prevention

+ + + + + +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + Another bit of content about this unbelievable item can go here on the second carousel item as + an example +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+
+
+
+ + + + + +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + Another bit of content about this unbelievable item can go here on the second carousel item as + an example +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+
+
+
+
+
+

Themed

@@ -286,7 +565,7 @@

autoplay

Launch onboarding - +

@@ -386,7 +665,7 @@

autoplay

Launch onboarding - +

@@ -462,8 +741,7 @@

autoplay

carousel.controlOverlay = event.target.checked; }); - durationInput.addEventListener("calciteInputInput", function () { - console.log(event.target.value); + durationInput.addEventListener("calciteInputNumberInput", function () { carousel.autoplayDuration = event.target.value; }); From b9b6a7dc1d18a07816096a9b8f564803850de19c Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 14 Sep 2024 10:21:53 -0700 Subject: [PATCH 02/15] WIP --- .../src/components/carousel/carousel.e2e.ts | 74 +++++++ .../src/components/carousel/carousel.scss | 4 +- .../components/carousel/carousel.stories.ts | 194 ++++++++++++++++++ .../src/components/carousel/carousel.tsx | 18 +- 4 files changed, 279 insertions(+), 11 deletions(-) diff --git a/packages/calcite-components/src/components/carousel/carousel.e2e.ts b/packages/calcite-components/src/components/carousel/carousel.e2e.ts index 64867a6d3df..8c51fd2a98b 100644 --- a/packages/calcite-components/src/components/carousel/carousel.e2e.ts +++ b/packages/calcite-components/src/components/carousel/carousel.e2e.ts @@ -1055,3 +1055,77 @@ describe("calcite-carousel", () => { expect(animationEndSpy).toHaveReceivedEventTimes(8); }); }); +describe("handles overflow of pagination items", () => { + it("correctly limits the number of slide pagination items shown when overflowing", async () => { + const page = await newE2EPage(); + await page.setContent( + html` +

first

+

second

+

third

+

fourth

+

fifth

+

sixth

+

seventh

+

eighth

+
`, + ); + + const items = await page.findAll(`calcite-carousel >>> .${CSS.paginationItemVisible}`); + expect(items).toHaveLength(3); + }); + it("correctly limits the number of slide pagination items shown when overflowing selected middle", async () => { + const page = await newE2EPage(); + await page.setContent( + html` +

first

+

second

+

third

+

fourth

+

fifth

+

sixth

+

seventh

+

eighth

+
`, + ); + + const items = await page.findAll(`calcite-carousel >>> .${CSS.paginationItemVisible}`); + expect(items).toHaveLength(5); + }); + it("correctly limits the number of slide pagination items shown when overflowing selected last", async () => { + const page = await newE2EPage(); + await page.setContent( + html` +

first

+

second

+

third

+

fourth

+

fifth

+

sixth

+

seventh

+

eighth

+
`, + ); + + const items = await page.findAll(`calcite-carousel >>> .${CSS.paginationItemVisible}`); + expect(items).toHaveLength(3); + }); + it("correctly adjusts as the container or carousel width changes", async () => { + const page = await newE2EPage(); + await page.setContent( + html` +

first

+

second

+

third

+

fourth

+

fifth

+

sixth

+

seventh

+

eighth

+
`, + ); + + const items = await page.findAll(`calcite-carousel >>> .${CSS.paginationItemVisible}`); + expect(items).toHaveLength(3); + }); +}); diff --git a/packages/calcite-components/src/components/carousel/carousel.scss b/packages/calcite-components/src/components/carousel/carousel.scss index ad7a4dc765e..fd80eef6d8d 100644 --- a/packages/calcite-components/src/components/carousel/carousel.scss +++ b/packages/calcite-components/src/components/carousel/carousel.scss @@ -236,7 +236,7 @@ calcite-carousel-item:not([selected]) { transition: 0.15s ease-in-out inline-size, 0.15s ease-in-out padding, - 0.25s ease-in-out opacity; + 0.3s ease-in-out opacity; &.visible { @apply w-8; pointer-events: all; @@ -248,7 +248,7 @@ calcite-carousel-item:not([selected]) { .pagination-item--range-edge calcite-icon { scale: 0.75; opacity: 0.75; - transition: 0.25s ease-in-out scale; + transition: 0.3s ease-in-out scale; } .container--overlaid .pagination-item { diff --git a/packages/calcite-components/src/components/carousel/carousel.stories.ts b/packages/calcite-components/src/components/carousel/carousel.stories.ts index a6812b7f86b..7fad5781b7c 100644 --- a/packages/calcite-components/src/components/carousel/carousel.stories.ts +++ b/packages/calcite-components/src/components/carousel/carousel.stories.ts @@ -76,6 +76,200 @@ export const simple = (args: CarouselStoryArgs): string =>
`; +export const simpleManyItems = (args: CarouselStoryArgs): string => + html`
+ + + + Some kind of carousel item content + In this case, in a card + + + + + + Some kind of carousel item content + In this case, in a card + + + + + + Some kind of carousel item content + In this case, in a card + + + + + + Some kind of carousel item content + In this case, in a card + + + + + + Some kind of carousel item content + In this case, in a card + + + + + + Some kind of carousel item content + In this case, in a card + + + + + + Some kind of carousel item content + In this case, in a card + + + + + + Some kind of carousel item content + In this case, in a card + + + + + + Some kind of carousel item content + In this case, in a card + + + + +
`; + +export const simpleManyItemsResizable = (args: CarouselStoryArgs): string => + html` + + + +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + Another bit of content about this unbelievable item can go here on the second carousel item as an example +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+ +

+ + An unbelievable new feature has arrived in this exciting product category. It's pretty neat. +

+
+
+
+
+
`; + export const carouselAutoplayFullImageWithOverlayAndEdge = (): string => html`