webui: [a11y] fix keyboard navigation issues in chat interface and sidebar (#23132)
* use child snippets for landing and chat message elements * make ... icon visible in conversation history menu * conversation history forward tab fix * add snippet fix for fork icon in conversation history * focus/keyboard fix for attachment x icon and scroll left/right * formatting * fix scroll down issue * simply Statistics and pointer events in scrolldown * create storybook tests and move to folder * improve tests to actually assert on element
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { Copy } from '@lucide/svelte';
|
||||
import ActionIcon from '$lib/components/app/actions/ActionIcon.svelte';
|
||||
import { expect } from 'storybook/test';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/ActionIcon/Accessibility',
|
||||
component: ActionIcon,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
tags: ['!dev']
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story
|
||||
asChild
|
||||
name="SingleTabStop"
|
||||
play={async ({ canvas, userEvent }) => {
|
||||
const before = await canvas.findByRole('button', { name: 'before' });
|
||||
const target = await canvas.findByRole('button', { name: 'Copy' });
|
||||
|
||||
before.focus();
|
||||
await userEvent.tab();
|
||||
|
||||
await expect(target).toHaveFocus();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<button type="button">before</button>
|
||||
<ActionIcon icon={Copy} tooltip="Copy" onclick={() => {}} />
|
||||
</div>
|
||||
</Story>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import ChatMessageStatistics from '$lib/components/app/chat/ChatMessages/ChatMessageStatistics/ChatMessageStatistics.svelte';
|
||||
import { expect } from 'storybook/test';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/ChatMessageStatistics/Accessibility',
|
||||
component: ChatMessageStatistics,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
tags: ['!dev']
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="ViewButtonsSingleTabStop"
|
||||
args={{
|
||||
promptTokens: 100,
|
||||
promptMs: 500,
|
||||
predictedTokens: 200,
|
||||
predictedMs: 1000,
|
||||
agenticTimings: {
|
||||
turns: 1,
|
||||
toolCallsCount: 1,
|
||||
toolsMs: 500,
|
||||
llm: { predicted_n: 200, predicted_ms: 1000, prompt_n: 100, prompt_ms: 500 }
|
||||
},
|
||||
hideSummary: false,
|
||||
isLive: false
|
||||
}}
|
||||
play={async ({ canvas, userEvent }) => {
|
||||
const reading = await canvas.findByRole('button', { name: 'Reading' });
|
||||
const generation = await canvas.findByRole('button', { name: 'Generation' });
|
||||
const tools = await canvas.findByRole('button', { name: 'Tools' });
|
||||
const summary = await canvas.findByRole('button', { name: 'Summary' });
|
||||
|
||||
reading.focus();
|
||||
await expect(reading).toHaveFocus();
|
||||
|
||||
await userEvent.tab();
|
||||
await expect(generation).toHaveFocus();
|
||||
|
||||
await userEvent.tab();
|
||||
await expect(tools).toHaveFocus();
|
||||
|
||||
await userEvent.tab();
|
||||
await expect(summary).toHaveFocus();
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import HorizontalScrollCarousel from '$lib/components/app/misc/HorizontalScrollCarousel.svelte';
|
||||
import { expect, waitFor } from 'storybook/test';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/HorizontalScrollCarousel/Accessibility',
|
||||
component: HorizontalScrollCarousel,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
tags: ['!dev']
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story
|
||||
asChild
|
||||
name="ArrowsNotInTabOrderWhenNotScrollable"
|
||||
play={async ({ canvas, userEvent }) => {
|
||||
const before = await canvas.findByRole('button', { name: 'before' });
|
||||
const after = await canvas.findByRole('button', { name: 'after' });
|
||||
const leftArrow = await canvas.findByRole('button', { name: 'Scroll left' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(leftArrow).toBeDisabled();
|
||||
});
|
||||
|
||||
before.focus();
|
||||
await userEvent.tab();
|
||||
|
||||
await expect(after).toHaveFocus();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<button type="button">before</button>
|
||||
<HorizontalScrollCarousel class="w-96">
|
||||
<div class="h-12 w-12 shrink-0 bg-muted"></div>
|
||||
<div class="h-12 w-12 shrink-0 bg-muted"></div>
|
||||
</HorizontalScrollCarousel>
|
||||
<button type="button">after</button>
|
||||
</div>
|
||||
</Story>
|
||||
|
||||
<Story
|
||||
asChild
|
||||
name="ArrowsInTabOrderWhenScrollable"
|
||||
play={async ({ canvas, userEvent }) => {
|
||||
const before = await canvas.findByRole('button', { name: 'before' });
|
||||
const rightArrow = await canvas.findByRole('button', { name: 'Scroll right' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(rightArrow).not.toBeDisabled();
|
||||
});
|
||||
|
||||
before.focus();
|
||||
await userEvent.tab();
|
||||
|
||||
await expect(rightArrow).toHaveFocus();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<button type="button">before</button>
|
||||
<HorizontalScrollCarousel class="w-48">
|
||||
{#each [...Array(20).keys()] as i (i)}
|
||||
<div class="h-12 w-24 shrink-0 bg-muted">{i}</div>
|
||||
{/each}
|
||||
</HorizontalScrollCarousel>
|
||||
</div>
|
||||
</Story>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import SidebarNavigationConversationItem from '$lib/components/app/navigation/SidebarNavigation/SidebarNavigationConversationItem.svelte';
|
||||
import { expect } from 'storybook/test';
|
||||
|
||||
const mockForkedConversation: DatabaseConversation = {
|
||||
id: 'conv-2',
|
||||
name: 'Forked Conversation',
|
||||
lastModified: Date.now(),
|
||||
currNode: 'msg-2',
|
||||
forkedFromConversationId: 'conv-1'
|
||||
};
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/SidebarNavigationConversationItem/Accessibility',
|
||||
component: SidebarNavigationConversationItem,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
tags: ['!dev']
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="ForkIconSingleTabStop"
|
||||
args={{ conversation: mockForkedConversation, depth: 1 }}
|
||||
play={async ({ canvas, userEvent }) => {
|
||||
const row = await canvas.findByRole('button', { name: /Forked Conversation/ });
|
||||
const forkIcon = await canvas.findByRole('link');
|
||||
|
||||
row.focus();
|
||||
await userEvent.tab();
|
||||
|
||||
await expect(forkIcon).toHaveFocus();
|
||||
}}
|
||||
/>
|
||||
Reference in New Issue
Block a user