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/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/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/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/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; + } +} 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; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36a7057782..0bc099c9d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -376,7 +376,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)(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) + 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 @@ -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 @@ -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:* @@ -10482,8 +10482,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: @@ -19979,7 +19979,7 @@ snapshots: sax: 1.6.1 stable: 0.1.8 - swiper@12.2.0: {} + swiper@14.2.0: {} symbol-tree@3.2.4: {}