fix(gui): keep sidebar controls when collapsed
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# @deepseek-ai/dsh-client-ui-layout
|
||||
|
||||
Shell plugin: three-column AppFrame (drag handles, concession chain) + ctx.layout viewing-state service (nav, panel widths, persist); defines the sidebar/conversation/details/conversation.empty slots. Contract: api-contracts v3 §5.
|
||||
Shell plugin: three-column AppFrame (drag handles, concession chain) + ctx.layout viewing-state service (nav, panel widths, persist); defines the sidebar/conversation/details/conversation.empty slots. A closed sidebar retains a 60px control rail while details closes to zero width. Contract: api-contracts v3 §5.
|
||||
|
||||
Slot declarations use the composed-props entry form (`owner` share, no full `props`): the exported OwnerShare contracts are `SidebarOwnerProps` / `ConvOwnerProps` / `DetailsOwnerProps` / `EmptyOwnerProps` — registrants reference them via `OwnerOf<'sidebar' | ...>` and compose their own injected share locally. The `conversation` entry authorizes `conversation.empty` delegation through `children`.
|
||||
|
||||
|
||||
@@ -27,13 +27,8 @@
|
||||
border-left: 1px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
/* Collapsed columns keep children mounted; the border must not paint a 1px seam.
|
||||
Flags live on the frame — DetailsColumn renders inside the provider body and
|
||||
does not know its own width. */
|
||||
.frame[data-sidebar-collapsed] .sidebarCol {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* The details subtree stays mounted at zero width, so its border must not paint
|
||||
a 1px seam. The collapsed sidebar instead retains a bordered compact rail. */
|
||||
.frame[data-details-collapsed] .detailsCol {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
@@ -137,12 +137,14 @@ export function AppFrame(props: AppFrameProps) {
|
||||
ref={frameRef}
|
||||
className={css.frame}
|
||||
style={{ gridTemplateColumns: `${cols.sidebar}px minmax(0, 1fr) ${cols.details}px` }}
|
||||
data-sidebar-collapsed={cols.sidebar === 0 || undefined}
|
||||
data-sidebar-collapsed={!sidebar.open || undefined}
|
||||
data-details-collapsed={cols.details === 0 || undefined}
|
||||
>
|
||||
<div className={css.sidebarCol}>{props.sidebar}</div>
|
||||
{props.children}
|
||||
{cols.sidebar > 0 && <DragHandle left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} />}
|
||||
{sidebar.open && cols.sidebar > 0
|
||||
? <DragHandle left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} />
|
||||
: null}
|
||||
{cols.details > 0 && <DragHandle left={viewport - cols.details} onStart={onDetailsStart} onDrag={onDetailsDrag} />}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Pure concession-chain column solver for the three-column AppFrame.
|
||||
* Chain order is fixed by contract: keep center >= CENTER_MIN by shrinking
|
||||
* details first, then sidebar, then auto-closing details (derived zero width —
|
||||
* persisted open/width preferences are never rewritten, so widening the window
|
||||
* restores them). Center absorbs any remaining deficit as the last resort.
|
||||
* details first, then sidebar, then auto-closing details. A closed sidebar
|
||||
* keeps its compact rail; persisted open/width preferences are never rewritten,
|
||||
* so widening the window restores them. Center absorbs any remaining deficit.
|
||||
*/
|
||||
|
||||
/** Panel viewing state consumed by the solver (mirrors LayoutService PanelState). */
|
||||
@@ -21,6 +21,8 @@ export const SIDEBAR_MIN = 240
|
||||
export const SIDEBAR_MAX = 420
|
||||
/** Sidebar width before any user drag. */
|
||||
export const SIDEBAR_DEFAULT = 300
|
||||
/** Closed-sidebar rail: one 28px control between 16px horizontal paddings. */
|
||||
export const SIDEBAR_COLLAPSED = 60
|
||||
/** Details drag clamp floor. */
|
||||
export const DETAILS_MIN = 300
|
||||
/** Details drag clamp ceiling. */
|
||||
@@ -47,13 +49,11 @@ export function clampWidth(px: number, min: number, max: number): number {
|
||||
* @param viewport - available frame width in px.
|
||||
* @param sidebar - sidebar preference (open flag + persisted width).
|
||||
* @param details - details preference (open flag + persisted width).
|
||||
* @returns resolved widths; details 0 means visually closed (never unmounted).
|
||||
* @returns resolved widths; details 0 means visually closed, while a closed sidebar keeps its compact rail.
|
||||
*/
|
||||
export function computeColumns(viewport: number, sidebar: PanelInput, details: PanelInput): Columns {
|
||||
const want = (p: PanelInput, min: number, max: number): number =>
|
||||
p.open ? clampWidth(p.width, min, max) : 0
|
||||
const s0 = want(sidebar, SIDEBAR_MIN, SIDEBAR_MAX)
|
||||
const d0 = want(details, DETAILS_MIN, DETAILS_MAX)
|
||||
const s0 = sidebar.open ? clampWidth(sidebar.width, SIDEBAR_MIN, SIDEBAR_MAX) : SIDEBAR_COLLAPSED
|
||||
const d0 = details.open ? clampWidth(details.width, DETAILS_MIN, DETAILS_MAX) : 0
|
||||
|
||||
// Step 1: everything fits at preferred widths.
|
||||
if (s0 + d0 + CENTER_MIN <= viewport) return { sidebar: s0, center: viewport - s0 - d0, details: d0 }
|
||||
@@ -63,14 +63,14 @@ export function computeColumns(viewport: number, sidebar: PanelInput, details: P
|
||||
if (s0 + d1 + CENTER_MIN <= viewport) return { sidebar: s0, center: CENTER_MIN, details: d1 }
|
||||
|
||||
// Step 3: shrink sidebar toward its minimum.
|
||||
const s1 = s0 === 0 ? 0 : Math.max(SIDEBAR_MIN, viewport - d1 - CENTER_MIN)
|
||||
const s1 = sidebar.open ? Math.max(SIDEBAR_MIN, viewport - d1 - CENTER_MIN) : SIDEBAR_COLLAPSED
|
||||
if (s1 + d1 + CENTER_MIN <= viewport) return { sidebar: s1, center: CENTER_MIN, details: d1 }
|
||||
|
||||
// Step 4: auto-close details (derived — preferences untouched). With the
|
||||
// details pressure gone the sidebar concession is re-solved from preference.
|
||||
if (d1 > 0) {
|
||||
if (s0 + CENTER_MIN <= viewport) return { sidebar: s0, center: viewport - s0, details: 0 }
|
||||
const s2 = s0 === 0 ? 0 : Math.max(SIDEBAR_MIN, viewport - CENTER_MIN)
|
||||
const s2 = sidebar.open ? Math.max(SIDEBAR_MIN, viewport - CENTER_MIN) : SIDEBAR_COLLAPSED
|
||||
return { sidebar: s2, center: Math.max(0, viewport - s2), details: 0 }
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ import { LayoutService } from './service.ts'
|
||||
export { AppFrame, CenterColumn, DetailsColumn, type AppFrameProps } from './AppFrame.tsx'
|
||||
export {
|
||||
clampWidth, computeColumns,
|
||||
CENTER_MIN, DETAILS_DEFAULT, DETAILS_MAX, DETAILS_MIN, SIDEBAR_DEFAULT, SIDEBAR_MAX, SIDEBAR_MIN,
|
||||
CENTER_MIN, DETAILS_DEFAULT, DETAILS_MAX, DETAILS_MIN,
|
||||
SIDEBAR_COLLAPSED, SIDEBAR_DEFAULT, SIDEBAR_MAX, SIDEBAR_MIN,
|
||||
type Columns, type PanelInput,
|
||||
} from './columns.ts'
|
||||
export { LayoutService, type NavState, type PanelState, type ViewId } from './service.ts'
|
||||
|
||||
@@ -10,7 +10,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, render } from '@testing-library/react'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { AppFrame, CenterColumn, DetailsColumn, type PanelState } from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
import { clampWidth } from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
import { clampWidth, SIDEBAR_COLLAPSED } from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
|
||||
/** Observer stub: captures the callback so tests can fire resizes manually. */
|
||||
let fireResize: (() => void) | null = null
|
||||
@@ -119,6 +119,15 @@ describe('AppFrame', () => {
|
||||
expect(frame.hasAttribute('data-details-collapsed')).toBe(true)
|
||||
})
|
||||
|
||||
it('closed sidebar keeps its compact rail and mounted slot content', () => {
|
||||
const { frame, sidebar, getByTestId } = mountFrame()
|
||||
act(() => { sidebar.update((d) => { d.open = false }) })
|
||||
expect(tracks(frame)).toEqual([SIDEBAR_COLLAPSED, 360])
|
||||
expect(getByTestId('sidebar-content')).toBeTruthy()
|
||||
expect(frame.hasAttribute('data-sidebar-collapsed')).toBe(true)
|
||||
expect(frame.querySelectorAll('[class*="handle"]')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('viewport shrink triggers the concession chain via ResizeObserver', () => {
|
||||
const { frame } = mountFrame()
|
||||
frameWidth = 1250
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
CENTER_MIN, clampWidth, computeColumns,
|
||||
DETAILS_DEFAULT, DETAILS_MIN, SIDEBAR_DEFAULT, SIDEBAR_MIN,
|
||||
DETAILS_DEFAULT, DETAILS_MIN, SIDEBAR_COLLAPSED, SIDEBAR_DEFAULT, SIDEBAR_MIN,
|
||||
} from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
|
||||
const open = (width: number) => ({ open: true, width })
|
||||
@@ -21,8 +21,9 @@ describe('computeColumns', () => {
|
||||
expect(cols).toEqual({ sidebar: 300, center: 1920 - 300 - 360, details: 360 })
|
||||
})
|
||||
|
||||
it('closed panels contribute zero width', () => {
|
||||
expect(computeColumns(1920, closed(300), closed(360))).toEqual({ sidebar: 0, center: 1920, details: 0 })
|
||||
it('closed sidebar keeps its compact rail while details contributes zero width', () => {
|
||||
expect(computeColumns(1920, closed(300), closed(360)))
|
||||
.toEqual({ sidebar: SIDEBAR_COLLAPSED, center: 1920 - SIDEBAR_COLLAPSED, details: 0 })
|
||||
})
|
||||
|
||||
it('preferences beyond the clamp range are clamped before solving', () => {
|
||||
@@ -69,10 +70,14 @@ describe('computeColumns', () => {
|
||||
})
|
||||
|
||||
it('sidebar-closed narrow window: details concedes then auto-closes', () => {
|
||||
const fits = computeColumns(DETAILS_MIN + CENTER_MIN, closed(300), open(DETAILS_DEFAULT))
|
||||
expect(fits).toEqual({ sidebar: 0, center: CENTER_MIN, details: DETAILS_MIN })
|
||||
const starved = computeColumns(DETAILS_MIN + CENTER_MIN - 1, closed(300), open(DETAILS_DEFAULT))
|
||||
expect(starved).toEqual({ sidebar: 0, center: DETAILS_MIN + CENTER_MIN - 1, details: 0 })
|
||||
const fits = computeColumns(SIDEBAR_COLLAPSED + DETAILS_MIN + CENTER_MIN, closed(300), open(DETAILS_DEFAULT))
|
||||
expect(fits).toEqual({ sidebar: SIDEBAR_COLLAPSED, center: CENTER_MIN, details: DETAILS_MIN })
|
||||
const starved = computeColumns(SIDEBAR_COLLAPSED + DETAILS_MIN + CENTER_MIN - 1, closed(300), open(DETAILS_DEFAULT))
|
||||
expect(starved).toEqual({
|
||||
sidebar: SIDEBAR_COLLAPSED,
|
||||
center: DETAILS_MIN + CENTER_MIN - 1,
|
||||
details: 0,
|
||||
})
|
||||
})
|
||||
|
||||
it('tiny viewport: both panels yield everything to center', () => {
|
||||
@@ -93,8 +98,8 @@ describe('computeColumns', () => {
|
||||
|
||||
describe('computeColumns — degenerate viewports', () => {
|
||||
it('sidebar closed and viewport below CENTER_MIN: details auto-closes, center takes all', () => {
|
||||
// Reaches step 4's re-solve with s0 = 0 (the closed-sidebar arm).
|
||||
// Reaches step 4's re-solve with the compact rail as the sidebar floor.
|
||||
expect(computeColumns(500, closed(300), open(DETAILS_DEFAULT)))
|
||||
.toEqual({ sidebar: 0, center: 500, details: 0 })
|
||||
.toEqual({ sidebar: SIDEBAR_COLLAPSED, center: 500 - SIDEBAR_COLLAPSED, details: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# @deepseek-ai/dsh-client-ui-sidebar
|
||||
|
||||
Sidebar plugin: session multi-level tree (cwd grouping + parentId nesting), search, by-workspace grouping, state dots, three creation entries. Contract: api-contracts v3 §6.
|
||||
Sidebar plugin: session multi-level tree (cwd grouping + parentId nesting), search, by-workspace grouping, state dots, three creation entries. Its collapsed render keeps the expand control and settings entry in the layout-owned compact rail. Contract: api-contracts v3 §6.
|
||||
|
||||
`src/client/contract/slots.ts` is the single-domain contract file: `SidebarRootInjected` (the registrant's own injected share — tree hook, current-session hook, actions) and `SidebarRootComponentProps = OwnerOf<'sidebar'> & SidebarRootInjected` (the owner share referenced from ui-layout's slot declaration, never re-stated). `apply` registers SidebarRoot cast-free against that composition; the inject factory binds layout/sessions off `RootBinding<ClientContext>`.
|
||||
`src/client/contract/slots.ts` is the single-domain contract file: `SidebarRootInjected` (the registrant's own injected share — tree hook, current-session/sidebar-open hooks, actions) and `SidebarRootComponentProps = OwnerOf<'sidebar'> & SidebarRootInjected` (the owner share referenced from ui-layout's slot declaration, never re-stated). `apply` registers SidebarRoot cast-free against that composition; the inject factory binds layout/sessions off `RootBinding<ClientContext>`.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -15,6 +15,28 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Closed state is a persistent rail: the layout reserves exactly the root's
|
||||
horizontal padding plus one icon control. */
|
||||
.root.collapsed {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.collapsed .headerBlock {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.collapsed .logoRow {
|
||||
justify-content: center;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.collapsed .foot {
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
margin-top: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Header block (figma 133:7630): logo row + New Session, gap 16, padBottom 12. */
|
||||
.headerBlock {
|
||||
flex: none;
|
||||
|
||||
@@ -24,12 +24,10 @@ const GROUP_BY_ITEMS = [
|
||||
{ id: 'status', label: 'Status', disabled: true },
|
||||
]
|
||||
|
||||
/**
|
||||
* Render the sidebar column.
|
||||
* @param props - composed slot props (owner share + injected surface, contract/slots.ts).
|
||||
* @returns the sidebar element tree.
|
||||
*/
|
||||
export function SidebarRoot({ useTree, useCurrent, actions, tree }: SidebarRootComponentProps) {
|
||||
type SidebarBodyProps = Pick<SidebarRootComponentProps, 'useTree' | 'useCurrent' | 'actions' | 'tree'>
|
||||
|
||||
/** Expanded-only content; unmounting drops tree/current subscriptions while the rail is collapsed. */
|
||||
function SidebarBody({ useTree, useCurrent, actions, tree }: SidebarBodyProps) {
|
||||
const rows = useTree((s) => s.rows)
|
||||
const query = useTree((s) => s.query)
|
||||
const groupBy = useTree((s) => s.groupBy)
|
||||
@@ -47,32 +45,7 @@ export function SidebarRoot({ useTree, useCurrent, actions, tree }: SidebarRootC
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css.root}>
|
||||
<div className={css.headerBlock}>
|
||||
<div className={css.logoRow}>
|
||||
<span className={css.brand}>
|
||||
{/* Wordmark svg not extracted yet (figma 88:8932) — text stands in at the same ink. */}
|
||||
<FishLogo size={23} />
|
||||
<span className={css.wordmark}>deepseek</span>
|
||||
<span className={css.badge}>HARNESS</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label="Collapse sidebar"
|
||||
onClick={() => { actions.toggleSidebar() }}
|
||||
>
|
||||
<IconPanelLeftOutline16 />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="button" className={css.newSession} onClick={() => { actions.create() }}>
|
||||
<IconNewChatOutline16 size={14} />
|
||||
New Session
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={css.listArea}>
|
||||
<div className={css.listArea}>
|
||||
<div className={css.sectionHeader}>
|
||||
<span className={css.sectionLabel}>WorkSpace</span>
|
||||
<Menu
|
||||
@@ -153,11 +126,66 @@ export function SidebarRoot({ useTree, useCurrent, actions, tree }: SidebarRootC
|
||||
))}
|
||||
</div>
|
||||
<span className={css.fade} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the sidebar column.
|
||||
* @param props - composed slot props (owner share + injected surface, contract/slots.ts).
|
||||
* @returns the sidebar element tree.
|
||||
*/
|
||||
export function SidebarRoot(props: SidebarRootComponentProps) {
|
||||
const open = props.useSidebarOpen()
|
||||
|
||||
return (
|
||||
<div className={clsx(css.root, !open && css.collapsed)}>
|
||||
<div className={css.headerBlock}>
|
||||
<div className={css.logoRow}>
|
||||
{open
|
||||
? (
|
||||
<span className={css.brand}>
|
||||
{/* Wordmark svg not extracted yet (figma 88:8932) — text stands in at the same ink. */}
|
||||
<FishLogo size={23} />
|
||||
<span className={css.wordmark}>deepseek</span>
|
||||
<span className={css.badge}>HARNESS</span>
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label={open ? 'Collapse sidebar' : 'Expand sidebar'}
|
||||
onClick={() => { props.actions.toggleSidebar() }}
|
||||
>
|
||||
<IconPanelLeftOutline16 />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{open
|
||||
? (
|
||||
<button type="button" className={css.newSession} onClick={() => { props.actions.create() }}>
|
||||
<IconNewChatOutline16 size={14} />
|
||||
New Session
|
||||
</button>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
|
||||
<div className={clsx(css.foot)} role="button" tabIndex={0} aria-label="Settings">
|
||||
{open
|
||||
? (
|
||||
<SidebarBody
|
||||
useTree={props.useTree}
|
||||
useCurrent={props.useCurrent}
|
||||
actions={props.actions}
|
||||
tree={props.tree}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
|
||||
<div className={css.foot} role="button" tabIndex={0} aria-label="Settings">
|
||||
<IconSettingsOutline14 />
|
||||
Settings
|
||||
{open ? <span>Settings</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -37,6 +37,8 @@ export type SidebarRootInjected = {
|
||||
useTree: SnapshotSelectorHook<SidebarTreeState>
|
||||
/** Current session selector (row highlight); undefined selects nothing. */
|
||||
useCurrent: () => SessionId | undefined
|
||||
/** Sidebar open selector; the collapsed render keeps only persistent rail controls. */
|
||||
useSidebarOpen: () => boolean
|
||||
actions: SidebarActions
|
||||
tree: SidebarTreeActions
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export function apply(ctx: ClientContext): void {
|
||||
return {
|
||||
useTree: tree.store.useSelector,
|
||||
useCurrent: () => layout.current.useSelector(s => s.sessionId),
|
||||
useSidebarOpen: () => layout.sidebar.useSelector(s => s.open),
|
||||
actions: {
|
||||
open: (id) => { layout.open(id) },
|
||||
create: (cwd) => {
|
||||
|
||||
@@ -31,10 +31,12 @@ async function bench() {
|
||||
byId: { [sid('a')]: { id: sid('a'), title: 'alpha', cwd: '/proj', running: false, updatedAt: 1 } },
|
||||
})
|
||||
const sessions = { list, create: vi.fn(async () => sid('minted')) }
|
||||
const sidebar = createSnapshotStore({ open: true, width: 300 })
|
||||
const layout = {
|
||||
current: createSnapshotStore<{ sessionId?: SessionId }>({}),
|
||||
sidebar,
|
||||
open: vi.fn(),
|
||||
toggleSidebar: vi.fn(),
|
||||
toggleSidebar: vi.fn(() => { sidebar.update((d) => { d.open = !d.open }) }),
|
||||
}
|
||||
ctx.provide('sessions', sessions)
|
||||
ctx.provide('layout', layout)
|
||||
@@ -83,6 +85,10 @@ describe('apply', () => {
|
||||
|
||||
act(() => { fireEvent.click(screen.getByLabelText('Collapse sidebar')) })
|
||||
expect(layout.toggleSidebar).toHaveBeenCalledOnce()
|
||||
expect(screen.getByLabelText('Expand sidebar')).toBeTruthy()
|
||||
expect(screen.getByLabelText('Settings')).toBeTruthy()
|
||||
act(() => { fireEvent.click(screen.getByLabelText('Expand sidebar')) })
|
||||
expect(layout.toggleSidebar).toHaveBeenCalledTimes(2)
|
||||
|
||||
act(() => { fireEvent.click(screen.getByText('proj')) })
|
||||
act(() => { fireEvent.click(screen.getByText('alpha')) })
|
||||
|
||||
@@ -51,20 +51,22 @@ function mount(...summaries: SessionSummary[]) {
|
||||
const list = createSnapshotStore<SessionListState>(listStateOf(...summaries))
|
||||
const tree: SidebarTreeStore = createSidebarTreeStore({ list })
|
||||
const current = createSnapshotStore<{ id: SessionId | undefined }>({ id: undefined })
|
||||
const sidebar = createSnapshotStore({ open: true })
|
||||
const actions: SidebarActions = {
|
||||
open: vi.fn((id: SessionId) => { current.update((d) => { d.id = id }) }),
|
||||
create: vi.fn(),
|
||||
toggleSidebar: vi.fn(),
|
||||
toggleSidebar: vi.fn(() => { sidebar.update((d) => { d.open = !d.open }) }),
|
||||
}
|
||||
const utils = render(
|
||||
<SidebarRoot
|
||||
useTree={tree.store.useSelector}
|
||||
useCurrent={() => current.useSelector((s) => s.id)}
|
||||
useSidebarOpen={() => sidebar.useSelector((s) => s.open)}
|
||||
actions={actions}
|
||||
tree={tree}
|
||||
/>,
|
||||
)
|
||||
return { list, tree, current, actions, ...utils }
|
||||
return { list, tree, current, sidebar, actions, ...utils }
|
||||
}
|
||||
|
||||
const projectData = () => [
|
||||
@@ -139,10 +141,22 @@ describe('SidebarRoot', () => {
|
||||
expect(actions.create).toHaveBeenLastCalledWith('/proj')
|
||||
})
|
||||
|
||||
it('collapse button and group-by menu behave', () => {
|
||||
it('collapsed rail keeps the expand and settings controls', () => {
|
||||
const { actions } = mount(...projectData())
|
||||
act(() => { fireEvent.click(screen.getByLabelText('Collapse sidebar')) })
|
||||
expect(actions.toggleSidebar).toHaveBeenCalledOnce()
|
||||
expect(screen.getByLabelText('Expand sidebar')).toBeTruthy()
|
||||
expect(screen.getByLabelText('Settings')).toBeTruthy()
|
||||
expect(screen.queryByText('HARNESS')).toBeNull()
|
||||
expect(screen.queryByText('New Session')).toBeNull()
|
||||
act(() => { fireEvent.click(screen.getByLabelText('Expand sidebar')) })
|
||||
expect(actions.toggleSidebar).toHaveBeenCalledTimes(2)
|
||||
expect(screen.getByLabelText('Collapse sidebar')).toBeTruthy()
|
||||
expect(screen.getByText('New Session')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('group-by menu behaves', () => {
|
||||
mount(...projectData())
|
||||
expect(screen.queryByText('Update')).toBeNull()
|
||||
act(() => { fireEvent.click(screen.getByLabelText('Group by')) })
|
||||
expect(screen.getByText('Update')).toBeTruthy()
|
||||
|
||||
Reference in New Issue
Block a user