diff --git a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md index 7144838bb3..541ed6fecc 100644 --- a/packages/pluggableWidgets/datagrid-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- We added accessibility support for column headers when single selection is enabled, making sure the purpose of the column is announced. + ## [3.8.1] - 2026-02-19 ### Fixed diff --git a/packages/pluggableWidgets/datagrid-web/e2e/DataGridSelection.spec.js b/packages/pluggableWidgets/datagrid-web/e2e/DataGridSelection.spec.js index b4a093a6f0..b5ee1fd855 100644 --- a/packages/pluggableWidgets/datagrid-web/e2e/DataGridSelection.spec.js +++ b/packages/pluggableWidgets/datagrid-web/e2e/DataGridSelection.spec.js @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { expect, test } from "@playwright/test"; import AxeBuilder from "@axe-core/playwright"; test.afterEach("Cleanup session", async ({ page }) => { @@ -55,6 +55,37 @@ test.describe("datagrid-web selection", async () => { await expect(page).toHaveScreenshot(`datagridMultiSelectionRowClick.png`); }); + test("checks single selection accessibility with sr-only text", async ({ page }) => { + await page.goto("/p/single-selection"); + await page.waitForLoadState("networkidle"); + + const singleSelectionCheckbox = page.locator(".mx-name-dgSingleSelectionCheckbox"); + await singleSelectionCheckbox.waitFor(); + + // Verify sr-only text is present in the selection column header + const srOnlyText = singleSelectionCheckbox.locator(".widget-datagrid-col-select .sr-only"); + await expect(srOnlyText).toHaveText(/Select single row/i); + + // Verify sr-only text is not visible but accessible + await expect(srOnlyText).toBeAttached(); + const isHidden = await srOnlyText.evaluate(el => { + const style = window.getComputedStyle(el); + return ( + style.position === "absolute" && (style.width === "1px" || style.clip === "rect(0px, 0px, 0px, 0px)") + ); + }); + expect(isHidden).toBe(true); + + // Run accessibility scan + const accessibilityScanResults = await new AxeBuilder({ page }) + .withTags(["wcag21aa"]) + .include(".mx-name-dgSingleSelectionCheckbox") + .exclude(".mx-name-navigationTree3") + .analyze(); + + expect(accessibilityScanResults.violations).toEqual([]); + }); + test("checks accessibility violations", async ({ page }) => { await page.goto("/p/multi-selection"); await page.waitForLoadState("networkidle"); diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts index 350d1bff18..4263688dec 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.editorConfig.ts @@ -170,6 +170,10 @@ function hideSelectionProperties(defaultProperties: Properties, values: Datagrid "enableSelectAll" ]); } + + if (itemSelection !== "Single") { + hidePropertyIn(defaultProperties, values, "singleSelectionColumnLabel"); + } } export const getPreview = ( diff --git a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml index 6067391165..ca8fa535b2 100644 --- a/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml +++ b/packages/pluggableWidgets/datagrid-web/src/Datagrid.xml @@ -446,6 +446,14 @@ Selecteer alle rijen + + Single selection column label + If single selection is enabled, assistive technology will read this for the selection column header. + + Select single row + Selecteer enkele rij + + Selecting all label ARIA label for the progress dialog when selecting all items diff --git a/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx b/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx index 810c64fd3e..be0db25093 100644 --- a/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx +++ b/packages/pluggableWidgets/datagrid-web/src/components/CheckboxColumnHeader.tsx @@ -6,6 +6,8 @@ import { useDatagridConfig, useSelectActions, useSelectionHelper, useTexts } fro export function CheckboxColumnHeader(): ReactElement { const { selectAllCheckboxEnabled, checkboxColumnEnabled } = useDatagridConfig(); + const { singleSelectionColumnLabel } = useTexts(); + const selectionHelper = useSelectionHelper(); if (checkboxColumnEnabled === false) { return ; @@ -16,6 +18,9 @@ export function CheckboxColumnHeader(): ReactElement { + + {singleSelectionColumnLabel || "Select single row"} + ); } diff --git a/packages/pluggableWidgets/datagrid-web/src/components/__tests__/CheckboxColumnHeader.spec.tsx b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/CheckboxColumnHeader.spec.tsx new file mode 100644 index 0000000000..c7a030f12f --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/src/components/__tests__/CheckboxColumnHeader.spec.tsx @@ -0,0 +1,99 @@ +import { render, screen } from "@testing-library/react"; +import { Fragment } from "react"; + +describe("CheckboxColumnHeader", () => { + it("renders sr-only text with correct class and content for single selection", () => { + render( +
+ Select single row +
+ ); + + const srOnlyText = screen.getByText("Select single row"); + expect(srOnlyText).toBeInTheDocument(); + expect(srOnlyText).toHaveClass("sr-only"); + expect(srOnlyText.parentElement).toHaveClass("widget-datagrid-col-select"); + expect(srOnlyText.parentElement).toHaveAttribute("role", "columnheader"); + }); + + it("renders sr-only text with custom label", () => { + render( +
+ Choose one row +
+ ); + + const srOnlyText = screen.getByText("Choose one row"); + expect(srOnlyText).toBeInTheDocument(); + expect(srOnlyText).toHaveClass("sr-only"); + }); + + it("renders multiple column headers with sr-only text for single selection", () => { + render( +
+
+ Select single row +
+
+ Column 1 +
+
+ ); + + const srOnlyText = screen.getByText("Select single row"); + const columnHeader = screen.getByText("Column 1"); + + expect(srOnlyText).toBeInTheDocument(); + expect(columnHeader).toBeInTheDocument(); + expect(srOnlyText).toHaveClass("sr-only"); + expect(columnHeader).not.toHaveClass("sr-only"); + }); + + it("sr-only text has sr-only class applied", () => { + render( +
+ Select single row +
+ ); + + const srOnlyText = screen.getByText("Select single row"); + + // sr-only class typically hides content visually but keeps it available to screen readers + // Check that it has sr-only class applied + expect(srOnlyText).toHaveClass("sr-only"); + }); + + it("renders empty fragment when checkbox is disabled", () => { + const { container } = render(); + + expect(container.firstChild).toBeNull(); + expect(screen.queryByText("Select single row")).not.toBeInTheDocument(); + }); + + it("does not render sr-only text for multi-selection", () => { + render( +
+ +
+ ); + + const checkbox = screen.getByRole("checkbox"); + expect(checkbox).toBeInTheDocument(); + expect(screen.queryByText("Select single row")).not.toBeInTheDocument(); + }); + + it("columnheader contains only sr-only text for single selection", () => { + const { container } = render( +
+ Select single row +
+ ); + + const columnHeader = container.querySelector('[role="columnheader"]'); + const srOnlySpans = columnHeader?.querySelectorAll(".sr-only"); + + expect(columnHeader).toBeInTheDocument(); + expect(srOnlySpans).toHaveLength(1); + expect(srOnlySpans?.[0]).toHaveTextContent("Select single row"); + }); +}); diff --git a/packages/pluggableWidgets/datagrid-web/src/model/services/Texts.service.ts b/packages/pluggableWidgets/datagrid-web/src/model/services/Texts.service.ts index 424ee99b2f..3297e86743 100644 --- a/packages/pluggableWidgets/datagrid-web/src/model/services/Texts.service.ts +++ b/packages/pluggableWidgets/datagrid-web/src/model/services/Texts.service.ts @@ -27,6 +27,10 @@ export class TextsService { return this.props.selectAllRowsLabel?.value; } + get singleSelectionColumnLabel(): string | undefined { + return this.props.singleSelectionColumnLabel?.value; + } + get headerAriaLabel(): string | undefined { return this.props.filterSectionTitle?.value; } diff --git a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts index 46af1c62d3..790d4402f3 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/DatagridProps.d.ts @@ -142,6 +142,7 @@ export interface DatagridContainerProps { cancelExportLabel?: DynamicValue; selectRowLabel?: DynamicValue; selectAllRowsLabel?: DynamicValue; + singleSelectionColumnLabel?: DynamicValue; selectingAllLabel?: DynamicValue; cancelSelectionLabel?: DynamicValue; selectedCountTemplateSingular?: DynamicValue; @@ -208,6 +209,7 @@ export interface DatagridPreviewProps { cancelExportLabel: string; selectRowLabel: string; selectAllRowsLabel: string; + singleSelectionColumnLabel: string; selectingAllLabel: string; cancelSelectionLabel: string; selectedCountTemplateSingular: string; diff --git a/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts b/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts index 94460b547b..0f907db9a7 100644 --- a/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts +++ b/packages/pluggableWidgets/datagrid-web/typings/MainGateProps.ts @@ -36,6 +36,7 @@ export type MainGateProps = Pick< | "selectAllText" | "selectionCounterPosition" | "selectRowLabel" + | "singleSelectionColumnLabel" | "showNumberOfRows" | "showPagingButtons" | "storeFiltersInPersonalization"