fix(plugin-manager): keep the collapsed bar when collapsing lets the list fit
Collapsing the page bar grows the list's scroll container. A list that scrolled only a few pixels then fit, the browser clamped it back to the top, and the bar reopened, so every scroll flipped the layout. The bar now reopens at the top only while the list still scrolls, and an upward wheel reopens it over a list that fits only under the collapsed bar. Co-authored-by: Yifffan <[email protected]>
This commit is contained in:
1 parent
dbccedbc13
commit
d04a96377a
3 files changed
+74
-1
No files matched your search
@@ -293,6 +293,38 @@ describe('web e2e: plugin manager', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the collapsed bar steady when collapsing lets a barely scrolling list fit', async () => {
|
||||
await page.setViewportSize({ width: 1000, height: 480 })
|
||||
const panel = await openPluginsPanel()
|
||||
const body = panel.locator('[data-plugin-body]')
|
||||
const compact = () => panel.locator('[data-plugin-bar]').evaluate(element => element.hasAttribute('data-compact'))
|
||||
try {
|
||||
await panel.locator('[data-plugin-package]').first().waitFor()
|
||||
// Size the window so the resting bar leaves the list 12px of scroll range, less than collapsing frees.
|
||||
const range = await body.evaluate(element => element.scrollHeight - element.clientHeight)
|
||||
await page.setViewportSize({ width: 1000, height: 480 + range - 12 })
|
||||
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight)).toBe(12)
|
||||
await page.mouse.move(500, 400)
|
||||
await page.mouse.wheel(0, 200)
|
||||
await expect.poll(compact).toBe(true)
|
||||
expect(await body.evaluate(element => element.scrollHeight - element.clientHeight)).toBeLessThanOrEqual(0)
|
||||
// The clamp back to the top must not reopen the bar and loop the layout.
|
||||
const flips = await panel.locator('[data-plugin-bar]').evaluate(element => new Promise<number>((resolve) => {
|
||||
let count = 0
|
||||
const observer = new MutationObserver(() => { count++ })
|
||||
observer.observe(element, { attributes: true, attributeFilter: ['data-compact'] })
|
||||
setTimeout(() => { observer.disconnect(); resolve(count) }, 500)
|
||||
}))
|
||||
expect(flips).toBe(0)
|
||||
expect(await compact()).toBe(true)
|
||||
await page.mouse.wheel(0, -200)
|
||||
await expect.poll(compact).toBe(false)
|
||||
} finally {
|
||||
await body.evaluate((element) => { element.scrollTop = 0 })
|
||||
await page.setViewportSize({ width: 1680, height: 1000 })
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the plugin page title and actions pinned while the cards scroll', async () => {
|
||||
await page.setViewportSize({ width: 1000, height: 480 })
|
||||
const panel = await openPluginsPanel()
|
||||
|
||||
@@ -1728,7 +1728,18 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode {
|
||||
) : null}
|
||||
{/* The page bar and the More list's head stay put; everything below scrolls. */}
|
||||
<div className={css.pageBody} data-plugin-body
|
||||
onScroll={(event) => { setCompactBar(event.currentTarget.scrollTop > COMPACT_BAR_SCROLL_TOP) }}>
|
||||
onScroll={(event) => {
|
||||
const body = event.currentTarget
|
||||
if (body.scrollTop > COMPACT_BAR_SCROLL_TOP) setCompactBar(true)
|
||||
// Collapsing grows the body, so a list that only just scrolled can fit and clamp to 0 without
|
||||
// the person scrolling up; the bar reopens at the top only while the list still scrolls.
|
||||
else if (body.scrollTop === 0 && body.scrollHeight > body.clientHeight) setCompactBar(false)
|
||||
}}
|
||||
onWheel={(event) => {
|
||||
// A list that fits only under the collapsed bar has no scroll range; scrolling up reopens the bar.
|
||||
const body = event.currentTarget
|
||||
if (event.deltaY < 0 && body.scrollHeight <= body.clientHeight) setCompactBar(false)
|
||||
}}>
|
||||
{!onInstalled
|
||||
? (
|
||||
<div role="tabpanel" id={`${tabsId}-${tab}-panel`} aria-labelledby={`${tabsId}-${tab}`} className={css.tabPanel}>
|
||||
|
||||
@@ -292,12 +292,42 @@ describe('PluginManagerPage', () => {
|
||||
expect(bar?.hasAttribute('data-compact')).toBe(true)
|
||||
act(() => {
|
||||
if (body === null) throw new Error('the page body is missing')
|
||||
Object.defineProperties(body, { scrollHeight: { configurable: true, value: 900 }, clientHeight: { configurable: true, value: 400 } })
|
||||
body.scrollTop = 0
|
||||
fireEvent.scroll(body)
|
||||
})
|
||||
expect(bar?.hasAttribute('data-compact')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the collapsed bar when collapsing makes the list fit, and reopens it on an upward scroll', () => {
|
||||
renderTab({ packages: [pkg()] })
|
||||
const bar = document.querySelector('[data-plugin-bar]')
|
||||
const body = document.querySelector<HTMLElement>('[data-plugin-body]')
|
||||
if (body === null) throw new Error('the page body is missing')
|
||||
const range = (scrollHeight: number): void => {
|
||||
Object.defineProperties(body, {
|
||||
scrollHeight: { configurable: true, value: scrollHeight },
|
||||
clientHeight: { configurable: true, value: 400 },
|
||||
})
|
||||
}
|
||||
range(440)
|
||||
act(() => { body.scrollTop = 20; fireEvent.scroll(body) })
|
||||
expect(bar?.hasAttribute('data-compact')).toBe(true)
|
||||
// The taller body now holds the whole list, and the browser clamps the scroll position to 0.
|
||||
range(400)
|
||||
act(() => { body.scrollTop = 0; fireEvent.scroll(body) })
|
||||
expect(bar?.hasAttribute('data-compact')).toBe(true)
|
||||
act(() => { fireEvent.wheel(body, { deltaY: 40 }) })
|
||||
expect(bar?.hasAttribute('data-compact')).toBe(true)
|
||||
act(() => { fireEvent.wheel(body, { deltaY: -40 }) })
|
||||
expect(bar?.hasAttribute('data-compact')).toBe(false)
|
||||
// A wheel over a list that still scrolls leaves the bar to the scroll position.
|
||||
range(900)
|
||||
act(() => { body.scrollTop = 20; fireEvent.scroll(body) })
|
||||
act(() => { fireEvent.wheel(body, { deltaY: -40 }) })
|
||||
expect(bar?.hasAttribute('data-compact')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the Installed tab panel through loading and the More list out of the tabs', () => {
|
||||
const worktree = '@deepseek-ai/dsh-experimental-tool-worktree'
|
||||
const { navigation, set } = renderTab({ status: 'loading', packages: [] }, {}, {
|
||||
|
||||
Reference in new issue
Block a user