From 71511ccd5ac62e08da84845827f2286c6dc70b56 Mon Sep 17 00:00:00 2001
From: Classic298 <27028174+Classic298@users.noreply.github.com>
Date: Mon, 27 Jul 2026 09:16:25 +0200
Subject: [PATCH] fix: make sidebar folder rows keyboard operable (WCAG 2.1.1,
4.1.2) (#27509)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
On latest `dev`, the sidebar folder row is a bare `
` carrying `on:click` (navigate into the folder) and `on:dblclick` (rename). It has **no `role`, no `tabindex` and no key handler**, so opening a folder is impossible from the keyboard.
The nested chevron `` is focusable, but it only expands the folder in place, it does not navigate to it, so there is no keyboard route to the folder page at all.
Breaks WCAG 2.1.1 Keyboard (Level A) and 4.1.2 Name, Role, Value (Level A). The Svelte compiler already flags this file with `a11y_click_events_have_key_events`; after this change the component compiles with zero a11y warnings.
Fix: apply the row pattern already used elsewhere in this codebase (`workspace/Prompts.svelte`, `workspace/Knowledge.svelte`, `admin/Functions.svelte`), namely `role="button"`, `tabindex="0"` and a keydown handler for Enter and Space, with the same `e.currentTarget !== e.target` guard and the same `shouldIgnoreRowClick` helper those files use.
That guard matters more here than in the files it was copied from: the rename ` ` is rendered **inside** this row, so without it typing a space in the rename field would be swallowed and navigate away, and Enter would both save the rename and navigate.
The navigation body is extracted to `openFolderHandler` because it now has two callers. The keyboard path calls it directly rather than through the 100ms `clickTimer`, which exists only to disambiguate single from double click and has no keyboard equivalent.
A dead `(e) => e.stopPropagation();` expression statement in the click handler is removed. It allocated an arrow function and discarded it without ever calling it.
The `…` folder menu is still `invisible group-hover:visible` and therefore unreachable, so rename, share, delete, export and new subfolder remain keyboard-inaccessible until that is addressed. That is fixed repo wide in a separate PR that replaces the `invisible group-hover:visible` pattern, so it is deliberately not touched here to avoid conflicting on the same line.
Folder reparenting by drag still has no keyboard alternative, which is a separate WCAG 2.5.7 issue needing a "Move" menu action.
Severity: Critical. Folders cannot be opened without a pointing device.
### Contributor License Agreement
- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
Co-authored-by: Tim Baek
---
.../layout/Sidebar/RecursiveFolder.svelte | 45 +++++++++++++------
1 file changed, 31 insertions(+), 14 deletions(-)
diff --git a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte
index 7dd38dec8e..9e907d928d 100644
--- a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte
+++ b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte
@@ -586,6 +586,26 @@
setFolderItems();
}
+ const shouldIgnoreRowClick = (target) => {
+ return target instanceof Element && !!target.closest('button, a, input, [role="menu"]');
+ };
+
+ const openFolderHandler = async () => {
+ const folder = await getFolderById(localStorage.token, folderId).catch((error) => {
+ toast.error(`${error}`);
+ return null;
+ });
+
+ if (folder) {
+ await selectedFolder.set({ ...folders[folderId], ...folder });
+ }
+
+ await goto(`/folders/${folderId}`);
+
+ if ($mobile) {
+ showSidebar.set(!$showSidebar);
+ }
+ };
$: if (!open && chats !== null) {
chats = null;
chatsPage = 1;
@@ -729,30 +749,27 @@
}
renameHandler();
}}
+ role="button"
+ tabindex="0"
on:click={async (e) => {
+ if (shouldIgnoreRowClick(e.target)) return;
if (clickTimer) {
clearTimeout(clickTimer);
clickTimer = null;
}
clickTimer = setTimeout(async () => {
- const folder = await getFolderById(localStorage.token, folderId).catch((error) => {
- toast.error(`${error}`);
- return null;
- });
-
- if (folder) {
- await selectedFolder.set({ ...folders[folderId], ...folder });
- }
-
- await goto(`/folders/${folderId}`);
-
- if ($mobile) {
- showSidebar.set(!$showSidebar);
- }
+ await openFolderHandler();
clickTimer = null;
}, 100); // 100ms delay (typical double-click threshold)
}}
+ on:keydown={(e) => {
+ if (e.currentTarget !== e.target) return;
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ openFolderHandler();
+ }
+ }}
on:pointerup={(e) => {
e.stopPropagation();
}}