diff --git a/surface/web/src/components/ClampedBlock.test.tsx b/surface/web/src/components/ClampedBlock.test.tsx index fa9f7e9b..09de5904 100644 --- a/surface/web/src/components/ClampedBlock.test.tsx +++ b/surface/web/src/components/ClampedBlock.test.tsx @@ -29,7 +29,12 @@ function setMetrics(element: Element, scrollHeight: number, clientHeight: number function renderClamp(children: React.ReactNode = 'Content') { return render( - + {children} , ); @@ -57,6 +62,34 @@ describe('ClampedBlock', () => { expect(screen.queryByRole('button')).toBeNull(); }); + // The fade advertises "there is more below". Content that fits gets no toggle, + // so a fade there would point at nothing and the user would have no control to + // clear it — the message would just render permanently dimmed. + it('withholds the overflow hint from content that fits, alongside the toggle', () => { + const { container } = renderClamp(); + const content = container.querySelector('.test-clamp'); + setMetrics(content!, 80, 80); + + act(() => observers[0]?.trigger()); + + expect(screen.queryByRole('button')).toBeNull(); + expect(container.querySelector('.test-fade')).toBeNull(); + }); + + it('applies the overflow hint while clamped content overflows', () => { + const { container } = renderClamp(); + const content = container.querySelector('.test-clamp'); + setMetrics(content!, 180, 80); + + act(() => observers[0]?.trigger()); + + expect(container.querySelector('.test-fade')).toBeTruthy(); + + // Expanding drops the hint with the constraint: nothing is hidden any more. + fireEvent.click(screen.getByRole('button', { name: 'Show more ↓' })); + expect(container.querySelector('.test-fade')).toBeNull(); + }); + it('toggles its caller-supplied label and expansion direction', () => { const { container } = renderClamp(); const content = container.querySelector('.test-clamp'); diff --git a/surface/web/src/components/ClampedBlock.tsx b/surface/web/src/components/ClampedBlock.tsx index 05cffa9a..ca606aa7 100644 --- a/surface/web/src/components/ClampedBlock.tsx +++ b/surface/web/src/components/ClampedBlock.tsx @@ -14,14 +14,27 @@ export function ClampedBlock({ contentClassName, enabled = true, expandLabel, + overflowingClassName, toggleClassName, }: { children: ReactNode; + /** + * The size constraint, applied whenever the block is collapsed. It has to be + * applied even when the content turns out to fit, because overflow is measured + * THROUGH it: without the constraint, scrollHeight === clientHeight and nothing + * would ever report as overflowing. + */ collapsedClassName: string; collapseLabel: ReactNode; contentClassName?: string; enabled?: boolean; expandLabel: ReactNode; + /** + * Styling that advertises there is more to see (a fade, say). Applied only when + * the content actually overflows, so it stays in step with the toggle: content + * that fits gets no toggle, and so must not get the hint either. + */ + overflowingClassName?: string; toggleClassName?: string; }) { const insideClamp = useContext(ClampContext); @@ -51,7 +64,14 @@ export function ClampedBlock({ return ( <> -
+
{children}
{showToggle ? ( diff --git a/surface/web/src/components/MessageText.tsx b/surface/web/src/components/MessageText.tsx index 823418cc..5e55ed02 100644 --- a/surface/web/src/components/MessageText.tsx +++ b/surface/web/src/components/MessageText.tsx @@ -798,7 +798,12 @@ export function MessageText({ // ancestor chain (GFM footnotes open with `

`) would // otherwise anchor above the clamp, escape the clip, and inflate the // scroll height into blank space (#544). - collapsedClassName="relative max-h-80 overflow-hidden [mask-image:linear-gradient(to_bottom,black_70%,transparent)]" + collapsedClassName="relative max-h-80 overflow-hidden" + // The fade rides on measured overflow, not on the length heuristic: a + // message long enough to trip the threshold but short enough to fit used + // to render faded with no toggle to clear it, because the toggle asks + // "does it overflow?" and the fade only asked "is it collapsed?". + overflowingClassName="[mask-image:linear-gradient(to_bottom,black_70%,transparent)]" expandLabel="Show more" collapseLabel="Show less" toggleClassName="mt-1 text-xs font-medium text-accent-text hover:underline"