Skip to content

Commit c6e7ed5

Browse files
authored
[WC-2946] Initial setup of skiplink widget (#1764)
2 parents aab1373 + 7b0a51d commit c6e7ed5

File tree

22 files changed

+666
-2
lines changed

22 files changed

+666
-2
lines changed

automation/run-e2e/docker/mxbuild.Dockerfile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ echo "Downloading mxbuild ${MENDIX_VERSION} and docker building for ${BUILDPLATF
2424
&& tar xfz /tmp/mxbuild.tar.gz --directory /tmp/mxbuild \
2525
&& rm /tmp/mxbuild.tar.gz && \
2626
\
27-
apt-get update -qqy && \
28-
apt-get install -qqy libicu70 && \
27+
rm -rf /var/lib/apt/lists/* && \
28+
apt-get update --allow-insecure-repositories -qqy && \
29+
apt-get install -qqy --allow-unauthenticated libicu70 && \
2930
apt-get -qqy remove --auto-remove wget && \
3031
apt-get clean && \
32+
rm -rf /var/lib/apt/lists/* && \
3133
\
3234
echo "#!/bin/bash -x" >/bin/mxbuild && \
3335
echo "/tmp/mxbuild/modeler/mxbuild --java-home=/opt/java/openjdk --java-exe-path=/opt/java/openjdk/bin/java \$@" >>/bin/mxbuild && \
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/tests/TestProjects/**/.classpath
2+
/tests/TestProjects/**/.project
3+
/tests/TestProjects/**/javascriptsource
4+
/tests/TestProjects/**/javasource
5+
/tests/TestProjects/**/resources
6+
/tests/TestProjects/**/userlib
7+
8+
/tests/TestProjects/Mendix8/theme/styles/native
9+
/tests/TestProjects/Mendix8/theme/styles/web/sass
10+
/tests/TestProjects/Mendix8/theme/*.*
11+
!/tests/TestProjects/Mendix8/theme/components.json
12+
!/tests/TestProjects/Mendix8/theme/favicon.ico
13+
!/tests/TestProjects/Mendix8/theme/LICENSE
14+
!/tests/TestProjects/Mendix8/theme/settings.json
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require("@mendix/prettier-config-web-widgets");
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Changelog
2+
3+
All notable changes to this widget will be documented in this file.
4+
5+
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).
6+
7+
## [Unreleased]
8+
9+
### Added
10+
11+
- Created skiplink widget.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Skip Link
2+
3+
Adds a skip navigation link for keyboard accessibility. The link is hidden until focused and allows users to jump directly to the main content.
4+
5+
## Usage
6+
7+
1. Add the Skip Link widget anywhere on your page, preferrably at the top or in a layout.
8+
2. Configure the **Link Text** and **Main Content ID** properties.
9+
3. Ensure your main content element has the specified ID, or there's a main tag on the page.
10+
11+
The widget automatically inserts the skip link as the first child of the `#root` element.
12+
13+
## Properties
14+
15+
- **Link Text**: Text displayed for the skip link (default: "Skip to main content").
16+
- **Main Content ID**: ID of the main content element to focus (optional).
17+
18+
If the target element is not found, the widget will focus the first `<main>` element instead.
19+
20+
## Accessibility
21+
22+
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.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
test.afterEach("Cleanup session", async ({ page }) => {
4+
// 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.
5+
await page.evaluate(() => window.mx.session.logout());
6+
});
7+
8+
test.beforeEach(async ({ page }) => {
9+
await page.goto("/");
10+
await page.waitForLoadState("networkidle");
11+
});
12+
13+
test.describe("SkipLink:", function () {
14+
test("skip link is present in DOM but initially hidden", async ({ page }) => {
15+
// Skip link should be in the DOM but not visible
16+
const skipLink = page.locator(".widget-skip-link").first();
17+
await expect(skipLink).toBeAttached();
18+
19+
// Check initial styling (hidden)
20+
const transform = await skipLink.evaluate(el => getComputedStyle(el).transform);
21+
expect(transform).toContain("matrix(1, 0, 0, 1, 0, -48)");
22+
});
23+
24+
test("skip link becomes visible when focused via keyboard", async ({ page }) => {
25+
// Tab to focus the skip link (should be first focusable element)
26+
const skipLink = page.locator(".widget-skip-link").first();
27+
await page.keyboard.press("Tab");
28+
29+
await expect(skipLink).toBeFocused();
30+
await page.waitForTimeout(1000);
31+
// Check that it becomes visible when focused
32+
const transform = await skipLink.evaluate(el => getComputedStyle(el).transform);
33+
expect(transform).toContain("matrix(1, 0, 0, 1, 0, 0)")
34+
});
35+
36+
test("skip link navigates to main content when activated", async ({ page }) => {
37+
// Tab to focus the skip link
38+
await page.keyboard.press("Tab");
39+
40+
const skipLink = page.locator(".widget-skip-link").first();
41+
await expect(skipLink).toBeFocused();
42+
43+
// Activate the skip link
44+
await page.keyboard.press("Enter");
45+
46+
// Check that main content is now focused
47+
const mainContent = page.locator("main");
48+
await expect(mainContent).toBeFocused();
49+
});
50+
51+
test("skip link has correct attributes and text", async ({ page }) => {
52+
const skipLink = page.locator(".widget-skip-link").first();
53+
54+
// Check default text
55+
await expect(skipLink).toHaveText("Skip to main content");
56+
57+
// Check href attribute
58+
await expect(skipLink).toHaveAttribute("href", "#");
59+
60+
// Check CSS class
61+
await expect(skipLink).toHaveClass("widget-skip-link mx-name-skipLink1");
62+
});
63+
64+
test("visual comparison", async ({ page }) => {
65+
// Tab to make skip link visible for screenshot
66+
await page.keyboard.press("Tab");
67+
68+
const skipLink = page.locator(".widget-skip-link").first();
69+
await expect(skipLink).toBeFocused();
70+
71+
// Visual comparison of focused skip link
72+
await expect(skipLink).toHaveScreenshot("skiplink-focused.png");
73+
});
74+
});
2.51 KB
Loading
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import config from "@mendix/eslint-config-web-widgets/widget-ts.mjs";
2+
3+
export default config;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
...require("@mendix/pluggable-widgets-tools/test-config/jest.enzyme-free.config.js")
3+
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"name": "@mendix/skiplink-web",
3+
"widgetName": "SkipLink",
4+
"version": "1.0.0",
5+
"description": "Adds a skip link to the top of the page for accessibility.",
6+
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
7+
"license": "Apache-2.0",
8+
"repository": {
9+
"type": "git",
10+
"url": "https://github.com/mendix/web-widgets.git"
11+
},
12+
"config": {},
13+
"mxpackage": {
14+
"name": "SkipLink",
15+
"type": "widget",
16+
"mpkName": "com.mendix.widget.web.SkipLink.mpk"
17+
},
18+
"packagePath": "com.mendix.widget.web",
19+
"marketplace": {
20+
"minimumMXVersion": "11.1.0",
21+
"appNumber": 119999,
22+
"appName": "SkipLink",
23+
"reactReady": true
24+
},
25+
"testProject": {
26+
"githubUrl": "https://github.com/mendix/testProjects",
27+
"branchName": "skiplink-web"
28+
},
29+
"scripts": {
30+
"build": "pluggable-widgets-tools build:web",
31+
"create-gh-release": "rui-create-gh-release",
32+
"create-translation": "rui-create-translation",
33+
"dev": "pluggable-widgets-tools start:web",
34+
"e2e": "MENDIX_VERSION=11.1.0.75979 run-e2e ci --no-update-project",
35+
"e2edev": "MENDIX_VERSION=11.1.0.75979 run-e2e dev --with-preps --no-update-project",
36+
"format": "prettier --ignore-path ./node_modules/@mendix/prettier-config-web-widgets/global-prettierignore --write .",
37+
"lint": "eslint src/ package.json",
38+
"publish-marketplace": "rui-publish-marketplace",
39+
"release": "pluggable-widgets-tools release:web",
40+
"start": "pluggable-widgets-tools start:server",
41+
"test": "jest --projects jest.config.js",
42+
"update-changelog": "rui-update-changelog-widget",
43+
"verify": "rui-verify-package-format"
44+
},
45+
"dependencies": {
46+
"@floating-ui/react": "^0.26.27",
47+
"@mendix/widget-plugin-component-kit": "workspace:*",
48+
"classnames": "^2.5.1"
49+
},
50+
"devDependencies": {
51+
"@mendix/automation-utils": "workspace:*",
52+
"@mendix/eslint-config-web-widgets": "workspace:*",
53+
"@mendix/pluggable-widgets-tools": "*",
54+
"@mendix/prettier-config-web-widgets": "workspace:*",
55+
"@mendix/run-e2e": "workspace:*",
56+
"@mendix/widget-plugin-hooks": "workspace:*",
57+
"@mendix/widget-plugin-platform": "workspace:*",
58+
"@mendix/widget-plugin-test-utils": "workspace:*"
59+
}
60+
}

0 commit comments

Comments
 (0)