fix(ui-sidebar-right): slide the panel in on its first open

The first expansion of an empty column mounts the seeded tab already open, so it appeared in place instead of sliding in like later opens and every close.
This commit is contained in:
Turtle committed 2026-10-21 22:59:34 +08:00
1 parent ded424132e
commit 3acbd08867
3 files changed
+57 -3

No files matched your search

@@ -67,6 +67,14 @@
transition: transform var(--ds-transition-duration-slow) var(--ds-ease-in-out);
}
/* An item mounted by the opening commit takes the closed style for one style
read (SidebarRight.tsx), so it slides in like the items already mounted. */
.panel[data-sidebar-right-open] :global([data-sidebar-right-settling]) {
transform: translateX(var(--dsh-sidebar-width));
visibility: hidden;
transition: none;
}
.panel[data-sidebar-right-panel='fullscreen'] {
--dsh-dockkit-dock-layer: 40;
}
@@ -295,6 +295,15 @@ function PanelChrome({ sessionId, fullscreen, actions, t, shortcuts, toggleFulls
)
}
/** The docked Grid items whose transform slides with the panel (`SidebarRight.module.css`). */
const DOCKED_ITEMS = "[data-dockkit-host='dock'], [data-dockkit-empty], [data-dockkit-divider]"
/** Whether an animation is a docked item's running open or close slide. */
function isSlide(animation: Animation): boolean {
return 'transitionProperty' in animation && animation.transitionProperty === 'transform'
&& animation.playState !== 'finished' && animation.playState !== 'idle'
}
/**
* The panel: the docked surface with the two controls in its top-right strip,
* anchored to the frame's right edge and slid off it while collapsed.
@@ -303,6 +312,23 @@ function SidebarPanel(panel: PanelProps & { width: number; panelRef: RefObject<H
const { sessionId, surface, actions, t, renderSlot, openTab, width, reportRoom, fullscreen, panelRef } = panel
const { expanded } = surface.layout
const types = panel.useTabTypes(value => value)
const wasExpanded = useRef(expanded)
// An item the opening commit mounts, such as the first tab seeded into an
// empty column, has no closed style to slide from: a style read finds it
// open with no transition. It takes the closed style for one read, so
// removing that style starts the same slide as the items already mounted.
// `@starting-style` cannot select only these items: it would also slide
// items that mount while the panel is open or that a Session switch shows.
useLayoutEffect(() => {
const opening = expanded && !wasExpanded.current
wasExpanded.current = expanded
if (!opening) return
const items = [...(panelRef.current as HTMLDivElement).querySelectorAll(DOCKED_ITEMS)]
.filter(item => !item.getAnimations().some(isSlide))
for (const item of items) item.setAttribute('data-sidebar-right-settling', '')
for (const item of items) void getComputedStyle(item).transform
for (const item of items) item.removeAttribute('data-sidebar-right-settling')
}, [expanded, panelRef])
return (
<div
ref={panelRef}
@@ -390,9 +416,7 @@ export function RightbarSeat({
if (disposed) return
// A shown panel renders unconditionally and attaches its ref before this effect.
const entering = shown && fullscreen
? (panelRef.current as HTMLDivElement).getAnimations({ subtree: true }).filter(animation =>
'transitionProperty' in animation && animation.transitionProperty === 'transform'
&& animation.playState !== 'finished' && animation.playState !== 'idle')
? (panelRef.current as HTMLDivElement).getAnimations({ subtree: true }).filter(isSlide)
: []
if (entering.length === 0) {
syncPresentation({ shown, track, fullscreen })
@@ -557,6 +557,28 @@ describe('RightbarSeat presentation', () => {
expect(h.frame.closeRightbar).toHaveBeenCalled()
})
it('starts an item the opening mounts from the closed style, leaving a sliding item alone', async () => {
const h = await mountSeat()
const settled: { readonly item: Element; readonly open: boolean }[] = []
const getComputedStyle = window.getComputedStyle.bind(window)
vi.spyOn(window, 'getComputedStyle').mockImplementation((item, pseudo) => {
const open = item.closest('[data-sidebar-right-panel]')?.hasAttribute('data-sidebar-right-open') === true
if (item.hasAttribute('data-sidebar-right-settling')) settled.push({ item, open })
return getComputedStyle(item, pseudo)
})
h.open()
const host = element(h.view.container, "[data-dockkit-host='dock']")
expect(settled).toEqual([{ item: host, open: true }])
expect(host.hasAttribute('data-sidebar-right-settling')).toBe(false)
fireEvent.click(element(h.view.container, '[data-sidebar-right-toggle]'))
// Reopened during its closing slide, the item reverses from where it is.
const closing = transition()
vi.spyOn(host, 'getAnimations').mockReturnValue([closing.animation])
act(() => { h.actions.setExpanded(SESSION, true) })
expect(settled).toHaveLength(1)
expect(element(h.view.container, '[data-sidebar-right-panel]').hasAttribute('data-sidebar-right-open')).toBe(true)
})
it('fills the viewport without replacing the content tree or releasing the wide track', async () => {
const h = await mountSeat()
const tab = h.open()