Skip to content

feat(a11y): add accessible names for canvas handles, edges, and toolbar controls - #14209

Open
olayinkaadelakun wants to merge 1 commit into
release-1.12.0from
LE-1514-A
Open

feat(a11y): add accessible names for canvas handles, edges, and toolbar controls#14209
olayinkaadelakun wants to merge 1 commit into
release-1.12.0from
LE-1514-A

Conversation

@olayinkaadelakun

@olayinkaadelakun olayinkaadelakun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds accessible names for the flow canvas's connection handles, edges, and three buttons in the canvas controls panel (CanvasControls.tsx, the bottom-center zoom/assistant/sticky-note/minimize-all panel) that only had a title attribute before. This is PR 1 of 2 in the flow canvas accessibility remediation (WCAG 1.1.1 Non-text Content) — canvas keyboard navigation (2.1.1) is scoped to a separate follow-up PR. The per-node toolbar (nodeToolbarComponent) is not touched in this PR.

  • handleRenderComponent: connection handles now expose role="button" + aria-label ("Input/Output handle for {field} on node {nodeId}")
  • CustomEdges/DefaultEdge: edges now expose role="img" + aria-label ("Edge from {source} to {target}"), using each node's display_name with a raw-id fallback
  • CanvasControls: assistant-button, canvas-add-note-button, and canvas_controls_minimize_all now have aria-label in addition to their existing title
  • New i18n keys handle.ariaLabelInput, handle.ariaLabelOutput, edge.ariaLabel added to en.json and translated into all 6 supported locales via the GP translation pipeline
  • Verified (no code change needed): MainPage flow list cards are already fully keyboard-accessible (existing list.a11y.test.tsx passes), and the lastSelection store the follow-up keyboard-nav PR depends on is already wired to ReactFlow's native onSelectionChange

Why

The canvas's connection points and relationships were unidentifiable to screen readers — handles and edges rendered with no accessible name at all. This is the additive, low-risk half of the remediation; the higher-risk canvas keyboard re-enablement (disableKeyboardA11y) is intentionally deferred to its own PR so it can be reviewed and tested in isolation.

Test plan

  • npx jest — new handleRenderComponent.a11y.test.tsx, CustomEdges.a11y.test.tsx, CanvasControls.a11y.test.tsx (jest-axe, zero violations) + full existing suite passing
  • npx biome check / npx tsc --noEmit — clean
  • Extended tests/core/unit/customEdgesComponent.spec.ts (Playwright) to assert handles/edges are reachable by accessible name, not just testid
  • Re-run IBM scan (RUN_A11Y=true RUN_A11Y_ASSERT=true npx playwright test tests/a11y/core-pages.a11y.spec.ts --project=chromium) and regenerate the flow-canvas-populated baseline — not run in this PR, needs the full app running

Summary by CodeRabbit

  • Accessibility

    • Improved screen-reader support for canvas edges, node handles, and canvas controls with descriptive, localized labels.
    • Added accessible names for assistant, sticky note, and expand/minimize controls, including read-only states.
    • Edge labels now identify their source and destination nodes.
  • Localization

    • Added accessibility translations across supported languages.
  • Tests

    • Added automated accessibility coverage for edges, handles, and canvas controls.
    • Expanded interaction tests to verify accessible names for handles and created edges.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Canvas accessibility

Layer / File(s) Summary
Localized accessibility label contracts
src/frontend/src/locales/*.json
Adds localized labels for canvas controls, input/output handles, and source-to-target edges.
Node handle accessible names
src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/...
Adds localized input/output handle labels with button semantics and accessibility tests for regular and minimized states.
Edge accessible names
src/frontend/src/CustomEdges/..., src/frontend/tests/core/unit/customEdgesComponent.spec.ts
Labels edges using node display names or ids, exposes them as images, and tests edge accessibility.
Canvas control accessible names
src/frontend/src/components/core/canvasControlsComponent/...
Adds state-aware labels to assistant, sticky-note, and minimize/expand controls with Axe and role/name tests.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • langflow-ai/langflow#14078: Both changes modify handleRenderComponent; this PR adds localized accessibility labels while the other adjusts minimized handle positioning.

Suggested reviewers: cristhianzl

🚥 Pre-merge checks | ✅ 8 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Test Quality And Coverage ⚠️ Warning Key coverage gaps remain: the assistant label isn’t asserted, handle tests use label queries instead of role, and the Playwright edge check isn’t tied to the specific elements. Add direct role/name assertions for the assistant, handles, and the created edge; bind Playwright checks to the exact connected elements and cover locked/unlocked variants.
✅ Passed checks (8 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main accessibility change across canvas handles, edges, and toolbar controls.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Coverage For New Implementations ✅ Passed PR adds real RTL a11y tests for handles/edges/canvas controls plus a Playwright spec for edge/handle names; filenames follow frontend test conventions.
Test File Naming And Structure ✅ Passed PASS: The new .a11y.test.tsx suites and Playwright .spec.ts match existing frontend conventions, use descriptive names, and include logical setup plus edge-case coverage.
Excessive Mock Usage Warning ✅ Passed PASS: The new tests mostly mock external hooks/assets to isolate rendering, while still exercising real DOM roles, names, and axe checks; no core logic is mocked.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch LE-1514-A

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
src/frontend/src/locales/de.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

src/frontend/src/locales/en.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

src/frontend/src/locales/es.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

  • 4 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the enhancement New feature or request label Jul 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Test Coverage Advisor

No source changes detected without accompanying tests. Thanks for keeping coverage up! 🎉

Advisory check only — never blocks merge.

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jul 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/frontend/src/components/core/canvasControlsComponent/__tests__/CanvasControls.a11y.test.tsx (1)

57-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the production button in the accessibility test.

Mocking Button as a native <button> hides the component whose forwarding and disabled semantics this Axe suite is meant to validate. Keep only infrastructure mocks and render the real button primitive.

As per coding guidelines, warn when frontend tests rely on excessive mocks that obscure what is actually being tested.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/components/core/canvasControlsComponent/__tests__/CanvasControls.a11y.test.tsx`
around lines 57 - 63, Remove the `@/components/ui/button` mock from
CanvasControls.a11y.test.tsx so the accessibility suite renders the production
Button component and validates its prop forwarding and disabled behavior. Keep
only mocks required for unrelated test infrastructure.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/frontend/src/components/core/canvasControlsComponent/__tests__/CanvasControls.a11y.test.tsx`:
- Around line 112-137: Add direct accessibility assertions for the assistant
control in the CanvasControls tests. Verify the assistant element exposes
“Langflow Assistant” when unlocked and “(Read-Only)” when rendered with
effectiveLocked, using the assistant control’s existing test identifier or role.

In
`@src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/__tests__/handleRenderComponent.a11y.test.tsx`:
- Around line 58-62: Update the three accessibility assertions in the
handleRenderComponent tests to use screen.getByRole with role "button" and the
existing accessible names, replacing getByLabelText for the input, output, and
minimized-handle cases.

In `@src/frontend/src/locales/es.json`:
- Line 2184: Correct the localized accessible-name templates: in
src/frontend/src/locales/es.json lines 2184-2184, update handle.ariaLabelOutput
to use a valid output-handle term such as “Conector de salida”; in
src/frontend/src/locales/fr.json lines 2185-2185, replace the malformed “Passage
d' {{source}} à {{target}}” template with grammatically valid French such as
“Connexion de {{source}} à {{target}}”.

In `@src/frontend/src/locales/ja.json`:
- Line 2185: Fully localize the edge accessible-name templates by replacing the
English “Edge” term with the appropriate localized graph term: use “エッジ” in
src/frontend/src/locales/ja.json at lines 2185-2185, “aresta” in
src/frontend/src/locales/pt.json at lines 2185-2185, and “边” in
src/frontend/src/locales/zh-Hans.json at lines 2185-2185. Preserve the existing
{{source}} and {{target}} placeholders.

In `@src/frontend/tests/core/unit/customEdgesComponent.spec.ts`:
- Around line 65-78: Update the accessibility assertions in the custom edge test
to scope handle role/name checks directly to the connected elements represented
by urlOutput and toolCallingAgentInput, rather than using global queries with
first(). Target the edge created by the current interaction explicitly instead
of selecting the last matching edge, while preserving the existing
accessible-name expectations.

---

Nitpick comments:
In
`@src/frontend/src/components/core/canvasControlsComponent/__tests__/CanvasControls.a11y.test.tsx`:
- Around line 57-63: Remove the `@/components/ui/button` mock from
CanvasControls.a11y.test.tsx so the accessibility suite renders the production
Button component and validates its prop forwarding and disabled behavior. Keep
only mocks required for unrelated test infrastructure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0b45ef76-5fae-4ff0-bd3a-c4e6a1c4f258

📥 Commits

Reviewing files that changed from the base of the PR and between 43e7ee6 and 1759c94.

📒 Files selected for processing (14)
  • src/frontend/src/CustomEdges/__tests__/CustomEdges.a11y.test.tsx
  • src/frontend/src/CustomEdges/index.tsx
  • src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/__tests__/handleRenderComponent.a11y.test.tsx
  • src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx
  • src/frontend/src/components/core/canvasControlsComponent/CanvasControls.tsx
  • src/frontend/src/components/core/canvasControlsComponent/__tests__/CanvasControls.a11y.test.tsx
  • src/frontend/src/locales/de.json
  • src/frontend/src/locales/en.json
  • src/frontend/src/locales/es.json
  • src/frontend/src/locales/fr.json
  • src/frontend/src/locales/ja.json
  • src/frontend/src/locales/pt.json
  • src/frontend/src/locales/zh-Hans.json
  • src/frontend/tests/core/unit/customEdgesComponent.spec.ts

Comment on lines +112 to +137
it("should_expose_accessible_name_for_add_note_button", () => {
render(<CanvasControls selectedNode={null} />);

expect(
screen.getByRole("button", { name: "Add Sticky Note" }),
).toBeInTheDocument();
});

it("should_expose_accessible_name_for_minimize_all_button", () => {
render(<CanvasControls selectedNode={null} />);

expect(
screen.getByRole("button", { name: "Minimize all" }),
).toBeInTheDocument();
});

it("should_expose_read_only_accessible_name_when_locked", () => {
render(<CanvasControls selectedNode={null} effectiveLocked />);

expect(screen.getByTestId("canvas-add-note-button")).toHaveAccessibleName(
"(Read-Only)",
);
expect(
screen.getByTestId("canvas_controls_minimize_all"),
).toHaveAccessibleName("(Read-Only)");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add direct coverage for the assistant label.

The assistant’s new aria-label is never asserted. Axe would still pass if it were removed because the nested image already supplies an accessible name. Assert "Langflow Assistant" when unlocked and "(Read-Only)" when locked.

Proposed coverage
+  it("should_expose_accessible_name_for_assistant_button", () => {
+    render(<CanvasControls selectedNode={null} />);
+
+    expect(
+      screen.getByRole("button", { name: "Langflow Assistant" }),
+    ).toBeInTheDocument();
+  });
+
   it("should_expose_read_only_accessible_name_when_locked", () => {
     render(<CanvasControls selectedNode={null} effectiveLocked />);
 
+    expect(screen.getByTestId("assistant-button")).toHaveAccessibleName(
+      "(Read-Only)",
+    );

As per coding guidelines, frontend tests must verify meaningful behavior for new functionality rather than only smoke-testing it.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("should_expose_accessible_name_for_add_note_button", () => {
render(<CanvasControls selectedNode={null} />);
expect(
screen.getByRole("button", { name: "Add Sticky Note" }),
).toBeInTheDocument();
});
it("should_expose_accessible_name_for_minimize_all_button", () => {
render(<CanvasControls selectedNode={null} />);
expect(
screen.getByRole("button", { name: "Minimize all" }),
).toBeInTheDocument();
});
it("should_expose_read_only_accessible_name_when_locked", () => {
render(<CanvasControls selectedNode={null} effectiveLocked />);
expect(screen.getByTestId("canvas-add-note-button")).toHaveAccessibleName(
"(Read-Only)",
);
expect(
screen.getByTestId("canvas_controls_minimize_all"),
).toHaveAccessibleName("(Read-Only)");
});
it("should_expose_accessible_name_for_assistant_button", () => {
render(<CanvasControls selectedNode={null} />);
expect(
screen.getByRole("button", { name: "Langflow Assistant" }),
).toBeInTheDocument();
});
it("should_expose_read_only_accessible_name_when_locked", () => {
render(<CanvasControls selectedNode={null} effectiveLocked />);
expect(screen.getByTestId("assistant-button")).toHaveAccessibleName(
"(Read-Only)",
);
expect(screen.getByTestId("canvas-add-note-button")).toHaveAccessibleName(
"(Read-Only)",
);
expect(
screen.getByTestId("canvas_controls_minimize_all"),
).toHaveAccessibleName("(Read-Only)");
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/components/core/canvasControlsComponent/__tests__/CanvasControls.a11y.test.tsx`
around lines 112 - 137, Add direct accessibility assertions for the assistant
control in the CanvasControls tests. Verify the assistant element exposes
“Langflow Assistant” when unlocked and “(Read-Only)” when rendered with
effectiveLocked, using the assistant control’s existing test identifier or role.

Source: Coding guidelines

Comment on lines +58 to +62
expect(
screen.getByLabelText(
"Input handle for Message on node ChatInput-abc123",
),
).toBeInTheDocument();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the required button role as well as the accessible name.

getByLabelText does not require the matched element to have role="button", so a regression removing the handle role could still pass these tests. Use getByRole("button", { name }) for all three assertions.

Proposed assertion update
-      screen.getByLabelText(
-        "Input handle for Message on node ChatInput-abc123",
-      ),
+      screen.getByRole("button", {
+        name: "Input handle for Message on node ChatInput-abc123",
+      }),

Apply the same change to the output and minimized-handle assertions.

Also applies to: 70-74, 89-93

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/__tests__/handleRenderComponent.a11y.test.tsx`
around lines 58 - 62, Update the three accessibility assertions in the
handleRenderComponent tests to use screen.getByRole with role "button" and the
existing accessible names, replacing getByLabelText for the input, output, and
minimized-handle cases.

"canvasControls.expandAll": "Expandir todo"
"canvasControls.expandAll": "Expandir todo",
"handle.ariaLabelInput": "Identificador de entrada para « {{field}} » en el nodo « {{node}} »",
"handle.ariaLabelOutput": "Mánula de salida para {{field}} en el nodo {{node}}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct malformed localized accessible names.

These strings are read as the controls’ accessible names, so the typo and malformed French elision reach screen-reader users.

  • src/frontend/src/locales/es.json#L2184-L2184: replace Mánula with a valid output-handle term, such as Conector de salida.
  • src/frontend/src/locales/fr.json#L2185-L2185: replace Passage d' {{source}} à {{target}} with a grammatically valid template, such as Connexion de {{source}} à {{target}}.
📍 Affects 2 files
  • src/frontend/src/locales/es.json#L2184-L2184 (this comment)
  • src/frontend/src/locales/fr.json#L2185-L2185
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/locales/es.json` at line 2184, Correct the localized
accessible-name templates: in src/frontend/src/locales/es.json lines 2184-2184,
update handle.ariaLabelOutput to use a valid output-handle term such as
“Conector de salida”; in src/frontend/src/locales/fr.json lines 2185-2185,
replace the malformed “Passage d' {{source}} à {{target}}” template with
grammatically valid French such as “Connexion de {{source}} à {{target}}”.

"canvasControls.expandAll": "すべて展開",
"handle.ariaLabelInput": "ノード「 {{node}} 」上の {{field}} 用の入力ハンドル",
"handle.ariaLabelOutput": "ノード「 {{node}} 」上の {{field}} の出力ハンドル",
"edge.ariaLabel": "{{source}} から {{target}} への Edge"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fully localize the edge accessible-name templates.

The localized templates still announce the English word “Edge,” producing mixed-language screen-reader output.

  • src/frontend/src/locales/ja.json#L2185-L2185: replace Edge with the Japanese graph term, e.g. エッジ.
  • src/frontend/src/locales/pt.json#L2185-L2185: replace Edge with the Portuguese graph term, e.g. aresta.
  • src/frontend/src/locales/zh-Hans.json#L2185-L2185: replace Edge with the Simplified Chinese graph term, e.g. .
📍 Affects 3 files
  • src/frontend/src/locales/ja.json#L2185-L2185 (this comment)
  • src/frontend/src/locales/pt.json#L2185-L2185
  • src/frontend/src/locales/zh-Hans.json#L2185-L2185
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/locales/ja.json` at line 2185, Fully localize the edge
accessible-name templates by replacing the English “Edge” term with the
appropriate localized graph term: use “エッジ” in src/frontend/src/locales/ja.json
at lines 2185-2185, “aresta” in src/frontend/src/locales/pt.json at lines
2185-2185, and “边” in src/frontend/src/locales/zh-Hans.json at lines 2185-2185.
Preserve the existing {{source}} and {{target}} placeholders.

Comment on lines +65 to +78
// The handles resolved above by testid must also be reachable by their
// accessible name — confirms the aria-label targets the same element.
await expect(
page.getByRole("button", { name: /Output handle for Toolset/ }).first(),
).toHaveCount(1);
await expect(
page.getByRole("button", { name: /Input handle for Tools/ }).first(),
).toHaveCount(1);

// The newly created edge exposes an accessible name identifying the
// connection's source and target.
await expect(
page.getByRole("img", { name: /^Edge from .* to .*/ }).last(),
).toBeVisible();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Bind accessibility assertions to the connected elements.

These fresh global role queries, combined with .first()/.last(), can pass for another matching handle or edge. Assert the role/name on urlOutput and toolCallingAgentInput, and target the edge created by this interaction.

As per coding guidelines, frontend tests should verify meaningful behavior rather than only smoke-testing it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/tests/core/unit/customEdgesComponent.spec.ts` around lines 65 -
78, Update the accessibility assertions in the custom edge test to scope handle
role/name checks directly to the connected elements represented by urlOutput and
toolCallingAgentInput, rather than using global queries with first(). Target the
edge created by the current interaction explicitly instead of selecting the last
matching edge, while preserving the existing accessible-name expectations.

Source: Coding guidelines

@github-actions

Copy link
Copy Markdown
Contributor

Frontend Unit Test Coverage Report

Coverage Summary

Lines Statements Branches Functions
Coverage: 46%
46.8% (66869/142881) 70.13% (9392/13392) 45.28% (1538/3396)

Unit Test Results

Tests Skipped Failures Errors Time
5343 0 💤 0 ❌ 0 🔥 19m 47s ⏱️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant