fix(a11y): arrow keys move through the input and integrations menus, and their items are menu items (#32940)

This commit is contained in:
G30 2026-10-22 15:41:17 -04:00 • committed by GitHub
parent 20f97c703f
commit 8ce855974f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 60 additions and 6 deletions

View file

@ -146,6 +146,7 @@
>
{#if toolPermissionsEnabled}
<button
role="menuitem"
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left"
on:click={() => {
tab = 'tool_permissions';
@ -194,6 +195,7 @@
className="w-full"
>
<button
role="menuitem"
class="flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
? 'opacity-50'
: ''}"
@ -220,6 +222,7 @@
className="w-full"
>
<button
role="menuitem"
class="flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
? 'opacity-50'
: ''}"
@ -245,6 +248,7 @@
</Tooltip>
<button
role="menuitem"
type="button"
class="flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left"
on:click={async () => {
@ -276,6 +280,7 @@
className="w-full"
>
<button
role="menuitem"
class="flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!webUploadEnabled
? 'opacity-50'
: ''}"
@ -301,6 +306,7 @@
className="w-full"
>
<button
role="menuitem"
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
? 'opacity-50'
: ''}"
@ -334,6 +340,7 @@
className="w-full"
>
<button
role="menuitem"
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
? 'opacity-50'
: ''}"
@ -365,6 +372,7 @@
className="w-full"
>
<button
role="menuitem"
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
? 'opacity-50'
: ''}"
@ -395,6 +403,7 @@
className="w-full"
>
<button
role="menuitem"
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
? 'opacity-50'
: ''}"
@ -419,6 +428,7 @@
{#if fileUploadEnabled}
{#if $config?.features?.enable_google_drive_integration}
<button
role="menuitem"
class="flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left"
type="button"
on:click={() => {
@ -462,6 +472,7 @@
{#if $config?.features?.enable_onedrive_integration && ($config?.features?.enable_onedrive_personal || $config?.features?.enable_onedrive_business)}
<button
role="menuitem"
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left {!fileUploadEnabled
? 'opacity-50'
: ''}"
@ -571,6 +582,7 @@
{:else if tab === 'tool_permissions'}
<div class="flex max-h-72 flex-col overflow-hidden" in:fly={{ x: 20, duration: 150 }}>
<button
role="menuitem"
class="flex w-full shrink-0 justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
on:click={() => {
tab = '';
@ -589,6 +601,8 @@
{#each toolApprovalModes as mode}
<Tooltip content={$i18n.t(mode.description)} className="w-full">
<button
role="menuitemradio"
aria-checked={toolApprovalMode === mode.value}
class="flex gap-2 w-full items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left"
on:click={() => {
toolApprovalMode = mode.value;
@ -620,6 +634,7 @@
{:else if tab === 'knowledge'}
<div class="flex max-h-72 flex-col overflow-hidden" in:fly={{ x: 20, duration: 150 }}>
<button
role="menuitem"
class="flex w-full shrink-0 justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
on:click={() => {
tab = '';
@ -639,6 +654,7 @@
{:else if tab === 'notes'}
<div class="flex max-h-72 flex-col overflow-hidden" in:fly={{ x: 20, duration: 150 }}>
<button
role="menuitem"
class="flex w-full shrink-0 justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
on:click={() => {
tab = '';
@ -658,6 +674,7 @@
{:else if tab === 'files'}
<div class="flex max-h-72 flex-col overflow-hidden" in:fly={{ x: 20, duration: 150 }}>
<button
role="menuitem"
class="flex w-full shrink-0 justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
on:click={() => {
tab = '';
@ -677,6 +694,7 @@
{:else if tab === 'chats'}
<div class="flex max-h-72 flex-col overflow-hidden" in:fly={{ x: 20, duration: 150 }}>
<button
role="menuitem"
class="flex w-full shrink-0 justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
on:click={() => {
tab = '';
@ -696,6 +714,7 @@
{:else if tab === 'microsoft_onedrive'}
<div in:fly={{ x: 20, duration: 150 }}>
<button
role="menuitem"
class="flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
on:click={() => {
tab = '';
@ -712,6 +731,7 @@
{#if $config?.features?.enable_onedrive_personal}
<button
role="menuitem"
class="flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left"
type="button"
on:click={() => {
@ -727,6 +747,7 @@
{#if $config?.features?.enable_onedrive_business}
<button
role="menuitem"
class="flex w-full gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal select-none cursor-pointer hover:bg-gray-50/40 dark:hover:bg-gray-800/40 rounded-xl text-left"
type="button"
on:click={() => {

View file

@ -315,6 +315,7 @@
{#if tools}
{#if Object.keys(tools).length > 0}
<button
role="menuitem"
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"
on:click={() => {
tab = 'tools';
@ -337,6 +338,7 @@
{#if skills && Object.keys(skills).length > 0}
<button
role="menuitem"
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"
on:click={() => {
tab = 'skills';
@ -375,7 +377,8 @@
>
<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"
aria-pressed={selectedFilterIds.includes(filter.id)}
role="menuitemcheckbox"
aria-checked={selectedFilterIds.includes(filter.id)}
on:click={() => {
if (selectedFilterIds.includes(filter.id)) {
selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id);
@ -447,7 +450,8 @@
<Tooltip content={$i18n.t('Search the internet')} placement="top-start">
<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"
aria-pressed={webSearchEnabled}
role="menuitemcheckbox"
aria-checked={webSearchEnabled}
on:click={() => {
webSearchEnabled = !webSearchEnabled;
// Left open, this menu would swallow the first click on the confirmation dialog
@ -478,7 +482,8 @@
<Tooltip content={$i18n.t('Generate an image')} placement="top-start">
<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"
aria-pressed={imageGenerationEnabled}
role="menuitemcheckbox"
aria-checked={imageGenerationEnabled}
on:click={() => {
imageGenerationEnabled = !imageGenerationEnabled;
}}
@ -504,7 +509,8 @@
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top-start">
<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"
aria-pressed={codeInterpreterEnabled}
role="menuitemcheckbox"
aria-checked={codeInterpreterEnabled}
on:click={() => {
codeInterpreterEnabled = !codeInterpreterEnabled;
}}
@ -529,6 +535,7 @@
{:else if tab === 'tools' && tools}
<div class="flex max-h-72 min-h-0 flex-col gap-0.5" in:fly={{ x: 20, duration: 150 }}>
<button
role="menuitem"
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"
on:click={() => {
toolQuery = '';
@ -555,7 +562,10 @@
{#each toolIds as toolId}
<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"
aria-pressed={(tools?.[toolId]?.authenticated ?? true)
role={(tools?.[toolId]?.authenticated ?? true)
? 'menuitemcheckbox'
: 'menuitem'}
aria-checked={(tools?.[toolId]?.authenticated ?? true)
? selectedToolIds.includes(toolId)
: undefined}
on:click={async (e) => {
@ -663,6 +673,7 @@
{:else if tab === 'skills' && skills}
<div class="flex max-h-72 min-h-0 flex-col gap-0.5" in:fly={{ x: 20, duration: 150 }}>
<button
role="menuitem"
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"
on:click={() => {
skillQuery = '';
@ -689,7 +700,8 @@
{#each skillIds as skillId}
<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"
aria-pressed={selectedSkillIds.includes(skillId)}
role="menuitemcheckbox"
aria-checked={selectedSkillIds.includes(skillId)}
on:click={async () => {
await toggleSkill(skillId);
}}

View file

@ -326,6 +326,27 @@
if (event.key === 'Escape' && show) {
closeDropdown();
}
if (
['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key) &&
event.target instanceof HTMLElement &&
event.target.matches('[role^="menuitem"]') &&
contentEl?.contains(event.target)
) {
const items = [
...contentEl.querySelectorAll<HTMLElement>('[role^="menuitem"]:not([disabled])')
].filter((item) => item.getClientRects().length > 0);
const index = items.indexOf(event.target);
const nextIndex =
event.key === 'Home'
? 0
: event.key === 'End'
? items.length - 1
: (index + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
event.preventDefault();
items[nextIndex]?.focus();
}
}
/** Close the dropdown programmatically */