mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 00:02:39 +00:00
feat(native-chat): a solid, fading Jump to latest (#25759)
"Jump to latest" was a faint, see-through pill that popped in and out. It is now a solid button that fades in and out, can't be clicked or tabbed to while hidden, and keeps focus in the composer when clicked. Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
This commit is contained in:
co-authored by
Kelvin Amoaba
parent
ab6389045e
commit
975e732ce3
@@ -0,0 +1,39 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { NativeChatJumpToLatest } from './NativeChatJumpToLatest'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
// The reader is usually mid-draft when they reach for this: a press must not take the caret.
|
||||
it('leaves focus in the composer when pressed with the pointer', async () => {
|
||||
const onJump = vi.fn()
|
||||
render(
|
||||
<>
|
||||
<NativeChatJumpToLatest visible onJump={onJump} />
|
||||
<textarea aria-label="Message" />
|
||||
</>
|
||||
)
|
||||
const composer = screen.getByRole('textbox', { name: 'Message' })
|
||||
composer.focus()
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Jump to latest' }))
|
||||
|
||||
// Anti-vacuous: the press did act.
|
||||
expect(onJump).toHaveBeenCalledOnce()
|
||||
expect(document.activeElement).toBe(composer)
|
||||
})
|
||||
|
||||
// Mounted while hidden so it can fade out; it must not stay reachable meanwhile.
|
||||
it('stays mounted but unreachable while hidden', () => {
|
||||
const { container } = render(<NativeChatJumpToLatest visible={false} onJump={vi.fn()} />)
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Jump to latest' })).toBeNull()
|
||||
const button = container.querySelector('button')
|
||||
expect(button).toHaveAttribute('inert')
|
||||
expect(button).toHaveAttribute('data-shown', 'false')
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
// The way back to the latest message once the reader has scrolled up.
|
||||
//
|
||||
// It stays mounted so it can fade rather than pop, and while hidden it is inert
|
||||
// and hidden from assistive tech, so it can be neither clicked, tabbed to nor read.
|
||||
|
||||
import { ArrowDown } from 'lucide-react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
/** Keeps focus where the reader was typing: a pointer press never moves it here. */
|
||||
function keepFocus(event: React.MouseEvent): void {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
export function NativeChatJumpToLatest({
|
||||
visible,
|
||||
onJump
|
||||
}: {
|
||||
visible: boolean
|
||||
onJump: () => void
|
||||
}): React.JSX.Element {
|
||||
const label = translate('components.native-chat.jumpToLatest', 'Jump to latest')
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-shown={visible}
|
||||
inert={!visible}
|
||||
aria-hidden={!visible || undefined}
|
||||
onMouseDown={keepFocus}
|
||||
onClick={onJump}
|
||||
className="absolute bottom-3 left-1/2 flex h-8 -translate-x-1/2 items-center gap-1.5 rounded-full border border-border bg-popover px-3 text-xs font-medium text-popover-foreground shadow-floating transition-[opacity,translate] duration-150 ease-out hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none data-[shown=false]:pointer-events-none data-[shown=false]:translate-y-1 data-[shown=false]:opacity-0"
|
||||
>
|
||||
<ArrowDown className="size-3.5" />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -2,11 +2,10 @@ import {
|
||||
NATIVE_CHAT_TRANSCRIPT_OUTER_CLASS,
|
||||
NATIVE_CHAT_TRANSCRIPT_COLUMN_CLASS
|
||||
} from './native-chat-appearance-style'
|
||||
import { NativeChatJumpToLatest } from './NativeChatJumpToLatest'
|
||||
import { useNativeChatRowTypography } from './use-native-chat-row-typography'
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ArrowDown } from 'lucide-react'
|
||||
import type { CommentMarkdownLinkClickHandler } from '@/components/sidebar/CommentMarkdown'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { NativeChatLiveSession } from './use-native-chat-live-session'
|
||||
import { useNativeChatTranscriptProjection } from './use-native-chat-transcript-projection'
|
||||
import { structuredQuestionTranscript } from './structured-agent-question-projection'
|
||||
@@ -404,17 +403,7 @@ export function NativeChatMessageList({
|
||||
onReaderScroll={beginNavigation}
|
||||
pendingId={railHistoryJump.pendingId}
|
||||
/>
|
||||
{showJump ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={jumpToLatest}
|
||||
aria-label={translate('components.native-chat.jumpToLatest', 'Jump to latest')}
|
||||
className="absolute bottom-3 left-1/2 flex -translate-x-1/2 items-center gap-1.5 rounded-full border border-border bg-card/90 px-3 py-1.5 text-xs text-muted-foreground shadow-sm backdrop-blur hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<ArrowDown className="size-3.5" />
|
||||
<span>{translate('components.native-chat.jumpToLatest', 'Jump to latest')}</span>
|
||||
</button>
|
||||
) : null}
|
||||
<NativeChatJumpToLatest visible={showJump} onJump={jumpToLatest} />
|
||||
</div>
|
||||
{taskListState.list && taskListState.list.tasks.length > 0 ? (
|
||||
<div className="shrink-0 px-3 pb-2 sm:px-4">
|
||||
|
||||
Reference in New Issue
Block a user