Skip to content

Commit b7c32aa

Browse files
committed
✨ feat(scroll-area): introduce ScrollArea component with customizable scrollbars and gradient fade
- Added `ScrollArea` component along with its subcomponents: `ScrollAreaRoot`, `ScrollAreaViewport`, `ScrollAreaContent`, `ScrollAreaScrollbar`, `ScrollAreaThumb`, and `ScrollAreaCorner`. - Implemented styles for the ScrollArea to support custom scrollbars and gradient scroll fade effects. - Created documentation and demos showcasing the usage of the ScrollArea component. - Defined types for props to enhance type safety and usability. This update enhances the layout capabilities of the UI library by providing a flexible and visually appealing scroll container. Signed-off-by: Innei <tukon479@gmail.com>
1 parent 2731b9a commit b7c32aa

File tree

9 files changed

+657
-0
lines changed

9 files changed

+657
-0
lines changed

src/ScrollArea/ScrollArea.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client';
2+
3+
import { type FC } from 'react';
4+
5+
import {
6+
ScrollAreaContent,
7+
ScrollAreaCorner,
8+
ScrollAreaRoot,
9+
ScrollAreaScrollbar,
10+
ScrollAreaThumb,
11+
ScrollAreaViewport,
12+
} from './atoms';
13+
import type { ScrollAreaProps } from './type';
14+
15+
const ScrollArea: FC<ScrollAreaProps> = ({
16+
children,
17+
contentProps,
18+
corner = false,
19+
cornerProps,
20+
scrollFade = false,
21+
scrollbarProps,
22+
thumbProps,
23+
viewportProps,
24+
...rest
25+
}) => {
26+
return (
27+
<ScrollAreaRoot {...rest}>
28+
<ScrollAreaViewport scrollFade={scrollFade} {...viewportProps}>
29+
<ScrollAreaContent {...contentProps}>{children}</ScrollAreaContent>
30+
</ScrollAreaViewport>
31+
<ScrollAreaScrollbar {...scrollbarProps}>
32+
<ScrollAreaThumb {...thumbProps} />
33+
</ScrollAreaScrollbar>
34+
{corner && <ScrollAreaCorner {...cornerProps} />}
35+
</ScrollAreaRoot>
36+
);
37+
};
38+
39+
ScrollArea.displayName = 'ScrollArea';
40+
41+
export default ScrollArea;

src/ScrollArea/atoms.tsx

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
'use client';
2+
3+
import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area';
4+
import { cx } from 'antd-style';
5+
import type React from 'react';
6+
7+
import { styles } from './style';
8+
9+
const mergeStateClassName = <TState,>(
10+
base: string,
11+
className: string | ((state: TState) => string | undefined) | undefined,
12+
) => {
13+
if (typeof className === 'function') return (state: TState) => cx(base, className(state));
14+
return cx(base, className);
15+
};
16+
17+
export type ScrollAreaRootProps = React.ComponentProps<typeof BaseScrollArea.Root>;
18+
export type ScrollAreaViewportProps = React.ComponentProps<typeof BaseScrollArea.Viewport> & {
19+
/**
20+
* Enable gradient scroll fade on the viewport edges.
21+
* @default false
22+
*/
23+
scrollFade?: boolean;
24+
};
25+
export type ScrollAreaContentProps = React.ComponentProps<typeof BaseScrollArea.Content>;
26+
export type ScrollAreaScrollbarProps = React.ComponentProps<typeof BaseScrollArea.Scrollbar>;
27+
export type ScrollAreaThumbProps = React.ComponentProps<typeof BaseScrollArea.Thumb>;
28+
export type ScrollAreaCornerProps = React.ComponentProps<typeof BaseScrollArea.Corner>;
29+
30+
export const ScrollAreaRoot = ({ className, ...rest }: ScrollAreaRootProps) => {
31+
return (
32+
<BaseScrollArea.Root {...rest} className={mergeStateClassName(styles.root, className) as any} />
33+
);
34+
};
35+
36+
ScrollAreaRoot.displayName = 'ScrollAreaRoot';
37+
38+
export const ScrollAreaViewport = ({
39+
className,
40+
scrollFade = false,
41+
...rest
42+
}: ScrollAreaViewportProps) => {
43+
return (
44+
<BaseScrollArea.Viewport
45+
{...rest}
46+
className={
47+
mergeStateClassName(
48+
cx(styles.viewport, scrollFade && styles.viewportFade),
49+
className,
50+
) as any
51+
}
52+
/>
53+
);
54+
};
55+
56+
ScrollAreaViewport.displayName = 'ScrollAreaViewport';
57+
58+
export const ScrollAreaContent = ({ className, ...rest }: ScrollAreaContentProps) => {
59+
return (
60+
<BaseScrollArea.Content
61+
{...rest}
62+
className={mergeStateClassName(styles.content, className) as any}
63+
/>
64+
);
65+
};
66+
67+
ScrollAreaContent.displayName = 'ScrollAreaContent';
68+
69+
export const ScrollAreaScrollbar = ({ className, ...rest }: ScrollAreaScrollbarProps) => {
70+
return (
71+
<BaseScrollArea.Scrollbar
72+
{...rest}
73+
className={mergeStateClassName(styles.scrollbar, className) as any}
74+
/>
75+
);
76+
};
77+
78+
ScrollAreaScrollbar.displayName = 'ScrollAreaScrollbar';
79+
80+
export const ScrollAreaThumb = ({ className, ...rest }: ScrollAreaThumbProps) => {
81+
return (
82+
<BaseScrollArea.Thumb
83+
{...rest}
84+
className={mergeStateClassName(styles.thumb, className) as any}
85+
/>
86+
);
87+
};
88+
89+
ScrollAreaThumb.displayName = 'ScrollAreaThumb';
90+
91+
export const ScrollAreaCorner = ({ className, ...rest }: ScrollAreaCornerProps) => {
92+
return (
93+
<BaseScrollArea.Corner
94+
{...rest}
95+
className={mergeStateClassName(styles.corner, className) as any}
96+
/>
97+
);
98+
};
99+
100+
ScrollAreaCorner.displayName = 'ScrollAreaCorner';

src/ScrollArea/demos/both.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {
2+
ScrollAreaContent,
3+
ScrollAreaCorner,
4+
ScrollAreaRoot,
5+
ScrollAreaScrollbar,
6+
ScrollAreaThumb,
7+
ScrollAreaViewport,
8+
} from '@lobehub/ui';
9+
10+
const items = Array.from({ length: 100 }, (_, index) => index + 1);
11+
12+
export default () => {
13+
return (
14+
<ScrollAreaRoot
15+
style={{
16+
height: 320,
17+
maxWidth: 'calc(100vw - 8rem)',
18+
width: '100%',
19+
}}
20+
>
21+
<ScrollAreaViewport>
22+
<ScrollAreaContent style={{ padding: 20 }}>
23+
<ul
24+
style={{
25+
display: 'grid',
26+
gap: 12,
27+
gridTemplateColumns: 'repeat(10, 6.25rem)',
28+
gridTemplateRows: 'repeat(10, 6.25rem)',
29+
listStyle: 'none',
30+
margin: 0,
31+
padding: 0,
32+
}}
33+
>
34+
{items.map((item) => (
35+
<li
36+
key={item}
37+
style={{
38+
alignItems: 'center',
39+
background: 'var(--lobe-color-fill-tertiary)',
40+
borderRadius: 8,
41+
color: 'var(--lobe-color-text-secondary)',
42+
display: 'flex',
43+
fontSize: 14,
44+
fontWeight: 500,
45+
justifyContent: 'center',
46+
}}
47+
>
48+
{item}
49+
</li>
50+
))}
51+
</ul>
52+
</ScrollAreaContent>
53+
</ScrollAreaViewport>
54+
<ScrollAreaScrollbar>
55+
<ScrollAreaThumb />
56+
</ScrollAreaScrollbar>
57+
<ScrollAreaScrollbar orientation="horizontal">
58+
<ScrollAreaThumb />
59+
</ScrollAreaScrollbar>
60+
<ScrollAreaCorner />
61+
</ScrollAreaRoot>
62+
);
63+
};

src/ScrollArea/demos/index.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ScrollArea } from '@lobehub/ui';
2+
3+
const paragraphs = [
4+
"Vernacular architecture is building done outside any academic tradition, and without professional guidance. It is not a particular architectural movement or style, but rather a broad category, encompassing a wide range and variety of building types, with differing methods of construction, from around the world, both historical and extant and classical and modern. Vernacular architecture constitutes 95% of the world's built environment, as estimated in 1995 by Amos Rapoport, as measured against the small percentage of new buildings every year designed by architects and built by engineers.",
5+
'This type of architecture usually serves immediate, local needs, is constrained by the materials available in its particular region and reflects local traditions and cultural practices. The study of vernacular architecture does not examine formally schooled architects, but instead that of the design skills and tradition of local builders, who were rarely given any attribution for the work. More recently, vernacular architecture has been examined by designers and the building industry in an effort to be more energy conscious with contemporary design and construction -- part of a broader interest in sustainable design.',
6+
];
7+
8+
export default () => {
9+
return (
10+
<ScrollArea
11+
scrollFade
12+
style={{
13+
height: 192,
14+
maxWidth: 'calc(100vw - 8rem)',
15+
width: '100%',
16+
}}
17+
>
18+
{paragraphs.map((paragraph) => (
19+
<p key={paragraph} style={{ margin: 0 }}>
20+
{paragraph}
21+
</p>
22+
))}
23+
</ScrollArea>
24+
);
25+
};

0 commit comments

Comments
 (0)