fix: make sidebar folder rows keyboard operable (WCAG 2.1.1, 4.1.2) (#27509)

On latest `dev`, the sidebar folder row is a bare `<div>` 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 `<button>` 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 `<input>` 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

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [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 <tim@openwebui.com>
This commit is contained in:
Classic298 2026-07-27 09:16:25 +02:00 committed by GitHub
parent 9707d3a5c2
commit 71511ccd5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 31 additions and 14 deletions

View File

@ -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();
}}