Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d71607f
feat: initial setup of skiplink widget
HedwigJDoets Jul 11, 2025
93c74e8
feat(skiplink-web): adding configuration
HedwigJDoets Jul 11, 2025
fa2060f
chore: improve config files, ran linting
HedwigJDoets Jul 15, 2025
6815e12
fix: small improvements
HedwigJDoets Nov 12, 2025
52c763c
fix: rewrite tests for current version of the widget
HedwigJDoets Nov 12, 2025
74d766d
fix: add changes in pnpm lock file
HedwigJDoets Nov 13, 2025
bef09bd
chore: update readme
HedwigJDoets Nov 24, 2025
95005b6
fix: fix lint issues
HedwigJDoets Nov 26, 2025
ef5e035
chore: add testproject info to package json
HedwigJDoets Nov 26, 2025
7ec99cd
fix: lint error on new testproject
HedwigJDoets Nov 26, 2025
fd33e2a
chore: add changelog file
HedwigJDoets Nov 26, 2025
8230160
fix: fix changelog
HedwigJDoets Nov 26, 2025
574c551
fix: add whitespace to changelog
HedwigJDoets Nov 27, 2025
74ebca9
fix: add added header to changelog
HedwigJDoets Nov 27, 2025
0ee3595
fix: remove react import
HedwigJDoets Nov 27, 2025
5958650
chore: add specific mendix version to e2e, add not required to xml
HedwigJDoets Nov 28, 2025
01492c8
test: fix Docker mxbuild for ARM arch and skip the atlas theme copy
leonardomendix Dec 2, 2025
73b48d0
feat: add e2e test
HedwigJDoets Dec 4, 2025
de74789
fix: e2e tests
HedwigJDoets Dec 8, 2025
d476c9b
fix: add screenshot for screenshot test
HedwigJDoets Dec 8, 2025
29de627
fix: rename snaphot
HedwigJDoets Dec 8, 2025
d72d9d3
fix: remove screenshot
HedwigJDoets Dec 9, 2025
9f1fd25
fix: readd screenshot
HedwigJDoets Dec 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions automation/run-e2e/docker/mxbuild.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand Down
14 changes: 14 additions & 0 deletions packages/pluggableWidgets/skiplink-web/.gitignore
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions packages/pluggableWidgets/skiplink-web/.prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require("@mendix/prettier-config-web-widgets");
11 changes: 11 additions & 0 deletions packages/pluggableWidgets/skiplink-web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions packages/pluggableWidgets/skiplink-web/README.md
Original file line number Diff line number Diff line change
@@ -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 `<main>` 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.
77 changes: 77 additions & 0 deletions packages/pluggableWidgets/skiplink-web/e2e/SkipLink.spec.js
Original file line number Diff line number Diff line change
@@ -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");
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/pluggableWidgets/skiplink-web/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import config from "@mendix/eslint-config-web-widgets/widget-ts.mjs";

export default config;
3 changes: 3 additions & 0 deletions packages/pluggableWidgets/skiplink-web/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
...require("@mendix/pluggable-widgets-tools/test-config/jest.enzyme-free.config.js")
};
60 changes: 60 additions & 0 deletions packages/pluggableWidgets/skiplink-web/package.json
Original file line number Diff line number Diff line change
@@ -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:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require("@mendix/run-e2e/playwright.config.cjs");
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ReactElement } from "react";
import { SkipLinkPreviewProps } from "../typings/SkipLinkProps";

export const preview = (props: SkipLinkPreviewProps): ReactElement => {
if (props.renderMode === "xray") {
return (
<div style={{ position: "relative", height: 40 }}>
<a
href={`#${props.mainContentId}`}
style={{
position: "absolute",
top: 0,
left: 0,
background: "#fff",
color: "#0078d4",
padding: "8px 16px",
zIndex: 1000,
textDecoration: "none",
border: "2px solid #0078d4",
borderRadius: 4,
fontWeight: "bold"
}}
>
{props.linkText}
</a>
</div>
);
} else {
return (
<a
href={`#${props.mainContentId}`}
style={{
position: "absolute",
top: 0,
left: 0,
background: "#fff",
color: "#0078d4",
padding: "8px 16px",
zIndex: 1000,
textDecoration: "none",
border: "2px solid #0078d4",
borderRadius: 4,
fontWeight: "bold"
}}
>
{props.linkText}
</a>
);
}
};

export function getPreviewCss(): string {
return require("./ui/SkipLink.scss");
}
Loading
Loading