Skip to content

Commit 3cb0853

Browse files
committed
fix(scroll): more flexible implementation using JS API
1 parent 5c8952e commit 3cb0853

File tree

1 file changed

+100
-46
lines changed

1 file changed

+100
-46
lines changed

index.tsx

Lines changed: 100 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const overflowStyles = (direction: ScrollDirection): React.CSSProperties => ({
1717
scrollTimelineAxis: direction === 'horizontal' ? 'x' : 'y',
1818
})
1919

20-
const fadeStyles = (direction: FadeDirection, id: string, horizontal: boolean, color: string): React.CSSProperties => ({
20+
const fadeStyles = (direction: FadeDirection, horizontal: boolean, color: string): React.CSSProperties => ({
2121
display: 'flex',
2222
position: 'absolute',
2323
outline: 'none',
@@ -27,10 +27,6 @@ const fadeStyles = (direction: FadeDirection, id: string, horizontal: boolean, c
2727
height: horizontal ? '100%' : 20,
2828
width: horizontal ? 20 : '100%',
2929
background: `linear-gradient(to ${direction}, rgba(255, 255, 255, 0), ${color})`,
30-
animationTimeline: '--indicate-scroll-element',
31-
animationTimingFunction: 'linear',
32-
animationFillMode: 'forwards',
33-
animationName: `indicate-${direction}-${id}`,
3430
left: direction === 'left' ? 0 : 'auto',
3531
right: direction === 'right' ? 0 : 'auto',
3632
top: direction === 'top' ? 0 : 'auto',
@@ -39,31 +35,47 @@ const fadeStyles = (direction: FadeDirection, id: string, horizontal: boolean, c
3935
visibility: direction === 'left' || direction === 'top' ? 'hidden' : 'visible',
4036
})
4137

42-
function updateScrollVariables(element: HTMLDivElement, id: string, style: HTMLStyleElement) {
38+
function setScrollTimelineAnimation(
39+
element: HTMLDivElement,
40+
fade: HTMLButtonElement,
41+
direction: FadeDirection,
42+
scrollDirection: ScrollDirection,
43+
) {
4344
const scrollHeight = element.scrollHeight - element.clientHeight
4445
const scrollWidth = element.scrollWidth - element.clientWidth
45-
const horizontalPercentage = 20 / (scrollWidth / element.clientWidth)
46-
const verticalPercentage = 20 / (scrollHeight / element.clientHeight)
46+
const horizontalPercentage = scrollWidth / element.clientWidth / 100
47+
const verticalPercentage = scrollHeight / element.clientHeight / 100
4748

48-
style.textContent = keyframes(id, scrollWidth < 1 ? 20 : horizontalPercentage, scrollHeight < 1 ? 20 : verticalPercentage)
49-
}
49+
const isHorizontal = direction === 'left' || direction === 'right'
5050

51-
const keyframes = (id: string, horizontalOffsetPercentage: number, verticalOffsetPercentage: number) => `@keyframes indicate-top-${id} {
52-
${verticalOffsetPercentage}% { opacity: 1; visibility: visible; }
53-
100% { opacity: 1; visibility: visible; }
54-
}
55-
@keyframes indicate-right-${id} {
56-
${100 - horizontalOffsetPercentage}% { opacity: 1; visibility: visible; }
57-
100% { opacity: 0; visibility: hidden; }
58-
}
59-
@keyframes indicate-bottom-${id} {
60-
${100 - verticalOffsetPercentage}% { opacity: 1; visibility: visible; }
61-
100% { opacity: 0; visibility: hidden; }
51+
if (isHorizontal && (scrollWidth < 1 || horizontalPercentage === Number.POSITIVE_INFINITY)) {
52+
return
53+
}
54+
55+
if (!isHorizontal && (scrollHeight < 1 || verticalPercentage === Number.POSITIVE_INFINITY)) {
56+
return
57+
}
58+
59+
const scrollTimeline = new ScrollTimeline({
60+
source: element,
61+
axis: scrollDirection === 'horizontal' ? 'inline' : 'block',
62+
})
63+
64+
const start = direction === 'left' || direction === 'top'
65+
fade.animate(
66+
{
67+
opacity: start ? [0, 0, 1] : [1, 1, 0],
68+
visibility: start ? ['hidden', 'visible', 'visible'] : ['visible', 'visible', 'hidden'],
69+
offset: start
70+
? [0, direction === 'top' ? verticalPercentage : horizontalPercentage, 1]
71+
: [0, direction === 'bottom' ? 1 - verticalPercentage : 1 - horizontalPercentage, 1],
72+
},
73+
{
74+
timeline: scrollTimeline,
75+
fill: 'both',
76+
},
77+
)
6278
}
63-
@keyframes indicate-left-${id} {
64-
${horizontalOffsetPercentage}% { opacity: 1; visibility: visible; }
65-
100% { opacity: 1; visibility: visible; }
66-
}`
6779

6880
const scrollByDirection = {
6981
top: (container: HTMLDivElement) => ({
@@ -139,26 +151,37 @@ function Fallback<T extends keyof React.JSX.IntrinsicElements = 'div'>({
139151

140152
function Fade({
141153
direction,
142-
id,
154+
scrollDirection,
143155
style,
144156
color,
145157
arrow,
158+
ref,
159+
element,
146160
}: {
147161
direction: FadeDirection
148-
id: string
162+
scrollDirection: ScrollDirection
149163
style?: React.CSSProperties
150164
color: string
151165
arrow: ArrowProps | boolean
166+
ref: React.RefObject<HTMLButtonElement | null>
167+
element: HTMLDivElement | null
152168
}) {
153169
const horizontal = direction === 'left' || direction === 'right'
154170
const arrowConfiguration = typeof arrow === 'object' ? arrow : defaultArrowProps
155171

172+
useEffect(() => {
173+
if (element && ref.current) {
174+
setScrollTimelineAnimation(element, ref.current, direction, scrollDirection)
175+
}
176+
}, [ref, direction, scrollDirection, element])
177+
156178
return (
157179
<button
180+
ref={ref}
158181
aria-label={`Scroll to ${direction}`}
159182
type="button"
160183
style={{
161-
...fadeStyles(direction, id, horizontal, color),
184+
...fadeStyles(direction, horizontal, color),
162185
...style,
163186
alignItems: getArrowPosition(arrowConfiguration),
164187
justifyContent: getArrowPosition(arrowConfiguration),
@@ -190,9 +213,11 @@ export function Scroll<T extends keyof React.JSX.IntrinsicElements = 'div'>({
190213
...props
191214
}: Props<T>) {
192215
const [hasOverflow, setHasOverflow] = useState(false)
193-
const [id] = useState(() => Math.random().toString(36).substring(2, 10))
194216
const scrollRef = useRef<HTMLDivElement>(null)
195-
const styleRef = useRef<HTMLStyleElement>(null)
217+
const fadeTopRef = useRef<HTMLButtonElement>(null)
218+
const fadeRightRef = useRef<HTMLButtonElement>(null)
219+
const fadeBottomRef = useRef<HTMLButtonElement>(null)
220+
const fadeLeftRef = useRef<HTMLButtonElement>(null)
196221

197222
useEffect(() => {
198223
const element = scrollRef.current
@@ -204,22 +229,12 @@ export function Scroll<T extends keyof React.JSX.IntrinsicElements = 'div'>({
204229

205230
checkOverflow(element, direction, hasOverflow, setHasOverflow)
206231

207-
if (styleRef.current) {
208-
updateScrollVariables(element, id, styleRef.current)
209-
}
210-
211232
const resizeObserver = new ResizeObserver(() => {
212233
checkOverflow(element, direction, hasOverflow, setHasOverflow)
213-
if (styleRef.current) {
214-
// updateScrollVariables(element, styleRef.current)
215-
}
216234
})
217235

218236
const observer = new MutationObserver(() => {
219237
checkOverflow(element, direction, hasOverflow, setHasOverflow)
220-
if (styleRef.current) {
221-
// updateScrollVariables(element, styleRef.current)
222-
}
223238
})
224239

225240
// Observe if more children are rendered.
@@ -237,7 +252,7 @@ export function Scroll<T extends keyof React.JSX.IntrinsicElements = 'div'>({
237252
resizeObserver.unobserve(element)
238253
}
239254
}
240-
}, [hasOverflow, direction, id])
255+
}, [hasOverflow, direction])
241256

242257
if (!supportsScrollTimeline) {
243258
return (
@@ -260,11 +275,50 @@ export function Scroll<T extends keyof React.JSX.IntrinsicElements = 'div'>({
260275
{children}
261276
{hasOverflow && (
262277
<>
263-
<style ref={styleRef}>{keyframes(id, 20, 20)}</style>
264-
{direction === 'vertical' && <Fade style={indicatorStyle} color={color} direction="top" id={id} arrow={arrow} />}
265-
{direction === 'horizontal' && <Fade style={indicatorStyle} color={color} direction="right" id={id} arrow={arrow} />}
266-
{direction === 'vertical' && <Fade style={indicatorStyle} color={color} direction="bottom" id={id} arrow={arrow} />}
267-
{direction === 'horizontal' && <Fade style={indicatorStyle} color={color} direction="left" id={id} arrow={arrow} />}
278+
{direction === 'vertical' && (
279+
<Fade
280+
ref={fadeTopRef}
281+
element={scrollRef.current}
282+
style={indicatorStyle}
283+
color={color}
284+
direction="top"
285+
scrollDirection={direction}
286+
arrow={arrow}
287+
/>
288+
)}
289+
{direction === 'horizontal' && (
290+
<Fade
291+
ref={fadeRightRef}
292+
element={scrollRef.current}
293+
style={indicatorStyle}
294+
color={color}
295+
direction="right"
296+
scrollDirection={direction}
297+
arrow={arrow}
298+
/>
299+
)}
300+
{direction === 'vertical' && (
301+
<Fade
302+
ref={fadeBottomRef}
303+
element={scrollRef.current}
304+
style={indicatorStyle}
305+
color={color}
306+
direction="bottom"
307+
scrollDirection={direction}
308+
arrow={arrow}
309+
/>
310+
)}
311+
{direction === 'horizontal' && (
312+
<Fade
313+
ref={fadeLeftRef}
314+
element={scrollRef.current}
315+
style={indicatorStyle}
316+
color={color}
317+
direction="left"
318+
scrollDirection={direction}
319+
arrow={arrow}
320+
/>
321+
)}
268322
</>
269323
)}
270324
</div>

0 commit comments

Comments
 (0)