fix(client): prepare rail search before expanding the sidebar
This commit is contained in:
1 parent
5616331997
commit
5a8c7c8f6b
6 files changed
+59
-12
No files matched your search
@@ -284,6 +284,7 @@ describe('web e2e: sidebar search alignment and row action targets', () => {
|
||||
paddingLeft: Number.parseFloat(style.paddingLeft), paddingRight: Number.parseFloat(style.paddingRight),
|
||||
gap: Number.parseFloat(style.columnGap),
|
||||
transitionDuration: style.transitionDuration,
|
||||
searchExpanded: header.querySelector('button[aria-label="Search sessions"]')?.getAttribute('aria-expanded'),
|
||||
}
|
||||
}
|
||||
const first = await new Promise<ReturnType<typeof measure>>((resolve) => {
|
||||
@@ -316,8 +317,8 @@ describe('web e2e: sidebar search alignment and row action targets', () => {
|
||||
expect(samples.first.transitionDuration).toBe('0s')
|
||||
expect(samples.first).toEqual(samples.last)
|
||||
expect(samples.first).toMatchObject(entry !== 'toggle'
|
||||
? { marginRight: 0, paddingLeft: 4, paddingRight: 4, gap: 0 }
|
||||
: { marginRight: -4, paddingLeft: 12, paddingRight: 1, gap: 4 })
|
||||
? { marginRight: 0, paddingLeft: 4, paddingRight: 4, gap: 0, searchExpanded: 'true' }
|
||||
: { marginRight: -4, paddingLeft: 12, paddingRight: 1, gap: 4, searchExpanded: 'false' })
|
||||
expect(samples.sameHeader).toBe(true)
|
||||
expect(samples.sameAdd).toBe(true)
|
||||
if (entry === 'search') {
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: a94394a5a9f70ed8
|
||||
zh: 62ce596a95395ad8
|
||||
/deepseek-ai-dsh-client-ui-workspace/use-this-package/search:
|
||||
en: f2aa51de8a7d94a3
|
||||
zh: 88dd6397c4772b0f
|
||||
en: 60d1486f5309a338
|
||||
zh: c16261d4dd6200de
|
||||
/deepseek-ai-dsh-client-ui-workspace/use-this-package/managing-sessions:
|
||||
en: 353ed9b66fab6486
|
||||
zh: e38a4a37627a9426
|
||||
|
||||
@@ -49,7 +49,7 @@ On devices without hover, Workspace rows keep their menu and New Session control
|
||||
|
||||
### Search
|
||||
|
||||
Collapsed search is one header action beside the view and add actions: activating it expands the field across the header. Opening or closing search in the expanded sidebar animates header spacing and field expansion together over 180 ms, disabled for reduced motion. Expanding the sidebar from its rail places the header at its final geometry while preserving the content fade-in, including when the rail search button opens the field. Search titles leave space before the Unarchive click target even when truncated. A non-blank query replaces either browsing mode with one flat result list — case-insensitive title, Workspace, and Session id substring matches appear immediately, while a 250 ms debounced Host request adds ranked current-conversation content matches and snippets. Each new query aborts the preceding request; a failed content search leaves metadata matches visible without an additional warning. The list is capped at 20. Choosing an unarchived result clears and collapses search, opens the Session, and scrolls its row into view in the configured browsing mode; grouped browsing also expands its Workspace and the full Session list when required. Archived results offer Unarchive; attempting to open one explains that restriction without clearing the query or navigating.
|
||||
Collapsed search is one header action beside the view and add actions: activating it expands the field across the header. Opening or closing search in the expanded sidebar animates header spacing and field expansion together over 180 ms, disabled for reduced motion. Expanding the sidebar from its rail places the header at its final geometry while preserving the content fade-in. Rail search prepares the open field before expanding the sidebar, so the first wide render already contains the expanded field. Search titles leave space before the Unarchive click target even when truncated. A non-blank query replaces either browsing mode with one flat result list — case-insensitive title, Workspace, and Session id substring matches appear immediately, while a 250 ms debounced Host request adds ranked current-conversation content matches and snippets. Each new query aborts the preceding request; a failed content search leaves metadata matches visible without an additional warning. The list is capped at 20. Choosing an unarchived result clears and collapses search, opens the Session, and scrolls its row into view in the configured browsing mode; grouped browsing also expands its Workspace and the full Session list when required. Archived results offer Unarchive; attempting to open one explains that restriction without clearing the query or navigating.
|
||||
|
||||
### Managing sessions
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ kind: "package-reference"
|
||||
|
||||
### 搜索
|
||||
|
||||
折叠搜索是视图和添加操作旁的一枚区头按钮:激活后输入框会扩展并占据区头。在展开的侧栏中打开或关闭搜索时,区头间距与输入框展开采用同步的 180 ms 过渡,减少动态效果模式下禁用。从折叠栏展开侧栏时,区头直接采用最终几何位置,同时保留内容淡入;通过折叠栏搜索按钮打开输入框时也如此。搜索标题即使被截断,也会在取消归档的点击区域前留出间距。非空白查询会以单一扁平结果列表替代任一浏览模式——不区分大小写的标题、Workspace 和 Session id 子串匹配项会立即显示,经 250 ms 防抖的 Host 请求则会加入经过排序的当前对话内容匹配项及其摘要片段。每次新查询都会中止前一个请求;内容搜索失败时,元数据匹配项仍会显示,不另给警告。列表最多显示 20 条结果。选择未归档结果会清空并收起搜索、打开 Session,并在当前浏览模式中将其行滚动到可见区域;分组浏览还会按需展开所属 Workspace 和完整 Session 列表。已归档结果提供取消归档操作;尝试打开时会说明限制,不清空查询,也不导航。
|
||||
折叠搜索是视图和添加操作旁的一枚区头按钮:激活后输入框会扩展并占据区头。在展开的侧栏中打开或关闭搜索时,区头间距与输入框展开采用同步的 180 ms 过渡,减少动态效果模式下禁用。从折叠栏展开侧栏时,区头直接采用最终几何位置,同时保留内容淡入。折叠栏搜索会在展开侧栏前准备好输入框的打开状态,因此首个宽栏渲染就包含已展开的输入框。搜索标题即使被截断,也会在取消归档的点击区域前留出间距。非空白查询会以单一扁平结果列表替代任一浏览模式——不区分大小写的标题、Workspace 和 Session id 子串匹配项会立即显示,经 250 ms 防抖的 Host 请求则会加入经过排序的当前对话内容匹配项及其摘要片段。每次新查询都会中止前一个请求;内容搜索失败时,元数据匹配项仍会显示,不另给警告。列表最多显示 20 条结果。选择未归档结果会清空并收起搜索、打开 Session,并在当前浏览模式中将其行滚动到可见区域;分组浏览还会按需展开所属 Workspace 和完整 Session 列表。已归档结果提供取消归档操作;尝试打开时会说明限制,不清空查询,也不导航。
|
||||
|
||||
### 管理会话
|
||||
|
||||
|
||||
@@ -1089,6 +1089,17 @@ export function WorkspaceBrowser({
|
||||
const [query, setQuery] = useState('')
|
||||
const [searchExpanded, setSearchExpanded] = useState(false)
|
||||
const [searchSpacingMotionEnabled, setSearchSpacingMotionEnabled] = useState(false)
|
||||
const [searchOnExpand, setSearchOnExpand] = useState(false)
|
||||
const [lastSearchRequest, setLastSearchRequest] = useState(0)
|
||||
// Search state must commit before the effect expands the shell's external store.
|
||||
if (lastSearchRequest !== shortcutState.searchRequest) {
|
||||
setLastSearchRequest(shortcutState.searchRequest)
|
||||
if (shortcutState.searchRequest !== 0) {
|
||||
setSearchExpanded(true)
|
||||
setSearchSpacingMotionEnabled(wide)
|
||||
if (!wide) setSearchOnExpand(true)
|
||||
}
|
||||
}
|
||||
// Search gestures animate spacing; entering wide layout from the rail lands at its final geometry.
|
||||
if (!wide && searchSpacingMotionEnabled) setSearchSpacingMotionEnabled(false)
|
||||
const changeSearchExpanded = (expanded: boolean): void => {
|
||||
@@ -1130,7 +1141,6 @@ export function WorkspaceBrowser({
|
||||
|
||||
// Rail search = expand + land in the search box: the flag arms before the
|
||||
// expand request; once the shell flips wide the input mounts and takes focus.
|
||||
const [searchOnExpand, setSearchOnExpand] = useState(false)
|
||||
useEffect(() => {
|
||||
if (wide && searchOnExpand) {
|
||||
const timer = window.setTimeout(() => {
|
||||
@@ -1143,11 +1153,8 @@ export function WorkspaceBrowser({
|
||||
useEffect(() => {
|
||||
if (shortcutState.searchRequest === 0) return
|
||||
closeAddWorkspace()
|
||||
changeSearchExpanded(true)
|
||||
if (!wide) {
|
||||
setSearchOnExpand(true)
|
||||
expandSidebar()
|
||||
} else searchInput.current?.focus({ preventScroll: true })
|
||||
if (!wide) expandSidebar()
|
||||
else searchInput.current?.focus({ preventScroll: true })
|
||||
}, [shortcutState.searchRequest])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
import type { GlobalStandardProps } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { Profiler } from 'react'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, createEvent, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import { allowAllAccess, bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
@@ -1913,6 +1915,42 @@ describe('WorkspaceBrowser', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it.each([false, true])('commits rail search intent before synchronously expanding the shell (prior search: %s)', (usedSearch) => {
|
||||
const layout = createSnapshotStore({ wide: usedSearch })
|
||||
const useLayout = bindSnapshotSelector(layout)
|
||||
const b = mount({ wide: false, expandSidebar: () => { layout.set({ wide: true }) } })
|
||||
const committedSearchStates: boolean[] = []
|
||||
function Shell() {
|
||||
return <WorkspaceBrowser {...b.props} wide={useLayout(state => state.wide)} />
|
||||
}
|
||||
b.view.rerender(<Profiler id="rail-search" onRender={() => {
|
||||
// The shell's external-store update can commit before state queued by a passive effect.
|
||||
if (screen.queryByPlaceholderText('搜索会话名称') === null) return
|
||||
committedSearchStates.push(screen.getByRole('button', { name: '搜索会话' }).getAttribute('aria-expanded') === 'true')
|
||||
}}><Shell /></Profiler>)
|
||||
if (usedSearch) {
|
||||
fireEvent.click(screen.getByRole('button', { name: '搜索会话' }))
|
||||
fireEvent.keyDown(screen.getByPlaceholderText('搜索会话名称'), { key: 'Escape' })
|
||||
act(() => { layout.set({ wide: false }) })
|
||||
}
|
||||
committedSearchStates.length = 0
|
||||
fireEvent.click(screen.getByRole('button', { name: '搜索会话' }))
|
||||
expect(committedSearchStates.length).toBeGreaterThan(0)
|
||||
expect(committedSearchStates.every(Boolean)).toBe(true)
|
||||
expect(screen.getByRole('button', { name: '搜索会话' }).getAttribute('aria-expanded')).toBe('true')
|
||||
})
|
||||
|
||||
it('treats a new shortcut source with no request as idle', () => {
|
||||
const b = mount()
|
||||
fireEvent.click(screen.getByRole('button', { name: '搜索会话' }))
|
||||
fireEvent.keyDown(screen.getByPlaceholderText('搜索会话名称'), { key: 'Escape' })
|
||||
const controls = createWorkspaceShortcutControls()
|
||||
rerender(b, { useWorkspaceShortcuts: bindSnapshotSelector(controls.state), requestSearch: controls.search })
|
||||
expect(screen.getByRole('button', { name: '搜索会话' }).getAttribute('aria-expanded')).toBe('false')
|
||||
act(() => { controls.search() })
|
||||
expect(screen.getByRole('button', { name: '搜索会话' }).getAttribute('aria-expanded')).toBe('true')
|
||||
})
|
||||
|
||||
it('rail state renders icon controls that request expansion', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
@@ -1926,6 +1964,7 @@ describe('WorkspaceBrowser', () => {
|
||||
// The wide flip mounts the input and focuses it after the slide.
|
||||
rerender(b, { wide: true })
|
||||
const input = screen.getByPlaceholderText('搜索会话名称')
|
||||
expect(document.activeElement).not.toBe(input)
|
||||
act(() => { vi.advanceTimersByTime(300) })
|
||||
expect(document.activeElement).toBe(input)
|
||||
// Wide search button is decorative (tabIndex -1, no expand call).
|
||||
|
||||
Reference in new issue
Block a user