Skip to content

Commit 4fbce8f

Browse files
committed
feat: add scrollable container
1 parent 3b0e155 commit 4fbce8f

File tree

3 files changed

+175
-0
lines changed

3 files changed

+175
-0
lines changed

packages/components/_provisional/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ export * from "./dock";
88

99
export * from "./breadcrumbs";
1010

11+
export * from "./scrollable-container";
12+
1113
export * from "./position-engine";
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
@import "@react-ck/theme";
2+
3+
.root {
4+
position: relative;
5+
overflow: auto;
6+
7+
// all shadows
8+
.shadow_x::before,
9+
.shadow_x::after,
10+
&::before,
11+
&::after {
12+
display: block;
13+
content: "";
14+
position: absolute;
15+
opacity: 0;
16+
transition: opacity 0.3s;
17+
box-shadow: 0 0 get-spacing(1.5) get-spacing(1.5) get-color(neutral-light-1);
18+
}
19+
20+
// shadow x
21+
.shadow_x {
22+
position: absolute;
23+
top: 0;
24+
left: 0;
25+
height: 100%;
26+
width: 100%;
27+
transform: translate(var(--scroll-x), var(--scroll-y));
28+
pointer-events: none;
29+
30+
&::before,
31+
&::after {
32+
height: 100%;
33+
width: 0;
34+
}
35+
}
36+
37+
// shadow y
38+
&::before,
39+
&::after {
40+
height: 0;
41+
width: 100%;
42+
transform: translate(var(--scroll-x), var(--scroll-y));
43+
pointer-events: none;
44+
}
45+
46+
// shadow top
47+
&::before {
48+
top: 0;
49+
left: 0;
50+
}
51+
52+
// shadow left
53+
.shadow_x::before {
54+
top: 0;
55+
left: 0;
56+
}
57+
58+
// shadow right
59+
.shadow_x::after {
60+
top: 0;
61+
right: 0;
62+
}
63+
64+
// shadow bottom
65+
&::after {
66+
bottom: 0;
67+
left: 0;
68+
}
69+
70+
// active shadow
71+
&.has-scroll-top::before,
72+
&.has-scroll-right .shadow_x::after,
73+
&.has-scroll-bottom::after,
74+
&.has-scroll-left .shadow_x::before {
75+
opacity: 1;
76+
}
77+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import classNames from "classnames";
2+
import React, { useCallback, useEffect, useRef, useState } from "react";
3+
import styles from "./index.module.scss";
4+
5+
export type ScrollableContainerProps = React.HTMLAttributes<HTMLDivElement>;
6+
7+
export const ScrollableContainer = ({
8+
className,
9+
onScroll,
10+
children,
11+
style,
12+
...otherProps
13+
}: Readonly<ScrollableContainerProps>): React.ReactElement => {
14+
const rootRef = useRef<HTMLDivElement>(null);
15+
const [hasScroll, setHasScroll] = useState({
16+
left: false,
17+
right: false,
18+
top: false,
19+
bottom: false,
20+
});
21+
22+
const [scrollPos, setScrollPos] = useState({
23+
x: "0px",
24+
y: "0px",
25+
});
26+
27+
const update = useCallback(() => {
28+
if (!rootRef.current) return;
29+
30+
const TOLERANCE = 14;
31+
32+
const { scrollLeft, scrollTop, scrollHeight, scrollWidth, offsetHeight, offsetWidth } =
33+
rootRef.current;
34+
35+
const hasScrollY = scrollHeight > offsetHeight;
36+
const hasScrollX = scrollWidth > offsetWidth;
37+
38+
const left = hasScrollX && scrollLeft > TOLERANCE;
39+
const right = hasScrollX && scrollLeft < scrollWidth - offsetWidth - TOLERANCE;
40+
41+
const top = hasScrollY && scrollTop > TOLERANCE;
42+
const bottom = hasScrollY && scrollTop < scrollHeight - offsetHeight - TOLERANCE;
43+
44+
setScrollPos({
45+
x: `${scrollLeft}px`,
46+
y: `${scrollTop}px`,
47+
});
48+
49+
setHasScroll({ left, right, top, bottom });
50+
}, []);
51+
52+
const handleScroll = useCallback<React.UIEventHandler<HTMLDivElement>>(
53+
(e) => {
54+
update();
55+
onScroll?.(e);
56+
},
57+
[onScroll, update],
58+
);
59+
60+
useEffect(update, [update]);
61+
62+
useEffect(() => {
63+
if (!rootRef.current) return;
64+
65+
const ro = new ResizeObserver(update);
66+
67+
ro.observe(rootRef.current);
68+
69+
return () => {
70+
ro.disconnect();
71+
};
72+
}, [update]);
73+
74+
return (
75+
<div
76+
ref={rootRef}
77+
{...otherProps}
78+
className={classNames(className, styles.root, {
79+
[`${styles["has-scroll-top"]}`]: hasScroll.top,
80+
[`${styles["has-scroll-right"]}`]: hasScroll.right,
81+
[`${styles["has-scroll-bottom"]}`]: hasScroll.bottom,
82+
[`${styles["has-scroll-left"]}`]: hasScroll.left,
83+
})}
84+
style={{
85+
...style,
86+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- needed since types do not include CSS variables
87+
["--scroll-x" as keyof React.CSSProperties]: scrollPos.x,
88+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- needed since types do not include CSS variablesz
89+
["--scroll-y" as keyof React.CSSProperties]: scrollPos.y,
90+
}}
91+
onScroll={handleScroll}>
92+
<div className={styles.shadow_x} />
93+
{children}
94+
</div>
95+
);
96+
};

0 commit comments

Comments
 (0)