From ba75978dc2212a0713c967bd526f8f12e9e40ced Mon Sep 17 00:00:00 2001 From: Diljohn Singh Date: Wed, 3 Jun 2026 10:30:38 +0200 Subject: [PATCH 1/5] feat: adding slides configuration --- .../carousel-web/src/Carousel.tsx | 39 ++++++++++++++----- .../carousel-web/src/Carousel.xml | 8 ++++ .../carousel-web/src/components/Carousel.tsx | 23 +++++++++-- .../components/__tests__/Carousel.spec.tsx | 7 ++++ .../carousel-web/typings/CarouselProps.d.ts | 4 ++ 5 files changed, 68 insertions(+), 13 deletions(-) diff --git a/packages/pluggableWidgets/carousel-web/src/Carousel.tsx b/packages/pluggableWidgets/carousel-web/src/Carousel.tsx index 8bfa4727fd..192fecd8d8 100644 --- a/packages/pluggableWidgets/carousel-web/src/Carousel.tsx +++ b/packages/pluggableWidgets/carousel-web/src/Carousel.tsx @@ -1,18 +1,40 @@ import classNames from "classnames"; import { GUID, ObjectItem, ValueStatus } from "mendix"; -import { ReactNode, useCallback, useId } from "react"; +import { ReactNode, useCallback, useId, useMemo } from "react"; import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; import { CarouselContainerProps } from "../typings/CarouselProps"; import { Carousel as CarouselComponent } from "./components/Carousel"; -import "./ui/Carousel.scss"; + import loadingCircleSvg from "./ui/loading-circle.svg"; +import "./ui/Carousel.scss"; + export function Carousel(props: CarouselContainerProps): ReactNode { - const { showPagination, loop, tabIndex, navigation, animation, delay, autoplay } = props; + const { + showPagination, + loop, + tabIndex, + navigation, + animation, + delay, + autoplay, + slidesPerView, + slidesPerGroup, + dataSource, + content + } = props; const onClick = useCallback(() => executeAction(props.onClickAction), [props.onClickAction]); const id = useId(); + const carouselItems = useMemo( + () => + dataSource?.items?.map((item: ObjectItem) => ({ + id: item.id as GUID, + content: content?.get(item) + })) ?? [], + [dataSource] + ); - if (props.dataSource?.status !== ValueStatus.Available) { + if (dataSource?.status !== ValueStatus.Available) { return (
@@ -27,16 +49,13 @@ export function Carousel(props: CarouselContainerProps): ReactNode { tabIndex={tabIndex} pagination={showPagination} loop={loop} + slidesPerView={slidesPerView} + slidesPerGroup={slidesPerGroup} animation={animation} autoplay={autoplay} delay={delay} navigation={navigation} - items={ - props.dataSource?.items?.map((item: ObjectItem) => ({ - id: item.id as GUID, - content: props.content?.get(item) - })) ?? [] - } + items={carouselItems} onClick={onClick} /> ); diff --git a/packages/pluggableWidgets/carousel-web/src/Carousel.xml b/packages/pluggableWidgets/carousel-web/src/Carousel.xml index 2db73936e0..a7b5afc6c5 100644 --- a/packages/pluggableWidgets/carousel-web/src/Carousel.xml +++ b/packages/pluggableWidgets/carousel-web/src/Carousel.xml @@ -37,6 +37,14 @@ Infinite loop + + Slides per view + + + + Slides per group + + Animation diff --git a/packages/pluggableWidgets/carousel-web/src/components/Carousel.tsx b/packages/pluggableWidgets/carousel-web/src/components/Carousel.tsx index fd0dfa216b..04b3349985 100644 --- a/packages/pluggableWidgets/carousel-web/src/components/Carousel.tsx +++ b/packages/pluggableWidgets/carousel-web/src/components/Carousel.tsx @@ -19,6 +19,8 @@ export interface CarouselProps { animation?: boolean; autoplay?: boolean; delay?: number; + slidesPerView?: number; + slidesPerGroup?: number; navigation: boolean; className: string; tabIndex?: number | undefined; @@ -27,7 +29,21 @@ export interface CarouselProps { } export function Carousel(props: CarouselProps): ReactElement { - const { items, pagination, loop, animation, autoplay, delay, navigation, className, tabIndex, id, onClick } = props; + const { + items, + pagination, + loop, + animation, + autoplay, + delay, + slidesPerView, + slidesPerGroup, + navigation, + className, + tabIndex, + id, + onClick + } = props; const [activeIndex, setActiveIndex] = useState(0); const getSlideId = useCallback( @@ -47,7 +63,8 @@ export function Carousel(props: CarouselProps): ReactElement { }; const options: SwiperOptions = { - slidesPerView: 1, + slidesPerView, + slidesPerGroup, centeredSlides: true, loop, navigation, @@ -76,9 +93,9 @@ export function Carousel(props: CarouselProps): ReactElement { {items?.map((item, index) => ( diff --git a/packages/pluggableWidgets/carousel-web/src/components/__tests__/Carousel.spec.tsx b/packages/pluggableWidgets/carousel-web/src/components/__tests__/Carousel.spec.tsx index 2510abe118..22ad7fa521 100644 --- a/packages/pluggableWidgets/carousel-web/src/components/__tests__/Carousel.spec.tsx +++ b/packages/pluggableWidgets/carousel-web/src/components/__tests__/Carousel.spec.tsx @@ -156,6 +156,7 @@ describe("Carousel", () => { jest.resetAllMocks(); jest.spyOn(Math, "random").mockReturnValue(0.123456789); }); + const defaultCarouselProps: CarouselProps = { id: "Carousel", className: "", @@ -163,6 +164,8 @@ describe("Carousel", () => { { id: "1" as GUID, content:
test1
}, { id: "2" as GUID, content:
test2
} ], + slidesPerView: 1, + slidesPerGroup: 1, pagination: true, animation: true, autoplay: true, @@ -177,21 +180,25 @@ describe("Carousel", () => { expect(asFragment()).toMatchSnapshot(); }); + it("renders correctly without pagination", () => { const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); + it("renders correctly without navigation", () => { const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); + it("renders correctly with minimal setup", () => { const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); + afterEach(() => { jest.restoreAllMocks(); }); diff --git a/packages/pluggableWidgets/carousel-web/typings/CarouselProps.d.ts b/packages/pluggableWidgets/carousel-web/typings/CarouselProps.d.ts index 4eee9f45dd..91e9d19bec 100644 --- a/packages/pluggableWidgets/carousel-web/typings/CarouselProps.d.ts +++ b/packages/pluggableWidgets/carousel-web/typings/CarouselProps.d.ts @@ -18,6 +18,8 @@ export interface CarouselContainerProps { autoplay: boolean; delay: number; loop: boolean; + slidesPerView: number; + slidesPerGroup: number; animation: boolean; onClickAction?: ActionValue; } @@ -40,6 +42,8 @@ export interface CarouselPreviewProps { autoplay: boolean; delay: number | null; loop: boolean; + slidesPerView: number | null; + slidesPerGroup: number | null; animation: boolean; onClickAction: {} | null; } From d01d1681540f5e38bea3cc0dec63018084ca542f Mon Sep 17 00:00:00 2001 From: gjulivan Date: Wed, 9 Sep 2026 10:45:58 +0200 Subject: [PATCH 2/5] feat: update carousel dependency --- .../pluggableWidgets/carousel-web/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/pluggableWidgets/carousel-web/package.json b/packages/pluggableWidgets/carousel-web/package.json index 6eb15cf4d8..2fe843ac8e 100644 --- a/packages/pluggableWidgets/carousel-web/package.json +++ b/packages/pluggableWidgets/carousel-web/package.json @@ -44,7 +44,7 @@ "dependencies": { "@mendix/widget-plugin-component-kit": "workspace:*", "classnames": "^2.5.1", - "swiper": "^12.1.2" + "swiper": "^14.2.0" }, "devDependencies": { "@mendix/automation-utils": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e0aa96d4f..ef8529d353 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -729,8 +729,8 @@ importers: specifier: ^2.5.1 version: 2.5.1 swiper: - specifier: ^12.1.2 - version: 12.2.0 + specifier: ^14.2.0 + version: 14.2.0 devDependencies: '@mendix/automation-utils': specifier: workspace:* @@ -1848,8 +1848,8 @@ importers: specifier: workspace:* version: link:../../shared/eslint-config-web-widgets '@mendix/pluggable-widgets-tools': - specifier: 11.13.0 - version: 11.13.0(patch_hash=1879adf9f5f058d67d2e08e79916d5adce82416899758529f812f4207c064fed)(@jest/transform@30.3.0)(@jest/types@30.4.1)(@types/babel__core@7.20.5)(@types/node@24.12.4)(canvas@3.2.3)(eslint@9.39.5(jiti@2.6.1))(jest-util@30.4.1)(prettier@3.9.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.8.1) + specifier: 11.12.1 + version: 11.12.1(patch_hash=9081455b6de1f5a4af4d792640f6266f917f9ced2d61aae16e64a379ad11faa2)(@jest/transform@30.3.0)(@jest/types@30.4.1)(@types/babel__core@7.20.5)(@types/node@24.12.4)(canvas@3.2.3)(eslint@9.39.5(jiti@2.6.1))(jest-util@30.4.1)(prettier@3.9.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.8.1) '@mendix/prettier-config-web-widgets': specifier: workspace:* version: link:../../shared/prettier-config-web-widgets @@ -10549,8 +10549,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - swiper@12.2.0: - resolution: {integrity: sha512-K8uXsBZU6ME97Ia3xbBge8IRCnR1lOmIILzvY/jGVic7dSTQ530s3uO8RvXbPUtkkXLWIwmZLRPbtDxRWVAFdg==} + swiper@14.2.0: + resolution: {integrity: sha512-GsL4M9Fq7Sg22OyL3SjdNj/0bxTfIxPJJHHW394Li0u5vYtExi0a09iBUc8CuLafJi5pAlH2AHUe1PocxHl45g==} engines: {node: '>= 4.7.0'} symbol-tree@3.2.4: @@ -20089,7 +20089,7 @@ snapshots: sax: 1.6.1 stable: 0.1.8 - swiper@12.2.0: {} + swiper@14.2.0: {} symbol-tree@3.2.4: {} From 46da8af517ff2996ef881ade886145d2a778ad21 Mon Sep 17 00:00:00 2001 From: gjulivan Date: Wed, 9 Sep 2026 16:23:53 +0200 Subject: [PATCH 3/5] fix: update carousel design preview --- .../src/Carousel.editorPreview.tsx | 18 +++++++++++------ .../carousel-web/src/ui/Carousel.scss | 4 ++++ .../carousel-web/src/ui/CarouselPreview.scss | 20 +++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 packages/pluggableWidgets/carousel-web/src/ui/CarouselPreview.scss diff --git a/packages/pluggableWidgets/carousel-web/src/Carousel.editorPreview.tsx b/packages/pluggableWidgets/carousel-web/src/Carousel.editorPreview.tsx index 4cd227e0b8..539df30f95 100644 --- a/packages/pluggableWidgets/carousel-web/src/Carousel.editorPreview.tsx +++ b/packages/pluggableWidgets/carousel-web/src/Carousel.editorPreview.tsx @@ -1,3 +1,4 @@ +import classNames from "classnames"; import { GUID } from "mendix"; import { ReactElement } from "react"; import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; @@ -5,23 +6,28 @@ import { CarouselPreviewProps } from "../typings/CarouselProps"; import { Carousel } from "./components/Carousel"; export function getPreviewCss(): string { - return require("./ui/Carousel.scss"); + return require("./ui/CarouselPreview.scss"); } export function CarouselPreviewComponent(props: CarouselPreviewProps): ReactElement { + const hasDataSource = props.dataSource != null; return ( ({ + items={["1", "2"].map(item => ({ id: item as GUID, - content: ( - -
{`Carousel item content ${item}`}
+ content: hasDataSource ? ( + +
+ ) : ( +
+
{`[No datasource selected]`}
+
) }))} /> diff --git a/packages/pluggableWidgets/carousel-web/src/ui/Carousel.scss b/packages/pluggableWidgets/carousel-web/src/ui/Carousel.scss index 7d70b087d5..bd06bea377 100644 --- a/packages/pluggableWidgets/carousel-web/src/ui/Carousel.scss +++ b/packages/pluggableWidgets/carousel-web/src/ui/Carousel.scss @@ -64,3 +64,7 @@ $focus-outline-offset: 2px !default; transform: rotate(360deg); } } + +ul.swiper-wrapper { + padding-inline-start: 0; +} diff --git a/packages/pluggableWidgets/carousel-web/src/ui/CarouselPreview.scss b/packages/pluggableWidgets/carousel-web/src/ui/CarouselPreview.scss new file mode 100644 index 0000000000..5e119f8504 --- /dev/null +++ b/packages/pluggableWidgets/carousel-web/src/ui/CarouselPreview.scss @@ -0,0 +1,20 @@ +.widget-carousel-editor-preview { + .swiper-slide { + justify-content: flex-start; + min-height: 100px; + align-content: center; + align-items: center; + + .carousel-item-content { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + } + } + + ul.swiper-wrapper { + padding-inline-start: 0; + } +} From 28b6d8f83843129bbd2aa496372da9910dbb9ade Mon Sep 17 00:00:00 2001 From: gjulivan Date: Wed, 9 Sep 2026 16:52:22 +0200 Subject: [PATCH 4/5] feat(carousel): update changelog and lock file --- packages/pluggableWidgets/carousel-web/CHANGELOG.md | 13 +++++++++++++ pnpm-lock.yaml | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/pluggableWidgets/carousel-web/CHANGELOG.md b/packages/pluggableWidgets/carousel-web/CHANGELOG.md index 0a8ca95d53..e13ffa55bf 100644 --- a/packages/pluggableWidgets/carousel-web/CHANGELOG.md +++ b/packages/pluggableWidgets/carousel-web/CHANGELOG.md @@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Changed + +- We updated swiper library dependency to v14.2 + +### Added + +- We added the option to configure number of slides per view (slides visible at the same time on slider's container) - contribution of @DiljohnSingh +- We added the option to configure the numbers of slides for group sliding. Useful to use with slidesPerView > 1 - contribution of @DiljohnSingh. + +### Fixed + +- We fixed design preview not showing proper editable content height for widget dropzone area. + ## [2.3.2] - 2026-04-13 ### Changed diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef8529d353..79139727c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -401,7 +401,7 @@ importers: version: link:../../shared/eslint-config-web-widgets '@mendix/pluggable-widgets-tools': specifier: 11.13.0 - version: 11.13.0(patch_hash=1879adf9f5f058d67d2e08e79916d5adce82416899758529f812f4207c064fed)(@jest/transform@30.3.0)(@jest/types@30.4.1)(@types/babel__core@7.20.5)(@types/node@24.12.4)(canvas@3.2.3)(eslint@9.39.5(jiti@2.6.1))(jest-util@30.4.1)(prettier@3.9.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.8.1) + version: 11.13.0(patch_hash=1879adf9f5f058d67d2e08e79916d5adce82416899758529f812f4207c064fed)(@jest/transform@30.3.0)(@jest/types@30.4.1)(@types/babel__core@7.20.5)(@types/node@24.12.4)(canvas@3.2.3)(eslint@9.39.5(jiti@2.6.1))(jest-util@30.4.1)(picomatch@4.0.5)(prettier@3.9.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.8.1) '@mendix/prettier-config-web-widgets': specifier: workspace:* version: link:../../shared/prettier-config-web-widgets @@ -1848,8 +1848,8 @@ importers: specifier: workspace:* version: link:../../shared/eslint-config-web-widgets '@mendix/pluggable-widgets-tools': - specifier: 11.12.1 - version: 11.12.1(patch_hash=9081455b6de1f5a4af4d792640f6266f917f9ced2d61aae16e64a379ad11faa2)(@jest/transform@30.3.0)(@jest/types@30.4.1)(@types/babel__core@7.20.5)(@types/node@24.12.4)(canvas@3.2.3)(eslint@9.39.5(jiti@2.6.1))(jest-util@30.4.1)(prettier@3.9.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.8.1) + specifier: 11.13.0 + version: 11.13.0(patch_hash=1879adf9f5f058d67d2e08e79916d5adce82416899758529f812f4207c064fed)(@jest/transform@30.3.0)(@jest/types@30.4.1)(@types/babel__core@7.20.5)(@types/node@24.12.4)(canvas@3.2.3)(eslint@9.39.5(jiti@2.6.1))(jest-util@30.4.1)(prettier@3.9.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.8.1) '@mendix/prettier-config-web-widgets': specifier: workspace:* version: link:../../shared/prettier-config-web-widgets From d377845ab7ca472c1baee47457b85e0e14cd3d5d Mon Sep 17 00:00:00 2001 From: gjulivan Date: Tue, 29 Sep 2026 14:24:03 +0200 Subject: [PATCH 5/5] chore: properly display slidesPerView and slidesPerGroup --- .../carousel-web/CHANGELOG.md | 1 + .../carousel-web/src/Carousel.editorConfig.ts | 3 + .../src/Carousel.editorPreview.tsx | 59 ++++++++++------- .../__tests__/Carousel.editorPreview.spec.tsx | 52 +++++++++++++++ .../carousel-web/src/components/Carousel.tsx | 24 ++++--- .../components/__tests__/Carousel.spec.tsx | 15 +++++ .../carousel-web/src/ui/CarouselPreview.scss | 65 +++++++++++++++++-- 7 files changed, 183 insertions(+), 36 deletions(-) create mode 100644 packages/pluggableWidgets/carousel-web/src/__tests__/Carousel.editorPreview.spec.tsx diff --git a/packages/pluggableWidgets/carousel-web/CHANGELOG.md b/packages/pluggableWidgets/carousel-web/CHANGELOG.md index e13ffa55bf..e783f78500 100644 --- a/packages/pluggableWidgets/carousel-web/CHANGELOG.md +++ b/packages/pluggableWidgets/carousel-web/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We added the option to configure number of slides per view (slides visible at the same time on slider's container) - contribution of @DiljohnSingh - We added the option to configure the numbers of slides for group sliding. Useful to use with slidesPerView > 1 - contribution of @DiljohnSingh. +- When more than one slide is shown or grouped, slides now align to the start of the carousel and the fade animation is turned off, because it only supports one slide at a time. ### Fixed diff --git a/packages/pluggableWidgets/carousel-web/src/Carousel.editorConfig.ts b/packages/pluggableWidgets/carousel-web/src/Carousel.editorConfig.ts index 6bddd851b8..8ddf0f0602 100644 --- a/packages/pluggableWidgets/carousel-web/src/Carousel.editorConfig.ts +++ b/packages/pluggableWidgets/carousel-web/src/Carousel.editorConfig.ts @@ -21,6 +21,9 @@ export function getProperties( if (!values.autoplay) { hidePropertiesIn(defaultProperties, values, ["delay"]); } + if ((values.slidesPerView ?? 1) > 1 || (values.slidesPerGroup ?? 1) > 1) { + hidePropertiesIn(defaultProperties, values, ["animation"]); + } if (platform === "web") { transformGroupsIntoTabs(defaultProperties); } diff --git a/packages/pluggableWidgets/carousel-web/src/Carousel.editorPreview.tsx b/packages/pluggableWidgets/carousel-web/src/Carousel.editorPreview.tsx index 539df30f95..3ebd58881c 100644 --- a/packages/pluggableWidgets/carousel-web/src/Carousel.editorPreview.tsx +++ b/packages/pluggableWidgets/carousel-web/src/Carousel.editorPreview.tsx @@ -1,36 +1,49 @@ import classNames from "classnames"; -import { GUID } from "mendix"; -import { ReactElement } from "react"; -import { generateUUID } from "@mendix/widget-plugin-platform/framework/generate-uuid"; +import { CSSProperties, ReactElement } from "react"; import { CarouselPreviewProps } from "../typings/CarouselProps"; -import { Carousel } from "./components/Carousel"; export function getPreviewCss(): string { return require("./ui/CarouselPreview.scss"); } +// Static preview: Swiper needs a live DOM to initialize, which the Studio Pro page editor does not provide. export function CarouselPreviewComponent(props: CarouselPreviewProps): ReactElement { const hasDataSource = props.dataSource != null; + const slidesPerView = Math.max(1, props.slidesPerView ?? 1); + const slides = Array.from({ length: slidesPerView }, (_, index) => index); + return ( - ({ - id: item as GUID, - content: hasDataSource ? ( - -
- - ) : ( -
-
{`[No datasource selected]`}
-
- ) - }))} - /> +
+
+ {props.navigation && } +
    + {slides.map(index => ( +
  • + {hasDataSource ? ( + +
    + + ) : ( +
    +
    [No datasource selected]
    +
    + )} +
  • + ))} +
+ {props.navigation && } +
+ {props.showPagination && ( +
+ + + +
+ )} +
); } diff --git a/packages/pluggableWidgets/carousel-web/src/__tests__/Carousel.editorPreview.spec.tsx b/packages/pluggableWidgets/carousel-web/src/__tests__/Carousel.editorPreview.spec.tsx new file mode 100644 index 0000000000..23546ef1a6 --- /dev/null +++ b/packages/pluggableWidgets/carousel-web/src/__tests__/Carousel.editorPreview.spec.tsx @@ -0,0 +1,52 @@ +import { render } from "@testing-library/react"; +import { ReactNode } from "react"; +import { CarouselPreviewProps } from "../../typings/CarouselProps"; +import { CarouselPreviewComponent } from "../Carousel.editorPreview"; + +const renderer = ({ children }: { children: ReactNode }): ReactNode => children; + +const defaultProps = { + className: "", + dataSource: {}, + content: { renderer }, + showPagination: true, + navigation: true, + autoplay: false, + delay: 1000, + loop: true, + slidesPerView: 1, + slidesPerGroup: 1, + animation: true, + onClickAction: null +} as unknown as CarouselPreviewProps; + +describe("Carousel preview", () => { + it.each([1, 3, 10])("renders %i slides for slidesPerView %i", slidesPerView => { + const { container } = render(); + + expect(container.querySelectorAll(".widget-carousel-preview-slide")).toHaveLength(slidesPerView); + }); + + it("falls back to a single slide when slidesPerView is empty or invalid", () => { + const { container, rerender } = render(); + expect(container.querySelectorAll(".widget-carousel-preview-slide")).toHaveLength(1); + + rerender(); + expect(container.querySelectorAll(".widget-carousel-preview-slide")).toHaveLength(1); + }); + + it("shows a placeholder when no datasource is selected", () => { + const { getAllByText } = render(); + + expect(getAllByText("[No datasource selected]")).toHaveLength(1); + }); + + it("hides navigation and pagination when disabled", () => { + const { container } = render( + + ); + + expect(container.querySelector(".widget-carousel-preview-nav")).toBeNull(); + expect(container.querySelector(".widget-carousel-preview-pagination")).toBeNull(); + }); +}); diff --git a/packages/pluggableWidgets/carousel-web/src/components/Carousel.tsx b/packages/pluggableWidgets/carousel-web/src/components/Carousel.tsx index 04b3349985..69c549f92f 100644 --- a/packages/pluggableWidgets/carousel-web/src/components/Carousel.tsx +++ b/packages/pluggableWidgets/carousel-web/src/components/Carousel.tsx @@ -62,18 +62,24 @@ export function Carousel(props: CarouselProps): ReactElement { )}" aria-label="Go to slide ${index}" class="${className}">` }; + const perView = Math.max(1, slidesPerView ?? 1); + const perGroup = Math.max(1, slidesPerGroup ?? 1); + // Swiper's fade effect forces a single slide per view/group, so only use it for single-slide carousels. + const isSingleSlide = perView === 1 && perGroup === 1; + const options: SwiperOptions = { - slidesPerView, - slidesPerGroup, - centeredSlides: true, + slidesPerView: perView, + slidesPerGroup: perGroup, + centeredSlides: isSingleSlide, loop, navigation, autoplay: autoplay ? { delay, stopOnLastSlide: true } : false, pagination: paginationOptions, - ...(animation && { - effect: "fade", - fadeEffect: { crossFade: true } - }), + ...(animation && + isSingleSlide && { + effect: "fade", + fadeEffect: { crossFade: true } + }), modules: [A11y, Navigation, Pagination, EffectFade, Autoplay, Keyboard], a11y: { enabled: true, @@ -84,6 +90,8 @@ export function Carousel(props: CarouselProps): ReactElement { } }; + const isSlideVisible = (index: number): boolean => (index - activeIndex + items.length) % items.length < perView; + const updateSwiperIndex = useCallback((swiper: SwiperClass) => { setActiveIndex(swiper.realIndex); }, []); @@ -98,7 +106,7 @@ export function Carousel(props: CarouselProps): ReactElement { {...options} > {items?.map((item, index) => ( - + {item.content} ))} diff --git a/packages/pluggableWidgets/carousel-web/src/components/__tests__/Carousel.spec.tsx b/packages/pluggableWidgets/carousel-web/src/components/__tests__/Carousel.spec.tsx index 22ad7fa521..1145d31e89 100644 --- a/packages/pluggableWidgets/carousel-web/src/components/__tests__/Carousel.spec.tsx +++ b/packages/pluggableWidgets/carousel-web/src/components/__tests__/Carousel.spec.tsx @@ -199,6 +199,21 @@ describe("Carousel", () => { expect(asFragment()).toMatchSnapshot(); }); + it("uses fade effect for a single slide per view", () => { + const { container } = render(); + + expect(container.querySelector(".swiper")).toHaveClass("swiper-fade"); + }); + + it("ignores fade animation when showing multiple slides per view", () => { + const items = [1, 2, 3, 4].map(i => ({ id: `${i}` as GUID, content:
test{i}
})); + const { container } = render( + + ); + + expect(container.querySelector(".swiper")).not.toHaveClass("swiper-fade"); + }); + afterEach(() => { jest.restoreAllMocks(); }); diff --git a/packages/pluggableWidgets/carousel-web/src/ui/CarouselPreview.scss b/packages/pluggableWidgets/carousel-web/src/ui/CarouselPreview.scss index 5e119f8504..9eed347d53 100644 --- a/packages/pluggableWidgets/carousel-web/src/ui/CarouselPreview.scss +++ b/packages/pluggableWidgets/carousel-web/src/ui/CarouselPreview.scss @@ -1,9 +1,42 @@ +$gray-200: #e3e3e5 !default; +$gray-500: #8e8e93 !default; +$spacing-small: 8px !default; + .widget-carousel-editor-preview { - .swiper-slide { - justify-content: flex-start; + --carousel-preview-slides-per-view: 1; + width: 100%; + + .widget-carousel-preview-viewport { + display: flex; + align-items: center; + gap: var(--spacing-small, $spacing-small); + } + + .widget-carousel-preview-slides { + display: flex; + flex: 1 1 auto; + min-width: 0; + gap: var(--spacing-small, $spacing-small); + margin: 0; + padding-inline-start: 0; + list-style: none; + } + + .widget-carousel-preview-slide { + display: flex; + flex: 0 0 + calc( + (100% - (var(--carousel-preview-slides-per-view) - 1) * var(--spacing-small, #{$spacing-small})) / + var(--carousel-preview-slides-per-view) + ); + min-width: 0; min-height: 100px; - align-content: center; align-items: center; + justify-content: center; + + > * { + width: 100%; + } .carousel-item-content { display: flex; @@ -14,7 +47,29 @@ } } - ul.swiper-wrapper { - padding-inline-start: 0; + .widget-carousel-preview-nav { + flex: 0 0 auto; + width: 30px; + height: 30px; + border-radius: 50%; + background-color: var(--gray-200, $gray-200); + } + + .widget-carousel-preview-pagination { + display: flex; + justify-content: center; + gap: 6px; + padding-top: var(--spacing-small, $spacing-small); + } + + .widget-carousel-preview-bullet { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--gray-200, $gray-200); + + &-active { + background-color: var(--brand-primary, $gray-500); + } } }