Skip to content

Commit f6d70b3

Browse files
authored
[ScrollArea] Viewport fixes (#2945)
1 parent 0f97cdb commit f6d70b3

File tree

4 files changed

+198
-31
lines changed

4 files changed

+198
-31
lines changed

.yarn/versions/a54ad5a9.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
releases:
2+
"@radix-ui/react-scroll-area": minor
3+
4+
declined:
5+
- primitives
6+
- ssr-testing

packages/react/scroll-area/src/ScrollArea.stories.tsx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,94 @@ export const Chromatic = () => (
307307
);
308308
Chromatic.parameters = { chromatic: { disable: false } };
309309

310+
export const ChromaticEllipsis = () => (
311+
<>
312+
<h1>Ellipsis at viewport width</h1>
313+
<ScrollAreaStory type="always" horizontal={false} vertical>
314+
{Array.from({ length: 10 }).map((_, index) => (
315+
<Copy
316+
key={index}
317+
style={{
318+
maxWidth: '100%',
319+
overflow: 'hidden',
320+
whiteSpace: 'nowrap',
321+
textOverflow: 'ellipsis',
322+
}}
323+
/>
324+
))}
325+
</ScrollAreaStory>
326+
327+
<h1>Ellipsis at content width</h1>
328+
<ScrollAreaStory type="always" horizontal vertical>
329+
{Array.from({ length: 10 }).map((_, index) => (
330+
<Copy
331+
key={index}
332+
style={{
333+
width: 500,
334+
overflow: 'hidden',
335+
whiteSpace: 'nowrap',
336+
textOverflow: 'ellipsis',
337+
}}
338+
/>
339+
))}
340+
</ScrollAreaStory>
341+
</>
342+
);
343+
ChromaticEllipsis.parameters = { chromatic: { disable: false } };
344+
345+
const COPY_SHORT = `
346+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet eros iaculis,
347+
bibendum tellus ac, lobortis odio. Aliquam bibendum elit est, in iaculis est commodo id.
348+
Donec pulvinar est libero. Proin consectetur pellentesque molestie.
349+
`;
350+
351+
export const ChromaticFillParentHeight = () => (
352+
<>
353+
<h1>Parent has fixed height, short content</h1>
354+
<div style={{ display: 'flex', width: 600, height: 300, overflow: 'hidden' }}>
355+
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
356+
<div>{COPY_SHORT}</div>
357+
</ScrollAreaStory>
358+
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
359+
<div>{COPY_SHORT}</div>
360+
</ScrollAreaStory>
361+
</div>
362+
363+
<h1>Parent has fixed height, tall content</h1>
364+
<div style={{ display: 'flex', width: 600, height: 300, overflow: 'hidden' }}>
365+
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
366+
<div>{COPY_SHORT}</div>
367+
</ScrollAreaStory>
368+
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
369+
<Copy style={{ width: 'auto' }} />
370+
</ScrollAreaStory>
371+
</div>
372+
373+
<h1>Parent has max height</h1>
374+
<div style={{ display: 'flex', width: 600, maxHeight: 300, overflow: 'hidden' }}>
375+
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
376+
<div>{COPY_SHORT}</div>
377+
</ScrollAreaStory>
378+
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
379+
<Copy style={{ width: 'auto' }} />
380+
</ScrollAreaStory>
381+
</div>
382+
383+
<h1>Parent has auto height</h1>
384+
<div style={{ display: 'flex', width: 600, overflow: 'hidden' }}>
385+
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
386+
<div>{COPY_SHORT}</div>
387+
</ScrollAreaStory>
388+
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
389+
<Copy style={{ width: 'auto' }} />
390+
</ScrollAreaStory>
391+
</div>
392+
393+
<div style={{ height: 200 }} />
394+
</>
395+
);
396+
ChromaticFillParentHeight.parameters = { chromatic: { disable: false } };
397+
310398
const DYNAMIC_CONTENT_DELAY = 2000;
311399

312400
export const ChromaticDynamicContentBeforeLoaded = () => {

packages/react/scroll-area/src/ScrollArea.tsx

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,22 +141,39 @@ interface ScrollAreaViewportProps extends PrimitiveDivProps {
141141

142142
const ScrollAreaViewport = React.forwardRef<ScrollAreaViewportElement, ScrollAreaViewportProps>(
143143
(props: ScopedProps<ScrollAreaViewportProps>, forwardedRef) => {
144-
const { __scopeScrollArea, children, nonce, ...viewportProps } = props;
144+
const { __scopeScrollArea, children, asChild, nonce, ...viewportProps } = props;
145145
const context = useScrollAreaContext(VIEWPORT_NAME, __scopeScrollArea);
146146
const ref = React.useRef<ScrollAreaViewportElement>(null);
147147
const composedRefs = useComposedRefs(forwardedRef, ref, context.onViewportChange);
148148
return (
149149
<>
150-
{/* Hide scrollbars cross-browser and enable momentum scroll for touch devices */}
151150
<style
152151
dangerouslySetInnerHTML={{
153-
__html: `[data-radix-scroll-area-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-radix-scroll-area-viewport]::-webkit-scrollbar{display:none}`,
152+
__html: `
153+
[data-radix-scroll-area-viewport] {
154+
scrollbar-width: none;
155+
-ms-overflow-style: none;
156+
-webkit-overflow-scrolling: touch;
157+
}
158+
[data-radix-scroll-area-viewport]::-webkit-scrollbar {
159+
display: none;
160+
}
161+
:where([data-radix-scroll-area-viewport]) {
162+
display: flex;
163+
flex-direction: column;
164+
align-items: stretch;
165+
}
166+
:where([data-radix-scroll-area-content]) {
167+
flex-grow: 1;
168+
}
169+
`,
154170
}}
155171
nonce={nonce}
156172
/>
157173
<Primitive.div
158174
data-radix-scroll-area-viewport=""
159175
{...viewportProps}
176+
asChild={asChild}
160177
ref={composedRefs}
161178
style={{
162179
/**
@@ -175,16 +192,22 @@ const ScrollAreaViewport = React.forwardRef<ScrollAreaViewportElement, ScrollAre
175192
...props.style,
176193
}}
177194
>
178-
{/**
179-
* `display: table` ensures our content div will match the size of its children in both
180-
* horizontal and vertical axis so we can determine if scroll width/height changed and
181-
* recalculate thumb sizes. This doesn't account for children with *percentage*
182-
* widths that change. We'll wait to see what use-cases consumers come up with there
183-
* before trying to resolve it.
184-
*/}
185-
<div ref={context.onContentChange} style={{ minWidth: '100%', display: 'table' }}>
186-
{children}
187-
</div>
195+
{getSubtree({ asChild, children }, (children) => (
196+
<div
197+
data-radix-scroll-area-content=""
198+
ref={context.onContentChange}
199+
/**
200+
* When horizontal scrollbar is visible: this element should be at least
201+
* as wide as its children for size calculations to work correctly.
202+
*
203+
* When horizontal scrollbar is NOT visible: this element's width should
204+
* be constrained by the parent container to enable `text-overflow: ellipsis`
205+
*/
206+
style={{ minWidth: context.scrollbarXEnabled ? 'fit-content' : undefined }}
207+
>
208+
{children}
209+
</div>
210+
))}
188211
</Primitive.div>
189212
</>
190213
);
@@ -1009,6 +1032,26 @@ function useResizeObserver(element: HTMLElement | null, onResize: () => void) {
10091032
}, [element, handleResize]);
10101033
}
10111034

1035+
/**
1036+
* This is a helper function that is used when a component supports `asChild`
1037+
* using the `Slot` component but its implementation contains nested DOM elements.
1038+
*
1039+
* Using it ensures if a consumer uses the `asChild` prop, the elements are in
1040+
* correct order in the DOM, adopting the intended consumer `children`.
1041+
*/
1042+
function getSubtree(
1043+
options: { asChild: boolean | undefined; children: React.ReactNode },
1044+
content: React.ReactNode | ((children: React.ReactNode) => React.ReactNode)
1045+
) {
1046+
const { asChild, children } = options;
1047+
if (!asChild) return typeof content === 'function' ? content(children) : content;
1048+
1049+
const firstChild = React.Children.only(children) as React.ReactElement;
1050+
return React.cloneElement(firstChild, {
1051+
children: typeof content === 'function' ? content(firstChild.props.children) : content,
1052+
});
1053+
}
1054+
10121055
/* -----------------------------------------------------------------------------------------------*/
10131056

10141057
const Root = ScrollArea;

ssr-testing/app/scroll-area/page.tsx

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,57 @@ import {
99

1010
export default function Page() {
1111
return (
12-
<ScrollArea style={{ width: '400px', height: '400px' }}>
13-
<Scrollbar orientation="vertical">
14-
<ScrollAreaThumb />
15-
</Scrollbar>
12+
<div>
13+
<ScrollArea>
14+
<Scrollbar orientation="vertical">
15+
<ScrollAreaThumb />
16+
</Scrollbar>
1617

17-
<Scrollbar orientation="horizontal">
18-
<ScrollAreaThumb />
19-
</Scrollbar>
18+
<Scrollbar orientation="horizontal">
19+
<ScrollAreaThumb />
20+
</Scrollbar>
2021

21-
<ScrollAreaViewport style={{ width: '2000px', padding: 20 }}>
22-
<LongContent />
23-
<LongContent />
24-
<LongContent />
25-
<LongContent />
26-
<LongContent />
27-
<LongContent />
28-
<LongContent />
29-
</ScrollAreaViewport>
22+
<ScrollAreaViewport style={{ width: '400px', height: '400px' }}>
23+
<div style={{ width: '2000px', padding: 20 }}>
24+
<LongContent />
25+
<LongContent />
26+
<LongContent />
27+
<LongContent />
28+
<LongContent />
29+
<LongContent />
30+
<LongContent />
31+
</div>
32+
</ScrollAreaViewport>
3033

31-
<ScrollAreaCorner />
32-
</ScrollArea>
34+
<ScrollAreaCorner />
35+
</ScrollArea>
36+
37+
<ScrollArea>
38+
<Scrollbar orientation="vertical">
39+
<ScrollAreaThumb />
40+
</Scrollbar>
41+
42+
<Scrollbar orientation="horizontal">
43+
<ScrollAreaThumb />
44+
</Scrollbar>
45+
46+
<ScrollAreaViewport style={{ width: '400px', height: '400px' }} asChild>
47+
<section style={{ border: '1px solid' }}>
48+
<div style={{ width: '2000px', padding: 20 }}>
49+
<LongContent />
50+
<LongContent />
51+
<LongContent />
52+
<LongContent />
53+
<LongContent />
54+
<LongContent />
55+
<LongContent />
56+
</div>
57+
</section>
58+
</ScrollAreaViewport>
59+
60+
<ScrollAreaCorner />
61+
</ScrollArea>
62+
</div>
3363
);
3464
}
3565

0 commit comments

Comments
 (0)