Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Regression test for IBM Equal Access `text_contrast_sufficient` violation:
* preset-colored note backgrounds (amber/rose/lime/blue/neutral) previously fell
* back to `text-muted-foreground` in light mode with no override, which fails
* WCAG AA contrast against light pastel backgrounds (e.g. 3.98:1 on amber vs the
* 4.5:1 required). The fix adds a `text-foreground` override for presets in light
* mode, alongside the pre-existing `dark:` overrides.
*/
import { render } from "@testing-library/react";
import { COLOR_OPTIONS } from "@/constants/constants";
import type { NoteDataType } from "@/types/flow";
import NoteNode from "../index";

jest.mock("@xyflow/react", () => ({
NodeResizer: () => null,
}));

jest.mock("@/contexts/permissionsContext", () => ({
useIsFlowReadOnly: () => false,
}));

jest.mock("@/controllers/API/queries/flows/use-get-note-translations", () => ({
useGetNoteTranslationsQuery: () => ({ data: undefined }),
}));

jest.mock("@/stores/flowStore", () => ({
__esModule: true,
default: (selector: (state: Record<string, unknown>) => unknown) =>
selector({
currentFlow: { id: "flow-1", data: { nodes: [] } },
setNode: jest.fn(),
}),
syncNoteTranslations: jest.fn(),
}));

jest.mock("../NoteToolbarComponent", () => ({
__esModule: true,
default: () => null,
}));

let lastNodeDescriptionProps: Record<string, unknown> = {};
jest.mock("../../GenericNode/components/NodeDescription", () => ({
__esModule: true,
default: (props: Record<string, unknown>) => {
lastNodeDescriptionProps = props;
return null;
},
}));

describe("NoteNode text contrast on preset backgrounds", () => {
beforeEach(() => {
lastNodeDescriptionProps = {};
});

Object.keys(COLOR_OPTIONS)
.filter((color) => COLOR_OPTIONS[color] !== null)
.forEach((color) => {
it(`applies a light-mode text-foreground override for the "${color}" preset`, () => {
const data = {
id: "note-1",
node: {
description: "Some note text",
template: { backgroundColor: color },
},
} as unknown as NoteDataType;

render(<NoteNode data={data} />);

expect(lastNodeDescriptionProps.inputClassName).toContain(
"text-foreground",
);
expect(lastNodeDescriptionProps.mdClassName).toContain(
"text-foreground",
);
// Dark-mode override must be preserved alongside the new light-mode fix.
expect(lastNodeDescriptionProps.inputClassName).toContain(
"dark:text-background",
);
expect(lastNodeDescriptionProps.mdClassName).toContain(
"dark:!text-background",
);
});
});

it("leaves the transparent/no-background preset without a forced text color", () => {
const data = {
id: "note-1",
node: {
description: "Some note text",
template: { backgroundColor: "transparent" },
},
} as unknown as NoteDataType;

render(<NoteNode data={data} />);

expect(lastNodeDescriptionProps.inputClassName).not.toContain(
"text-foreground",
);
});
Comment on lines +69 to +99

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 | 🟡 Minor | ⚡ Quick win

Cover the changed placeholder contrast contract.

The test never asserts placeholderClassName, so a regression in the new text-foreground/70 dark:!text-background behavior at NoteNode/index.tsx Line 239 would pass unnoticed. Assert it for both opaque presets and the transparent preset.

Proposed test additions
         expect(lastNodeDescriptionProps.mdClassName).toContain(
           "dark:!text-background",
         );
+        expect(lastNodeDescriptionProps.placeholderClassName).toContain(
+          "text-foreground/70",
+        );
+        expect(lastNodeDescriptionProps.placeholderClassName).toContain(
+          "dark:!text-background",
+        );
...
     expect(lastNodeDescriptionProps.inputClassName).not.toContain(
       "text-foreground",
     );
+    expect(lastNodeDescriptionProps.placeholderClassName).not.toContain(
+      "text-foreground",
+    );

As per coding guidelines, frontend tests must “verify the tests actually cover the new or changed behavior rather than acting as placeholders.”

📝 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
expect(lastNodeDescriptionProps.inputClassName).toContain(
"text-foreground",
);
expect(lastNodeDescriptionProps.mdClassName).toContain(
"text-foreground",
);
// Dark-mode override must be preserved alongside the new light-mode fix.
expect(lastNodeDescriptionProps.inputClassName).toContain(
"dark:text-background",
);
expect(lastNodeDescriptionProps.mdClassName).toContain(
"dark:!text-background",
);
});
});
it("leaves the transparent/no-background preset without a forced text color", () => {
const data = {
id: "note-1",
node: {
description: "Some note text",
template: { backgroundColor: "transparent" },
},
} as unknown as NoteDataType;
render(<NoteNode data={data} />);
expect(lastNodeDescriptionProps.inputClassName).not.toContain(
"text-foreground",
);
});
expect(lastNodeDescriptionProps.inputClassName).toContain(
"text-foreground",
);
expect(lastNodeDescriptionProps.mdClassName).toContain(
"text-foreground",
);
// Dark-mode override must be preserved alongside the new light-mode fix.
expect(lastNodeDescriptionProps.inputClassName).toContain(
"dark:text-background",
);
expect(lastNodeDescriptionProps.mdClassName).toContain(
"dark:!text-background",
);
expect(lastNodeDescriptionProps.placeholderClassName).toContain(
"text-foreground/70",
);
expect(lastNodeDescriptionProps.placeholderClassName).toContain(
"dark:!text-background",
);
});
});
it("leaves the transparent/no-background preset without a forced text color", () => {
const data = {
id: "note-1",
node: {
description: "Some note text",
template: { backgroundColor: "transparent" },
},
} as unknown as NoteDataType;
render(<NoteNode data={data} />);
expect(lastNodeDescriptionProps.inputClassName).not.toContain(
"text-foreground",
);
expect(lastNodeDescriptionProps.placeholderClassName).not.toContain(
"text-foreground",
);
});
🤖 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/NoteNode/__tests__/note-node-contrast.test.tsx`
around lines 69 - 99, Extend the NoteNode contrast tests around the existing
lastNodeDescriptionProps assertions to verify placeholderClassName: require
text-foreground/70 and dark:!text-background for opaque presets, and require
that the transparent/no-background preset does not include the forced
placeholder color classes.

Source: Coding guidelines

});
6 changes: 3 additions & 3 deletions src/frontend/src/CustomNodes/NoteNode/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,14 @@ function NoteNode({
? getTextColorClass()
: COLOR_OPTIONS[bgColorKey] === null
? ""
: "dark:!ring-background dark:text-background",
: "text-foreground dark:!ring-background dark:text-background",
)}
mdClassName={cn(
hasCustomColor
? getTextColorClass()
: COLOR_OPTIONS[bgColorKey] === null
? "dark:prose-invert"
: "dark:!text-background",
: "text-foreground dark:!text-background",
"min-w-full max-h-full overflow-auto pr-4 sticky-note-scroll",
)}
style={{ backgroundColor: resolvedBgColor }}
Expand All @@ -236,7 +236,7 @@ function NoteNode({
: "!text-black/70"
: COLOR_OPTIONS[bgColorKey] === null
? ""
: "dark:!text-background",
: "text-foreground/70 dark:!text-background",
"px-2",
)}
editNameDescription={isEditingDescription}
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"account.github": "GitHub",
"account.latest": "(aktuell)",
"account.logout": "Abmelden",
"sidebar.componentsPanel": "Komponenten-Seitenleiste",
"sidebar.addComponentToCanvas": "{{name}} zur Arbeitsfläche hinzufügen",
"account.settings": "Einstellungen",
"account.theme": "Motiv",
"account.twitter": "X",
Expand Down Expand Up @@ -822,6 +824,8 @@
"flow.restoreVersion": "Diese Version Ihres Ablaufs wiederherstellen",
"flow.runTimestampPrefix": "Letzter Lauf: ",
"flow.saveAndExit": "Speichern und beenden",
"flow.canvasLabel": "Flow-Arbeitsfläche",
"flow.nodeAriaLabel": "Knoten {{name}}",
"flow.saveChanges": "Änderungen speichern",
"flow.saveVersion": "Speichere eine Version deines Ablaufs",
"flow.savedHover": "Zuletzt gespeichert: ",
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@
"flow.toAvoidLosingProgress": "to avoid losing progress.",
"flow.exitAnyway": "Exit anyway",
"flow.saveAndExit": "Save and Exit",
"flow.canvasLabel": "Flow canvas",
"flow.nodeAriaLabel": "{{name}} node",
"toolsModal.searchPlaceholder": "Search tools...",
"toolsModal.columnName": "Name",
"toolsModal.columnFlowName": "Flow Name",
Expand Down Expand Up @@ -503,6 +505,8 @@
"account.twitter": "X",
"account.theme": "Theme",
"account.logout": "Logout",
"sidebar.componentsPanel": "Components sidebar",
"sidebar.addComponentToCanvas": "Add {{name}} to canvas",
"sidebar.projects": "Projects",
"sidebar.uploadSuccess": "Uploaded successfully",
"sidebar.projectUploadSuccess": "Project uploaded successfully.",
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"account.github": "GitHub",
"account.latest": "(última versión)",
"account.logout": "Fin de sesión",
"sidebar.componentsPanel": "Barra lateral de componentes",
"sidebar.addComponentToCanvas": "Agregar {{name}} al lienzo",
"account.settings": "Valores",
"account.theme": "Tema",
"account.twitter": "X",
Expand Down Expand Up @@ -822,6 +824,8 @@
"flow.restoreVersion": "Restaurar esta versión de tu flujo",
"flow.runTimestampPrefix": "Última carrera: ",
"flow.saveAndExit": "Guardar y salir",
"flow.canvasLabel": "Lienzo del flujo",
"flow.nodeAriaLabel": "Nodo {{name}}",
"flow.saveChanges": "Guardar cambios",
"flow.saveVersion": "Guarda una versión de tu flujo",
"flow.savedHover": "Última vez guardado: ",
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"account.github": "GitHub",
"account.latest": "(dernière version)",
"account.logout": "Déconnexion",
"sidebar.componentsPanel": "Barre latérale des composants",
"sidebar.addComponentToCanvas": "Ajouter {{name}} au canevas",
"account.settings": "Paramètres",
"account.theme": "Thème",
"account.twitter": "X",
Expand Down Expand Up @@ -822,6 +824,8 @@
"flow.restoreVersion": "Restaurer cette version de votre flux",
"flow.runTimestampPrefix": "Dernière course : ",
"flow.saveAndExit": "Enregistrer et quitter",
"flow.canvasLabel": "Canevas du flux",
"flow.nodeAriaLabel": "Nœud {{name}}",
"flow.saveChanges": "Sauvegarder les modifications",
"flow.saveVersion": "Enregistrer une version de votre flux",
"flow.savedHover": "Dernière sauvegarde : ",
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"account.github": "GitHub",
"account.latest": "(最新)",
"account.logout": "ログアウト",
"sidebar.componentsPanel": "コンポーネントサイドバー",
"sidebar.addComponentToCanvas": "{{name}} をキャンバスに追加",
"account.settings": "設定",
"account.theme": "テーマ",
"account.twitter": "X",
Expand Down Expand Up @@ -822,6 +824,8 @@
"flow.restoreVersion": "このバージョンのフローを復元する",
"flow.runTimestampPrefix": "最終走行: ",
"flow.saveAndExit": "保存して終了",
"flow.canvasLabel": "フローキャンバス",
"flow.nodeAriaLabel": "{{name}} ノード",
"flow.saveChanges": "変更を保存",
"flow.saveVersion": "フローのバージョンを保存する",
"flow.savedHover": "最終保存日時: ",
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"account.github": "GitHub",
"account.latest": "(mais recente)",
"account.logout": "Efetuar Logout",
"sidebar.componentsPanel": "Barra lateral de componentes",
"sidebar.addComponentToCanvas": "Adicionar {{name}} à tela",
"account.settings": "Configurações",
"account.theme": "Tema",
"account.twitter": "X",
Expand Down Expand Up @@ -822,6 +824,8 @@
"flow.restoreVersion": "Restaurar esta versão do seu fluxo",
"flow.runTimestampPrefix": "Última corrida: ",
"flow.saveAndExit": "Salvar e sair",
"flow.canvasLabel": "Tela do flow",
"flow.nodeAriaLabel": "Nó {{name}}",
"flow.saveChanges": "Salvar mudanças",
"flow.saveVersion": "Salve uma versão do seu fluxo",
"flow.savedHover": "Última gravação: ",
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/src/locales/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"account.github": "GitHub",
"account.latest": "(最新)",
"account.logout": "注销",
"sidebar.componentsPanel": "组件侧边栏",
"sidebar.addComponentToCanvas": "将 {{name}} 添加到画布",
"account.settings": "设置",
"account.theme": "主题",
"account.twitter": "X",
Expand Down Expand Up @@ -822,6 +824,8 @@
"flow.restoreVersion": "恢复此版本的流程",
"flow.runTimestampPrefix": "最后一次运行: ",
"flow.saveAndExit": "保存并退出",
"flow.canvasLabel": "流程画布",
"flow.nodeAriaLabel": "{{name}} 节点",
"flow.saveChanges": "保存更改",
"flow.saveVersion": "保存流程的版本",
"flow.savedHover": "最后保存时间: ",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type MouseEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
Expand Down Expand Up @@ -116,6 +117,17 @@ export default function Page({
const edges = useFlowStore((state) => state.edges);
const isEmptyFlow = useRef(nodes.length === 0);

const nodesWithAriaLabel = useMemo(
() =>
nodes.map((node) => ({
...node,
ariaLabel: t("flow.nodeAriaLabel", {
name: node.data?.node?.display_name ?? node.data?.type,
}),
Comment on lines +124 to +126

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 | 🟠 Major | ⚡ Quick win

Fall back when display_name is empty.

The Note creation path sets display_name to an empty string at Line [835], so ?? produces " node" instead of falling back to the node type. Use a truthy fallback:

Proposed fix
-          name: node.data?.node?.display_name ?? node.data?.type,
+          name:
+            node.data?.node?.display_name || node.data?.type || node.type,
📝 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
ariaLabel: t("flow.nodeAriaLabel", {
name: node.data?.node?.display_name ?? node.data?.type,
}),
ariaLabel: t("flow.nodeAriaLabel", {
name:
node.data?.node?.display_name || node.data?.type || node.type,
}),
🤖 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/pages/FlowPage/components/PageComponent/index.tsx` around
lines 124 - 126, Update the ariaLabel construction in PageComponent to treat an
empty display_name as missing by using a truthy fallback to node.data?.type
instead of nullish-only fallback. Preserve the existing translation call and
node name precedence for non-empty display names.

})),
[nodes, t],
);

const previewLabel = useVersionPreviewStore((s) => s.previewLabel);
const isPreviewActive = previewLabel !== null;
const isWelcomeOpen = useFlowBuilderWelcomeStore((state) => state.isOpen);
Expand Down Expand Up @@ -918,7 +930,8 @@ export default function Page({
onClick={handleGroupNode}
/>
<ReactFlow<AllNodeType, EdgeType>
nodes={nodes}
aria-label={t("flow.canvasLabel")}
nodes={nodesWithAriaLabel}
edges={edges}
onNodesChange={onNodesChangeWithHelperLines}
onEdgesChange={onEdgesChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,19 @@ describe("SidebarDraggableComponent", () => {
expect(container).toHaveAttribute("tabIndex", "0");
});

it("should expose role=button and an accessible name on the draggable container", () => {
render(<SidebarDraggableComponent {...defaultProps} />);

const container = screen.getByTestId(
"testsection_test component_draggable",
);
expect(container).toHaveAttribute("role", "button");
expect(container).toHaveAttribute(
"aria-label",
"Add Test Component to canvas",
);
});

it("should have add button with tabIndex -1", () => {
render(<SidebarDraggableComponent {...defaultProps} />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,26 +357,29 @@ describe("SidebarSegmentedNav", () => {

it("renders a separator element before the memories nav item", () => {
const { container } = render(<SidebarSegmentedNav />);
// The separator is a <li role="separator" aria-hidden="true"> injected before memories
const separator = container.querySelector('li[role="separator"]');
// The separator is a plain <li aria-hidden="true"> injected before memories.
// It must NOT carry role="separator" — that's an invalid ARIA role on <li>
// (IBM Equal Access aria_role_valid) and is unnecessary since the element
// is already fully hidden from the accessibility tree via aria-hidden.
const separator = container.querySelector("li[aria-hidden='true']");
expect(separator).toBeInTheDocument();
expect(separator).toHaveAttribute("aria-hidden", "true");
expect(separator).not.toHaveAttribute("role");
});

it("separator renders only once", () => {
const { container } = render(<SidebarSegmentedNav />);
const separators = container.querySelectorAll('li[role="separator"]');
const separators = container.querySelectorAll("li[aria-hidden='true']");
expect(separators).toHaveLength(1);
});

it("separator appears immediately before the first feature tab in the DOM order", () => {
const { container } = render(<SidebarSegmentedNav />);
const menuItems = container.querySelectorAll(
'[data-testid="sidebar-menu-item"], li[role="separator"]',
"[data-testid='sidebar-menu-item'], li[aria-hidden='true']",
);
const nodes = Array.from(menuItems);
const separatorIndex = nodes.findIndex(
(n) => n.getAttribute("role") === "separator",
(n) => n.tagName === "LI" && !n.hasAttribute("data-testid"),
);
const agentButton = container.querySelector(
'[data-testid="sidebar-nav-agent"]',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ export const SidebarDraggableComponent = forwardRef(
}}
key={itemName}
data-tooltip-id={itemName}
role="button"
aria-label={t("sidebar.addComponentToCanvas", {
name: display_name,
})}
tabIndex={0}
Comment on lines +134 to 138

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate file"
fd -a 'sidebarDraggableComponent\.tsx$' . || true

echo "Inspect outline"
path="$(fd 'sidebarDraggableComponent\.tsx$' . | head -n1)"
if [ -n "${path:-}" ]; then
  ast-grep outline "$path" --match 'SidebarDraggableComponent' --view expanded || true
  echo "--- relevant lines ---"
  sed -n '1,230p' "$path" | nl -ba
fi

echo "Search related attributes"
if [ -n "${path:-}" ]; then
  rg -n "role=\"button\"|tabIndex|onKeyDown|aria-disabled|isDisabled|disabled|SelectTrigger" "$path" || true
fi

Repository: langflow-ai/langflow

Length of output: 538


🏁 Script executed:

#!/bin/bash
set -euo pipefail

path="./src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent.tsx"

echo "File line count"
wc -l "$path"

echo "Relevant lines 1-260"
awk '{printf "%5d\t%s\n", NR, $0}' "$path" | sed -n '1,260p'

echo "Search within file"
rg -n "role=\"button\"|ariaLabel|tabIndex|onKeyDown|aria-disabled|isDisabled|disabled|SelectTrigger|Button" "$path" || true

Repository: langflow-ai/langflow

Length of output: 11329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Use add component hook implementation"
rg -n "export function useAddComponent|function useAddComponent|const useAddComponent|m:\\/\\/.*useAddComponent|useAddComponent" src/frontend/src/hooks src/frontend/src -g '*.ts' -g '*.tsx' || true
hook_file="$(rg -l "useAddComponent" src/frontend/src/hooks src/frontend/src/components src/frontend/src/stores 2>/dev/null | head -n1 || true)"
if [ -n "${hook_file:-}" ]; then
  echo "--- hook file: $hook_file ---"
  sed -n '1,220p' "$hook_file" | awk '{printf "%5d\t%s\n", NR, $0}'
fi

echo "Interactive controls in parent/sidebar context"
parent="$(dirname "$(fd 'sidebarDraggableComponent\.tsx$' . | head -n1)")"
rg -n "SidebarDraggableComponent|disabled|component\\.disabled|isDisabled|disabledTooltip" "$parent"/.. src/frontend/src/pages src/frontend/src/components src/frontend/src/stores -g '*.ts' -g '*.tsx' || true

Repository: langflow-ai/langflow

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

path="./src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent.tsx"

echo "Remaining relevant lines 260-276"
sed -n '260,276p' "$path" | awk '{printf "%5d\t%s\n", NR+259, $0}'

echo "Check for aria-disabled or activation guard in file"
rg -n "aria-disabled|disabled\\?|!disabled|handleKeyDown|role=\"button\"" "$path" || true

echo "Static check of activation functions referenced around role button"
python3 - <<'PY'
from pathlib import Path
import re
p = Path("./src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent.tsx")
text = p.read_text()
checks = {
    "role_button": r'<div[^>]*role\s*=\s*"\bbutton\b"',
    "tabindex_0_wrapper": r'<div[^>]*tabIndex\s*=\s*\{0\}[^>]*onKeyDown\s*=\s*\{handleKeyDown\}[^>]*>',
    "handle_keydown_add_component": r'function handleKeyDown.*?[^}]?\n[^\n]*addComponent\(\s*apiClass\s*,\s*itemName\s*\)',
    "disabled_reference_in_keydown": re.compile(r'handleKeyDown\([^)]+\)\s*\{(?P<body>.*?)[\}]', re.S),
    "child_tabindex_minus_1": r'tabIndex\s*=\s*\{-1\}',
    "conditional_add_button": r'!\s*disabled\s*\{\s*<Button',
}
for name, pattern in checks.items():
    m = re.search(pattern, text, flags=re.S if isinstance(pattern, str) else re.S)
    body = m.group("body") if isinstance(pattern, str) and name == "disabled_reference_in_keydown" else None
    print(f"{name}: {bool(m)}" + (f" | body={repr(body.strip())}" if body else ""))
PY

Repository: langflow-ai/langflow

Length of output: 1429


🏁 Script executed:

#!/bin/bash
set -euo pipefail

path="./src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent.tsx"

echo "Relevant lines around keyboard handler and wrapper"
sed -n '95,145p' "$path" | awk '{printf "%5d\t%s\n", NR+94, $0}'

echo "Search for disabled gating around handler or wrapper"
rg -n "handleKeyDown|disabled|aria-disabled|role=\"button\"" "$path" || true

echo "Read-only structural validation"
python3 - <<'PY'
from pathlib import Path
p = Path("./src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent.tsx")
text = p.read_text()
print("has_handleKeyDown_addComponent_no_disabled_guard=", "handleKeyDown" in text and "addComponent(apiClass, itemName)" in text and "disabled" not in text.split("const handleKeyDown",1)[1].split("};",1)[0])
print("has_wrapper_role_button=", 'role="button"' in text and 'tabIndex={0}' in text and 'onKeyDown={handleKeyDown}' in text)
print("has_child_tabindex_minus_1=", 'tabIndex={-1}' in text)
print("has_conditional_add_button=", '{"} {<Button' in text or '{!disabled && (' in text)
PY

Repository: langflow-ai/langflow

Length of output: 2640


Do not expose this composite as a single button.

The wrapper is focusable and traps keyboard activation on the Add/Select actions via tabIndex={-1}, while its Enter/Space handler bypasses disabled. Split the drag target from the interactive controls, or make the wrapper a real activation target with guard/aria-disabled and keep the child actions independently focusable.

🤖 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/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent.tsx`
around lines 134 - 138, The draggable wrapper around the Add/Select controls
should not be exposed as a composite button. Update the wrapper and its keyboard
handling in sidebarDraggableComponent so the drag target is separate from
independently focusable child actions, or make the wrapper a guarded activation
target with aria-disabled; ensure Enter/Space cannot activate disabled controls
and do not trap focus with tabIndex={-1}.

onKeyDown={handleKeyDown}
className="rounded-md outline-none ring-ring focus-visible:ring-1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ const SidebarSegmentedNav = ({
<Fragment key={item.id}>
{index === firstFeatureIndex && (
<li
role="separator"
aria-hidden="true"
className="mx-auto my-1 w-5 border-t border-border"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,8 @@ export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
collapsible="offcanvas"
data-testid="shad-sidebar"
className="noflow select-none"
role="navigation"
aria-label={t("sidebar.componentsPanel")}
>
<div className="flex h-full">
{ENABLE_NEW_SIDEBAR && (
Expand Down
Loading