diff --git a/automation/run-e2e/docker/mxbuild.Dockerfile b/automation/run-e2e/docker/mxbuild.Dockerfile index 45cd2e9c9a..c6a3934271 100644 --- a/automation/run-e2e/docker/mxbuild.Dockerfile +++ b/automation/run-e2e/docker/mxbuild.Dockerfile @@ -24,10 +24,12 @@ echo "Downloading mxbuild ${MENDIX_VERSION} and docker building for ${BUILDPLATF && tar xfz /tmp/mxbuild.tar.gz --directory /tmp/mxbuild \ && rm /tmp/mxbuild.tar.gz && \ \ - apt-get update -qqy && \ - apt-get install -qqy libicu70 && \ + rm -rf /var/lib/apt/lists/* && \ + apt-get update --allow-insecure-repositories -qqy && \ + apt-get install -qqy --allow-unauthenticated libicu70 && \ apt-get -qqy remove --auto-remove wget && \ apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ \ echo "#!/bin/bash -x" >/bin/mxbuild && \ echo "/tmp/mxbuild/modeler/mxbuild --java-home=/opt/java/openjdk --java-exe-path=/opt/java/openjdk/bin/java \$@" >>/bin/mxbuild && \ diff --git a/packages/pluggableWidgets/skiplink-web/.gitignore b/packages/pluggableWidgets/skiplink-web/.gitignore new file mode 100644 index 0000000000..a1bd0102fd --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/.gitignore @@ -0,0 +1,14 @@ +/tests/TestProjects/**/.classpath +/tests/TestProjects/**/.project +/tests/TestProjects/**/javascriptsource +/tests/TestProjects/**/javasource +/tests/TestProjects/**/resources +/tests/TestProjects/**/userlib + +/tests/TestProjects/Mendix8/theme/styles/native +/tests/TestProjects/Mendix8/theme/styles/web/sass +/tests/TestProjects/Mendix8/theme/*.* +!/tests/TestProjects/Mendix8/theme/components.json +!/tests/TestProjects/Mendix8/theme/favicon.ico +!/tests/TestProjects/Mendix8/theme/LICENSE +!/tests/TestProjects/Mendix8/theme/settings.json diff --git a/packages/pluggableWidgets/skiplink-web/.prettierrc.js b/packages/pluggableWidgets/skiplink-web/.prettierrc.js new file mode 100644 index 0000000000..0892704ab0 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require("@mendix/prettier-config-web-widgets"); diff --git a/packages/pluggableWidgets/skiplink-web/CHANGELOG.md b/packages/pluggableWidgets/skiplink-web/CHANGELOG.md new file mode 100644 index 0000000000..b1c9496c7b --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this widget will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Created skiplink widget. diff --git a/packages/pluggableWidgets/skiplink-web/README.md b/packages/pluggableWidgets/skiplink-web/README.md new file mode 100644 index 0000000000..3516757513 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/README.md @@ -0,0 +1,22 @@ +# Skip Link + +Adds a skip navigation link for keyboard accessibility. The link is hidden until focused and allows users to jump directly to the main content. + +## Usage + +1. Add the Skip Link widget anywhere on your page, preferrably at the top or in a layout. +2. Configure the **Link Text** and **Main Content ID** properties. +3. Ensure your main content element has the specified ID, or there's a main tag on the page. + +The widget automatically inserts the skip link as the first child of the `#root` element. + +## Properties + +- **Link Text**: Text displayed for the skip link (default: "Skip to main content"). +- **Main Content ID**: ID of the main content element to focus (optional). + +If the target element is not found, the widget will focus the first `
` element instead. + +## Accessibility + +The skip link is positioned absolutely at the top-left of the page, hidden by default with `transform: translateY(-120%)`, and becomes visible when focused via keyboard navigation. diff --git a/packages/pluggableWidgets/skiplink-web/e2e/SkipLink.spec.js b/packages/pluggableWidgets/skiplink-web/e2e/SkipLink.spec.js new file mode 100644 index 0000000000..4b6ad94c80 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/e2e/SkipLink.spec.js @@ -0,0 +1,77 @@ +import { test, expect } from "@playwright/test"; + +test.afterEach("Cleanup session", async ({ page }) => { + // Because the test isolation that will open a new session for every test executed, and that exceeds Mendix's license limit of 5 sessions, so we need to force logout after each test. + await page.evaluate(() => window.mx.session.logout()); +}); + +test.beforeEach(async ({ page }) => { + await page.goto("/"); + await page.waitForLoadState("networkidle"); +}); + +test.describe("SkipLink:", function () { + test("skip link is present in DOM but initially hidden", async ({ page }) => { + // Skip link should be in the DOM but not visible + const skipLink = page.locator(".skip-link").first(); + await expect(skipLink).toBeAttached(); + + // Check initial styling (hidden) + const transform = await skipLink.evaluate(el => getComputedStyle(el).transform); + expect(transform).toContain("matrix(1, 0, 0, 1, 0, -48)"); + }); + + test("skip link becomes visible when focused via keyboard", async ({ page }) => { + // Tab to focus the skip link (should be first focusable element) + const skipLink = page.locator(".skip-link").first(); + await page.keyboard.press("Tab"); + + await expect(skipLink).toBeFocused(); + await page.waitForTimeout(1000); + // Check that it becomes visible when focused + const transform = await skipLink.evaluate(el => getComputedStyle(el).transform); + expect(transform).toContain("matrix(1, 0, 0, 1, 0, 0)") + }); + + test("skip link navigates to main content when activated", async ({ page }) => { + // Tab to focus the skip link + await page.keyboard.press("Tab"); + + const skipLink = page.locator(".skip-link").first(); + await expect(skipLink).toBeFocused(); + + // Activate the skip link + await page.keyboard.press("Enter"); + + // Check that main content is now focused + const mainContent = page.locator("main"); + await expect(mainContent).toBeFocused(); + }); + + test("skip link has correct attributes and text", async ({ page }) => { + const skipLink = page.locator(".skip-link").first(); + + // Check default text + await expect(skipLink).toHaveText("Skip to main content"); + + // Check href attribute + await expect(skipLink).toHaveAttribute("href", "#"); + + // Check tabindex + await expect(skipLink).toHaveAttribute("tabindex", "0"); + + // Check CSS class + await expect(skipLink).toHaveClass("skip-link"); + }); + + test("visual comparison", async ({ page }) => { + // Tab to make skip link visible for screenshot + await page.keyboard.press("Tab"); + + const skipLink = page.locator(".skip-link").first(); + await expect(skipLink).toBeFocused(); + + // Visual comparison of focused skip link + await expect(skipLink).toHaveScreenshot("skiplink-focused.png"); + }); +}); \ No newline at end of file diff --git a/packages/pluggableWidgets/skiplink-web/e2e/SkipLink.spec.js-snapshots/skiplink-focused-chromium-linux.png b/packages/pluggableWidgets/skiplink-web/e2e/SkipLink.spec.js-snapshots/skiplink-focused-chromium-linux.png new file mode 100644 index 0000000000..34157374d2 Binary files /dev/null and b/packages/pluggableWidgets/skiplink-web/e2e/SkipLink.spec.js-snapshots/skiplink-focused-chromium-linux.png differ diff --git a/packages/pluggableWidgets/skiplink-web/eslint.config.mjs b/packages/pluggableWidgets/skiplink-web/eslint.config.mjs new file mode 100644 index 0000000000..ed68ae9e78 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/eslint.config.mjs @@ -0,0 +1,3 @@ +import config from "@mendix/eslint-config-web-widgets/widget-ts.mjs"; + +export default config; diff --git a/packages/pluggableWidgets/skiplink-web/jest.config.js b/packages/pluggableWidgets/skiplink-web/jest.config.js new file mode 100644 index 0000000000..88999d5568 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + ...require("@mendix/pluggable-widgets-tools/test-config/jest.enzyme-free.config.js") +}; diff --git a/packages/pluggableWidgets/skiplink-web/package.json b/packages/pluggableWidgets/skiplink-web/package.json new file mode 100644 index 0000000000..934124d975 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/package.json @@ -0,0 +1,60 @@ +{ + "name": "@mendix/skiplink-web", + "widgetName": "SkipLink", + "version": "1.0.0", + "description": "Adds a skip link to the top of the page for accessibility.", + "copyright": "© Mendix Technology BV 2025. All rights reserved.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/mendix/web-widgets.git" + }, + "config": {}, + "mxpackage": { + "name": "SkipLink", + "type": "widget", + "mpkName": "com.mendix.widget.web.SkipLink.mpk" + }, + "packagePath": "com.mendix.widget.web", + "marketplace": { + "minimumMXVersion": "11.1.0", + "appNumber": 119999, + "appName": "SkipLink", + "reactReady": true + }, + "testProject": { + "githubUrl": "https://github.com/mendix/testProjects", + "branchName": "skiplink-web" + }, + "scripts": { + "build": "pluggable-widgets-tools build:web", + "create-gh-release": "rui-create-gh-release", + "create-translation": "rui-create-translation", + "dev": "pluggable-widgets-tools start:web", + "e2e": "MENDIX_VERSION=11.1.0.75979 run-e2e ci --no-update-project", + "e2edev": "MENDIX_VERSION=11.1.0.75979 run-e2e dev --with-preps --no-update-project", + "format": "prettier --ignore-path ./node_modules/@mendix/prettier-config-web-widgets/global-prettierignore --write .", + "lint": "eslint src/ package.json", + "publish-marketplace": "rui-publish-marketplace", + "release": "pluggable-widgets-tools release:web", + "start": "pluggable-widgets-tools start:server", + "test": "jest --projects jest.config.js", + "update-changelog": "rui-update-changelog-widget", + "verify": "rui-verify-package-format" + }, + "dependencies": { + "@floating-ui/react": "^0.26.27", + "@mendix/widget-plugin-component-kit": "workspace:*", + "classnames": "^2.5.1" + }, + "devDependencies": { + "@mendix/automation-utils": "workspace:*", + "@mendix/eslint-config-web-widgets": "workspace:*", + "@mendix/pluggable-widgets-tools": "*", + "@mendix/prettier-config-web-widgets": "workspace:*", + "@mendix/run-e2e": "workspace:*", + "@mendix/widget-plugin-hooks": "workspace:*", + "@mendix/widget-plugin-platform": "workspace:*", + "@mendix/widget-plugin-test-utils": "workspace:*" + } +} diff --git a/packages/pluggableWidgets/skiplink-web/playwright.config.cjs b/packages/pluggableWidgets/skiplink-web/playwright.config.cjs new file mode 100644 index 0000000000..29045fc372 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/playwright.config.cjs @@ -0,0 +1 @@ +module.exports = require("@mendix/run-e2e/playwright.config.cjs"); diff --git a/packages/pluggableWidgets/skiplink-web/src/SkipLink.editorConfig.ts b/packages/pluggableWidgets/skiplink-web/src/SkipLink.editorConfig.ts new file mode 100644 index 0000000000..60cb6fb97f --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/src/SkipLink.editorConfig.ts @@ -0,0 +1,74 @@ +import { Problem, Properties } from "@mendix/pluggable-widgets-tools"; +import { + ContainerProps, + RowLayoutProps, + structurePreviewPalette, + StructurePreviewProps, + TextProps +} from "@mendix/widget-plugin-platform/preview/structure-preview-api"; + +export function getProperties(defaultValues: Properties): Properties { + // No conditional properties for skiplink, but function provided for consistency + return defaultValues; +} + +export function check(values: any): Problem[] { + const errors: Problem[] = []; + if (!values.linkText) { + errors.push({ + property: "linkText", + message: "Link text is required" + }); + } + return errors; +} + +export function getPreview(values: any, isDarkMode: boolean): StructurePreviewProps | null { + const palette = structurePreviewPalette[isDarkMode ? "dark" : "light"]; + const titleHeader: RowLayoutProps = { + type: "RowLayout", + columnSize: "grow", + backgroundColor: palette.background.topbarStandard, + borders: true, + borderWidth: 1, + children: [ + { + type: "Container", + padding: 4, + children: [ + { + type: "Text", + content: "SkipLink", + fontColor: palette.text.secondary + } as TextProps + ] + } + ] + }; + const linkContent: RowLayoutProps = { + type: "RowLayout", + columnSize: "grow", + borders: true, + padding: 0, + children: [ + { + type: "Container", + padding: 6, + children: [ + { + type: "Text", + content: values.linkText || "Skip to main content", + fontSize: 14, + fontColor: palette.text.primary, + bold: true + } as TextProps + ] + } + ] + }; + return { + type: "Container", + borders: true, + children: [titleHeader, linkContent] + } as ContainerProps; +} diff --git a/packages/pluggableWidgets/skiplink-web/src/SkipLink.editorPreview.tsx b/packages/pluggableWidgets/skiplink-web/src/SkipLink.editorPreview.tsx new file mode 100644 index 0000000000..27f1a351c0 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/src/SkipLink.editorPreview.tsx @@ -0,0 +1,54 @@ +import { ReactElement } from "react"; +import { SkipLinkPreviewProps } from "../typings/SkipLinkProps"; + +export const preview = (props: SkipLinkPreviewProps): ReactElement => { + if (props.renderMode === "xray") { + return ( +
+ + {props.linkText} + +
+ ); + } else { + return ( + + {props.linkText} + + ); + } +}; + +export function getPreviewCss(): string { + return require("./ui/SkipLink.scss"); +} diff --git a/packages/pluggableWidgets/skiplink-web/src/SkipLink.tsx b/packages/pluggableWidgets/skiplink-web/src/SkipLink.tsx new file mode 100644 index 0000000000..108a382cd2 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/src/SkipLink.tsx @@ -0,0 +1,73 @@ +import "./ui/SkipLink.scss"; +import { useEffect } from "react"; + +export interface SkipLinkProps { + /** + * The text displayed for the skip link. + */ + linkText: string; + /** + * The id of the main content element to jump to. + */ + mainContentId: string; +} + +/** + * Inserts a skip link as the first child of the element with ID 'root'. + * When activated, focus is programmatically set to the main content. + */ +export function SkipLink({ linkText, mainContentId }: SkipLinkProps): null { + useEffect(() => { + // Create the skip link element + const link = document.createElement("a"); + link.href = `#${mainContentId}`; + link.className = "skip-link"; + link.textContent = linkText; + link.tabIndex = 0; + + // Handler to move focus to the main content + function handleClick(event: MouseEvent): void { + event.preventDefault(); + let main: HTMLElement; + const mainByID = document.getElementById(mainContentId); + if (mainContentId !== "" && mainByID !== null) { + main = mainByID; + } else { + main = document.getElementsByTagName("main")[0]; + } + + if (main) { + // Store previous tabindex + const prevTabIndex = main.getAttribute("tabindex"); + // Ensure main is focusable + if (!main.hasAttribute("tabindex")) { + main.setAttribute("tabindex", "-1"); + } + main.focus(); + // Clean up tabindex if it was not present before + if (prevTabIndex === null) { + main.addEventListener("blur", () => main.removeAttribute("tabindex"), { once: true }); + } + } + } + + link.addEventListener("click", handleClick); + + // Insert as the first child of the element with ID 'root' + const root = document.getElementById("root"); + if (root) { + root.insertBefore(link, root.firstChild); + } + + // Cleanup on unmount + return () => { + link.removeEventListener("click", handleClick); + if (link.parentNode) { + link.parentNode.removeChild(link); + } + }; + }, [linkText, mainContentId]); + + // This component does not render anything in the React tree + return null; +} diff --git a/packages/pluggableWidgets/skiplink-web/src/SkipLink.xml b/packages/pluggableWidgets/skiplink-web/src/SkipLink.xml new file mode 100644 index 0000000000..d3de605963 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/src/SkipLink.xml @@ -0,0 +1,20 @@ + + + SkipLink + A skip link for accessibility, allowing users to jump directly to the main content. + Accessibility + Accessibility + + + + + Link text + The text displayed in the skip link. + + + Main content ID + The id of the main content element to jump to, if left empty the skip link widget will search for a main tag on the page. + + + + diff --git a/packages/pluggableWidgets/skiplink-web/src/__tests__/SkipLink.spec.tsx b/packages/pluggableWidgets/skiplink-web/src/__tests__/SkipLink.spec.tsx new file mode 100644 index 0000000000..d635ee1c17 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/src/__tests__/SkipLink.spec.tsx @@ -0,0 +1,84 @@ +import "@testing-library/jest-dom"; +import { render } from "@testing-library/react"; +import { SkipLinkContainerProps } from "../../typings/SkipLinkProps"; +import { SkipLink } from "../SkipLink"; + +describe("SkipLink", () => { + let defaultProps: SkipLinkContainerProps; + let rootElement: HTMLElement; + + beforeEach(() => { + // Set up the DOM structure that SkipLink expects + document.body.innerHTML = ""; + rootElement = document.createElement("div"); + rootElement.id = "root"; + document.body.appendChild(rootElement); + + defaultProps = { + name: "SkipLink1", + class: "mx-skiplink", + style: {}, + linkText: "Skip to main content", + mainContentId: "main-content" + }; + }); + + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("renders skiplink widget and adds skip link to DOM", () => { + render(); + + // Check that the skip link was added to the root element + const skipLink = rootElement.querySelector(".skip-link") as HTMLAnchorElement; + expect(skipLink).toBeInTheDocument(); + expect(skipLink.textContent).toBe("Skip to main content"); + expect(skipLink.href).toBe(`${window.location.origin}/#main-content`); + expect(skipLink.tabIndex).toBe(0); + + // Snapshot the actual root element that contains the skip link + expect(rootElement).toMatchSnapshot(); + }); + + it("renders with custom link text", () => { + render(); + + const skipLink = rootElement.querySelector(".skip-link") as HTMLAnchorElement; + expect(skipLink).toBeInTheDocument(); + expect(skipLink.textContent).toBe("Jump to content"); + + expect(rootElement).toMatchSnapshot(); + }); + + it("renders with custom main content id", () => { + render(); + + const skipLink = rootElement.querySelector(".skip-link") as HTMLAnchorElement; + expect(skipLink).toBeInTheDocument(); + expect(skipLink.href).toBe(`${window.location.origin}/#content-area`); + + expect(rootElement).toMatchSnapshot(); + }); + + it("renders with empty main content id", () => { + render(); + + const skipLink = rootElement.querySelector(".skip-link") as HTMLAnchorElement; + expect(skipLink).toBeInTheDocument(); + expect(skipLink.href).toBe(`${window.location.origin}/#`); + + expect(rootElement).toMatchSnapshot(); + }); + + it("cleans up skip link when component unmounts", () => { + const { unmount } = render(); + + // Verify skip link is present + expect(rootElement.querySelector(".skip-link")).toBeInTheDocument(); + + // Unmount and verify cleanup + unmount(); + expect(rootElement.querySelector(".skip-link")).not.toBeInTheDocument(); + }); +}); diff --git a/packages/pluggableWidgets/skiplink-web/src/__tests__/__snapshots__/SkipLink.spec.tsx.snap b/packages/pluggableWidgets/skiplink-web/src/__tests__/__snapshots__/SkipLink.spec.tsx.snap new file mode 100644 index 0000000000..1dc3dc6f8c --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/src/__tests__/__snapshots__/SkipLink.spec.tsx.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SkipLink renders skiplink widget and adds skip link to DOM 1`] = ` + +`; + +exports[`SkipLink renders with custom link text 1`] = ` + +`; + +exports[`SkipLink renders with custom main content id 1`] = ` + +`; + +exports[`SkipLink renders with empty main content id 1`] = ` + +`; diff --git a/packages/pluggableWidgets/skiplink-web/src/package.xml b/packages/pluggableWidgets/skiplink-web/src/package.xml new file mode 100644 index 0000000000..811a87e4ab --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/src/package.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/pluggableWidgets/skiplink-web/src/ui/SkipLink.scss b/packages/pluggableWidgets/skiplink-web/src/ui/SkipLink.scss new file mode 100644 index 0000000000..5a613ceb39 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/src/ui/SkipLink.scss @@ -0,0 +1,20 @@ +.skip-link { + position: absolute; + top: 0; + left: 0; + background: #fff; + color: #0078d4; + padding: 8px 16px; + z-index: 1000; + transform: translateY(-120%); + transition: transform 0.2s; + text-decoration: none; + border: 2px solid #0078d4; + border-radius: 4px; + font-weight: bold; +} + +.skip-link:focus { + transform: translateY(0); + outline: none; +} diff --git a/packages/pluggableWidgets/skiplink-web/tsconfig.json b/packages/pluggableWidgets/skiplink-web/tsconfig.json new file mode 100644 index 0000000000..7aa60df0c9 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/tsconfig.json @@ -0,0 +1,30 @@ +{ + "include": ["./src", "./typings"], + "compilerOptions": { + "baseUrl": "./", + "noEmitOnError": true, + "sourceMap": true, + "module": "esnext", + "target": "es6", + "lib": ["esnext", "dom"], + "types": ["jest", "node"], + "moduleResolution": "node", + "declaration": false, + "noLib": false, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "strict": true, + "strictFunctionTypes": false, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "useUnknownInCatchVariables": false, + "exactOptionalPropertyTypes": false, + "paths": { + "react-hot-loader/root": ["./hot-typescript.ts"] + } + } +} diff --git a/packages/pluggableWidgets/skiplink-web/typings/SkipLinkProps.d.ts b/packages/pluggableWidgets/skiplink-web/typings/SkipLinkProps.d.ts new file mode 100644 index 0000000000..dd4d4c8a82 --- /dev/null +++ b/packages/pluggableWidgets/skiplink-web/typings/SkipLinkProps.d.ts @@ -0,0 +1,30 @@ +/** + * This file was generated from SkipLink.xml + * WARNING: All changes made to this file will be overwritten + * @author Mendix Widgets Framework Team + */ +import { CSSProperties } from "react"; + +export interface SkipLinkContainerProps { + name: string; + class: string; + style?: CSSProperties; + tabIndex?: number; + linkText: string; + mainContentId: string; +} + +export interface SkipLinkPreviewProps { + /** + * @deprecated Deprecated since version 9.18.0. Please use class property instead. + */ + className: string; + class: string; + style: string; + styleObject?: CSSProperties; + readOnly: boolean; + renderMode: "design" | "xray" | "structure"; + translate: (text: string) => string; + linkText: string; + mainContentId: string; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 684f01279c..6980f94c52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2268,6 +2268,43 @@ importers: specifier: ^7.0.3 version: 7.0.3 + packages/pluggableWidgets/skiplink-web: + dependencies: + '@floating-ui/react': + specifier: ^0.26.27 + version: 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mendix/widget-plugin-component-kit': + specifier: workspace:* + version: link:../../shared/widget-plugin-component-kit + classnames: + specifier: ^2.5.1 + version: 2.5.1 + devDependencies: + '@mendix/automation-utils': + specifier: workspace:* + version: link:../../../automation/utils + '@mendix/eslint-config-web-widgets': + specifier: workspace:* + version: link:../../shared/eslint-config-web-widgets + '@mendix/pluggable-widgets-tools': + specifier: 10.21.2 + version: 10.21.2(@jest/transform@29.7.0)(@jest/types@30.2.0)(@swc/core@1.13.5)(@types/babel__core@7.20.5)(@types/node@22.14.1)(jest-util@30.2.0)(picomatch@4.0.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.82.0(@babel/core@7.28.4)(@types/react@19.2.2)(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 + '@mendix/run-e2e': + specifier: workspace:* + version: link:../../../automation/run-e2e + '@mendix/widget-plugin-hooks': + specifier: workspace:* + version: link:../../shared/widget-plugin-hooks + '@mendix/widget-plugin-platform': + specifier: workspace:* + version: link:../../shared/widget-plugin-platform + '@mendix/widget-plugin-test-utils': + specifier: workspace:* + version: link:../../shared/widget-plugin-test-utils + packages/pluggableWidgets/slider-web: dependencies: '@mendix/widget-plugin-component-kit':