ui: fix accessibility for hover-gated interactive elements assisted by claude(in debugging and tests) (#24727)

This commit is contained in:
Sanjay Ahari 2026-06-26 16:25:38 +05:30 committed by GitHub
parent 9df06805ee
commit ded1561b42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 58 additions and 83 deletions

View File

@ -33,7 +33,7 @@
{#if !readonly && onRemove} {#if !readonly && onRemove}
<div <div
class="absolute top-10 right-2 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100" class="absolute top-10 right-2 flex items-center justify-center opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
> >
<ActionIcon icon={X} tooltip="Remove" stopPropagationOnClick onclick={() => onRemove?.()} /> <ActionIcon icon={X} tooltip="Remove" stopPropagationOnClick onclick={() => onRemove?.()} />
</div> </div>

View File

@ -56,7 +56,7 @@
<div class="relative flex h-6 items-center justify-between"> <div class="relative flex h-6 items-center justify-between">
<div class="right-0 flex items-center gap-2 opacity-100 transition-opacity"> <div class="right-0 flex items-center gap-2 opacity-100 transition-opacity">
<div <div
class="pointer-events-auto inset-0 flex items-center gap-1 opacity-0 transition-all duration-150 group-hover:opacity-100" class="pointer-events-auto inset-0 flex items-center gap-1 opacity-0 transition-all duration-150 group-focus-within:opacity-100 group-hover:opacity-100"
> >
<ActionIcon icon={Edit} tooltip="Edit" onclick={editCtx.handleEdit} /> <ActionIcon icon={Edit} tooltip="Edit" onclick={editCtx.handleEdit} />
<ActionIcon icon={Trash2} tooltip="Delete" onclick={onDelete} /> <ActionIcon icon={Trash2} tooltip="Delete" onclick={onDelete} />

View File

@ -39,7 +39,6 @@
depth = 0 depth = 0
}: Props = $props(); }: Props = $props();
let renderActionsDropdown = $state(false);
let dropdownOpen = $state(false); let dropdownOpen = $state(false);
let isLoading = $derived(getAllLoadingChats().includes(conversation.id)); let isLoading = $derived(getAllLoadingChats().includes(conversation.id));
@ -71,26 +70,10 @@
} }
} }
function handleMouseLeave() {
if (!dropdownOpen) {
renderActionsDropdown = false;
}
}
function handleMouseOver() {
renderActionsDropdown = true;
}
function handleSelect() { function handleSelect() {
onSelect?.(conversation.id); onSelect?.(conversation.id);
} }
$effect(() => {
if (!dropdownOpen) {
renderActionsDropdown = false;
}
});
onMount(() => { onMount(() => {
document.addEventListener('edit-active-conversation', handleGlobalEditEvent as EventListener); document.addEventListener('edit-active-conversation', handleGlobalEditEvent as EventListener);
@ -103,23 +86,19 @@
}); });
</script> </script>
<!-- svelte-ignore a11y_mouse_events_have_key_events --> <div
<button class="conversation-item group relative flex min-h-9 w-full items-center justify-between space-x-3 rounded-lg py-1.5 transition-colors hover:bg-foreground/10 {isActive
class="group flex min-h-9 w-full cursor-pointer items-center justify-between space-x-3 rounded-lg py-1.5 text-left transition-colors hover:bg-foreground/10 {isActive
? 'bg-foreground/5 text-accent-foreground' ? 'bg-foreground/5 text-accent-foreground'
: ''} px-3" : ''} px-3"
onclick={handleSelect}
onmouseover={handleMouseOver}
onmouseleave={handleMouseLeave}
onfocusin={handleMouseOver}
onfocusout={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
handleMouseLeave();
}
}}
> >
<button
class="absolute inset-0 z-0 cursor-pointer rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onclick={handleSelect}
aria-label={conversation.name}
>
</button>
<div <div
class="flex min-w-0 flex-1 items-center gap-2" class="pointer-events-none relative z-10 flex min-w-0 flex-1 items-center gap-2"
style:padding-left="{depth * FORK_TREE_DEPTH_PADDING}px" style:padding-left="{depth * FORK_TREE_DEPTH_PADDING}px"
> >
{#if depth > 0} {#if depth > 0}
@ -130,7 +109,7 @@
<a <a
{...props} {...props}
href={RouterService.chat(conversation.forkedFromConversationId)} href={RouterService.chat(conversation.forkedFromConversationId)}
class="flex shrink-0 items-center text-muted-foreground transition-colors hover:text-foreground" class="pointer-events-auto flex shrink-0 items-center text-muted-foreground transition-colors hover:text-foreground"
> >
<GitBranch class="h-3.5 w-3.5" /> <GitBranch class="h-3.5 w-3.5" />
</a> </a>
@ -146,18 +125,15 @@
{#if isLoading} {#if isLoading}
<Tooltip.Root> <Tooltip.Root>
<Tooltip.Trigger> <Tooltip.Trigger>
<div <button
class="stop-button flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground" class="stop-button pointer-events-auto flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground"
onclick={handleStop} onclick={handleStop}
onkeydown={(e) => e.key === 'Enter' && handleStop(e)}
role="button"
tabindex="0"
aria-label="Stop generation" aria-label="Stop generation"
> >
<Loader2 class="loading-icon h-3.5 w-3.5 animate-spin" /> <Loader2 class="loading-icon h-3.5 w-3.5 animate-spin" />
<Square class="stop-icon hidden h-3 w-3 fill-current text-destructive" /> <Square class="stop-icon hidden h-3 w-3 fill-current text-destructive" />
</div> </button>
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content> <Tooltip.Content>
@ -169,52 +145,50 @@
<TruncatedText text={conversation.name} class="text-sm font-medium" showTooltip={false} /> <TruncatedText text={conversation.name} class="text-sm font-medium" showTooltip={false} />
</div> </div>
{#if renderActionsDropdown} <div class="actions pointer-events-auto relative z-20 flex items-center">
<div class="actions flex items-center"> <DropdownMenuActions
<DropdownMenuActions triggerIcon={MoreHorizontal}
triggerIcon={MoreHorizontal} triggerTooltip="More actions"
triggerTooltip="More actions" bind:open={dropdownOpen}
bind:open={dropdownOpen} actions={[
actions={[ {
{ icon: conversation.pinned ? PinOff : Pin,
icon: conversation.pinned ? PinOff : Pin, label: conversation.pinned ? 'Unpin' : 'Pin',
label: conversation.pinned ? 'Unpin' : 'Pin', onclick: (e: Event) => {
onclick: (e: Event) => { e.stopPropagation();
e.stopPropagation(); handleTogglePin();
handleTogglePin();
}
},
{
icon: Pencil,
label: 'Edit',
onclick: handleEdit,
shortcut: ['shift', 'cmd', 'e']
},
{
icon: Download,
label: 'Export',
onclick: (e: Event) => {
e.stopPropagation();
conversationsStore.downloadConversation(conversation.id);
},
shortcut: ['shift', 'cmd', 's']
},
{
icon: Trash2,
label: 'Delete',
onclick: handleDelete,
variant: 'destructive',
shortcut: ['shift', 'cmd', 'd'],
separator: true
} }
]} },
/> {
</div> icon: Pencil,
{/if} label: 'Edit',
</button> onclick: handleEdit,
shortcut: ['shift', 'cmd', 'e']
},
{
icon: Download,
label: 'Export',
onclick: (e: Event) => {
e.stopPropagation();
conversationsStore.downloadConversation(conversation.id);
},
shortcut: ['shift', 'cmd', 's']
},
{
icon: Trash2,
label: 'Delete',
onclick: handleDelete,
variant: 'destructive',
shortcut: ['shift', 'cmd', 'd'],
separator: true
}
]}
/>
</div>
</div>
<style> <style>
button { .conversation-item {
:global([data-slot='dropdown-menu-trigger']:not([data-state='open'])) { :global([data-slot='dropdown-menu-trigger']:not([data-state='open'])) {
opacity: 0; opacity: 0;
} }
@ -239,7 +213,8 @@
} }
} }
&:is(:hover) .stop-button { &:is(:hover) .stop-button,
&:focus-within .stop-button {
:global(.stop-icon) { :global(.stop-icon) {
display: block; display: block;
} }