Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ScrollArea] Viewport fixes #2945

Merged
merged 10 commits into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
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
97 changes: 89 additions & 8 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 Expand Up @@ -424,14 +512,7 @@ const scrollAreaClass = css({
border: '1px solid black',
});

const RECOMMENDED_CSS__SCROLLAREA__VIEWPORT: any = {
width: '100%',
height: '100%',
};

const scrollAreaViewportClass = css({
...RECOMMENDED_CSS__SCROLLAREA__VIEWPORT,
});
const scrollAreaViewportClass = css();

const RECOMMENDED_CSS__SCROLLBAR__ROOT: any = {
display: 'flex',
Expand Down
71 changes: 58 additions & 13 deletions packages/react/scroll-area/src/ScrollArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,22 +142,41 @@ 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;
width: 100%;
height: 100%;
}
: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 @@ -176,16 +195,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 @@ -1010,6 +1035,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,
});
}

Comment on lines +1036 to +1055
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We worked this out in radix-ui/themes#311

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

const Root = ScrollArea;
Expand Down
32 changes: 24 additions & 8 deletions ssr-testing/app/scroll-area/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,30 @@ export default function Page() {
<ScrollAreaThumb />
</Scrollbar>

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

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

<ScrollAreaCorner />
Expand Down
Loading