From c1731722af31347362096da1cd40bbf9e1d8ba78 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 10 Jun 2026 17:06:33 +0800 Subject: [PATCH] fix(site): stop sidebar scrollbar hit-area from covering chat row controls (#26209) The Agents chat list scrollbar swallowed clicks on the per-row controls (the actions menu, timestamp, and unread/shared indicators). PR #26016 added a 24px invisible hit-target to the shared `ScrollArea` thumb, anchored to the thumb's right edge and extending left. On this sidebar the visible scrollbar is only ~6px wide (`w-1.5`), so the hit-target reached ~18px left of it, on top of the controls just inside the edge. This adds an optional, default-preserving `scrollThumbClassName` prop to the shared `ScrollArea`, threaded through `ScrollBar` to the thumb the same way `scrollBarClassName` already is. When the prop is omitted the output is byte-for-byte unchanged, so every other scrollbar keeps the 24px target. The Agents chat list passes `before:hidden` to drop only the expanded hit-target: the visible 6px thumb stays draggable and the scrollbar looks identical, so there is no visible change while the controls beside it become clickable again. A new `ScrollArea` story (`ThumbHitAreaOverride`) uses `type="always"` so the thumb is guaranteed to render, then asserts the default keeps the 24px `::before` target while the override sets `display: none`. Refs #26016 --- .../ScrollArea/ScrollArea.stories.tsx | 71 +++++++++++++++++++ site/src/components/ScrollArea/ScrollArea.tsx | 14 +++- .../ChatsSidebar/chats/ChatsPanel.tsx | 4 ++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/site/src/components/ScrollArea/ScrollArea.stories.tsx b/site/src/components/ScrollArea/ScrollArea.stories.tsx index 44e82a14d7..2546719e8f 100644 --- a/site/src/components/ScrollArea/ScrollArea.stories.tsx +++ b/site/src/components/ScrollArea/ScrollArea.stories.tsx @@ -104,3 +104,74 @@ export const Accessibility: Story = { ).toBeGreaterThanOrEqual(3); }, }; + +// type="always" forces the thumb to render so its `::before` hit-target +// pseudo-element is measurable. +export const ThumbHitAreaOverride: Story = { + render: () => ( +
+
+ + + +
+
+ + + +
+
+ ), + play: async ({ canvasElement }) => { + const getThumb = ( + testid: string, + orientation: "vertical" | "horizontal", + ) => { + const area = canvasElement.querySelector(`[data-testid='${testid}']`); + return area?.querySelector(`[data-orientation="${orientation}"]`) + ?.firstElementChild as HTMLElement | null | undefined; + }; + + await waitFor(() => { + expect(getThumb("default-area", "vertical")).toBeTruthy(); + expect(getThumb("override-area", "vertical")).toBeTruthy(); + expect(getThumb("override-area", "horizontal")).toBeTruthy(); + }); + + const defaultThumb = getThumb("default-area", "vertical"); + const overrideThumb = getThumb("override-area", "vertical"); + const overrideHorizontalThumb = getThumb("override-area", "horizontal"); + if (!defaultThumb || !overrideThumb || !overrideHorizontalThumb) { + throw new Error("scrollbar thumbs not found"); + } + + const defaultBefore = getComputedStyle(defaultThumb, "::before"); + await expect(defaultBefore.display).not.toBe("none"); + await expect(Number.parseFloat(defaultBefore.width)).toBeGreaterThanOrEqual( + 24, + ); + + await expect(getComputedStyle(overrideThumb, "::before").display).toBe( + "none", + ); + await expect( + getComputedStyle(overrideHorizontalThumb, "::before").display, + ).toBe("none"); + await expect(overrideThumb.getBoundingClientRect().width).toBeGreaterThan( + 0, + ); + }, +}; diff --git a/site/src/components/ScrollArea/ScrollArea.tsx b/site/src/components/ScrollArea/ScrollArea.tsx index c20fabd541..dbb0721a19 100644 --- a/site/src/components/ScrollArea/ScrollArea.tsx +++ b/site/src/components/ScrollArea/ScrollArea.tsx @@ -10,6 +10,8 @@ interface ScrollAreaProps extends React.ComponentPropsWithRef { scrollBarClassName?: string; horizontalScrollBarClassName?: string; + /** Extra thumb classes; also reaches the thumb's `::before` hit-target. */ + scrollThumbClassName?: string; viewportClassName?: string; viewportTabIndex?: number; /** Which scrollbar(s) to show. Defaults to "vertical". */ @@ -20,6 +22,7 @@ export const ScrollArea: React.FC = ({ className, scrollBarClassName, horizontalScrollBarClassName, + scrollThumbClassName, viewportClassName, viewportTabIndex, orientation = "vertical", @@ -61,6 +64,7 @@ export const ScrollArea: React.FC = ({ )} {(orientation === "horizontal" || orientation === "both") && ( @@ -72,6 +76,7 @@ export const ScrollArea: React.FC = ({ ? horizontalScrollBarClassName : (horizontalScrollBarClassName ?? scrollBarClassName), )} + thumbClassName={scrollThumbClassName} /> )} @@ -80,8 +85,12 @@ export const ScrollArea: React.FC = ({ }; export const ScrollBar: React.FC< - React.ComponentPropsWithRef -> = ({ className, orientation = "vertical", ...props }) => { + React.ComponentPropsWithRef< + typeof ScrollAreaPrimitive.ScrollAreaScrollbar + > & { + thumbClassName?: string; + } +> = ({ className, orientation = "vertical", thumbClassName, ...props }) => { return ( diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/chats/ChatsPanel.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/chats/ChatsPanel.tsx index 8bdadc5a44..2eb4b4d1da 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/chats/ChatsPanel.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/chats/ChatsPanel.tsx @@ -447,6 +447,10 @@ export const ChatsPanel: FC = ({