-
Notifications
You must be signed in to change notification settings - Fork 64
[WC-2946] Initial setup of skiplink widget #1764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
a145b0d
feat: initial setup of skiplink widget
HedwigJDoets 921a5cc
feat(skiplink-web): adding configuration
HedwigJDoets e2b2c0e
chore: improve config files, ran linting
HedwigJDoets 372fefd
fix: small improvements
HedwigJDoets 0a1cb51
fix: rewrite tests for current version of the widget
HedwigJDoets 5b51240
fix: add changes in pnpm lock file
HedwigJDoets 97c2966
chore: update readme
HedwigJDoets 910d05f
fix: fix lint issues
HedwigJDoets 7bbb531
chore: add testproject info to package json
HedwigJDoets ae6eee7
fix: lint error on new testproject
HedwigJDoets 5fa3671
chore: add changelog file
HedwigJDoets a7ea347
fix: fix changelog
HedwigJDoets d7eb3ee
fix: add whitespace to changelog
HedwigJDoets a90fa77
fix: add added header to changelog
HedwigJDoets ded729a
fix: remove react import
HedwigJDoets 4dfe899
chore: add specific mendix version to e2e, add not required to xml
HedwigJDoets 4c3d5e5
test: fix Docker mxbuild for ARM arch and skip the atlas theme copy
leonardomendix ff7537f
feat: add e2e test
HedwigJDoets 4ae5d2d
fix: e2e tests
HedwigJDoets 75883a6
fix: add screenshot for screenshot test
HedwigJDoets ef35ca7
fix: rename snaphot
HedwigJDoets de0d587
fix: remove screenshot
HedwigJDoets 3e571bf
fix: readd screenshot
HedwigJDoets 296117c
fix: refactor based on review comments
HedwigJDoets 943d7fa
fix: apply lint fixes
HedwigJDoets 0278a6f
fix: rename css class, fix click event type
HedwigJDoets fc8f5e9
fix: implement review comments
HedwigJDoets 0685840
fix: ran linting
HedwigJDoets ebe01b4
fix: Add styling for editor preview
HedwigJDoets 79ff479
fix: update skiplink selector in e2e tests
HedwigJDoets 920412d
fix: update snapshot
HedwigJDoets 7b0a51d
fix: e2e test
HedwigJDoets File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| module.exports = require("@mendix/prettier-config-web-widgets"); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
74 changes: 74 additions & 0 deletions
74
packages/pluggableWidgets/skiplink-web/e2e/SkipLink.spec.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| 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(".widget-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(".widget-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(".widget-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(".widget-skip-link").first(); | ||
|
|
||
| // Check default text | ||
| await expect(skipLink).toHaveText("Skip to main content"); | ||
|
|
||
| // Check href attribute | ||
| await expect(skipLink).toHaveAttribute("href", "#"); | ||
|
|
||
| // Check CSS class | ||
| await expect(skipLink).toHaveClass("widget-skip-link mx-name-skipLink1"); | ||
| }); | ||
|
|
||
| test("visual comparison", async ({ page }) => { | ||
| // Tab to make skip link visible for screenshot | ||
| await page.keyboard.press("Tab"); | ||
|
|
||
| const skipLink = page.locator(".widget-skip-link").first(); | ||
| await expect(skipLink).toBeFocused(); | ||
|
|
||
| // Visual comparison of focused skip link | ||
| await expect(skipLink).toHaveScreenshot("skiplink-focused.png"); | ||
| }); | ||
| }); |
Binary file added
BIN
+2.51 KB
...skiplink-web/e2e/SkipLink.spec.js-snapshots/skiplink-focused-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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") | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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:*" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| module.exports = require("@mendix/run-e2e/playwright.config.cjs"); |
74 changes: 74 additions & 0 deletions
74
packages/pluggableWidgets/skiplink-web/src/SkipLink.editorConfig.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
24 changes: 24 additions & 0 deletions
24
packages/pluggableWidgets/skiplink-web/src/SkipLink.editorPreview.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { ReactElement } from "react"; | ||
| import { SkipLinkPreviewProps } from "../typings/SkipLinkProps"; | ||
|
|
||
| export const preview = (props: SkipLinkPreviewProps): ReactElement => { | ||
HedwigAR marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (props.renderMode === "xray") { | ||
| return ( | ||
| <div style={{ position: "relative", height: 40 }}> | ||
| <a href={`#${props.mainContentId}`} className={"widget-skip-link-preview"} style={props.styleObject}> | ||
| {props.linkText} | ||
| </a> | ||
| </div> | ||
| ); | ||
| } else { | ||
| return ( | ||
| <a href={`#${props.mainContentId}`} className={"widget-skip-link-preview"} style={props.styleObject}> | ||
| {props.linkText} | ||
| </a> | ||
| ); | ||
| } | ||
| }; | ||
|
|
||
| export function getPreviewCss(): string { | ||
| return require("./ui/SkipLink.scss"); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import { MouseEvent, useState } from "react"; | ||
| import { createPortal } from "react-dom"; | ||
| import "./ui/SkipLink.scss"; | ||
| import { SkipLinkContainerProps } from "typings/SkipLinkProps"; | ||
|
|
||
| /** | ||
| * 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(props: SkipLinkContainerProps) { | ||
| const [linkRoot] = useState(() => { | ||
| const link = document.createElement("div"); | ||
| const root = document.getElementById("root"); | ||
| // Insert as first child immediately | ||
| if (root && root.firstElementChild) { | ||
| root.insertBefore(link, root.firstElementChild); | ||
| } else if (root) { | ||
| root.appendChild(link); | ||
| } else { | ||
| console.error("No root element found on page"); | ||
| } | ||
| return link; | ||
| }); | ||
|
|
||
| function handleClick(event: MouseEvent): void { | ||
| event.preventDefault(); | ||
| let main: HTMLElement; | ||
| if (props.mainContentId !== "") { | ||
| const mainByID = document.getElementById(props.mainContentId); | ||
| if (mainByID !== null) { | ||
| main = mainByID; | ||
| } else { | ||
| console.error(`Element with id: ${props.mainContentId} not found on page`); | ||
| return; | ||
| } | ||
| } 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 }); | ||
| } | ||
| } else { | ||
| console.error("Could not find a main element on page and no mainContentId specified in widget properties."); | ||
| } | ||
| } | ||
|
|
||
| return createPortal( | ||
| <a | ||
| className={`widget-skip-link ${props.class}`} | ||
| href={`#${props.mainContentId}`} | ||
| tabIndex={props.tabIndex} | ||
| onClick={handleClick} | ||
| > | ||
| {props.linkText} | ||
| </a>, | ||
| linkRoot | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.