From 71a7445adac726f7f0cf1674631d3250b570a9f2 Mon Sep 17 00:00:00 2001 From: Alim Maasoglu Date: Tue, 21 Jul 2026 21:35:49 +0200 Subject: [PATCH] fix(desktop): hide sidebar top fade at scroll origin Signed-off-by: Alim Maasoglu --- .../src/features/sidebar/ui/AppSidebar.tsx | 10 +++ .../src/shared/styles/globals/components.css | 6 ++ desktop/tests/e2e/sidebar.spec.ts | 61 +++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx index 8b51e3f566..a16b9de2d8 100644 --- a/desktop/src/features/sidebar/ui/AppSidebar.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx @@ -243,9 +243,17 @@ export function AppSidebar({ const showSidebarUpdateCard = canShowSidebarUpdateCard && !isSidebarUpdateCardDismissed; const [dmActionsMenuOpen, setDmActionsMenuOpen] = React.useState(false); + const [hasScrolledFromTop, setHasScrolledFromTop] = React.useState(false); const scrollRef = React.useRef(null); useSidebarScrollLock(scrollRef); + const handleSidebarScroll = React.useCallback( + (event: React.UIEvent) => { + setHasScrolledFromTop(event.currentTarget.scrollTop > 0); + }, + [], + ); + React.useEffect(() => { const scrollElement = scrollRef.current; if (!scrollElement) return; @@ -549,6 +557,7 @@ export function AppSidebar({ @@ -585,6 +594,7 @@ export function AppSidebar({
getComputedStyle(element, "::before").opacity); +} + +async function expectTopEdgeFadeTracksSidebarScroll(page: Page) { + const scroller = page + .getByTestId("app-sidebar") + .locator('[data-sidebar="content"]'); + const pinnedHeader = page.getByTestId("sidebar-pinned-header"); + const inbox = page + .getByTestId("sidebar-primary-menu") + .getByRole("button", { name: "Inbox" }); + const scrollContent = page.getByTestId("sidebar-scroll-content"); + + await expect(scroller).toBeVisible(); + await expect(inbox).toBeVisible(); + const [headerBottom, inboxTop] = await Promise.all([ + pinnedHeader.evaluate((element) => element.getBoundingClientRect().bottom), + inbox.evaluate((element) => element.getBoundingClientRect().top), + ]); + expect(inboxTop).toBeCloseTo(headerBottom); + await expect(scrollContent).toHaveCSS("padding-top", "0px"); + await expect.poll(() => topEdgeFadeOpacity(page)).toBe("0"); + + await scroller.evaluate((element) => { + element.scrollTop = 24; + }); + await expect + .poll(() => scroller.evaluate((element) => element.scrollTop)) + .toBeGreaterThan(0); + await expect.poll(() => topEdgeFadeOpacity(page)).toBe("1"); + + await scroller.evaluate((element) => { + element.scrollTop = 0; + }); + await expect + .poll(() => scroller.evaluate((element) => element.scrollTop)) + .toBe(0); + await expect.poll(() => topEdgeFadeOpacity(page)).toBe("0"); + await expect + .poll(() => + inbox.evaluate((element) => element.getBoundingClientRect().top), + ) + .toBeCloseTo(headerBottom); +} + // Regression guard for the "Leave channel" lockup: with two bundled copies of // @radix-ui/react-dismissable-layer, opening a modal AlertDialog from a modal // Radix ContextMenu left `pointer-events: none` stuck on after the @@ -324,6 +372,19 @@ test("fades the pinned sidebar chrome edges outside the Buzz theme", async ({ expect(fadeStyles.footerZIndex).toBe("5"); expect(fadeStyles.channelBeforeBackground).toBe("none"); expect(fadeStyles.channelAfterBackground).toBe("none"); + + await expectTopEdgeFadeTracksSidebarScroll(page); +}); + +test("shows the dark-theme header fade only after the sidebar scrolls", async ({ + page, +}) => { + await page.addInitScript(() => { + window.localStorage.setItem("buzz-theme", "github-dark"); + }); + await page.goto("/"); + + await expectTopEdgeFadeTracksSidebarScroll(page); }); test("aligns the sidebar search with the channel title outside the Buzz theme", async ({