Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
065ce2a
feat: add IBM Accessibility Level 1 validation tracker and compliance…
deon-sanchez Jul 1, 2026
77ff778
feat(a11y): add IBM Accessibility Level 1 criteria and validation tra…
deon-sanchez Jul 1, 2026
6262d7e
feat(a11y): introduce a11y-spec-authoring skill for Playwright access…
deon-sanchez Jul 6, 2026
5bc604c
docs(a11y): update README for improved accessibility testing guidance
deon-sanchez Jul 6, 2026
a2cfe58
feat: enhance accessibility and localization in knowledge base compon…
deon-sanchez Jul 6, 2026
5d09a62
chore: update secrets baseline with new line numbers for hashed secre…
deon-sanchez Jul 6, 2026
39af62d
feat(a11y): introduce a11y-spec-authoring skill and enhance accessibi…
deon-sanchez Jul 7, 2026
cbeb7d6
feat(a11y): implement feature-specific accessibility rule suppression…
deon-sanchez Jul 7, 2026
1c1d324
merge: integrate ibm-a11y-tracker into kb-a11y
deon-sanchez Jul 7, 2026
0f7060d
Merge branch 'release-1.11.0' of https://github.com/langflow-ai/langf…
deon-sanchez Jul 7, 2026
6107988
chore: update secrets baseline and adjust line numbers for hashed sec…
deon-sanchez Jul 7, 2026
c1cd90f
feat(a11y): enhance keyboard navigation and accessibility in Knowledg…
deon-sanchez Jul 7, 2026
904dfb4
Merge branch 'release-1.11.0' of https://github.com/langflow-ai/langf…
deon-sanchez Jul 7, 2026
cf5c19b
feat(a11y): implement per-scan accessibility rule suppression and rep…
deon-sanchez Jul 7, 2026
c56c3bf
Merge branch 'release-1.11.0' of https://github.com/langflow-ai/langf…
deon-sanchez Jul 7, 2026
339f259
Merge branch 'release-1.11.0' of https://github.com/langflow-ai/langf…
deon-sanchez Jul 8, 2026
179cbc9
Merge branch 'release-1.11.0' into kb-a11y
deon-sanchez Jul 8, 2026
8b3289e
refactor(a11y): streamline accessibility rule handling and update UI …
deon-sanchez Jul 8, 2026
63f1236
Merge branch 'kb-a11y' of https://github.com/langflow-ai/langflow int…
deon-sanchez Jul 8, 2026
07d58aa
Merge branch 'release-1.11.0' into kb-a11y
deon-sanchez Jul 8, 2026
67cb4a3
feat(a11y): add frontend accessibility check skill and enhance UI com…
deon-sanchez Jul 8, 2026
99d27b2
Merge branch 'kb-a11y' of https://github.com/langflow-ai/langflow int…
deon-sanchez Jul 8, 2026
fb3ad83
feat(knowledgeBase): implement row actions for knowledge base table
deon-sanchez Jul 8, 2026
09b7f03
feat(a11y): enhance Radix components for improved accessibility
deon-sanchez Jul 8, 2026
b9c003e
refactor(a11y): clean up comments and improve code clarity
deon-sanchez Jul 8, 2026
ec82363
refactor(a11y): remove frontend accessibility check skill and enhance…
deon-sanchez Jul 8, 2026
9577a60
Merge branch 'release-1.11.0' into kb-a11y
deon-sanchez Jul 9, 2026
2268597
feat(a11y): enhance accessibility in various components
deon-sanchez Jul 9, 2026
8fa857a
Merge branch 'release-1.11.0' into kb-a11y
deon-sanchez Jul 10, 2026
dfa6a56
fix(locales): add missing commas in translation files for consistency
deon-sanchez Jul 10, 2026
4a0706d
Merge branch 'release-1.11.0' of https://github.com/langflow-ai/langf…
deon-sanchez Jul 10, 2026
6c7f9ce
Merge branch 'release-1.11.0' into kb-a11y
deon-sanchez Jul 10, 2026
957a034
Merge branch 'release-1.11.0' into kb-a11y
deon-sanchez Jul 10, 2026
dfcdf2a
fix(a11y): enhance keyboard navigation for actions in knowledge base …
deon-sanchez Jul 10, 2026
47fc93c
Merge branch 'release-1.11.0' into kb-a11y
deon-sanchez Jul 10, 2026
768f387
Merge branch 'release-1.11.0' into kb-a11y
deon-sanchez Jul 10, 2026
01ead49
Merge branch 'release-1.11.0' of https://github.com/langflow-ai/langf…
deon-sanchez Jul 13, 2026
f9a22ab
Merge branch 'release-1.11.0' into kb-a11y
deon-sanchez Jul 13, 2026
edb8308
fix(a11y): update test ID for OpenAI text embedding option in accessi…
deon-sanchez Jul 13, 2026
c92564e
Merge branch 'release-1.11.0' of https://github.com/langflow-ai/langf…
deon-sanchez Jul 13, 2026
2774e07
Merge branch 'release-1.11.0' into kb-a11y
deon-sanchez Jul 13, 2026
a9847e7
chore: regenerate secrets baseline
erichare Jul 13, 2026
9bbaf63
Merge release-1.11.0 into kb-a11y
erichare Jul 13, 2026
242d040
chore: update .gitignore and enhance IBM Level 1 audit skill document…
deon-sanchez Jul 21, 2026
c3d1e8b
Merge branch 'release-1.12.0' of https://github.com/langflow-ai/langf…
deon-sanchez Jul 21, 2026
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
218 changes: 218 additions & 0 deletions .agents/skills/frontend-a11y-check/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
---
name: frontend-a11y-check
description: "Plan, write, run, or review accessibility checks for Langflow frontend work. Use when changes touch React UI, routes, pages, modals, forms, tables, navigation, Radix/shadcn primitives, keyboard behavior, ARIA labels, focus order, or when the user asks for an a11y check, axe test, Playwright a11y scan, or accessibility coverage."
---

# Frontend Accessibility Check

## When To Use

Use this skill for frontend work under `src/frontend` when the change affects:

- UI components, primitives, pages, routes, dialogs, drawers, popovers, dropdowns, tabs, accordions, tables, forms, navigation, or canvas controls.
- ARIA attributes, labels, roles, focus management, tab order, keyboard interaction, disabled states, loading states, or error states.
- A new route or existing route surface that should be scanned.
- A request to add, fix, run, or review accessibility tests.

Use alongside:

- `frontend-testing` for Jest/React Testing Library patterns.
- `e2e-testing` for Playwright patterns.
- `frontend-i18n` when adding or changing user-facing labels, accessible names, `aria-label`, tooltips, or visible strings.
- `ibm-a11y-automation` when the user asks to run the Python route scanner or produce Markdown/HTML reports.

## Goal

Do not stop at "run axe on default render." Inspect the changed UI and cover every meaningful user-visible state that can expose accessibility bugs.

## First Pass

1. Read the changed files and nearby tests.
2. Identify the UI surface:
- Primitive/component only
- Composed component
- Page/route
- Stateful workflow
- Shared helper that changes labels, roles, tab order, or focus behavior
3. List interactive surfaces:
- Buttons, links, inputs, checkboxes, switches, radios, selects, comboboxes
- Dropdown menus, popovers, dialogs, drawers, tooltips
- Tabs, accordions, tables/treegrids, row actions, pagination
- Keyboard-only interactions and focus traps
4. List states worth scanning:
- Default
- Populated data
- Empty data
- Loading
- Error/validation visible
- Disabled/read-only
- Selected/expanded/open
- Modal/dropdown/popover open
- Mobile viewport when layout changes

## Choose Test Layer

### Jest + axe

Use Jest when the changed surface is a primitive or component that can be rendered in jsdom without full app routing.

Good for:

- `src/frontend/src/components/ui/*`
- Reusable form fields, buttons, inputs, dialogs, popovers, tables
- Local component states: loading, disabled, error, selected, invalid
- Accessible name/role assertions

Pattern:

```tsx
import { render, screen } from "@testing-library/react";
import { axe } from "@/utils/a11y-test";

describe("Component accessibility", () => {
it("should_have_no_axe_violations", async () => {
const { container } = render(<Component />);

expect(await axe(container)).toHaveNoViolations();
});

it("should_expose_accessible_name", () => {
render(<Component />);

expect(screen.getByRole("button", { name: "Save Flow" })).toBeInTheDocument();
});
});
```

Notes:

- Use `src/frontend/src/utils/a11y-test.ts`; it disables `color-contrast` because jsdom cannot check real layout.
- Prefer role/label/text queries. Use test ids only when semantic queries are not stable enough.
- Test accessible names explicitly when labels are visually hidden, loading, icon-only, or generated from props.
- Add a11y tests next to component tests: `__tests__/<name>.a11y.test.tsx`.

Command:

```bash
cd src/frontend
npx jest path/to/<name>.a11y.test.tsx --runInBand
```

### Playwright + `page.runA11yScan`

Use Playwright when the changed surface needs real browser layout, routing, app state, data mocking, modals, tables, focus order, keyboard behavior, or full-page interactions.

Good for:

- Routes and pages
- Settings pages
- Flow canvas workflows
- Tables/treegrids and row selection
- Dialogs, dropdowns, popovers, navigation sidebars
- Any issue involving tab order, focus trap, viewport layout, or real CSS

Pattern:

```ts
import { expect, test } from "../fixtures";
import { awaitBootstrapTest } from "../utils/await-bootstrap-test";

test.describe("Feature route accessibility", () => {
test(
"scans populated state",
{ tag: ["@release", "@api"] },
async ({ page }) => {
await awaitBootstrapTest(page, { skipModal: true });
await page.goto("/settings/example");
await expect(page.getByText("Example")).toBeVisible();

await page.runA11yScan("settings-example-populated");
},
);
});
```

Rules:

- Put focused route/state specs under `src/frontend/tests/a11y/<feature>.a11y.spec.ts`.
- Import `test` and `expect` from `../fixtures`, not `@playwright/test`.
- Every test must include `@release` plus valid domain tag(s): `@workspace`, `@api`, `@database`, `@components`, `@starter-projects`.
- Use stable scan names: lowercase, feature-first, state-specific.
- Mock API responses for deterministic empty/populated/error states.
- Disable animations if timing or focus assertions are flaky.
- Use explicit interactions. Do not randomly crawl or click arbitrary destructive controls.
- If route belongs in static route coverage, update `scripts/a11y/a11y_routes.json` with stable `ready` check.

Commands:

```bash
cd src/frontend
RUN_A11Y=true npx playwright test tests/a11y/<feature>.a11y.spec.ts --project=chromium --workers=1
npm run a11y:html-report --silent
npm run a11y:job-summary --silent
```

To assert against checker baselines:

```bash
cd src/frontend
RUN_A11Y=true RUN_A11Y_ASSERT=true npx playwright test tests/a11y/<feature>.a11y.spec.ts --project=chromium --workers=1
```

## Route/Page Coverage Matrix

For route or page changes, cover the smallest matrix that represents real user states:

- Loaded/default page
- Empty data state
- Populated data state
- Primary modal open
- Dropdown/popover open
- Validation or error visible
- Selected/expanded table row or bulk action state
- Mobile viewport when responsive layout changes

Do not force every state if the page cannot enter it. Do explain skipped states briefly.

## PR #13953 Pattern

Use `src/frontend/tests/a11y/api-keys.a11y.spec.ts` as model for route-level work:

- Mock API data.
- Scan populated table.
- Scan empty table.
- Open create modal and scan.
- Submit form to generated-result modal and scan.
- Open text-cell modal and scan.
- Select table row and scan selected state.
- Set mobile viewport and scan responsive state.

This is the expected bar for data-rich routes.

## Manual Code Review Checklist

While reading changed UI code, check:

- Interactive icon-only controls have accessible names.
- Inputs have labels or valid `aria-labelledby`/`aria-label`.
- Visible label and accessible name stay aligned.
- Dialogs have title/description and trap/restore focus.
- Dropdown/popover content is reachable by keyboard and closes with Escape.
- Custom buttons/links use semantic elements where possible.
- Disabled controls are not focusable unless intentionally discoverable.
- Tables/treegrids have one logical tab stop and no duplicate row/header tab stops.
- Pagination/disabled table controls are not in tab order.
- Error text is associated with inputs.
- Loading state keeps useful accessible name.
- `aria-hidden` does not hide focusable descendants.
- New user-facing labels go through i18n in every locale.

## Report Back

When done, state:

- Test files added or updated.
- States scanned.
- Commands run.
- Any states not covered and why.
- Any remaining a11y risk.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function CrashErrorComponent({
return (
<div className="z-50 flex h-screen w-screen items-center justify-center bg-foreground bg-opacity-50">
<div className="flex h-screen w-screen flex-col bg-background text-start shadow-lg">
<div className="m-auto grid w-1/2 justify-center gap-5 text-center">
<main className="m-auto grid w-1/2 justify-center gap-5 text-center">
<Card className="p-8">
<CardHeader>
<div className="m-auto">
Expand Down Expand Up @@ -62,7 +62,7 @@ export default function CrashErrorComponent({
</div>
</CardFooter>
</Card>
</div>
</main>
</div>
</div>
);
Expand Down
103 changes: 103 additions & 0 deletions src/frontend/src/components/common/radixAriaControlsFix/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useEffect } from "react";
import { useInertForAriaHiddenElements } from "@/components/ui/use-inert-for-aria-hidden";

const CONTROLS_CACHE_ATTR = "data-radix-aria-controls-cache";

/**
* Radix sets aria-controls on closed popovers/selects/dialogs while the
* referenced node is aria-hidden or unmounted. IBM's checker treats that as
* aria_id_unique violation. Strip the attribute whenever the trigger is closed.
*
* The stripped value is cached on the node first: since the id Radix passes
* in is stable across renders, React's reconciler sees an "unchanged" prop
* on the next render and won't re-apply it once we've removed it from the
* DOM directly, so a trigger that closes and later reopens would otherwise
* be left with no aria-controls at all. Restoring from the cache when the
* trigger re-opens keeps that reference intact.
*
* Also applies `inert` to Radix `data-aria-hidden` subtrees (same helper
* Dialog uses) so open Selects/Popovers do not leave focusable controls
* under aria-hidden (aria_hidden_nontabbable). For an expanded combobox,
* unwraps aria-hidden on its ancestor path so the open Select trigger is
* neither inert nor under aria-hidden (combobox_focusable_elements).
*/
export function RadixAriaControlsFix() {
useInertForAriaHiddenElements();

useEffect(() => {
const fixClosedTriggers = () => {
document
.querySelectorAll('[data-state="closed"][aria-controls]')
.forEach((node) => {
const controls = node.getAttribute("aria-controls");
if (controls) node.setAttribute(CONTROLS_CACHE_ATTR, controls);
node.removeAttribute("aria-controls");
});

document
.querySelectorAll(
`[data-state="open"][${CONTROLS_CACHE_ATTR}]:not([aria-controls])`,
)
.forEach((node) => {
const cached = node.getAttribute(CONTROLS_CACHE_ATTR);
if (cached) node.setAttribute("aria-controls", cached);
});

document.querySelectorAll("[data-radix-focus-guard]").forEach((node) => {
node.setAttribute("tabindex", "-1");
node.setAttribute("aria-hidden", "true");
});

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File context ==\n'
sed -n '1,140p' src/frontend/src/components/common/radixAriaControlsFix/index.tsx

printf '\n== Search for focus guard usage ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' 'data-radix-focus-guard|aria-hidden="true"|tabindex="-1"|tabIndex=\{0\}' src/frontend src | sed -n '1,200p'

printf '\n== Package versions ==\n'
python3 - <<'PY'
import json, pathlib
for path in [pathlib.Path('package.json'), pathlib.Path('src/frontend/package.json')]:
    if path.exists():
        print(f'## {path}')
        data = json.loads(path.read_text())
        deps = {}
        for k in ('dependencies','devDependencies','peerDependencies','optionalDependencies'):
            deps.update(data.get(k, {}))
        for name in ('`@radix-ui/react-focus-scope`','`@radix-ui/react-dialog`','`@radix-ui/react-popover`','`@radix-ui/react-dropdown-menu`','`@radix-ui/react-menu`'):
            if name in deps:
                print(name, deps[name])
PY

printf '\n== Relevant Radix source references in repo ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' '`@radix-ui/react-focus-scope`|focus guard|focus trap|trapped' . | sed -n '1,200p'

Repository: langflow-ai/langflow

Length of output: 28653


🌐 Web query:

Radix UI focus guard data-radix-focus-guard tabIndex 0 onFocus redirect focus-scope source

💡 Result:

In Radix UI, the data-radix-focus-guard attribute is used to identify invisible, focusable elements that ensure keyboard focus management works reliably across the entire document [1][2]. These guards are essential components injected by the @radix-ui/react-focus-guards package to help components like Dialog and Popover trap and maintain focus [1][3]. Key technical details regarding these guards include: Mechanism: Radix UI injects two invisible elements into the document body—one at the very beginning (afterbegin) and one at the very end (beforeend) [1][2]. These elements are explicitly marked with the data-radix-focus-guard attribute [1][2]. Purpose: By assigning them tabIndex={0}, Radix UI makes these elements focusable [1]. This ensures that when a user reaches the beginning or end of a focus-trapped area, the browser's focus events (like focusin and focusout) are consistently fired [1]. This allows the library's focus-trapping logic to detect when the user is trying to move focus outside the trapped region and redirect it accordingly, preventing the focus from "leaking" into the browser UI or other non-trapped areas [1][3]. Properties: To keep them invisible and non-interactive to the user, they are styled with opacity: 0, position: fixed, and pointer-events: none, while their outline is set to none [1][2]. Reference Counting: The implementation uses a reference counter to manage these guards [1]. Multiple components can "request" focus guards, but only one pair exists in the document at any time; they are only removed when the last component requiring them unmounts [1][2]. Relationship with FocusScope: While FocusGuards provides the infrastructure for capturing focus at the document edges, FocusScope is the component that performs the actual logic of trapping, looping, and restoring focus [3]. When FocusScope is set to trapped={true}, it listens for focus events and, should the focus land on a guard or attempt to exit the scope, it uses its internal logic to redirect focus back to a valid element within the designated scope [3][4].

Citations:


🌐 Web query:

@radix-ui/react-focus-scope focus guards tabIndex 0 focusin trapped source implementation

💡 Result:

In Radix UI, focus management relies on two primary utilities working in tandem: @radix-ui/react-focus-guards and @radix-ui/react-focus-scope [1][2]. Focus Guards (Implementation) Focus guards are invisible, focusable elements strategically placed in the DOM to ensure focus events are captured correctly at the document boundaries [1]. - Structure: They are elements with an empty data-radix-focus-guard attribute [1]. - Styling: They are set to tabIndex={0} (making them focusable), with CSS properties like position: fixed, opacity: 0, and pointer-events: none to remain invisible and non-obstructive [1]. - Functionality: They are injected at the very start and end of document.body [1]. When a user tabs into these guards, they provide a reliable target for the browser to trigger focusin and focusout events, which the FocusScope can then monitor [1]. This prevents "focus leakage" where focus might otherwise accidentally move into the browser's chrome or become lost when navigating at the edges of a trapped focus area [1]. Focus Scope and Trapping The FocusScope component manages the trapping of focus within a specific DOM container [2]. - Trapping Mechanism: When trapped={true}, FocusScope listens for focusin and focusout events [2]. If the focus moves to an element outside the defined scope, the component programmatically redirects it back inside [2]. - Tab Navigation: It implements a "looping" behavior: when a user tabs from the last focusable element in the scope, focus is programmatically moved back to the first element (and vice versa for Shift+Tab) [2][3]. - Implementation Logic: FocusScope uses a function—often identified in community discussions as a variation of getTabbableCandidates—to scan the DOM tree within its container [3]. It evaluates element properties, including tabIndex, to determine the order of focusable items [4][3]. - tabIndex Usage: While focus guards use tabIndex={0} to participate in the natural tab order [1], the container of the FocusScope itself often uses tabIndex={-1} [3]. This makes the container programmatically focusable (allowing it to receive focus via JavaScript) without adding it to the natural keyboard tab order [3]. These systems ensure that even in complex UI components like Modals or Popovers, keyboard focus remains strictly contained, predictable, and compliant with accessibility standards [1][2][5].

Citations:


Don't force Radix focus guards to tabindex="-1" data-radix-focus-guard elements need to stay tabbable for Radix to catch edge Tab/Shift+Tab navigation and redirect focus back into open dialogs/popovers. Remove this mutation and exclude these guards from the scanner instead.

🤖 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/common/radixAriaControlsFix/index.tsx` around
lines 37 - 40, The DOM mutation in the Radix focus guard fix is making
`data-radix-focus-guard` elements untabbable, which breaks Radix edge
Tab/Shift+Tab handling. Update the logic in `radixAriaControlsFix` so it no
longer sets `tabindex="-1"` or `aria-hidden` on these guards. Instead, adjust
the scanner/exclusion logic in the same area to skip `data-radix-focus-guard`
nodes entirely while leaving their default tabbable behavior intact.


// cmdk renders its option list as role="listbox" with role="option"
// children that carry no tabindex. When the Command has a search input
// (cmdk-input), focus stays on the input and aria-activedescendant marks
// the active option — a pattern the checker accepts. But the model and
// db-provider dropdowns use cmdk WITHOUT an input, so the listbox has no
// tabbable descendant and the checker flags aria_child_tabbable. For those
// input-less lists only, make the first enabled option tabbable so the
// listbox is keyboard-reachable.
//
// The same input-less roots also get a visually-hidden <label
// cmdk-label htmlFor={inputId}> from cmdk. With no CommandInput, that
// for= target does not exist (label_ref_valid). Strip htmlFor only in
// that case; searchable Commands keep the association to their input.
document.querySelectorAll("[cmdk-list]").forEach((list) => {
const root = list.closest("[cmdk-root]");
if (root?.querySelector("[cmdk-input]")) return;

root
?.querySelectorAll<HTMLLabelElement>("label[cmdk-label][for]")
.forEach((label) => {
label.removeAttribute("for");
});

const options = Array.from(
list.querySelectorAll<HTMLElement>(
'[role="option"]:not([aria-disabled="true"])',
),
);
if (options.length === 0) return;
if (options.some((option) => option.getAttribute("tabindex") === "0")) {
return;
}
options[0].setAttribute("tabindex", "0");
});
};

fixClosedTriggers();

const observer = new MutationObserver(fixClosedTriggers);
observer.observe(document.body, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ["data-state", "aria-controls", "for"],
});

return () => observer.disconnect();
}, []);

return null;
}

export default RadixAriaControlsFix;
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { useGetGlobalVariables } from "@/controllers/API/queries/variables";
import type { GlobalVariable } from "@/types/global_variables";
import { cn } from "@/utils/utils";
import { focusCommandListOnOpen } from "../../utils/focus-command-list-on-open";

export type DBProviderSelection = {
// Wire keys stay snake_case to match the backend payload format.
Expand Down Expand Up @@ -191,10 +192,17 @@ export function DBProviderInput({
<PopoverContentWithoutPortal
side="bottom"
avoidCollisions={true}
onOpenAutoFocus={focusCommandListOnOpen}
className="noflow nowheel nopan nodelete nodrag p-0"
style={{ minWidth: refButton?.current?.clientWidth ?? "240px" }}
>
<Command className="flex flex-col">
{/* Section 1 — the provider list (a self-contained listbox). Keeping the
manage action out of <Command> stops it from being swept into the
listbox's composite keyboard/focus model. */}
<Command
label={t("knowledge.dbProviderLabel")}
className="flex flex-col"
>
<CommandList className="max-h-[300px] overflow-y-auto">
{selectableOptions.map(({ provider, configured }) => (
<DBProviderOptionItem
Expand All @@ -206,8 +214,11 @@ export function DBProviderInput({
/>
))}
</CommandList>
</Command>
{/* Section 2 — footer action, reachable by Tab after the list. */}
<div className="flex flex-col border-t border-border bg-background">
<Button
className="w-full flex cursor-pointer items-center justify-start gap-2 truncate py-2 text-xs text-muted-foreground px-3 hover:bg-accent group"
className="w-full flex cursor-pointer items-center justify-start gap-2 truncate py-2 text-xs text-muted-foreground px-3 hover:bg-accent group focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring"
unstyled
data-testid="manage-db-providers"
onClick={handleManageProviders}
Expand All @@ -220,7 +231,7 @@ export function DBProviderInput({
/>
</div>
</Button>
</Command>
</div>
</PopoverContentWithoutPortal>
</Popover>
);
Expand Down
Loading
Loading