fix: make sidebar section headers keyboard operable (WCAG 2.1.1, 4.1.2) (#27489)

On latest `dev`, each sidebar section header in `Sidebar/Section.svelte` is a real `<button>` carrying `aria-expanded` and `aria-controls`, but it has **no activation handler**. The toggle comes only from `on:pointerup` on the wrapper inside `common/Collapsible.svelte`.

Keyboard activation dispatches a synthetic `click`, never `pointerup`, and that wrapper's own `on:click` handler calls `stopPropagation()`. So pressing Enter or Space on the header does nothing at all, while `aria-expanded` tells assistive technology this is a working disclosure control.

This affects every section in the sidebar: Models, Notes, Channels, Folders and Chats. Section state is persisted to `localStorage`, so a user whose section was collapsed on a previous visit has no keyboard way to open it again, and the content stays unreachable.

Breaks WCAG 2.1.1 Keyboard (Level A), and 4.1.2 Name, Role, Value (Level A), because the exposed expanded state belongs to a control that cannot be operated.

Fix: handle activation on the header button itself, where focus actually lands, and stop the now duplicate pointer path so a mouse click does not toggle twice. The existing inline `onChange` body is extracted to `setOpen` so the `change` dispatch and the `localStorage` write stay in one place and fire exactly once per toggle in both input modes. The adjacent "+" (`onAdd`) button already stops both `pointerup` and `click`, so it still does not toggle the section.

`Collapsible`'s wrapper cannot simply become a `<button>` instead, because its slot receives buttons from this component and others, so the fix belongs here.

`common/Folder.svelte` and `Sidebar/RecursiveFolder.svelte` have the same latent defect and are not touched by this PR.

Severity: Critical. Sidebar navigation cannot be expanded without a mouse.

### 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.
This commit is contained in:
Classic298
2026-07-26 17:38:06 -04:00
committed by GitHub
parent 15f724b0f2
commit ba4c92c4f0
@@ -24,6 +24,12 @@
let loaded = false;
let draggedOver = false;
const setOpen = (state: boolean) => {
open = state;
dispatch('change', state);
localStorage.setItem(`${id}-folder-state`, `${state}`);
};
const onDragOver = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -118,21 +124,15 @@
{/if}
{#if collapsible}
<Collapsible
bind:open
className="w-full"
buttonClassName="w-full"
onChange={(state: boolean) => {
dispatch('change', state);
localStorage.setItem(`${id}-folder-state`, `${state}`);
}}
>
<Collapsible bind:open className="w-full" buttonClassName="w-full" onChange={setOpen}>
<div class="flex items-center justify-between h-6 w-full pl-3.5 pr-1.5 shrink-0">
<button
type="button"
class="group flex flex-1 h-full items-center gap-1 text-left text-xs text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 transition-colors duration-100 {buttonClassName}"
aria-expanded={open}
aria-controls="{id}-content"
on:pointerup|stopPropagation
on:click={() => setOpen(!open)}
>
<span>{name}</span>
<span