Skip to content

Commit

Permalink
[ScrollArea] Viewport fixes (#2945)
Browse files Browse the repository at this point in the history
  • Loading branch information
vladmoroz committed Aug 11, 2024
1 parent 0f97cdb commit f6d70b3
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 31 deletions.
6 changes: 6 additions & 0 deletions .yarn/versions/a54ad5a9.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
releases:
"@radix-ui/react-scroll-area": minor

declined:
- primitives
- ssr-testing
88 changes: 88 additions & 0 deletions packages/react/scroll-area/src/ScrollArea.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,94 @@ export const Chromatic = () => (
);
Chromatic.parameters = { chromatic: { disable: false } };

export const ChromaticEllipsis = () => (
<>
<h1>Ellipsis at viewport width</h1>
<ScrollAreaStory type="always" horizontal={false} vertical>
{Array.from({ length: 10 }).map((_, index) => (
<Copy
key={index}
style={{
maxWidth: '100%',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
/>
))}
</ScrollAreaStory>

<h1>Ellipsis at content width</h1>
<ScrollAreaStory type="always" horizontal vertical>
{Array.from({ length: 10 }).map((_, index) => (
<Copy
key={index}
style={{
width: 500,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
/>
))}
</ScrollAreaStory>
</>
);
ChromaticEllipsis.parameters = { chromatic: { disable: false } };

const COPY_SHORT = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet eros iaculis,
bibendum tellus ac, lobortis odio. Aliquam bibendum elit est, in iaculis est commodo id.
Donec pulvinar est libero. Proin consectetur pellentesque molestie.
`;

export const ChromaticFillParentHeight = () => (
<>
<h1>Parent has fixed height, short content</h1>
<div style={{ display: 'flex', width: 600, height: 300, overflow: 'hidden' }}>
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
<div>{COPY_SHORT}</div>
</ScrollAreaStory>
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
<div>{COPY_SHORT}</div>
</ScrollAreaStory>
</div>

<h1>Parent has fixed height, tall content</h1>
<div style={{ display: 'flex', width: 600, height: 300, overflow: 'hidden' }}>
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
<div>{COPY_SHORT}</div>
</ScrollAreaStory>
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
<Copy style={{ width: 'auto' }} />
</ScrollAreaStory>
</div>

<h1>Parent has max height</h1>
<div style={{ display: 'flex', width: 600, maxHeight: 300, overflow: 'hidden' }}>
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
<div>{COPY_SHORT}</div>
</ScrollAreaStory>
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
<Copy style={{ width: 'auto' }} />
</ScrollAreaStory>
</div>

<h1>Parent has auto height</h1>
<div style={{ display: 'flex', width: 600, overflow: 'hidden' }}>
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
<div>{COPY_SHORT}</div>
</ScrollAreaStory>
<ScrollAreaStory type="always" vertical horizontal style={{ width: '50%', height: 'auto' }}>
<Copy style={{ width: 'auto' }} />
</ScrollAreaStory>
</div>

<div style={{ height: 200 }} />
</>
);
ChromaticFillParentHeight.parameters = { chromatic: { disable: false } };

const DYNAMIC_CONTENT_DELAY = 2000;

export const ChromaticDynamicContentBeforeLoaded = () => {
Expand Down
69 changes: 56 additions & 13 deletions packages/react/scroll-area/src/ScrollArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,22 +141,39 @@ interface ScrollAreaViewportProps extends PrimitiveDivProps {

const ScrollAreaViewport = React.forwardRef<ScrollAreaViewportElement, ScrollAreaViewportProps>(
(props: ScopedProps<ScrollAreaViewportProps>, forwardedRef) => {
const { __scopeScrollArea, children, nonce, ...viewportProps } = props;
const { __scopeScrollArea, children, asChild, nonce, ...viewportProps } = props;
const context = useScrollAreaContext(VIEWPORT_NAME, __scopeScrollArea);
const ref = React.useRef<ScrollAreaViewportElement>(null);
const composedRefs = useComposedRefs(forwardedRef, ref, context.onViewportChange);
return (
<>
{/* Hide scrollbars cross-browser and enable momentum scroll for touch devices */}
<style
dangerouslySetInnerHTML={{
__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}`,
__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;
}
:where([data-radix-scroll-area-viewport]) {
display: flex;
flex-direction: column;
align-items: stretch;
}
:where([data-radix-scroll-area-content]) {
flex-grow: 1;
}
`,
}}
nonce={nonce}
/>
<Primitive.div
data-radix-scroll-area-viewport=""
{...viewportProps}
asChild={asChild}
ref={composedRefs}
style={{
/**
Expand All @@ -175,16 +192,22 @@ const ScrollAreaViewport = React.forwardRef<ScrollAreaViewportElement, ScrollAre
...props.style,
}}
>
{/**
* `display: table` ensures our content div will match the size of its children in both
* horizontal and vertical axis so we can determine if scroll width/height changed and
* recalculate thumb sizes. This doesn't account for children with *percentage*
* widths that change. We'll wait to see what use-cases consumers come up with there
* before trying to resolve it.
*/}
<div ref={context.onContentChange} style={{ minWidth: '100%', display: 'table' }}>
{children}
</div>
{getSubtree({ asChild, children }, (children) => (
<div
data-radix-scroll-area-content=""
ref={context.onContentChange}
/**
* When horizontal scrollbar is visible: this element should be at least
* as wide as its children for size calculations to work correctly.
*
* When horizontal scrollbar is NOT visible: this element's width should
* be constrained by the parent container to enable `text-overflow: ellipsis`
*/
style={{ minWidth: context.scrollbarXEnabled ? 'fit-content' : undefined }}
>
{children}
</div>
))}
</Primitive.div>
</>
);
Expand Down Expand Up @@ -1009,6 +1032,26 @@ function useResizeObserver(element: HTMLElement | null, onResize: () => void) {
}, [element, handleResize]);
}

/**
* This is a helper function that is used when a component supports `asChild`
* using the `Slot` component but its implementation contains nested DOM elements.
*
* Using it ensures if a consumer uses the `asChild` prop, the elements are in
* correct order in the DOM, adopting the intended consumer `children`.
*/
function getSubtree(
options: { asChild: boolean | undefined; children: React.ReactNode },
content: React.ReactNode | ((children: React.ReactNode) => React.ReactNode)
) {
const { asChild, children } = options;
if (!asChild) return typeof content === 'function' ? content(children) : content;

const firstChild = React.Children.only(children) as React.ReactElement;
return React.cloneElement(firstChild, {
children: typeof content === 'function' ? content(firstChild.props.children) : content,
});
}

/* -----------------------------------------------------------------------------------------------*/

const Root = ScrollArea;
Expand Down
66 changes: 48 additions & 18 deletions ssr-testing/app/scroll-area/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,57 @@ import {

export default function Page() {
return (
<ScrollArea style={{ width: '400px', height: '400px' }}>
<Scrollbar orientation="vertical">
<ScrollAreaThumb />
</Scrollbar>
<div>
<ScrollArea>
<Scrollbar orientation="vertical">
<ScrollAreaThumb />
</Scrollbar>

<Scrollbar orientation="horizontal">
<ScrollAreaThumb />
</Scrollbar>
<Scrollbar orientation="horizontal">
<ScrollAreaThumb />
</Scrollbar>

<ScrollAreaViewport style={{ width: '2000px', padding: 20 }}>
<LongContent />
<LongContent />
<LongContent />
<LongContent />
<LongContent />
<LongContent />
<LongContent />
</ScrollAreaViewport>
<ScrollAreaViewport style={{ width: '400px', height: '400px' }}>
<div style={{ width: '2000px', padding: 20 }}>
<LongContent />
<LongContent />
<LongContent />
<LongContent />
<LongContent />
<LongContent />
<LongContent />
</div>
</ScrollAreaViewport>

<ScrollAreaCorner />
</ScrollArea>
<ScrollAreaCorner />
</ScrollArea>

<ScrollArea>
<Scrollbar orientation="vertical">
<ScrollAreaThumb />
</Scrollbar>

<Scrollbar orientation="horizontal">
<ScrollAreaThumb />
</Scrollbar>

<ScrollAreaViewport style={{ width: '400px', height: '400px' }} asChild>
<section style={{ border: '1px solid' }}>
<div style={{ width: '2000px', padding: 20 }}>
<LongContent />
<LongContent />
<LongContent />
<LongContent />
<LongContent />
<LongContent />
<LongContent />
</div>
</section>
</ScrollAreaViewport>

<ScrollAreaCorner />
</ScrollArea>
</div>
);
}

Expand Down

0 comments on commit f6d70b3

Please sign in to comment.