@huin-core/react-scroll-area 1.0.1 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
package/dist/index.mjs ADDED
@@ -0,0 +1,835 @@
1
+ "use client";
2
+
3
+ // packages/react/scroll-area/src/ScrollArea.tsx
4
+ import * as React4 from "react";
5
+ import { Primitive as Primitive3 } from "@huin-core/react-primitive";
6
+ import { createContextScope } from "@huin-core/react-context";
7
+ import { useComposedRefs as useComposedRefs2 } from "@huin-core/react-compose-refs";
8
+ import { useDirection } from "@huin-core/react-direction";
9
+
10
+ // packages/react/scroll-area/src/ScrollAreaScrollbar.tsx
11
+ import React3 from "react";
12
+ import { useCallbackRef as useCallbackRef2 } from "@huin-core/react-use-callback-ref";
13
+ import { Primitive as Primitive2 } from "@huin-core/react-primitive";
14
+ import { Presence } from "@huin-core/react-presence";
15
+
16
+ // packages/react/scroll-area/src/useStateMachine.ts
17
+ import * as React from "react";
18
+ function useStateMachine(initialState, machine) {
19
+ return React.useReducer((state, event) => {
20
+ const nextState = machine[state][event];
21
+ return nextState ?? state;
22
+ }, initialState);
23
+ }
24
+
25
+ // packages/react/scroll-area/src/ScrollAreaScrollbar.tsx
26
+ import { composeEventHandlers } from "@huin-core/primitive";
27
+
28
+ // packages/react/scroll-area/src/ScrollAreaCorner.tsx
29
+ import React2 from "react";
30
+ import { Primitive } from "@huin-core/react-primitive";
31
+ import { useCallbackRef } from "@huin-core/react-use-callback-ref";
32
+ import { useLayoutEffect } from "@huin-core/react-use-layout-effect";
33
+ import { jsx } from "react/jsx-runtime";
34
+ var CORNER_NAME = "ScrollAreaCorner";
35
+ var ScrollAreaCorner = React2.forwardRef((props, forwardedRef) => {
36
+ const context = useScrollAreaContext(CORNER_NAME, props.__scopeScrollArea);
37
+ const hasBothScrollbarsVisible = Boolean(
38
+ context.scrollbarX && context.scrollbarY
39
+ );
40
+ const hasCorner = context.type !== "scroll" && hasBothScrollbarsVisible;
41
+ return hasCorner ? /* @__PURE__ */ jsx(ScrollAreaCornerImpl, { ...props, ref: forwardedRef }) : null;
42
+ });
43
+ ScrollAreaCorner.displayName = CORNER_NAME;
44
+ var ScrollAreaCornerImpl = React2.forwardRef((props, forwardedRef) => {
45
+ const { __scopeScrollArea, ...cornerProps } = props;
46
+ const context = useScrollAreaContext(CORNER_NAME, __scopeScrollArea);
47
+ const [width, setWidth] = React2.useState(0);
48
+ const [height, setHeight] = React2.useState(0);
49
+ const hasSize = Boolean(width && height);
50
+ useResizeObserver(context.scrollbarX, () => {
51
+ const height2 = context.scrollbarX?.offsetHeight || 0;
52
+ context.onCornerHeightChange(height2);
53
+ setHeight(height2);
54
+ });
55
+ useResizeObserver(context.scrollbarY, () => {
56
+ const width2 = context.scrollbarY?.offsetWidth || 0;
57
+ context.onCornerWidthChange(width2);
58
+ setWidth(width2);
59
+ });
60
+ return hasSize ? /* @__PURE__ */ jsx(
61
+ Primitive.div,
62
+ {
63
+ ...cornerProps,
64
+ ref: forwardedRef,
65
+ style: {
66
+ width,
67
+ height,
68
+ position: "absolute",
69
+ right: context.dir === "ltr" ? 0 : void 0,
70
+ left: context.dir === "rtl" ? 0 : void 0,
71
+ bottom: 0,
72
+ ...props.style
73
+ }
74
+ }
75
+ ) : null;
76
+ });
77
+ function useResizeObserver(element, onResize) {
78
+ const handleResize = useCallbackRef(onResize);
79
+ useLayoutEffect(() => {
80
+ let rAF = 0;
81
+ if (element) {
82
+ const resizeObserver = new ResizeObserver(() => {
83
+ cancelAnimationFrame(rAF);
84
+ rAF = window.requestAnimationFrame(handleResize);
85
+ });
86
+ resizeObserver.observe(element);
87
+ return () => {
88
+ window.cancelAnimationFrame(rAF);
89
+ resizeObserver.unobserve(element);
90
+ };
91
+ }
92
+ }, [element, handleResize]);
93
+ }
94
+
95
+ // packages/react/scroll-area/src/ScrollAreaScrollbar.tsx
96
+ import { useComposedRefs } from "@huin-core/react-compose-refs";
97
+ import { clamp } from "@huin-core/number";
98
+ import { jsx as jsx2 } from "react/jsx-runtime";
99
+ var SCROLLBAR_NAME = "ScrollAreaScrollbar";
100
+ var ScrollAreaScrollbar = React3.forwardRef(
101
+ (props, forwardedRef) => {
102
+ const { forceMount, ...scrollbarProps } = props;
103
+ const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
104
+ const { onScrollbarXEnabledChange, onScrollbarYEnabledChange } = context;
105
+ const isHorizontal = props.orientation === "horizontal";
106
+ React3.useEffect(() => {
107
+ isHorizontal ? onScrollbarXEnabledChange(true) : onScrollbarYEnabledChange(true);
108
+ return () => {
109
+ isHorizontal ? onScrollbarXEnabledChange(false) : onScrollbarYEnabledChange(false);
110
+ };
111
+ }, [isHorizontal, onScrollbarXEnabledChange, onScrollbarYEnabledChange]);
112
+ return context.type === "hover" ? /* @__PURE__ */ jsx2(ScrollAreaScrollbarHover, { ...scrollbarProps, ref: forwardedRef, forceMount }) : context.type === "scroll" ? /* @__PURE__ */ jsx2(ScrollAreaScrollbarScroll, { ...scrollbarProps, ref: forwardedRef, forceMount }) : context.type === "auto" ? /* @__PURE__ */ jsx2(ScrollAreaScrollbarAuto, { ...scrollbarProps, ref: forwardedRef, forceMount }) : context.type === "always" ? /* @__PURE__ */ jsx2(ScrollAreaScrollbarVisible, { ...scrollbarProps, ref: forwardedRef }) : null;
113
+ }
114
+ );
115
+ ScrollAreaScrollbar.displayName = SCROLLBAR_NAME;
116
+ var ScrollAreaScrollbarHover = React3.forwardRef((props, forwardedRef) => {
117
+ const { forceMount, ...scrollbarProps } = props;
118
+ const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
119
+ const [visible, setVisible] = React3.useState(false);
120
+ React3.useEffect(() => {
121
+ const scrollArea = context.scrollArea;
122
+ let hideTimer = 0;
123
+ if (scrollArea) {
124
+ const handlePointerEnter = () => {
125
+ window.clearTimeout(hideTimer);
126
+ setVisible(true);
127
+ };
128
+ const handlePointerLeave = () => {
129
+ hideTimer = window.setTimeout(
130
+ () => setVisible(false),
131
+ context.scrollHideDelay
132
+ );
133
+ };
134
+ scrollArea.addEventListener("pointerenter", handlePointerEnter);
135
+ scrollArea.addEventListener("pointerleave", handlePointerLeave);
136
+ return () => {
137
+ window.clearTimeout(hideTimer);
138
+ scrollArea.removeEventListener("pointerenter", handlePointerEnter);
139
+ scrollArea.removeEventListener("pointerleave", handlePointerLeave);
140
+ };
141
+ }
142
+ }, [context.scrollArea, context.scrollHideDelay]);
143
+ return /* @__PURE__ */ jsx2(Presence, { present: forceMount || visible, children: /* @__PURE__ */ jsx2(
144
+ ScrollAreaScrollbarAuto,
145
+ {
146
+ "data-state": visible ? "visible" : "hidden",
147
+ ...scrollbarProps,
148
+ ref: forwardedRef
149
+ }
150
+ ) });
151
+ });
152
+ var ScrollAreaScrollbarScroll = React3.forwardRef((props, forwardedRef) => {
153
+ const { forceMount, ...scrollbarProps } = props;
154
+ const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
155
+ const isHorizontal = props.orientation === "horizontal";
156
+ const debounceScrollEnd = useDebounceCallback(() => send("SCROLL_END"), 100);
157
+ const [state, send] = useStateMachine("hidden", {
158
+ hidden: {
159
+ SCROLL: "scrolling"
160
+ },
161
+ scrolling: {
162
+ SCROLL_END: "idle",
163
+ POINTER_ENTER: "interacting"
164
+ },
165
+ interacting: {
166
+ SCROLL: "interacting",
167
+ POINTER_LEAVE: "idle"
168
+ },
169
+ idle: {
170
+ HIDE: "hidden",
171
+ SCROLL: "scrolling",
172
+ POINTER_ENTER: "interacting"
173
+ }
174
+ });
175
+ React3.useEffect(() => {
176
+ if (state === "idle") {
177
+ const hideTimer = window.setTimeout(
178
+ () => send("HIDE"),
179
+ context.scrollHideDelay
180
+ );
181
+ return () => window.clearTimeout(hideTimer);
182
+ }
183
+ }, [state, context.scrollHideDelay, send]);
184
+ React3.useEffect(() => {
185
+ const viewport = context.viewport;
186
+ const scrollDirection = isHorizontal ? "scrollLeft" : "scrollTop";
187
+ if (viewport) {
188
+ let prevScrollPos = viewport[scrollDirection];
189
+ const handleScroll = () => {
190
+ const scrollPos = viewport[scrollDirection];
191
+ const hasScrollInDirectionChanged = prevScrollPos !== scrollPos;
192
+ if (hasScrollInDirectionChanged) {
193
+ send("SCROLL");
194
+ debounceScrollEnd();
195
+ }
196
+ prevScrollPos = scrollPos;
197
+ };
198
+ viewport.addEventListener("scroll", handleScroll);
199
+ return () => viewport.removeEventListener("scroll", handleScroll);
200
+ }
201
+ }, [context.viewport, isHorizontal, send, debounceScrollEnd]);
202
+ return /* @__PURE__ */ jsx2(Presence, { present: forceMount || state !== "hidden", children: /* @__PURE__ */ jsx2(
203
+ ScrollAreaScrollbarVisible,
204
+ {
205
+ "data-state": state === "hidden" ? "hidden" : "visible",
206
+ ...scrollbarProps,
207
+ ref: forwardedRef,
208
+ onPointerEnter: composeEventHandlers(
209
+ props.onPointerEnter,
210
+ () => send("POINTER_ENTER")
211
+ ),
212
+ onPointerLeave: composeEventHandlers(
213
+ props.onPointerLeave,
214
+ () => send("POINTER_LEAVE")
215
+ )
216
+ }
217
+ ) });
218
+ });
219
+ var ScrollAreaScrollbarAuto = React3.forwardRef((props, forwardedRef) => {
220
+ const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
221
+ const { forceMount, ...scrollbarProps } = props;
222
+ const [visible, setVisible] = React3.useState(false);
223
+ const isHorizontal = props.orientation === "horizontal";
224
+ const handleResize = useDebounceCallback(() => {
225
+ if (context.viewport) {
226
+ const isOverflowX = context.viewport.offsetWidth < context.viewport.scrollWidth;
227
+ const isOverflowY = context.viewport.offsetHeight < context.viewport.scrollHeight;
228
+ setVisible(isHorizontal ? isOverflowX : isOverflowY);
229
+ }
230
+ }, 10);
231
+ useResizeObserver(context.viewport, handleResize);
232
+ useResizeObserver(context.content, handleResize);
233
+ return /* @__PURE__ */ jsx2(Presence, { present: forceMount || visible, children: /* @__PURE__ */ jsx2(
234
+ ScrollAreaScrollbarVisible,
235
+ {
236
+ "data-state": visible ? "visible" : "hidden",
237
+ ...scrollbarProps,
238
+ ref: forwardedRef
239
+ }
240
+ ) });
241
+ });
242
+ var ScrollAreaScrollbarVisible = React3.forwardRef((props, forwardedRef) => {
243
+ const { orientation = "vertical", ...scrollbarProps } = props;
244
+ const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
245
+ const thumbRef = React3.useRef(null);
246
+ const pointerOffsetRef = React3.useRef(0);
247
+ const [sizes, setSizes] = React3.useState({
248
+ content: 0,
249
+ viewport: 0,
250
+ scrollbar: { size: 0, paddingStart: 0, paddingEnd: 0 }
251
+ });
252
+ const thumbRatio = getThumbRatio(sizes.viewport, sizes.content);
253
+ const commonProps = {
254
+ ...scrollbarProps,
255
+ sizes,
256
+ onSizesChange: setSizes,
257
+ hasThumb: Boolean(thumbRatio > 0 && thumbRatio < 1),
258
+ onThumbChange: (thumb) => thumbRef.current = thumb,
259
+ onThumbPointerUp: () => pointerOffsetRef.current = 0,
260
+ onThumbPointerDown: (pointerPos) => pointerOffsetRef.current = pointerPos
261
+ };
262
+ function getScrollPosition(pointerPos, dir) {
263
+ return getScrollPositionFromPointer(
264
+ pointerPos,
265
+ pointerOffsetRef.current,
266
+ sizes,
267
+ dir
268
+ );
269
+ }
270
+ if (orientation === "horizontal") {
271
+ return /* @__PURE__ */ jsx2(
272
+ ScrollAreaScrollbarX,
273
+ {
274
+ ...commonProps,
275
+ ref: forwardedRef,
276
+ onThumbPositionChange: () => {
277
+ if (context.viewport && thumbRef.current) {
278
+ const scrollPos = context.viewport.scrollLeft;
279
+ const offset = getThumbOffsetFromScroll(
280
+ scrollPos,
281
+ sizes,
282
+ context.dir
283
+ );
284
+ thumbRef.current.style.transform = `translate3d(${offset}px, 0, 0)`;
285
+ }
286
+ },
287
+ onWheelScroll: (scrollPos) => {
288
+ if (context.viewport) context.viewport.scrollLeft = scrollPos;
289
+ },
290
+ onDragScroll: (pointerPos) => {
291
+ if (context.viewport) {
292
+ context.viewport.scrollLeft = getScrollPosition(
293
+ pointerPos,
294
+ context.dir
295
+ );
296
+ }
297
+ }
298
+ }
299
+ );
300
+ }
301
+ if (orientation === "vertical") {
302
+ return /* @__PURE__ */ jsx2(
303
+ ScrollAreaScrollbarY,
304
+ {
305
+ ...commonProps,
306
+ ref: forwardedRef,
307
+ onThumbPositionChange: () => {
308
+ if (context.viewport && thumbRef.current) {
309
+ const scrollPos = context.viewport.scrollTop;
310
+ const offset = getThumbOffsetFromScroll(scrollPos, sizes);
311
+ thumbRef.current.style.transform = `translate3d(0, ${offset}px, 0)`;
312
+ }
313
+ },
314
+ onWheelScroll: (scrollPos) => {
315
+ if (context.viewport) context.viewport.scrollTop = scrollPos;
316
+ },
317
+ onDragScroll: (pointerPos) => {
318
+ if (context.viewport)
319
+ context.viewport.scrollTop = getScrollPosition(pointerPos);
320
+ }
321
+ }
322
+ );
323
+ }
324
+ return null;
325
+ });
326
+ var ScrollAreaScrollbarX = React3.forwardRef((props, forwardedRef) => {
327
+ const { sizes, onSizesChange, ...scrollbarProps } = props;
328
+ const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
329
+ const [computedStyle, setComputedStyle] = React3.useState();
330
+ const ref = React3.useRef(null);
331
+ const composeRefs = useComposedRefs(
332
+ forwardedRef,
333
+ ref,
334
+ context.onScrollbarXChange
335
+ );
336
+ React3.useEffect(() => {
337
+ if (ref.current) setComputedStyle(getComputedStyle(ref.current));
338
+ }, [ref]);
339
+ return /* @__PURE__ */ jsx2(
340
+ ScrollAreaScrollbarImpl,
341
+ {
342
+ "data-orientation": "horizontal",
343
+ ...scrollbarProps,
344
+ ref: composeRefs,
345
+ sizes,
346
+ style: {
347
+ bottom: 0,
348
+ left: context.dir === "rtl" ? "var(--huin-core-scroll-area-corner-width)" : 0,
349
+ right: context.dir === "ltr" ? "var(--huin-core-scroll-area-corner-width)" : 0,
350
+ ["--huin-core-scroll-area-thumb-width"]: getThumbSize(sizes) + "px",
351
+ ...props.style
352
+ },
353
+ onThumbPointerDown: (pointerPos) => props.onThumbPointerDown(pointerPos.x),
354
+ onDragScroll: (pointerPos) => props.onDragScroll(pointerPos.x),
355
+ onWheelScroll: (event, maxScrollPos) => {
356
+ if (context.viewport) {
357
+ const scrollPos = context.viewport.scrollLeft + event.deltaX;
358
+ props.onWheelScroll(scrollPos);
359
+ if (isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos)) {
360
+ event.preventDefault();
361
+ }
362
+ }
363
+ },
364
+ onResize: () => {
365
+ if (ref.current && context.viewport && computedStyle) {
366
+ onSizesChange({
367
+ content: context.viewport.scrollWidth,
368
+ viewport: context.viewport.offsetWidth,
369
+ scrollbar: {
370
+ size: ref.current.clientWidth,
371
+ paddingStart: toInt(computedStyle.paddingLeft),
372
+ paddingEnd: toInt(computedStyle.paddingRight)
373
+ }
374
+ });
375
+ }
376
+ }
377
+ }
378
+ );
379
+ });
380
+ var ScrollAreaScrollbarY = React3.forwardRef((props, forwardedRef) => {
381
+ const { sizes, onSizesChange, ...scrollbarProps } = props;
382
+ const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
383
+ const [computedStyle, setComputedStyle] = React3.useState();
384
+ const ref = React3.useRef(null);
385
+ const composeRefs = useComposedRefs(
386
+ forwardedRef,
387
+ ref,
388
+ context.onScrollbarYChange
389
+ );
390
+ React3.useEffect(() => {
391
+ if (ref.current) setComputedStyle(getComputedStyle(ref.current));
392
+ }, [ref]);
393
+ return /* @__PURE__ */ jsx2(
394
+ ScrollAreaScrollbarImpl,
395
+ {
396
+ "data-orientation": "vertical",
397
+ ...scrollbarProps,
398
+ ref: composeRefs,
399
+ sizes,
400
+ style: {
401
+ top: 0,
402
+ right: context.dir === "ltr" ? 0 : void 0,
403
+ left: context.dir === "rtl" ? 0 : void 0,
404
+ bottom: "var(--huin-core-scroll-area-corner-height)",
405
+ ["--huin-core-scroll-area-thumb-height"]: getThumbSize(sizes) + "px",
406
+ ...props.style
407
+ },
408
+ onThumbPointerDown: (pointerPos) => props.onThumbPointerDown(pointerPos.y),
409
+ onDragScroll: (pointerPos) => props.onDragScroll(pointerPos.y),
410
+ onWheelScroll: (event, maxScrollPos) => {
411
+ if (context.viewport) {
412
+ const scrollPos = context.viewport.scrollTop + event.deltaY;
413
+ props.onWheelScroll(scrollPos);
414
+ if (isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos)) {
415
+ event.preventDefault();
416
+ }
417
+ }
418
+ },
419
+ onResize: () => {
420
+ if (ref.current && context.viewport && computedStyle) {
421
+ onSizesChange({
422
+ content: context.viewport.scrollHeight,
423
+ viewport: context.viewport.offsetHeight,
424
+ scrollbar: {
425
+ size: ref.current.clientHeight,
426
+ paddingStart: toInt(computedStyle.paddingTop),
427
+ paddingEnd: toInt(computedStyle.paddingBottom)
428
+ }
429
+ });
430
+ }
431
+ }
432
+ }
433
+ );
434
+ });
435
+ var ScrollAreaScrollbarImpl = React3.forwardRef((props, forwardedRef) => {
436
+ const {
437
+ __scopeScrollArea,
438
+ sizes,
439
+ hasThumb,
440
+ onThumbChange,
441
+ onThumbPointerUp,
442
+ onThumbPointerDown,
443
+ onThumbPositionChange,
444
+ onDragScroll,
445
+ onWheelScroll,
446
+ onResize,
447
+ ...scrollbarProps
448
+ } = props;
449
+ const context = useScrollAreaContext(SCROLLBAR_NAME, __scopeScrollArea);
450
+ const [scrollbar, setScrollbar] = React3.useState(null);
451
+ const composeRefs = useComposedRefs(
452
+ forwardedRef,
453
+ (node) => setScrollbar(node)
454
+ );
455
+ const rectRef = React3.useRef(null);
456
+ const prevWebkitUserSelectRef = React3.useRef("");
457
+ const viewport = context.viewport;
458
+ const maxScrollPos = sizes.content - sizes.viewport;
459
+ const handleWheelScroll = useCallbackRef2(onWheelScroll);
460
+ const handleThumbPositionChange = useCallbackRef2(onThumbPositionChange);
461
+ const handleResize = useDebounceCallback(onResize, 10);
462
+ function handleDragScroll(event) {
463
+ if (rectRef.current) {
464
+ const x = event.clientX - rectRef.current.left;
465
+ const y = event.clientY - rectRef.current.top;
466
+ onDragScroll({ x, y });
467
+ }
468
+ }
469
+ React3.useEffect(() => {
470
+ const handleWheel = (event) => {
471
+ const element = event.target;
472
+ const isScrollbarWheel = scrollbar?.contains(element);
473
+ if (isScrollbarWheel) handleWheelScroll(event, maxScrollPos);
474
+ };
475
+ document.addEventListener("wheel", handleWheel, { passive: false });
476
+ return () => document.removeEventListener("wheel", handleWheel, {
477
+ passive: false
478
+ });
479
+ }, [viewport, scrollbar, maxScrollPos, handleWheelScroll]);
480
+ React3.useEffect(handleThumbPositionChange, [
481
+ sizes,
482
+ handleThumbPositionChange
483
+ ]);
484
+ useResizeObserver(scrollbar, handleResize);
485
+ useResizeObserver(context.content, handleResize);
486
+ return /* @__PURE__ */ jsx2(
487
+ ScrollbarProvider,
488
+ {
489
+ scope: __scopeScrollArea,
490
+ scrollbar,
491
+ hasThumb,
492
+ onThumbChange: useCallbackRef2(onThumbChange),
493
+ onThumbPointerUp: useCallbackRef2(onThumbPointerUp),
494
+ onThumbPositionChange: handleThumbPositionChange,
495
+ onThumbPointerDown: useCallbackRef2(onThumbPointerDown),
496
+ children: /* @__PURE__ */ jsx2(
497
+ Primitive2.div,
498
+ {
499
+ ...scrollbarProps,
500
+ ref: composeRefs,
501
+ style: { position: "absolute", ...scrollbarProps.style },
502
+ onPointerDown: composeEventHandlers(props.onPointerDown, (event) => {
503
+ const mainPointer = 0;
504
+ if (event.button === mainPointer) {
505
+ const element = event.target;
506
+ element.setPointerCapture(event.pointerId);
507
+ rectRef.current = scrollbar.getBoundingClientRect();
508
+ prevWebkitUserSelectRef.current = document.body.style.webkitUserSelect;
509
+ document.body.style.webkitUserSelect = "none";
510
+ if (context.viewport)
511
+ context.viewport.style.scrollBehavior = "auto";
512
+ handleDragScroll(event);
513
+ }
514
+ }),
515
+ onPointerMove: composeEventHandlers(
516
+ props.onPointerMove,
517
+ handleDragScroll
518
+ ),
519
+ onPointerUp: composeEventHandlers(props.onPointerUp, (event) => {
520
+ const element = event.target;
521
+ if (element.hasPointerCapture(event.pointerId)) {
522
+ element.releasePointerCapture(event.pointerId);
523
+ }
524
+ document.body.style.webkitUserSelect = prevWebkitUserSelectRef.current;
525
+ if (context.viewport) context.viewport.style.scrollBehavior = "";
526
+ rectRef.current = null;
527
+ })
528
+ }
529
+ )
530
+ }
531
+ );
532
+ });
533
+ function useDebounceCallback(callback, delay) {
534
+ const handleCallback = useCallbackRef2(callback);
535
+ const debounceTimerRef = React3.useRef(0);
536
+ React3.useEffect(
537
+ () => () => window.clearTimeout(debounceTimerRef.current),
538
+ []
539
+ );
540
+ return React3.useCallback(() => {
541
+ window.clearTimeout(debounceTimerRef.current);
542
+ debounceTimerRef.current = window.setTimeout(handleCallback, delay);
543
+ }, [handleCallback, delay]);
544
+ }
545
+ function toInt(value) {
546
+ return value ? parseInt(value, 10) : 0;
547
+ }
548
+ function getThumbRatio(viewportSize, contentSize) {
549
+ const ratio = viewportSize / contentSize;
550
+ return isNaN(ratio) ? 0 : ratio;
551
+ }
552
+ function getThumbSize(sizes) {
553
+ const ratio = getThumbRatio(sizes.viewport, sizes.content);
554
+ const scrollbarPadding = sizes.scrollbar.paddingStart + sizes.scrollbar.paddingEnd;
555
+ const thumbSize = (sizes.scrollbar.size - scrollbarPadding) * ratio;
556
+ return Math.max(thumbSize, 18);
557
+ }
558
+ function getScrollPositionFromPointer(pointerPos, pointerOffset, sizes, dir = "ltr") {
559
+ const thumbSizePx = getThumbSize(sizes);
560
+ const thumbCenter = thumbSizePx / 2;
561
+ const offset = pointerOffset || thumbCenter;
562
+ const thumbOffsetFromEnd = thumbSizePx - offset;
563
+ const minPointerPos = sizes.scrollbar.paddingStart + offset;
564
+ const maxPointerPos = sizes.scrollbar.size - sizes.scrollbar.paddingEnd - thumbOffsetFromEnd;
565
+ const maxScrollPos = sizes.content - sizes.viewport;
566
+ const scrollRange = dir === "ltr" ? [0, maxScrollPos] : [maxScrollPos * -1, 0];
567
+ const interpolate = linearScale(
568
+ [minPointerPos, maxPointerPos],
569
+ scrollRange
570
+ );
571
+ return interpolate(pointerPos);
572
+ }
573
+ function getThumbOffsetFromScroll(scrollPos, sizes, dir = "ltr") {
574
+ const thumbSizePx = getThumbSize(sizes);
575
+ const scrollbarPadding = sizes.scrollbar.paddingStart + sizes.scrollbar.paddingEnd;
576
+ const scrollbar = sizes.scrollbar.size - scrollbarPadding;
577
+ const maxScrollPos = sizes.content - sizes.viewport;
578
+ const maxThumbPos = scrollbar - thumbSizePx;
579
+ const scrollClampRange = dir === "ltr" ? [0, maxScrollPos] : [maxScrollPos * -1, 0];
580
+ const scrollWithoutMomentum = clamp(
581
+ scrollPos,
582
+ scrollClampRange
583
+ );
584
+ const interpolate = linearScale([0, maxScrollPos], [0, maxThumbPos]);
585
+ return interpolate(scrollWithoutMomentum);
586
+ }
587
+ function linearScale(input, output) {
588
+ return (value) => {
589
+ if (input[0] === input[1] || output[0] === output[1]) return output[0];
590
+ const ratio = (output[1] - output[0]) / (input[1] - input[0]);
591
+ return output[0] + ratio * (value - input[0]);
592
+ };
593
+ }
594
+ function isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos) {
595
+ return scrollPos > 0 && scrollPos < maxScrollPos;
596
+ }
597
+
598
+ // packages/react/scroll-area/src/ScrollArea.tsx
599
+ import { jsx as jsx3 } from "react/jsx-runtime";
600
+ var SCROLL_AREA_NAME = "ScrollArea";
601
+ var [createScrollAreaContext, createScrollAreaScope] = createContextScope(SCROLL_AREA_NAME);
602
+ var [ScrollbarProvider, useScrollbarContext] = createScrollAreaContext(SCROLLBAR_NAME);
603
+ var [ScrollAreaProvider, useScrollAreaContext] = createScrollAreaContext(SCROLL_AREA_NAME);
604
+ var ScrollArea = React4.forwardRef(
605
+ (props, forwardedRef) => {
606
+ const {
607
+ __scopeScrollArea,
608
+ type = "hover",
609
+ dir,
610
+ scrollHideDelay = 600,
611
+ ...scrollAreaProps
612
+ } = props;
613
+ const [scrollArea, setScrollArea] = React4.useState(null);
614
+ const [viewport, setViewport] = React4.useState(null);
615
+ const [content, setContent] = React4.useState(null);
616
+ const [scrollbarX, setScrollbarX] = React4.useState(null);
617
+ const [scrollbarY, setScrollbarY] = React4.useState(null);
618
+ const [cornerWidth, setCornerWidth] = React4.useState(0);
619
+ const [cornerHeight, setCornerHeight] = React4.useState(0);
620
+ const [scrollbarXEnabled, setScrollbarXEnabled] = React4.useState(false);
621
+ const [scrollbarYEnabled, setScrollbarYEnabled] = React4.useState(false);
622
+ const composedRefs = useComposedRefs2(
623
+ forwardedRef,
624
+ (node) => setScrollArea(node)
625
+ );
626
+ const direction = useDirection(dir);
627
+ return /* @__PURE__ */ jsx3(
628
+ ScrollAreaProvider,
629
+ {
630
+ scope: __scopeScrollArea,
631
+ type,
632
+ dir: direction,
633
+ scrollHideDelay,
634
+ scrollArea,
635
+ viewport,
636
+ onViewportChange: setViewport,
637
+ content,
638
+ onContentChange: setContent,
639
+ scrollbarX,
640
+ onScrollbarXChange: setScrollbarX,
641
+ scrollbarXEnabled,
642
+ onScrollbarXEnabledChange: setScrollbarXEnabled,
643
+ scrollbarY,
644
+ onScrollbarYChange: setScrollbarY,
645
+ scrollbarYEnabled,
646
+ onScrollbarYEnabledChange: setScrollbarYEnabled,
647
+ onCornerWidthChange: setCornerWidth,
648
+ onCornerHeightChange: setCornerHeight,
649
+ children: /* @__PURE__ */ jsx3(
650
+ Primitive3.div,
651
+ {
652
+ dir: direction,
653
+ ...scrollAreaProps,
654
+ ref: composedRefs,
655
+ style: {
656
+ position: "relative",
657
+ // Pass corner sizes as CSS vars to reduce re-renders of context consumers
658
+ ["--huin-core-scroll-area-corner-width"]: cornerWidth + "px",
659
+ ["--huin-core-scroll-area-corner-height"]: cornerHeight + "px",
660
+ ...props.style
661
+ }
662
+ }
663
+ )
664
+ }
665
+ );
666
+ }
667
+ );
668
+ ScrollArea.displayName = SCROLL_AREA_NAME;
669
+ var Root = ScrollArea;
670
+
671
+ // packages/react/scroll-area/src/ScrollAreaViewport.tsx
672
+ import React5 from "react";
673
+ import { Primitive as Primitive4 } from "@huin-core/react-primitive";
674
+ import { useComposedRefs as useComposedRefs3 } from "@huin-core/react-compose-refs";
675
+ import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
676
+ var VIEWPORT_NAME = "ScrollAreaViewport";
677
+ var ScrollAreaViewport = React5.forwardRef((props, forwardedRef) => {
678
+ const { __scopeScrollArea, children, nonce, ...viewportProps } = props;
679
+ const context = useScrollAreaContext(VIEWPORT_NAME, __scopeScrollArea);
680
+ const ref = React5.useRef(null);
681
+ const composedRefs = useComposedRefs3(
682
+ forwardedRef,
683
+ ref,
684
+ context.onViewportChange
685
+ );
686
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
687
+ /* @__PURE__ */ jsx4(
688
+ "style",
689
+ {
690
+ dangerouslySetInnerHTML: {
691
+ __html: `[data-huin-core-scroll-area-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-huin-core-scroll-area-viewport]::-webkit-scrollbar{display:none}`
692
+ },
693
+ nonce
694
+ }
695
+ ),
696
+ /* @__PURE__ */ jsx4(
697
+ Primitive4.div,
698
+ {
699
+ "data-huin-core-scroll-area-viewport": "",
700
+ ...viewportProps,
701
+ ref: composedRefs,
702
+ style: {
703
+ /**
704
+ * We don't support `visible` because the intention is to have at least one scrollbar
705
+ * if this component is used and `visible` will behave like `auto` in that case
706
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/overflow#description
707
+ *
708
+ * We don't handle `auto` because the intention is for the native implementation
709
+ * to be hidden if using this component. We just want to ensure the node is scrollable
710
+ * so could have used either `scroll` or `auto` here. We picked `scroll` to prevent
711
+ * the browser from having to work out whether to render native scrollbars or not,
712
+ * we tell it to with the intention of hiding them in CSS.
713
+ */
714
+ overflowX: context.scrollbarXEnabled ? "scroll" : "hidden",
715
+ overflowY: context.scrollbarYEnabled ? "scroll" : "hidden",
716
+ ...props.style
717
+ },
718
+ children: /* @__PURE__ */ jsx4(
719
+ Primitive4.div,
720
+ {
721
+ ref: context.onContentChange,
722
+ style: { minWidth: "100%", display: "table" },
723
+ children
724
+ }
725
+ )
726
+ }
727
+ )
728
+ ] });
729
+ });
730
+ ScrollAreaViewport.displayName = VIEWPORT_NAME;
731
+
732
+ // packages/react/scroll-area/src/ScrollAreaThumb.tsx
733
+ import React6 from "react";
734
+ import { Presence as Presence2 } from "@huin-core/react-presence";
735
+ import { useComposedRefs as useComposedRefs4 } from "@huin-core/react-compose-refs";
736
+ import { Primitive as Primitive5 } from "@huin-core/react-primitive";
737
+ import { composeEventHandlers as composeEventHandlers2 } from "@huin-core/primitive";
738
+ import { jsx as jsx5 } from "react/jsx-runtime";
739
+ var THUMB_NAME = "ScrollAreaThumb";
740
+ var ScrollAreaThumb = React6.forwardRef((props, forwardedRef) => {
741
+ const { forceMount, ...thumbProps } = props;
742
+ const scrollbarContext = useScrollbarContext(
743
+ THUMB_NAME,
744
+ props.__scopeScrollArea
745
+ );
746
+ return /* @__PURE__ */ jsx5(Presence2, { present: forceMount || scrollbarContext.hasThumb, children: /* @__PURE__ */ jsx5(ScrollAreaThumbImpl, { ref: forwardedRef, ...thumbProps }) });
747
+ });
748
+ var ScrollAreaThumbImpl = React6.forwardRef((props, forwardedRef) => {
749
+ const { __scopeScrollArea, style, ...thumbProps } = props;
750
+ const scrollAreaContext = useScrollAreaContext(THUMB_NAME, __scopeScrollArea);
751
+ const scrollbarContext = useScrollbarContext(THUMB_NAME, __scopeScrollArea);
752
+ const { onThumbPositionChange } = scrollbarContext;
753
+ const composedRef = useComposedRefs4(
754
+ forwardedRef,
755
+ (node) => scrollbarContext.onThumbChange(node)
756
+ );
757
+ const removeUnlinkedScrollListenerRef = React6.useRef();
758
+ const debounceScrollEnd = useDebounceCallback(() => {
759
+ if (removeUnlinkedScrollListenerRef.current) {
760
+ removeUnlinkedScrollListenerRef.current();
761
+ removeUnlinkedScrollListenerRef.current = void 0;
762
+ }
763
+ }, 100);
764
+ React6.useEffect(() => {
765
+ const viewport = scrollAreaContext.viewport;
766
+ if (viewport) {
767
+ const handleScroll = () => {
768
+ debounceScrollEnd();
769
+ if (!removeUnlinkedScrollListenerRef.current) {
770
+ const listener = addUnlinkedScrollListener(
771
+ viewport,
772
+ onThumbPositionChange
773
+ );
774
+ removeUnlinkedScrollListenerRef.current = listener;
775
+ onThumbPositionChange();
776
+ }
777
+ };
778
+ onThumbPositionChange();
779
+ viewport.addEventListener("scroll", handleScroll);
780
+ return () => viewport.removeEventListener("scroll", handleScroll);
781
+ }
782
+ }, [scrollAreaContext.viewport, debounceScrollEnd, onThumbPositionChange]);
783
+ return /* @__PURE__ */ jsx5(
784
+ Primitive5.div,
785
+ {
786
+ "data-state": scrollbarContext.hasThumb ? "visible" : "hidden",
787
+ ...thumbProps,
788
+ ref: composedRef,
789
+ style: {
790
+ width: "var(--huin-core-scroll-area-thumb-width)",
791
+ height: "var(--huin-core-scroll-area-thumb-height)",
792
+ ...style
793
+ },
794
+ onPointerDownCapture: composeEventHandlers2(
795
+ props.onPointerDownCapture,
796
+ (event) => {
797
+ const thumb = event.target;
798
+ const thumbRect = thumb.getBoundingClientRect();
799
+ const x = event.clientX - thumbRect.left;
800
+ const y = event.clientY - thumbRect.top;
801
+ scrollbarContext.onThumbPointerDown({ x, y });
802
+ }
803
+ ),
804
+ onPointerUp: composeEventHandlers2(
805
+ props.onPointerUp,
806
+ scrollbarContext.onThumbPointerUp
807
+ )
808
+ }
809
+ );
810
+ });
811
+ ScrollAreaThumb.displayName = THUMB_NAME;
812
+ var addUnlinkedScrollListener = (node, handler = () => {
813
+ }) => {
814
+ let prevPosition = { left: node.scrollLeft, top: node.scrollTop };
815
+ let rAF = 0;
816
+ (function loop() {
817
+ const position = { left: node.scrollLeft, top: node.scrollTop };
818
+ const isHorizontalScroll = prevPosition.left !== position.left;
819
+ const isVerticalScroll = prevPosition.top !== position.top;
820
+ if (isHorizontalScroll || isVerticalScroll) handler();
821
+ prevPosition = position;
822
+ rAF = window.requestAnimationFrame(loop);
823
+ })();
824
+ return () => window.cancelAnimationFrame(rAF);
825
+ };
826
+ export {
827
+ Root,
828
+ ScrollArea,
829
+ ScrollAreaCorner,
830
+ ScrollAreaScrollbar,
831
+ ScrollAreaThumb,
832
+ ScrollAreaViewport,
833
+ createScrollAreaScope
834
+ };
835
+ //# sourceMappingURL=index.mjs.map