fix: announce toggle state of integrations menu rows to screen readers (#27667)

Every toggle row in the chat integrations menu (filters, Web Search, Image, Code Interpreter, Tools, Skills) is a button whose on/off state was carried only by the decorative Switch inside it. Screen readers announced the row name and nothing else, so there was no way to tell whether a tool or feature was active without looking at it.

Each row button now carries aria-pressed, and the Switch wrapper is marked inert so the nested role=switch stops competing with the row for the announcement and stops adding a nameless tab stop. Hit testing skips inert content, so clicking the switch still toggles the row.

Tool rows that are not yet authenticated omit aria-pressed: activating those starts an OAuth redirect rather than toggling, so announcing them as an unpressed toggle would be wrong.

The Web Search, Image and Code Interpreter rows also had a state-flipping aria-label ("Disable Web Search") on top of aria-pressed, which announces as "Disable Web Search, pressed" and reads as the opposite of the truth. Removed: the visible row text already names each control.

Fixes #17150
This commit is contained in:
Classic298 authored and GitHub committed 2026-08-13 13:04:13 -06:00
1 parent ba885d0026
commit 5cb87e6630
1 file changed
+15 -43
@@ -201,6 +201,7 @@
<Tooltip content={filter?.description} placement="top-start"> <Tooltip content={filter?.description} placement="top-start">
<button <button
class="flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40" class="flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
aria-pressed={selectedFilterIds.includes(filter.id)}
on:click={() => { on:click={() => {
if (selectedFilterIds.includes(filter.id)) { if (selectedFilterIds.includes(filter.id)) {
selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id); selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id);
@@ -253,14 +254,8 @@
</div> </div>
{/if} {/if}
<div class=" shrink-0"> <div class=" shrink-0" inert>
<Switch <Switch state={selectedFilterIds.includes(filter.id)} />
state={selectedFilterIds.includes(filter.id)}
on:change={async (e) => {
const state = e.detail;
await tick();
}}
/>
</div> </div>
</button> </button>
</Tooltip> </Tooltip>
@@ -272,9 +267,6 @@
<button <button
class="flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40" class="flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
aria-pressed={webSearchEnabled} aria-pressed={webSearchEnabled}
aria-label={webSearchEnabled
? $i18n.t('Disable Web Search')
: $i18n.t('Enable Web Search')}
on:click={() => { on:click={() => {
webSearchEnabled = !webSearchEnabled; webSearchEnabled = !webSearchEnabled;
onWebSearchToggle(webSearchEnabled); onWebSearchToggle(webSearchEnabled);
@@ -290,14 +282,8 @@
</div> </div>
</div> </div>
<div class=" shrink-0"> <div class=" shrink-0" inert>
<Switch <Switch state={webSearchEnabled} />
state={webSearchEnabled}
on:change={async (e) => {
const state = e.detail;
await tick();
}}
/>
</div> </div>
</button> </button>
</Tooltip> </Tooltip>
@@ -308,9 +294,6 @@
<button <button
class="flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40" class="flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
aria-pressed={imageGenerationEnabled} aria-pressed={imageGenerationEnabled}
aria-label={imageGenerationEnabled
? $i18n.t('Disable Image Generation')
: $i18n.t('Enable Image Generation')}
on:click={() => { on:click={() => {
imageGenerationEnabled = !imageGenerationEnabled; imageGenerationEnabled = !imageGenerationEnabled;
}} }}
@@ -325,14 +308,8 @@
</div> </div>
</div> </div>
<div class=" shrink-0"> <div class=" shrink-0" inert>
<Switch <Switch state={imageGenerationEnabled} />
state={imageGenerationEnabled}
on:change={async (e) => {
const state = e.detail;
await tick();
}}
/>
</div> </div>
</button> </button>
</Tooltip> </Tooltip>
@@ -343,9 +320,6 @@
<button <button
class="flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40" class="flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
aria-pressed={codeInterpreterEnabled} aria-pressed={codeInterpreterEnabled}
aria-label={codeInterpreterEnabled
? $i18n.t('Disable Code Interpreter')
: $i18n.t('Enable Code Interpreter')}
on:click={() => { on:click={() => {
codeInterpreterEnabled = !codeInterpreterEnabled; codeInterpreterEnabled = !codeInterpreterEnabled;
}} }}
@@ -360,14 +334,8 @@
</div> </div>
</div> </div>
<div class=" shrink-0"> <div class=" shrink-0" inert>
<Switch <Switch state={codeInterpreterEnabled} />
state={codeInterpreterEnabled}
on:change={async (e) => {
const state = e.detail;
await tick();
}}
/>
</div> </div>
</button> </button>
</Tooltip> </Tooltip>
@@ -394,6 +362,9 @@
{#each Object.keys(tools) as toolId} {#each Object.keys(tools) as toolId}
<button <button
class="relative flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40" class="relative flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
aria-pressed={(tools[toolId]?.authenticated ?? true)
? tools[toolId].enabled
: undefined}
on:click={async (e) => { on:click={async (e) => {
if (!(tools[toolId]?.authenticated ?? true)) { if (!(tools[toolId]?.authenticated ?? true)) {
e.preventDefault(); e.preventDefault();
@@ -490,7 +461,7 @@
</div> </div>
{/if} {/if}
<div class=" shrink-0"> <div class=" shrink-0" inert>
<Switch state={tools[toolId].enabled} /> <Switch state={tools[toolId].enabled} />
</div> </div>
</button> </button>
@@ -517,6 +488,7 @@
{#each Object.keys(skills) as skillId} {#each Object.keys(skills) as skillId}
<button <button
class="relative flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40" class="relative flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
aria-pressed={skills[skillId].enabled}
on:click={async () => { on:click={async () => {
skills[skillId].enabled = !skills[skillId].enabled; skills[skillId].enabled = !skills[skillId].enabled;
@@ -543,7 +515,7 @@
</div> </div>
</div> </div>
<div class=" shrink-0"> <div class=" shrink-0" inert>
<Switch state={skills[skillId].enabled} /> <Switch state={skills[skillId].enabled} />
</div> </div>
</button> </button>