diff --git a/packages/pluggableWidgets/carousel-web/CHANGELOG.md b/packages/pluggableWidgets/carousel-web/CHANGELOG.md index 9f31da8c2c..e3770e34a9 100644 --- a/packages/pluggableWidgets/carousel-web/CHANGELOG.md +++ b/packages/pluggableWidgets/carousel-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Changed + +- We update swiper library dependency from v9 to v11. + ## [2.2.1] - 2023-09-27 ### Fixed diff --git a/packages/pluggableWidgets/carousel-web/package.json b/packages/pluggableWidgets/carousel-web/package.json index df4280b261..d33019f96f 100644 --- a/packages/pluggableWidgets/carousel-web/package.json +++ b/packages/pluggableWidgets/carousel-web/package.json @@ -1,7 +1,7 @@ { "name": "@mendix/carousel-web", "widgetName": "Carousel", - "version": "2.2.1", + "version": "2.3.0", "description": "Displays images in a carousel", "copyright": "© Mendix Technology BV 2025. All rights reserved.", "license": "Apache-2.0", @@ -44,7 +44,7 @@ "dependencies": { "@mendix/widget-plugin-component-kit": "workspace:*", "classnames": "^2.5.1", - "swiper": "^9.4.1" + "swiper": "^11.2.10" }, "devDependencies": { "@mendix/automation-utils": "workspace:*", @@ -53,10 +53,6 @@ "@mendix/prettier-config-web-widgets": "workspace:*", "@mendix/run-e2e": "workspace:*", "@mendix/widget-plugin-platform": "workspace:*", - "cross-env": "^7.0.3", - "mime-types": "^2.1.35", - "postcss": "^8.5.6", - "postcss-url": "^10.1.3", - "shelljs": "^0.8.5" + "cross-env": "^7.0.3" } } diff --git a/packages/pluggableWidgets/carousel-web/rollup.config.mjs b/packages/pluggableWidgets/carousel-web/rollup.config.mjs deleted file mode 100644 index 4b75e58fb9..0000000000 --- a/packages/pluggableWidgets/carousel-web/rollup.config.mjs +++ /dev/null @@ -1,79 +0,0 @@ -import { writeFile } from "fs"; -import { join } from "path"; -import shelljs from "shelljs"; -const { mkdir } = shelljs; -import mime from "mime-types"; -import crypto from "crypto"; -import postcssUrl from "postcss-url"; -import { pathToFileURL } from "url"; -import { postCssPlugin } from "@mendix/pluggable-widgets-tools/configs/rollup.config.mjs"; - -const processPath = path => { - if (process.platform === "win32" && !process.env.JEST_WORKER_ID) { - // on windows import("C:\\path\\to\\file") is not valid, so we need to - // use file:// URLs - return pathToFileURL(path).toString(); - } else { - return path; - } -}; -const sourcePath = process.cwd(); -const outDir = join(sourcePath, "/dist/tmp/widgets/"); -const widgetPackageJson = (await import(processPath(join(sourcePath, "package.json")), { with: { type: "json" } })) - .default; -const widgetName = widgetPackageJson.widgetName; -const widgetPackage = widgetPackageJson.packagePath; -const outWidgetDir = join(widgetPackage.replace(/\./g, "/"), widgetName.toLowerCase()); -const absoluteOutPackageDir = join(outDir, outWidgetDir); -const assetsDirName = "assets"; -const absoluteOutAssetsDir = join(absoluteOutPackageDir, assetsDirName); -const outAssetsDir = join(outWidgetDir, assetsDirName); - -/** - * Take inlined base64 assets and transform them into concrete files into the `assets` folder. - */ -function custom(asset) { - const { url } = asset; - if (url.startsWith("data:")) { - const [, mimeType, data] = url.match(/data:([^;]*).*;base64,(.*)/); - let extension = mime.extension(mimeType); - // Only add extension if we mimeType has associated extension - extension = extension ? `.${extension}` : ""; - const fileHash = crypto.createHash("md5").update(data).digest("hex"); - const filename = `${fileHash}${extension}`; - const filePath = join(absoluteOutAssetsDir, filename); - - mkdir("-p", absoluteOutAssetsDir); - - writeFile(filePath, data, "base64", err => { - if (err) { - if (err.code === "EEXIST") { - return; - } - - throw err; - } - }); - - return `${outAssetsDir}/${filename}`; - } - - return asset.url; -} - -export default args => { - const production = args.configProduction; - const result = args.configDefaultConfig; - const [jsConfig, mJsConfig] = result; - - [jsConfig, mJsConfig].forEach(config => { - const postCssPluginIndex = config.plugins.findIndex(plugin => Boolean(plugin) && plugin.name === "postcss"); - if (postCssPluginIndex >= 0) { - config.plugins[postCssPluginIndex] = postCssPlugin(config.output.format, production, [ - postcssUrl({ url: custom, assetsPath: absoluteOutPackageDir }) - ]); - } - }); - - return result; -}; diff --git a/packages/pluggableWidgets/carousel-web/src/Carousel.tsx b/packages/pluggableWidgets/carousel-web/src/Carousel.tsx index b0b221e4f2..557157487b 100644 --- a/packages/pluggableWidgets/carousel-web/src/Carousel.tsx +++ b/packages/pluggableWidgets/carousel-web/src/Carousel.tsx @@ -1,46 +1,43 @@ -import { createElement, ReactNode, useCallback, ReactElement, useId } from "react"; -import { ValueStatus, GUID, ObjectItem } from "mendix"; import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; +import classNames from "classnames"; +import { GUID, ObjectItem, ValueStatus } from "mendix"; +import { createElement, ReactNode, useCallback, useId } from "react"; import { CarouselContainerProps } from "../typings/CarouselProps"; import { Carousel as CarouselComponent } from "./components/Carousel"; -import loadingCircleSvg from "./ui/loading-circle.svg"; -import classNames from "classnames"; import "./ui/Carousel.scss"; +import loadingCircleSvg from "./ui/loading-circle.svg"; export function Carousel(props: CarouselContainerProps): ReactNode { const { showPagination, loop, tabIndex, navigation, animation, delay, autoplay } = props; const onClick = useCallback(() => executeAction(props.onClickAction), [props.onClickAction]); const id = useId(); - const renderCarousel = (): ReactElement => { - return ( - ({ - id: item.id as GUID, - content: props.content?.get(item) - })) ?? [] - } - onClick={onClick} - /> - ); - }; - const renderLoading = (): ReactNode => { + if (props.dataSource?.status !== ValueStatus.Available) { return (
); - }; + } - return props.dataSource?.status !== ValueStatus.Available ? renderLoading() : renderCarousel(); + return ( + ({ + id: item.id as GUID, + content: props.content?.get(item) + })) ?? [] + } + onClick={onClick} + /> + ); } diff --git a/packages/pluggableWidgets/carousel-web/src/components/Carousel.tsx b/packages/pluggableWidgets/carousel-web/src/components/Carousel.tsx index 5758a0cab8..e5574a3604 100644 --- a/packages/pluggableWidgets/carousel-web/src/components/Carousel.tsx +++ b/packages/pluggableWidgets/carousel-web/src/components/Carousel.tsx @@ -1,9 +1,11 @@ import { createElement, ReactNode, ReactElement, useCallback, useState } from "react"; import { GUID } from "mendix"; import classNames from "classnames"; -import { SwiperOptions, A11y, Navigation, Pagination, EffectFade, Autoplay } from "swiper"; -import { Swiper as ReactSwiper, SwiperClass, SwiperSlide } from "swiper/react"; -import { PaginationOptions } from "swiper/types"; +import { A11y, Navigation, Pagination, EffectFade, Autoplay } from "swiper/modules"; +import { Swiper, SwiperClass, SwiperSlide } from "swiper/react"; +import { PaginationOptions, SwiperOptions } from "swiper/types"; +import "swiper/css"; +import "swiper/css/bundle"; interface CarouselItem { id: GUID; @@ -68,7 +70,7 @@ export function Carousel(props: CarouselProps): ReactElement { return (
- ))} - +
); } 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 70359033ff..e831171997 100644 --- a/packages/pluggableWidgets/carousel-web/src/components/__tests__/Carousel.spec.tsx +++ b/packages/pluggableWidgets/carousel-web/src/components/__tests__/Carousel.spec.tsx @@ -3,6 +3,154 @@ import { Carousel, CarouselProps } from "../Carousel"; import { render } from "@testing-library/react"; import { GUID } from "mendix"; +jest.mock("swiper/css", () => ({})); +jest.mock("swiper/css/bundle", () => ({})); + +jest.mock("swiper/modules", () => ({ + A11y: jest.fn(), + Navigation: jest.fn(), + Pagination: jest.fn(), + EffectFade: jest.fn(), + Autoplay: jest.fn() +})); + +jest.mock("swiper/react", () => ({ + Swiper: ({ + children, + navigation, + pagination, + onClick, + onSwiper, + onActiveIndexChange, + wrapperTag = "div", + ...props + }: any) => { + if (onSwiper) { + const mockSwiper = { realIndex: 0 }; + onSwiper(mockSwiper); + } + if (onActiveIndexChange) { + const mockSwiper = { realIndex: 0 }; + onActiveIndexChange(mockSwiper); + } + + const swiperClasses = [ + "swiper", + props.effect === "fade" ? "swiper-fade" : "", + "swiper-initialized", + "swiper-horizontal", + "swiper-watch-progress" + ] + .filter(Boolean) + .join(" "); + + const wrapperId = "swiper-wrapper-2222222222222222"; + + const navigationElements = navigation + ? [ + createElement("div", { + key: "prev", + className: "swiper-button-prev", + role: "button", + tabIndex: 0, + "aria-controls": wrapperId, + "aria-label": "Previous slide" + }), + createElement("div", { + key: "next", + className: "swiper-button-next", + role: "button", + tabIndex: 0, + "aria-controls": wrapperId, + "aria-label": "Next slide" + }) + ] + : []; + + const paginationElement = pagination + ? createElement( + "div", + { + key: "pagination", + className: + "swiper-pagination swiper-pagination-clickable swiper-pagination-bullets swiper-pagination-horizontal" + }, + Array.isArray(children) + ? children.map((_, index) => + createElement("span", { + key: index, + className: `swiper-pagination-bullet ${ + index === 0 ? "swiper-pagination-bullet-active" : "" + }`, + role: "button", + tabIndex: 0, + "aria-label": `Go to slide ${index}`, + "aria-controls": `carousel-slide-Carousel-${index + 1}`, + "aria-current": index === 0 ? "true" : undefined + }) + ) + : [] + ) + : null; + + const notificationElement = createElement("span", { + key: "notification", + className: "swiper-notification", + "aria-live": "assertive", + "aria-atomic": "true" + }); + + const WrapperTag = wrapperTag as any; + + return createElement( + "div", + { + className: swiperClasses, + onClick + }, + [ + createElement( + WrapperTag, + { + key: "wrapper", + className: "swiper-wrapper", + id: wrapperId, + "aria-live": "off", + style: { transitionDuration: "0ms" } + }, + children + ), + ...navigationElements, + paginationElement, + notificationElement + ].filter(Boolean) + ); + }, + SwiperSlide: ({ children, tag = "div", id }: any) => { + let slideIndex = 0; + const totalSlides = 2; + + if (id && id.includes("carousel-slide-Carousel-")) { + const slideIdPart = id.split("carousel-slide-Carousel-")[1]; + slideIndex = parseInt(slideIdPart, 10) - 1; + } + + const SlideTag = tag as any; + return createElement( + SlideTag, + { + className: "swiper-slide", + id, + role: "listitem", + "aria-hidden": slideIndex !== 0 ? "true" : "false", + "aria-label": `${slideIndex + 1} / ${totalSlides}`, + "data-swiper-slide-index": slideIndex + }, + children + ); + } +})); + describe("Carousel", () => { beforeEach(() => { jest.resetAllMocks(); @@ -39,6 +187,11 @@ describe("Carousel", () => { 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/components/__tests__/__snapshots__/Carousel.spec.tsx.snap b/packages/pluggableWidgets/carousel-web/src/components/__tests__/__snapshots__/Carousel.spec.tsx.snap index 37045c39d3..519f9c2db7 100644 --- a/packages/pluggableWidgets/carousel-web/src/components/__tests__/__snapshots__/Carousel.spec.tsx.snap +++ b/packages/pluggableWidgets/carousel-web/src/components/__tests__/__snapshots__/Carousel.spec.tsx.snap @@ -67,7 +67,7 @@ exports[`Carousel renders correctly 1`] = ` @@ -82,6 +82,55 @@ exports[`Carousel renders correctly 1`] = ` `; +exports[`Carousel renders correctly with minimal setup 1`] = ` + + + +`; + exports[`Carousel renders correctly without navigation 1`] = `
diff --git a/packages/pluggableWidgets/carousel-web/src/package.xml b/packages/pluggableWidgets/carousel-web/src/package.xml index b9758cf8e1..615fd6dccc 100644 --- a/packages/pluggableWidgets/carousel-web/src/package.xml +++ b/packages/pluggableWidgets/carousel-web/src/package.xml @@ -1,6 +1,6 @@ - + diff --git a/packages/pluggableWidgets/carousel-web/src/ui/Carousel.scss b/packages/pluggableWidgets/carousel-web/src/ui/Carousel.scss index 09aa75d1e7..0a2cc4c54e 100644 --- a/packages/pluggableWidgets/carousel-web/src/ui/Carousel.scss +++ b/packages/pluggableWidgets/carousel-web/src/ui/Carousel.scss @@ -1,6 +1,3 @@ -@use "~swiper/swiper"; -@use "~swiper/swiper-bundle.css"; - .swiper { width: 100%; height: 100%; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07ca88ba50..d0e62e134f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -714,8 +714,8 @@ importers: specifier: ^2.5.1 version: 2.5.1 swiper: - specifier: ^9.4.1 - version: 9.4.1 + specifier: ^11.2.10 + version: 11.2.10 devDependencies: '@mendix/automation-utils': specifier: workspace:* @@ -738,18 +738,6 @@ importers: cross-env: specifier: ^7.0.3 version: 7.0.3 - mime-types: - specifier: ^2.1.35 - version: 2.1.35(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) - postcss: - specifier: ^8.5.6 - version: 8.5.6 - postcss-url: - specifier: ^10.1.3 - version: 10.1.3(postcss@8.5.6) - shelljs: - specifier: ^0.8.5 - version: 0.8.5 packages/pluggableWidgets/chart-playground-web: dependencies: @@ -10504,9 +10492,6 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - ssr-window@4.0.2: - resolution: {integrity: sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==} - stable@0.1.8: resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' @@ -10697,8 +10682,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - swiper@9.4.1: - resolution: {integrity: sha512-1nT2T8EzUpZ0FagEqaN/YAhRj33F2x/lN6cyB0/xoYJDMf8KwTFT3hMOeoB8Tg4o3+P/CKqskP+WX0Df046fqA==} + swiper@11.2.10: + resolution: {integrity: sha512-RMeVUUjTQH+6N3ckimK93oxz6Sn5la4aDlgPzB+rBrG/smPdCTicXyhxa+woIpopz+jewEloiEE3lKo1h9w2YQ==} engines: {node: '>= 4.7.0'} symbol-observable@1.2.0: @@ -21433,8 +21418,6 @@ snapshots: sprintf-js@1.0.3: {} - ssr-window@4.0.2: {} - stable@0.1.8: {} stack-trace@0.0.9: {} @@ -21650,9 +21633,7 @@ snapshots: picocolors: 1.1.1 stable: 0.1.8 - swiper@9.4.1: - dependencies: - ssr-window: 4.0.2 + swiper@11.2.10: {} symbol-observable@1.2.0: {}