Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
19 changes: 10 additions & 9 deletions a11y-action-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,30 +228,31 @@ Work top-to-bottom. Early phases fix shared primitives first, then page surfaces

### 4.1 — Fix validation message announcement

- [ ] **Fix:** In auth flows, make validation messages screen-reader discoverable and announced when they appear.
- [x] **Fix:** In auth flows, make validation messages screen-reader discoverable and announced when they appear.
- `LoginPage`
- `SignUpPage`
- related shared form primitives
- [ ] **Auto:** Submit empty/invalid auth form in test; assert accessible error message appears.
- [x] **Auto:** Submit empty/invalid auth form in test; assert accessible error message appears.
- [ ] **Manual:** Screen reader announces required/mismatch errors immediately after submit.

### 4.2 — Fix alert/toast semantics

- [ ] **Fix:** Add live-region behavior to `src/frontend/src/alerts/displayArea/index.tsx` so success/error/notice messages are announced appropriately.
- [ ] **Auto:** Unit test dispatches alert and checks `aria-live` / urgent semantics.
- [x] **Fix:** Add live-region behavior to `src/frontend/src/alerts/displayArea/index.tsx` so success/error/notice messages are announced appropriately.
- [x] **Auto:** Unit test dispatches alert and checks `aria-live` / urgent semantics.
- [ ] **Manual:** Trigger login/server error and confirm announcement without manual navigation.

### 4.3 — Re-verify `3.3.3` Error Suggestion

- [ ] **Fix:** After auth/message changes, re-test signup and auth error flows. Add concrete suggestion text where current flows only show weak mismatch feedback.
- [ ] **Auto:** Form tests cover password mismatch and server-side auth failures.
- [x] **Fix:** After auth/message changes, re-test signup and auth error flows. Add concrete suggestion text where current flows only show weak mismatch feedback.
- [x] **Auto:** Form tests cover password mismatch and server-side auth failures.
- [ ] **Manual:** Screen reader hears actionable correction suggestion, not only failure state.

### 4.4 — Re-verify `3.3.4` Error Prevention

- [ ] **Fix:** Audit destructive/data-affecting user flows after dialog fixes. Ensure confirmation and recovery flows satisfy Level 1 requirement before marking criterion done.
- [ ] **Auto:** Add representative tests for destructive confirmation flow.
- [ ] **Manual:** Verify delete/confirm flow with keyboard + screen reader end-to-end.
- [x] **Fix:** Audit destructive/data-affecting user flows after dialog fixes. Criterion resolved to `FAIL` in the gap report because delete-account is a visible stub.
- [x] **Auto:** Add representative tests for destructive confirmation flow.
- [x] **Manual:** Document keyboard + screen-reader structural walkthrough for shared delete confirmation.
- [ ] **Manual:** Execute live screen-reader walkthrough once the target environment is available.

---

Expand Down
38 changes: 26 additions & 12 deletions a11y-gap-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
> **Scope:** Frontend code analysis against IBM `Level 1` filtered requirements
> **Reference:** [a11y-level-1-requirements.md](/Users/viktoravelino/projects/langflow/a11y-level-1-requirements.md)
> **Method:** Direct source code analysis + targeted re-validation of current frontend sources
> **Latest targeted update:** 2026-06-17, LE-1518 error handling and announcements

---

Expand Down Expand Up @@ -45,10 +46,10 @@ IBM's `Level 1` filter currently returns these `21` requirements:
| `2.4.7` | Focus Visible | FAIL | Global and local CSS suppress visible focus indicators |
| `3.1.1` | Language of Page | PASS | `<html lang="en">` is set |
| `3.2.4` | Consistent Identification | FAIL | Icon-only actions remain inconsistently and often invisibly identified |
| `3.3.1` | Error Identification | FAIL | Validation and toast error feedback are not robustly announced |
| `3.3.1` | Error Identification | PARTIAL | Auth validation and toast announcements fixed; non-auth error surfaces still need broader audit |
| `3.3.2` | Labels or Instructions | FAIL | Placeholder-only inputs still exist in key flows |
| `3.3.3` | Error Suggestion | AT RISK | Weak in signup/auth flows; needs runtime confirmation for final severity |
| `3.3.4` | Error Prevention (Legal, Financial, Data) | NEEDS TARGETED AUDIT | Destructive flows exist, but criterion not fully re-verified in this pass |
| `3.3.3` | Error Suggestion | PASS (TARGETED AUTH) | Login/signup required, mismatch, and server-error flows now expose actionable suggestions |
| `3.3.4` | Error Prevention (Legal, Financial, Data) | FAIL | Flow/folder/deployment confirmations exist, but delete-account is a visible stub and cannot be verified end-to-end |
| `4.1.2` | Name, Role, Value | FAIL | Widespread issues across controls, dialogs, toggles, list cards, and canvas |

---
Expand Down Expand Up @@ -150,25 +151,39 @@ IBM's `Level 1` filter currently returns these `21` requirements:
- Notification, toolbar, dropdown, canvas, and list-card actions rely heavily on unlabeled or inconsistently labeled icon-only patterns.
- Same action types are not consistently exposed with the same accessible naming strategy.

### `3.3.1` Error Identification — FAIL
### `3.3.1` Error Identification — PARTIAL

- Login form validation messages are not robustly announced in [LoginPage/index.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/pages/LoginPage/index.tsx:106).
- Alert/toast area still has no live region semantics in [alerts/displayArea/index.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/alerts/displayArea/index.tsx:16).
- Login and signup required-field errors are now rendered as `role="alert"` messages and associated with their controls via `aria-describedby` in [LoginPage/index.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/pages/LoginPage/index.tsx:78) and [SignUpPage/index.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/pages/SignUpPage/index.tsx:86).
- Toasts now map severity to live-region semantics in [alerts/displayArea/index.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/alerts/displayArea/index.tsx:18): errors use `role="alert"` / `aria-live="assertive"`; success and notice use `role="status"` / `aria-live="polite"`.
- Automated evidence:
- [LoginPage.a11y.test.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/pages/LoginPage/__tests__/LoginPage.a11y.test.tsx:63)
- [SignUpPage.a11y.test.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/pages/SignUpPage/__tests__/SignUpPage.a11y.test.tsx:65)
- [displayArea.a11y.test.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/alerts/displayArea/__tests__/displayArea.a11y.test.tsx:34)
- Remaining risk: non-auth validation/error surfaces were not exhaustively re-audited in this LE-1518 pass.

### `3.3.2` Labels or Instructions — FAIL

- Placeholder-only inputs remain in delete-account and admin-login flows.
- Required markers and instructions are still not clearly exposed in all auth forms.
- Several admin/settings/search surfaces still depend on placeholder-only or weakly associated labels.

### `3.3.3` Error Suggestion — AT RISK
### `3.3.3` Error Suggestion — PASS (TARGETED AUTH)

- Signup password mismatch handling is weak in [SignUpPage/index.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/pages/SignUpPage/index.tsx:156).
- This criterion needs runtime confirmation before finalizing severity, but current implementation is not strong enough to call compliant.
- Signup password mismatch now announces actionable correction text: "Passwords do not match. Re-enter both passwords so they match."
- Server-side login errors append "Check your username and password, then try again."
- Server-side signup errors append "Use a different username or contact an administrator if you already have an account."
- Automated evidence:
- [LoginPage.a11y.test.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/pages/LoginPage/__tests__/LoginPage.a11y.test.tsx:87)
- [SignUpPage.a11y.test.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/pages/SignUpPage/__tests__/SignUpPage.a11y.test.tsx:86)

### `3.3.4` Error Prevention (Legal, Financial, Data) — NEEDS TARGETED AUDIT
### `3.3.4` Error Prevention (Legal, Financial, Data) — FAIL

- Destructive confirmations exist, but this pass did not fully validate the requirement end-to-end against all relevant user data flows.
- Folder/flow deletion uses [DeleteConfirmationModal](/Users/viktoravelino/projects/langflow/src/frontend/src/modals/deleteConfirmationModal/index.tsx:33), which presents a named destructive confirmation dialog, explicit permanent-delete copy, a cancel action, and a destructive delete action.
- Shared dialog title detection now recognizes nested `DialogHeader > DialogTitle` structures in [dialog.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/components/ui/dialog.tsx:32), so delete dialogs announce their specific title instead of the fallback "Dialog".
- Automated evidence: [DeleteConfirmationModal.a11y.test.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/modals/deleteConfirmationModal/__tests__/DeleteConfirmationModal.a11y.test.tsx:5) verifies the named dialog, permanent-delete warning, cancel path, and no accidental confirmation on cancel.
- Deployment deletion has stronger type-to-confirm protection in [type-to-confirm-delete-dialog.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/type-to-confirm-delete-dialog.tsx:25).
- Account deletion remains unimplemented: [DeleteAccountPage/index.tsx](/Users/viktoravelino/projects/langflow/src/frontend/src/pages/DeleteAccountPage/index.tsx:12) contains only placeholder comments and no real deletion/recovery path. This prevents a pass for the criterion across the targeted destructive/data-affecting flows.
- Keyboard walkthrough, shared delete dialog: open the delete trigger, focus enters the named "Delete" dialog container, `Tab` reaches `Cancel`, `Delete`, and `Close`, `Cancel` exits without calling confirm, and activating `Delete` runs the destructive callback. Screen-reader structural announcement is supported by `role="dialog"` plus the visible `DialogTitle`; live assistive-technology execution was not performed in this source/test pass.

### `4.1.2` Name, Role, Value — FAIL

Expand All @@ -191,4 +206,3 @@ IBM's `Level 1` filter currently returns these `21` requirements:
6. Fix high-impact semantic primitives: `Input`, `CheckBoxDiv`, `AccordionTrigger`, full-screen modal container.
7. Repair route titles so non-playground pages set meaningful `document.title`.
8. Adjust failing text and non-text contrast tokens before page-level QA.

Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,41 @@ describe("AlertDisplayArea accessibility", () => {
expect(screen.getByText("Flow saved")).toBeInTheDocument();
});

// Known gap (a11y-action-plan 4.2): the alert display area has no
// aria-live region, so success/error/notice messages are never announced
// to screen readers. Fails until the fix lands.
it("should_announce_alerts_via_live_region", () => {
it("keeps_live_regions_mounted_before_alerts_are_inserted", () => {
const { container } = render(<AlertDisplayArea />);

const wrapper = container.firstElementChild;
expect(wrapper).not.toBeNull();
expect(wrapper).toHaveAttribute("aria-live");
expect(
container.querySelector('[aria-live="assertive"]'),
).toBeInTheDocument();
expect(screen.getByRole("status")).toHaveAttribute("aria-live", "polite");
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});

it("announces_success_and_notice_alerts_politely", () => {
render(<AlertDisplayArea />);

act(() => {
useAlertStore.getState().setSuccessData({ title: "Flow saved" });
});

const status = screen.getByRole("status");
expect(status).toHaveAttribute("aria-live", "polite");
expect(status).toHaveAttribute("aria-atomic", "true");
expect(status).toHaveTextContent("Flow saved");
});

it("announces_error_alerts_assertively", () => {
const { container } = render(<AlertDisplayArea />);

act(() => {
useAlertStore.getState().setErrorData({ title: "Build failed" });
});

const assertiveRegion = container.querySelector('[aria-live="assertive"]');
expect(assertiveRegion).toHaveAttribute("aria-atomic", "true");
expect(assertiveRegion).toHaveTextContent("Build failed");

const alert = screen.getByRole("alert");
expect(alert).toHaveTextContent("Build failed");
});
});
42 changes: 28 additions & 14 deletions src/frontend/src/alerts/displayArea/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,39 @@ export default function AlertDisplayArea() {
const removeAlert = (id: string) => {
removeFromTempNotificationList(id);
};
const errorAlerts = tempNotificationList.filter(
(alert) => alert.type === "error",
);
const politeAlerts = tempNotificationList.filter(
(alert) => alert.type !== "error",
);

return (
<div
aria-live="polite"
role="status"
className="flex flex-col-reverse"
style={{ zIndex: 999 }}
>
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "error" ? (
<div style={{ zIndex: 999 }}>
<div
aria-atomic="true"
aria-live="assertive"
className="flex flex-col-reverse"
>
{errorAlerts.map((alert) => (
<div key={alert.id} role="alert">
<ErrorAlert
key={alert.id}
title={alert.title}
list={alert.list}
id={alert.id}
removeAlert={removeAlert}
/>
) : alert.type === "notice" ? (
</div>
))}
</div>
<div
aria-atomic="true"
aria-live="polite"
className="flex flex-col-reverse"
role="status"
>
{politeAlerts.map((alert) =>
alert.type === "notice" ? (
<NoticeAlert
key={alert.id}
title={alert.title}
Expand All @@ -48,9 +62,9 @@ export default function AlertDisplayArea() {
removeAlert={removeAlert}
/>
)
)}
</div>
))}
),
)}
</div>
</div>
);
}
9 changes: 8 additions & 1 deletion src/frontend/src/components/common/GradientWrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import type { ReactNode } from "react";
export function GradientWrapper({ children }: { children: ReactNode }) {
return (
<>
<svg width="0" height="0" className="absolute">
<svg
width="0"
height="0"
className="absolute"
aria-hidden="true"
focusable="false"
role="presentation"
>
<defs>
<linearGradient id="x-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="-35.61%" stopColor="#e6b1e1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface FormInputBranchProps {
blurOnEnter: boolean;
name?: string;
id: string;
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
}

function FormInputBranch({
Expand All @@ -49,6 +50,7 @@ function FormInputBranch({
blurOnEnter,
name,
id,
inputProps,
}: FormInputBranchProps) {
const [cursor, setCursor] = useState<number | null>(null);

Expand Down Expand Up @@ -88,6 +90,7 @@ function FormInputBranch({
return (
<Form.Control asChild>
<Input
{...inputProps}
name={name}
id={"form-" + id}
ref={refInput}
Expand Down Expand Up @@ -145,6 +148,7 @@ export default function InputComponent({
objectOptions,
isObjectOption = false,
name,
inputProps,
onChangeFolderName,
nodeStyle,
isToolMode,
Expand Down Expand Up @@ -190,6 +194,7 @@ export default function InputComponent({
blurOnEnter={blurOnEnter}
name={name}
id={id}
inputProps={inputProps}
/>
) : (
<>
Expand Down
44 changes: 44 additions & 0 deletions src/frontend/src/components/ui/__tests__/dialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "../dialog";

Expand Down Expand Up @@ -75,4 +76,47 @@ describe("DialogContent", () => {
screen.queryByRole("button", { name: /close/i }),
).not.toBeInTheDocument();
});

it("should_detect_dialog_title_inside_dialog_header", () => {
renderWithProviders(
<Dialog open>
<DialogContent>
<DialogHeader>
<DialogTitle>Nested title</DialogTitle>
</DialogHeader>
<DialogDescription>Test description</DialogDescription>
</DialogContent>
</Dialog>,
);

expect(
screen.getByRole("dialog", { name: "Nested title" }),
).toBeInTheDocument();
expect(screen.queryByText("Dialog")).not.toBeInTheDocument();
});

it("should_stop_scanning_for_dialog_title_after_safe_depth", () => {
renderWithProviders(
<Dialog open>
<DialogContent>
<div>
<div>
<div>
<div>
<div>
<div>
<DialogTitle>Too deep</DialogTitle>
</div>
</div>
</div>
</div>
</div>
</div>
<DialogDescription>Test description</DialogDescription>
</DialogContent>
</Dialog>,
);

expect(screen.getByText("Dialog")).toBeInTheDocument();
});
});
29 changes: 22 additions & 7 deletions src/frontend/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@ const DialogOverlay = React.forwardRef<
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

const MAX_DIALOG_CHILD_SCAN_DEPTH = 4;

const hasChildOfType = (
children: React.ReactNode,
targetType: React.ElementType,
depth = 0,
): boolean =>
React.Children.toArray(children).some((child) => {
if (!React.isValidElement<{ children?: React.ReactNode }>(child)) {
return false;
}
if (child.type === targetType) {
return true;
}
if (depth >= MAX_DIALOG_CHILD_SCAN_DEPTH) {
return false;
}
return hasChildOfType(child.props.children, targetType, depth + 1);
});

// Create a VisuallyHidden component for accessibility
const VisuallyHidden = React.forwardRef<
HTMLSpanElement,
Expand Down Expand Up @@ -73,13 +93,8 @@ const DialogContent = React.forwardRef<
ref,
) => {
// Check if DialogTitle is included in children
const hasDialogTitle = React.Children.toArray(children).some(
(child) => React.isValidElement(child) && child.type === DialogTitle,
);
const hasDialogDescription = React.Children.toArray(children).some(
(child) =>
React.isValidElement(child) && child.type === DialogDescription,
);
const hasDialogTitle = hasChildOfType(children, DialogTitle);
const hasDialogDescription = hasChildOfType(children, DialogDescription);

return (
<DialogPortal>
Expand Down
Loading
Loading