Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion surface/web/src/components/ClampedBlock.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ function setMetrics(element: Element, scrollHeight: number, clientHeight: number

function renderClamp(children: React.ReactNode = 'Content') {
return render(
<ClampedBlock collapsedClassName="test-clamp" expandLabel="Show more ↓" collapseLabel="Show less ↑">
<ClampedBlock
collapsedClassName="test-clamp"
overflowingClassName="test-fade"
expandLabel="Show more ↓"
collapseLabel="Show less ↑"
>
{children}
</ClampedBlock>,
);
Expand Down Expand Up @@ -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');
Expand Down
22 changes: 21 additions & 1 deletion surface/web/src/components/ClampedBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -51,7 +64,14 @@ export function ClampedBlock({

return (
<>
<div ref={contentRef} className={classes(contentClassName, clamped && collapsedClassName)}>
<div
ref={contentRef}
className={classes(
contentClassName,
clamped && collapsedClassName,
clamped && overflows && overflowingClassName,
)}
>
<ClampContext.Provider value={insideClamp || canClamp}>{children}</ClampContext.Provider>
</div>
{showToggle ? (
Expand Down
7 changes: 6 additions & 1 deletion surface/web/src/components/MessageText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,12 @@ export function MessageText({
// ancestor chain (GFM footnotes open with `<h3 class="sr-only">`) 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"
Expand Down
Loading