]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
webui : [ChatFormActionAdd][a11y] fix accessibility issues in add menu trigger and...
authorviggy <redacted>
Sat, 16 May 2026 10:00:46 +0000 (03:00 -0700)
committerGitHub <redacted>
Sat, 16 May 2026 10:00:46 +0000 (12:00 +0200)
* fix tab order on attach button, and dont focus on disabled mennu item

* add a11y tests

tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionsAdd.svelte
tools/ui/tests/stories/ChatScreenForm.a11y.stories.svelte [new file with mode: 0644]

index e053e6f836b63de2f7d56212fc7058dae2125bff..a6bb0fc2a80fbb5fb973b614bfc869242191ead6 100644 (file)
@@ -1,11 +1,14 @@
 <script lang="ts">
-       import type { Snippet } from 'svelte';
+       import { Plus } from '@lucide/svelte';
        import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
        import * as Tooltip from '$lib/components/ui/tooltip';
+       import { buttonVariants } from '$lib/components/ui/button';
+       import { cn } from '$lib/components/ui/utils';
        import {
                ATTACHMENT_FILE_ITEMS,
                ATTACHMENT_EXTRA_ITEMS,
                ATTACHMENT_MCP_ITEMS,
+               ATTACHMENT_TOOLTIP_TEXT,
                TOOLTIP_DELAY_DURATION
        } from '$lib/constants';
        import { AttachmentMenuItemId } from '$lib/enums';
@@ -28,7 +31,6 @@
                onMcpPromptClick?: () => void;
                onMcpSettingsClick?: () => void;
                onMcpResourcesClick?: () => void;
-               trigger: Snippet<[{ disabled: boolean }]>;
        }
 
        let {
@@ -42,8 +44,7 @@
                onSystemPromptClick,
                onMcpPromptClick,
                onMcpSettingsClick,
-               onMcpResourcesClick,
-               trigger
+               onMcpResourcesClick
        }: Props = $props();
 
        let dropdownOpen = $state(false);
 
 <div class="flex items-center gap-1 {className}">
        <DropdownMenu.Root bind:open={dropdownOpen}>
-               <DropdownMenu.Trigger name="Attach files" {disabled}>
-                       {@render trigger({ disabled })}
-               </DropdownMenu.Trigger>
+               <Tooltip.Root>
+                       <Tooltip.Trigger>
+                               {#snippet child({ props })}
+                                       <DropdownMenu.Trigger
+                                               {...props}
+                                               class={cn(
+                                                       buttonVariants({ variant: 'secondary' }),
+                                                       'file-upload-button h-8 w-8 cursor-pointer rounded-full p-0'
+                                               )}
+                                               {disabled}
+                                       >
+                                               <span class="sr-only">{ATTACHMENT_TOOLTIP_TEXT}</span>
+
+                                               <Plus class="h-4 w-4" />
+                                       </DropdownMenu.Trigger>
+                               {/snippet}
+                       </Tooltip.Trigger>
+
+                       <Tooltip.Content>
+                               <p>{ATTACHMENT_TOOLTIP_TEXT}</p>
+                       </Tooltip.Content>
+               </Tooltip.Root>
 
                <DropdownMenu.Content align="start" class="w-48">
                        {#each ATTACHMENT_FILE_ITEMS as item (item.id)}
                                        </DropdownMenu.Item>
                                {:else if item.disabledTooltip}
                                        <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
-                                               <Tooltip.Trigger class="w-full">
-                                                       <DropdownMenu.Item
-                                                               class="{item.class ?? ''} flex cursor-pointer items-center gap-2"
-                                                               disabled
-                                                       >
-                                                               <item.icon class="h-4 w-4" />
-
-                                                               <span>{item.label}</span>
-                                                       </DropdownMenu.Item>
+                                               <Tooltip.Trigger tabindex={-1}>
+                                                       {#snippet child({ props })}
+                                                               <div {...props} class="cursor-default">
+                                                                       <DropdownMenu.Item class="{item.class ?? ''} flex items-center gap-2" disabled>
+                                                                               <item.icon class="h-4 w-4" />
+
+                                                                               <span>{item.label}</span>
+                                                                       </DropdownMenu.Item>
+                                                               </div>
+                                                       {/snippet}
                                                </Tooltip.Trigger>
 
                                                <Tooltip.Content side="right">
 
                        {#if !attachmentMenu.isItemEnabled('hasVisionModality')}
                                <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
-                                       <Tooltip.Trigger class="w-full">
-                                               <DropdownMenu.Item
-                                                       class="flex cursor-pointer items-center gap-2"
-                                                       onclick={attachmentMenu.callbacks.onFileUpload}
-                                               >
-                                                       {@const pdfItem = ATTACHMENT_FILE_ITEMS.find(
-                                                               (i) => i.id === AttachmentMenuItemId.PDF
-                                                       )}
-                                                       {#if pdfItem}
-                                                               <pdfItem.icon class="h-4 w-4" />
-
-                                                               <span>{pdfItem.label}</span>
-                                                       {/if}
-                                               </DropdownMenu.Item>
+                                       <Tooltip.Trigger>
+                                               {#snippet child({ props })}
+                                                       <DropdownMenu.Item
+                                                               {...props}
+                                                               class="flex cursor-pointer items-center gap-2"
+                                                               onclick={attachmentMenu.callbacks.onFileUpload}
+                                                       >
+                                                               {@const pdfItem = ATTACHMENT_FILE_ITEMS.find(
+                                                                       (i) => i.id === AttachmentMenuItemId.PDF
+                                                               )}
+                                                               {#if pdfItem}
+                                                                       <pdfItem.icon class="h-4 w-4" />
+
+                                                                       <span>{pdfItem.label}</span>
+                                                               {/if}
+                                                       </DropdownMenu.Item>
+                                               {/snippet}
                                        </Tooltip.Trigger>
 
                                        <Tooltip.Content side="right">
                        {#each ATTACHMENT_EXTRA_ITEMS as item (item.id)}
                                {#if item.id === AttachmentMenuItemId.SYSTEM_MESSAGE}
                                        <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
-                                               <Tooltip.Trigger class="w-full">
-                                                       <DropdownMenu.Item
-                                                               class="flex cursor-pointer items-center gap-2"
-                                                               onclick={() => attachmentMenu.callbacks[item.action]()}
-                                                       >
-                                                               <item.icon class="h-4 w-4" />
-
-                                                               <span>{item.label}</span>
-                                                       </DropdownMenu.Item>
+                                               <Tooltip.Trigger>
+                                                       {#snippet child({ props })}
+                                                               <DropdownMenu.Item
+                                                                       {...props}
+                                                                       class="flex cursor-pointer items-center gap-2"
+                                                                       onclick={() => attachmentMenu.callbacks[item.action]()}
+                                                               >
+                                                                       <item.icon class="h-4 w-4" />
+
+                                                                       <span>{item.label}</span>
+                                                               </DropdownMenu.Item>
+                                                       {/snippet}
                                                </Tooltip.Trigger>
 
                                                <Tooltip.Content side="right">
index 8cfd7d809d51ca85d7034a5ed5eb3f11b57491e2..5d3a20b16b9cbaecaf796279ca14964baccbd1ad 100644 (file)
@@ -60,9 +60,5 @@
                {onMcpResourcesClick}
                {onMcpSettingsClick}
                {onSystemPromptClick}
-       >
-               {#snippet trigger()}
-                       <ChatFormActionAddButton {disabled} />
-               {/snippet}
-       </ChatFormActionAddDropdown>
+       />
 {/if}
diff --git a/tools/ui/tests/stories/ChatScreenForm.a11y.stories.svelte b/tools/ui/tests/stories/ChatScreenForm.a11y.stories.svelte
new file mode 100644 (file)
index 0000000..5d28f42
--- /dev/null
@@ -0,0 +1,50 @@
+<script module lang="ts">
+       import { defineMeta } from '@storybook/addon-svelte-csf';
+       import ChatScreenForm from '$lib/components/app/chat/ChatScreen/ChatScreenForm.svelte';
+       import { expect, screen, waitFor } from 'storybook/test';
+       import { ATTACHMENT_TOOLTIP_TEXT } from '$lib/constants';
+
+       const { Story } = defineMeta({
+               title: 'Components/ChatScreen/ChatScreenForm/Accessibility',
+               component: ChatScreenForm,
+               parameters: {
+                       layout: 'centered'
+               },
+               tags: ['!dev']
+       });
+</script>
+
+<Story
+       name="AddButtonSingleTabStop"
+       args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
+       play={async ({ canvas, userEvent }) => {
+               const textarea = await canvas.findByRole('textbox');
+               await userEvent.clear(textarea);
+               await userEvent.type(textarea, 'What is the meaning of life?');
+
+               const trigger = await canvas.findByRole('button', { name: ATTACHMENT_TOOLTIP_TEXT });
+
+               trigger.focus();
+               await expect(trigger).toHaveFocus();
+
+               await userEvent.tab();
+
+               await expect(trigger).not.toHaveFocus();
+       }}
+/>
+
+<Story
+       name="AddDropdownFocusesFirstEnabled"
+       args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
+       play={async ({ canvas, userEvent }) => {
+               const trigger = await canvas.findByRole('button', { name: ATTACHMENT_TOOLTIP_TEXT });
+
+               trigger.focus();
+               await userEvent.keyboard('{Enter}');
+               await screen.findByRole('menu');
+
+               await waitFor(() => {
+                       expect(document.activeElement).toHaveTextContent('Text Files');
+               });
+       }}
+/>