Skip to content

Commit a91c3fd

Browse files
olayinkaadelakunviktoravelinoOlayinka Adelakunautofix-ci[bot]
authored
fix(a11y): fix traces page accessibility violations — rowgroup tabbable (#13744)
* chore: add accessibility action plan and gap report for IBM Level 1 compliance * feat: integrate accessibility checker for automated a11y scans - Added `accessibility-checker` package to the project dependencies. - Enhanced test fixtures to include accessibility scanning capabilities. - Implemented `runA11yScan` method in the `LangflowPage` type for running accessibility checks. - Created utility functions for building accessibility scan labels, formatting failure messages, and generating summary attachments. - Updated test setup to manage accessibility sessions and assert compliance based on scan results. * feat: add accessibility test usage documentation for IBM scans * feat: enhance accessibility testing with JSON report aggregation and new tests * feat: add GitHub Actions workflow for IBM accessibility scans * feat: update a11y scan workflow to allow testing from feat/a11y branch * feat: skip Puppeteer download during npm install in Dockerfiles for accessibility testing * feat: enhance a11y scan workflow to resolve and scan latest release branches * test: add component a11y unit tests (jest-axe) (#13613) * test: add component a11y unit tests (jest-axe) Cover 14 components in two waves. 16 tests fail by design - each encodes a known gap from a11y-action-plan (phases 0-3) and flips to a regression lock once the component fix lands. 23 tests pass as regression locks for already-correct semantics. * ci: split a11y unit tests into separate workflow Main jest CI excludes *.a11y.test.* so known-gap failures don't block PRs; new a11y-unit-tests.yml runs them as informational check. * fix(a11y): resolve component gaps flagged by a11y unit tests Fixes all 16 failing jest-axe gap tests: - TableHead defaults scope=col - alert display area gets aria-live region - app header becomes <header> landmark; bell button labeled - icon wrapper decorative by default (aria-hidden), ariaLabel opt-in - Input drops wrapping <label> (name pollution) and hides placeholder span - password toggle back in tab order with aria-label/aria-pressed - CheckBoxDiv exposes checkbox role + state - accordion trigger renders native Radix button - dialogs focus container on open instead of suppressing autofocus - full-screen modal exposes dialog role/aria-modal/ariaLabel - flow list card focusable with keyboard activation Also wraps test focus calls in act() and mocks the icon loader so the a11y suite runs without React warnings. * fix: skip Puppeteer Chrome download in remaining Dockerfiles frontend and base images still ran bare npm install; puppeteer (via accessibility-checker, test-only) tried to fetch Chrome and broke the Docker image build in CI. * test: extend a11y unit test coverage (jest-axe) Adds axe + semantics tests for untested primitives (button, alert, textarea, radio-group, dropdown-menu, tooltip, popover) and two known-gap tests that fail by design until fixes land: - copyFieldAreaComponent copy action is a bare div onClick (plan 2.4) - DialogContentWithouFixed renders an empty fragment (plan 1.6) * fix: make webhook copy field a real button (a11y) Copy action was a bare div onClick - no role, name, or keyboard support (a11y-action-plan 2.4). Drops the no-longer-needed DialogContentWithouFixed known-gap test. * feat: update a11y unit tests and improve accessibility semantics in components * feat: enable pull request trigger for a11y scan workflow * feat: Add a11y regression scan suite (#13663) * test: add component a11y unit tests (jest-axe) Cover 14 components in two waves. 16 tests fail by design - each encodes a known gap from a11y-action-plan (phases 0-3) and flips to a regression lock once the component fix lands. 23 tests pass as regression locks for already-correct semantics. * ci: split a11y unit tests into separate workflow Main jest CI excludes *.a11y.test.* so known-gap failures don't block PRs; new a11y-unit-tests.yml runs them as informational check. * fix(a11y): resolve component gaps flagged by a11y unit tests Fixes all 16 failing jest-axe gap tests: - TableHead defaults scope=col - alert display area gets aria-live region - app header becomes <header> landmark; bell button labeled - icon wrapper decorative by default (aria-hidden), ariaLabel opt-in - Input drops wrapping <label> (name pollution) and hides placeholder span - password toggle back in tab order with aria-label/aria-pressed - CheckBoxDiv exposes checkbox role + state - accordion trigger renders native Radix button - dialogs focus container on open instead of suppressing autofocus - full-screen modal exposes dialog role/aria-modal/ariaLabel - flow list card focusable with keyboard activation Also wraps test focus calls in act() and mocks the icon loader so the a11y suite runs without React warnings. * fix: skip Puppeteer Chrome download in remaining Dockerfiles frontend and base images still ran bare npm install; puppeteer (via accessibility-checker, test-only) tried to fetch Chrome and broke the Docker image build in CI. * test: extend a11y unit test coverage (jest-axe) Adds axe + semantics tests for untested primitives (button, alert, textarea, radio-group, dropdown-menu, tooltip, popover) and two known-gap tests that fail by design until fixes land: - copyFieldAreaComponent copy action is a bare div onClick (plan 2.4) - DialogContentWithouFixed renders an empty fragment (plan 1.6) * fix: make webhook copy field a real button (a11y) Copy action was a bare div onClick - no role, name, or keyboard support (a11y-action-plan 2.4). Drops the no-longer-needed DialogContentWithouFixed known-gap test. * feat: update a11y unit tests and improve accessibility semantics in components * feat: enable pull request trigger for a11y scan workflow * test: add a11y regression scan suite * test: make a11y scans non-blocking * test: discover a11y scan specs * ci: group a11y workflow checks * fix(a11y): fix traces page accessibility violations — rowgroup tabbable, button name, and heading order * [autofix.ci] apply automated fixes * fix lighthouse errors on panel * [autofix.ci] apply automated fixes * fix(a11y): restore focus-visible indicators (#13664) * fix(a11y): restore focus-visible indicators * [autofix.ci] apply automated fixes --------- Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com> * a11y: improve auth accessibility coverage (#13724) * fix: improve auth accessibility coverage * fix(a11y): restore delete trigger tab focus * test(dialog): cover nested description * [autofix.ci] apply automated fixes * fix generic typing * remove uneccesary files * fix(traces): restore table a11y * Fix a11y(trace): fix WCAG AA contrast, ARIA tree widget, and hook decomposition for Trace Detail panel (#13777) * a11y: fix WCAG AA contrast and icon accessibility violations * a11y: refactor Trace panel into focused hooks with ARIA tree and WCAG fixes * [autofix.ci] apply automated fixes * fix(a11y): fix stale span tree focus, replace data-testid DOM queries with ref map, and make code tab light theme explici * [autofix.ci] apply automated fixes * fix: reorder import statements for consistency --------- Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com> Co-authored-by: Viktor Avelino <viktor.avelino@gmail.com> --------- Co-authored-by: Viktor Avelino <viktor.avelino@gmail.com> Co-authored-by: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com> Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 538506a commit a91c3fd

28 files changed

Lines changed: 1402 additions & 101 deletions

src/frontend/src/components/core/codeTabsComponent/index.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { useState } from "react";
2+
import { useTranslation } from "react-i18next";
23
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
3-
import {
4-
oneDark,
5-
oneLight,
6-
} from "react-syntax-highlighter/dist/cjs/styles/prism";
4+
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
5+
import a11yLight from "react-syntax-highlighter/dist/cjs/styles/prism/a11y-one-light";
76
import { useDarkStore } from "@/stores/darkStore";
87
import IconComponent from "../../common/genericIconComponent";
98
import { Button } from "../../ui/button";
@@ -12,13 +11,19 @@ type SimplifiedCodeTabProps = {
1211
code: string;
1312
language: string;
1413
maxHeight?: string;
14+
// Override the light-mode syntax theme. Defaults to a11yLight (WCAG-compliant
15+
// contrast on bg-canvas). Pass a different prism style object if your call
16+
// site uses a background where a11yLight's palette causes contrast issues.
17+
lightStyle?: { [key: string]: React.CSSProperties };
1518
};
1619

1720
export default function SimplifiedCodeTabComponent({
1821
code,
1922
language,
2023
maxHeight,
24+
lightStyle = a11yLight,
2125
}: SimplifiedCodeTabProps) {
26+
const { t } = useTranslation();
2227
const [isCopied, setIsCopied] = useState<boolean>(false);
2328
const dark = useDarkStore((state) => state.dark);
2429

@@ -51,6 +56,7 @@ export default function SimplifiedCodeTabComponent({
5156
className="text-muted-foreground hover:bg-card"
5257
data-testid="copy-code-button"
5358
onClick={copyToClipboard}
59+
aria-label={isCopied ? t("chat.copiedCode") : t("chat.copyCode")}
5460
>
5561
{isCopied ? (
5662
<IconComponent name="Check" className="h-4 w-4" />
@@ -61,7 +67,7 @@ export default function SimplifiedCodeTabComponent({
6167
</div>
6268
<SyntaxHighlighter
6369
language={language.toLowerCase()}
64-
style={dark ? oneDark : oneLight}
70+
style={dark ? oneDark : lightStyle}
6571
className="!mt-0 h-full w-full overflow-auto !bg-canvas [&_code]:!bg-canvas !rounded-b-md !rounded-t-none border border-border text-left custom-scroll"
6672
customStyle={maxHeight ? { maxHeight } : undefined}
6773
>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { render, waitFor } from "@testing-library/react";
2+
import type { ForwardedRef } from "react";
3+
import TableComponent from "../index";
4+
5+
const mockSetGridAriaProperty = jest.fn();
6+
const mockApi = {
7+
applyColumnState: jest.fn(),
8+
getColumnDefs: jest.fn(() => []),
9+
getColumnState: jest.fn(() => []),
10+
getColumns: jest.fn(() => []),
11+
getSelectedRows: jest.fn(() => []),
12+
hideOverlay: jest.fn(),
13+
isDestroyed: jest.fn(() => false),
14+
setGridAriaProperty: mockSetGridAriaProperty,
15+
setGridOption: jest.fn(),
16+
sizeColumnsToFit: jest.fn(),
17+
};
18+
19+
jest.mock("ag-grid-react", () => {
20+
const React = require("react");
21+
type MockGridReadyParams = {
22+
api: typeof mockApi;
23+
columnApi: { getAllGridColumns: jest.Mock };
24+
};
25+
type MockAgGridProps = {
26+
onGridReady?: (params: MockGridReadyParams) => void;
27+
};
28+
29+
return {
30+
AgGridReact: React.forwardRef(
31+
(props: MockAgGridProps, ref: ForwardedRef<{ api: typeof mockApi }>) => {
32+
React.useImperativeHandle(ref, () => ({ api: mockApi }));
33+
React.useEffect(() => {
34+
props.onGridReady?.({
35+
api: mockApi,
36+
columnApi: { getAllGridColumns: jest.fn(() => []) },
37+
});
38+
}, [props.onGridReady]);
39+
40+
return <div role="treegrid" />;
41+
},
42+
),
43+
};
44+
});
45+
46+
describe("TableComponent accessibility", () => {
47+
beforeEach(() => {
48+
jest.clearAllMocks();
49+
});
50+
51+
it("applies aria-label to the ag-grid panel", async () => {
52+
render(
53+
<TableComponent
54+
aria-label="Trace runs table"
55+
columnDefs={[{ field: "name" }]}
56+
rowData={[{ name: "Run 1" }]}
57+
/>,
58+
);
59+
60+
await waitFor(() => {
61+
expect(mockSetGridAriaProperty).toHaveBeenCalledWith(
62+
"label",
63+
"Trace runs table",
64+
);
65+
});
66+
});
67+
});

src/frontend/src/components/core/parameterRenderComponent/components/tableComponent/index.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,15 @@ import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the
1111
import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid
1212
import { AgGridReact, type AgGridReactProps } from "ag-grid-react";
1313
import cloneDeep from "lodash";
14-
import { type ElementRef, forwardRef, useRef, useState } from "react";
14+
import {
15+
type ElementRef,
16+
forwardRef,
17+
useEffect,
18+
useRef,
19+
useState,
20+
} from "react";
1521
import TableOptions from "./components/TableOptions";
22+
import { applyRowTabIndices } from "./utils/applyRowTabIndices";
1623
import resetGrid from "./utils/reset-grid-columns";
1724

1825
export interface TableComponentProps extends AgGridReactProps {
@@ -264,9 +271,11 @@ const TableComponent = forwardRef<
264271
// @ts-ignore
265272
const realRef: React.MutableRefObject<AgGridReact> =
266273
useRef<AgGridReact | null>(null);
274+
const containerRef = useRef<HTMLDivElement>(null);
267275
const dark = useDarkStore((state) => state.dark);
268276
const initialColumnDefs = useRef(colDef);
269277
const [columnStateChange, setColumnStateChange] = useState(false);
278+
const ariaLabel = props["aria-label"] as string | undefined;
270279
// Only use visible columns for the store reference
271280
const storeReference = props.columnDefs
272281
.filter((col) => !col.hide)
@@ -276,6 +285,7 @@ const TableComponent = forwardRef<
276285
const onGridReady = (params) => {
277286
// @ts-ignore
278287
realRef.current = params;
288+
params.api.setGridAriaProperty("label", ariaLabel ?? null);
279289
const updatedColumnDefs = [...colDef];
280290
params.api.setGridOption("columnDefs", updatedColumnDefs);
281291
const customInit = localStorage.getItem(storeReference);
@@ -302,6 +312,10 @@ const TableComponent = forwardRef<
302312
}, 1000);
303313
if (props.onGridReady) props.onGridReady(params);
304314
};
315+
316+
useEffect(() => {
317+
realRef.current?.api?.setGridAriaProperty("label", ariaLabel ?? null);
318+
}, [ariaLabel]);
305319
const onColumnMoved = (params) => {
306320
const updatedColumnDefs = cloneDeep(
307321
params.columnApi.getAllGridColumns().map((col) => col.getColDef()),
@@ -382,6 +396,7 @@ const TableComponent = forwardRef<
382396

383397
return (
384398
<div
399+
ref={containerRef}
385400
className={cn(
386401
dark ? "ag-theme-quartz-dark" : "ag-theme-quartz",
387402
"ag-theme-shadcn flex h-full flex-col",
@@ -494,6 +509,14 @@ const TableComponent = forwardRef<
494509
setColumnStateChange(true);
495510
}
496511
}}
512+
onFirstDataRendered={(e) => {
513+
applyRowTabIndices(containerRef.current);
514+
props.onFirstDataRendered?.(e);
515+
}}
516+
onRowDataUpdated={(e) => {
517+
applyRowTabIndices(containerRef.current);
518+
props.onRowDataUpdated?.(e);
519+
}}
497520
/>
498521
{!props.tableOptions?.hide_options && props.pagination && (
499522
<TableOptions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function applyRowTabIndices(containerEl: HTMLElement | null): void {
2+
if (!containerEl) return;
3+
containerEl
4+
.querySelectorAll<HTMLElement>(".ag-center-cols-container [role='row']")
5+
.forEach((row, idx) => {
6+
row.setAttribute("tabindex", idx === 0 ? "0" : "-1");
7+
});
8+
}

src/frontend/src/components/ui/alert.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ const Alert = React.forwardRef<
3232
Alert.displayName = "Alert";
3333

3434
const AlertTitle = React.forwardRef<
35-
HTMLParagraphElement,
36-
React.HTMLAttributes<HTMLHeadingElement>
37-
>(({ className, ...props }, ref) => (
38-
<h5
35+
HTMLElement,
36+
React.HTMLAttributes<HTMLElement> & { as?: React.ElementType }
37+
>(({ className, as: Comp = "div", ...props }, ref) => (
38+
<Comp
3939
ref={ref}
4040
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
4141
{...props}

src/frontend/src/components/ui/dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ const DialogContent = React.forwardRef<
142142
closeButtonClassName,
143143
)}
144144
>
145-
<Cross2Icon className="h-[18px] w-[18px]" />
145+
<Cross2Icon className="h-[18px] w-[18px]" aria-hidden={true} />
146146
<span className="sr-only">Close</span>
147147
</DialogPrimitive.Close>
148148
</ShadTooltip>

src/frontend/src/locales/de.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@
218218
"chat.cannotOpenDescription": "Dies ist kein Chat-Ablauf.",
219219
"chat.cannotOpenTitle": "Der Chat lässt sich nicht öffnen",
220220
"chat.copied": "Kopiert!",
221+
"chat.copyCode": "Copy code",
222+
"chat.copiedCode": "Code copied",
221223
"chat.copyMessage": "Nachricht kopieren",
222224
"chat.defaultSession": "Standardsitzung",
223225
"chat.deleteSessionsCount_one": "{{count}} -Sitzung löschen",
@@ -1957,6 +1959,10 @@
19571959
"trace.clearAll": "Alles löschen",
19581960
"trace.clearAllConfirm": "Möchten Sie wirklich alle Datensätze löschen? Dadurch werden alle zugehörigen Flow-Aktivitätsverläufe endgültig gelöscht; dieser Vorgang kann nicht rückgängig gemacht werden.",
19591961
"trace.clearAllRecords": "Alle Datensätze löschen",
1962+
"trace.tableAriaLabel": "Trace runs table",
1963+
"trace.statusFilterLabel": "Filter by status",
1964+
"trace.expandSpan": "Expand span",
1965+
"trace.collapseSpan": "Collapse span",
19601966
"trace.clearDates": "Termine löschen",
19611967
"trace.clearError": "Fehler beim Löschen von Datensätzen",
19621968
"trace.clearedSuccess": "Datensätze wurden erfolgreich gelöscht",

src/frontend/src/locales/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,10 @@
13461346
"trace.dateRangeLabel": "{{start}} - {{end}}",
13471347
"trace.invalidDateRangeMessage": "End date cannot be earlier than start date.",
13481348
"trace.clearDates": "Clear dates",
1349+
"trace.tableAriaLabel": "Trace runs table",
1350+
"trace.statusFilterLabel": "Filter by status",
1351+
"trace.expandSpan": "Expand span",
1352+
"trace.collapseSpan": "Collapse span",
13491353
"flow.restoreVersion": "Restore this version of your flow",
13501354
"flow.saveVersion": "Save a version of your flow",
13511355
"flow.moreOptions": "More options",
@@ -1520,6 +1524,8 @@
15201524
"chat.options": "Options",
15211525
"chat.editMessage": "Edit message",
15221526
"chat.copied": "Copied!",
1527+
"chat.copyCode": "Copy code",
1528+
"chat.copiedCode": "Code copied",
15231529
"chat.copyMessage": "Copy message",
15241530
"chat.helpful": "Helpful",
15251531
"chat.notHelpful": "Not helpful",

src/frontend/src/locales/es.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@
218218
"chat.cannotOpenDescription": "Esto no es un flujo de chat.",
219219
"chat.cannotOpenTitle": "No se puede abrir el chat",
220220
"chat.copied": "Copiado",
221+
"chat.copyCode": "Copy code",
222+
"chat.copiedCode": "Code copied",
221223
"chat.copyMessage": "Copiar mensaje",
222224
"chat.defaultSession": "Sesión predeterminada",
223225
"chat.deleteSessionsCount_one": "Eliminar una sesión de « {{count}} »",
@@ -1957,6 +1959,10 @@
19571959
"trace.clearAll": "Borrar todo",
19581960
"trace.clearAllConfirm": "¿Estás seguro de que quieres borrar todos los registros? Esto eliminará de forma permanente todos los registros de actividad de Flow relacionados y no se podrá deshacer.",
19591961
"trace.clearAllRecords": "Borrar todos los registros",
1962+
"trace.tableAriaLabel": "Trace runs table",
1963+
"trace.statusFilterLabel": "Filter by status",
1964+
"trace.expandSpan": "Expand span",
1965+
"trace.collapseSpan": "Collapse span",
19601966
"trace.clearDates": "Borrar fechas",
19611967
"trace.clearError": "Error al borrar registros",
19621968
"trace.clearedSuccess": "Los registros se han borrado correctamente",

src/frontend/src/locales/fr.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@
218218
"chat.cannotOpenDescription": "Ce n'est pas un flux de conversation.",
219219
"chat.cannotOpenTitle": "Impossible d'ouvrir la discussion",
220220
"chat.copied": "Copié",
221+
"chat.copyCode": "Copy code",
222+
"chat.copiedCode": "Code copied",
221223
"chat.copyMessage": "Copier le message",
222224
"chat.defaultSession": "Session par défaut",
223225
"chat.deleteSessionsCount_one": "Supprimer la session d' {{count}}",
@@ -1957,6 +1959,10 @@
19571959
"trace.clearAll": "Effacer tout",
19581960
"trace.clearAllConfirm": "Êtes-vous sûr de vouloir effacer tous les enregistrements? Cette opération supprimera définitivement toutes les traces d'activité de flux associées et ne pourra pas être annulée.",
19591961
"trace.clearAllRecords": "Effacer tous les enregistrements",
1962+
"trace.tableAriaLabel": "Trace runs table",
1963+
"trace.statusFilterLabel": "Filter by status",
1964+
"trace.expandSpan": "Expand span",
1965+
"trace.collapseSpan": "Collapse span",
19601966
"trace.clearDates": "Effacer les dates",
19611967
"trace.clearError": "Erreur lors de la suppression des enregistrements",
19621968
"trace.clearedSuccess": "Les enregistrements ont été effacés avec succès",

0 commit comments

Comments
 (0)