@botiverse/raft-ui 0.59.2 → 0.60.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/dist/components/composer/composer-attachment.recipe.mjs +2 -3
- package/dist/components/composer/composer-attachments-scroll-area.d.mts +6 -0
- package/dist/components/composer/composer-attachments-scroll-area.mjs +20 -31
- package/dist/components/composer/composer.d.mts +8 -2
- package/dist/components/composer/composer.mjs +7 -9
- package/dist/components/composer/composer.recipe.mjs +1 -0
- package/dist/components/context-menu/context-menu.d.mts +1 -1
- package/dist/components/context-menu/context-menu.mjs +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.d.mts +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.mjs +2 -2
- package/dist/components/kbd/index.d.mts +5 -2
- package/dist/components/kbd/kbd-keys.d.mts +12 -0
- package/dist/components/kbd/kbd-keys.mjs +297 -0
- package/dist/components/kbd/kbd-shortcut.d.mts +34 -0
- package/dist/components/kbd/kbd-shortcut.mjs +79 -0
- package/dist/components/kbd/kbd.d.mts +55 -2
- package/dist/components/kbd/kbd.mjs +39 -15
- package/dist/components/markdown/markdown-parts.d.mts +7 -2
- package/dist/components/markdown/markdown-parts.mjs +8 -3
- package/dist/components/markdown/markdown.recipe.mjs +1 -5
- package/dist/components/message-list/message-list.mjs +9 -15
- package/dist/components/message-list/message-list.recipe.mjs +1 -1
- package/dist/components/scroll-area/class-names.mjs +19 -0
- package/dist/components/scroll-area/gutters-binding.mjs +4 -0
- package/dist/components/scroll-area/index.d.mts +6 -2
- package/dist/components/scroll-area/scroll-area-gutters.d.mts +15 -0
- package/dist/components/scroll-area/scroll-area-gutters.mjs +60 -0
- package/dist/components/scroll-area/scroll-area-textarea.d.mts +14 -0
- package/dist/components/scroll-area/scroll-area-textarea.mjs +50 -0
- package/dist/components/scroll-area/scroll-area.css +720 -0
- package/dist/components/scroll-area/scroll-area.d.mts +24 -15
- package/dist/components/scroll-area/scroll-area.mjs +67 -45
- package/dist/components/scroll-area/scroll-geometry.mjs +58 -0
- package/dist/components/scroll-area/scroll-to-element.mjs +67 -0
- package/dist/components/scroll-area/types.d.mts +224 -0
- package/dist/components/scroll-area/use-rail-pointer.mjs +93 -0
- package/dist/components/scroll-area/use-scroll-area-container.d.mts +26 -0
- package/dist/components/scroll-area/use-scroll-area-container.mjs +150 -0
- package/dist/components/scroll-area/use-scrollbars.mjs +480 -0
- package/dist/components/search/search-trigger.d.mts +1 -1
- package/dist/components/sidebar/sidebar.d.mts +1 -1
- package/dist/components/sidebar/sidebar.mjs +24 -20
- package/dist/components/sidebar/sidebar.recipe.mjs +1 -6
- package/dist/components/tooltip/tooltip.mjs +8 -5
- package/dist/components/virtual-list/VirtualList.mjs +1 -1
- package/dist/index.d.mts +7 -3
- package/dist/index.mjs +5 -2
- package/dist/lib/inline-part.mjs +13 -0
- package/dist/lib/use-merged-ref.mjs +73 -47
- package/dist/lib/use-platform.d.mts +13 -0
- package/dist/lib/use-platform.mjs +29 -0
- package/dist/styles.css +1 -0
- package/dist/wip.d.mts +5 -1
- package/dist/wip.mjs +3 -1
- package/package.json +1 -1
- package/dist/components/scroll-area/scroll-area.recipe.mjs +0 -23
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ScrollArea } from "@base-ui/react/scroll-area";
|
|
1
|
+
import { ScrollAreaContentProps, ScrollAreaProps } from "./types.mjs";
|
|
3
2
|
//#region src/components/scroll-area/scroll-area.d.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/**
|
|
4
|
+
* An optional box around the content, inside the container (the demo's
|
|
5
|
+
* `ScrollAreaResizableContent`): for padding, so the scrollbars stay at the
|
|
6
|
+
* edges, or a layout of the content's own. In a horizontal (or `both`) area
|
|
7
|
+
* it is as wide as its widest child. It is watched for size changes like any
|
|
8
|
+
* child of the container.
|
|
9
|
+
*/
|
|
9
10
|
export declare function ScrollAreaContent({ className, ...props }: ScrollAreaContentProps): import("react").JSX.Element;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
/**
|
|
12
|
+
* A scrolling box with thin overlay scrollbars that look the same on every
|
|
13
|
+
* platform and theme. Scrolling itself stays native (wheel, touch,
|
|
14
|
+
* keyboard); the scrollbars can also be dragged, and a press on a gutter
|
|
15
|
+
* pages.
|
|
16
|
+
*
|
|
17
|
+
* Modelled on 07akioni/react-scroll-area-demo: one element, the container
|
|
18
|
+
* (the element that scrolls), with the gutters (the scrollbars) as its first
|
|
19
|
+
* child, a zero-height sticky layer. `ref`, `className`, `style` and the
|
|
20
|
+
* native props are the container's; `render` puts another element in its
|
|
21
|
+
* place; `actionsRef` adds `scrollToElement` and `measure`. For an element of
|
|
22
|
+
* your own that can't take children through ScrollArea (a textarea,
|
|
23
|
+
* VirtualList's scroller), see `useScrollAreaContainer`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ScrollArea({ actionsRef, children, className, contentClassName, contentRef, contentStyle, fade, hideDelay, onEdgeChange, onScroll, orientation, overscroll, railInset, ref, render, size, style, tabIndex, trigger, ...props }: ScrollAreaProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
26
|
+
//#endregion
|
|
@@ -1,55 +1,77 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { cn } from "../../cn.mjs";
|
|
2
|
+
import { ScrollAreaGutters } from "./scroll-area-gutters.mjs";
|
|
3
|
+
import { useScrollAreaContainer } from "./use-scroll-area-container.mjs";
|
|
4
|
+
import { useMemo, useRef } from "react";
|
|
5
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
7
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
4
8
|
//#region src/components/scroll-area/scroll-area.tsx
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
function ScrollAreaViewport({ className, ...props }) {
|
|
14
|
-
const { viewport } = scrollArea();
|
|
15
|
-
return /* @__PURE__ */ jsx(ScrollArea.Viewport, {
|
|
16
|
-
"data-slot": "scroll-area-viewport",
|
|
17
|
-
className: viewport({ className }),
|
|
18
|
-
...props
|
|
19
|
-
});
|
|
20
|
-
}
|
|
9
|
+
/**
|
|
10
|
+
* An optional box around the content, inside the container (the demo's
|
|
11
|
+
* `ScrollAreaResizableContent`): for padding, so the scrollbars stay at the
|
|
12
|
+
* edges, or a layout of the content's own. In a horizontal (or `both`) area
|
|
13
|
+
* it is as wide as its widest child. It is watched for size changes like any
|
|
14
|
+
* child of the container.
|
|
15
|
+
*/
|
|
21
16
|
function ScrollAreaContent({ className, ...props }) {
|
|
22
|
-
|
|
23
|
-
return /* @__PURE__ */ jsx(ScrollArea.Content, {
|
|
17
|
+
return /* @__PURE__ */ jsx("div", {
|
|
24
18
|
"data-slot": "scroll-area-content",
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
...props,
|
|
20
|
+
className: cn("r-scroll-area__content", className)
|
|
27
21
|
});
|
|
28
22
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
/**
|
|
24
|
+
* A scrolling box with thin overlay scrollbars that look the same on every
|
|
25
|
+
* platform and theme. Scrolling itself stays native (wheel, touch,
|
|
26
|
+
* keyboard); the scrollbars can also be dragged, and a press on a gutter
|
|
27
|
+
* pages.
|
|
28
|
+
*
|
|
29
|
+
* Modelled on 07akioni/react-scroll-area-demo: one element, the container
|
|
30
|
+
* (the element that scrolls), with the gutters (the scrollbars) as its first
|
|
31
|
+
* child, a zero-height sticky layer. `ref`, `className`, `style` and the
|
|
32
|
+
* native props are the container's; `render` puts another element in its
|
|
33
|
+
* place; `actionsRef` adds `scrollToElement` and `measure`. For an element of
|
|
34
|
+
* your own that can't take children through ScrollArea (a textarea,
|
|
35
|
+
* VirtualList's scroller), see `useScrollAreaContainer`.
|
|
36
|
+
*/
|
|
37
|
+
function ScrollArea({ actionsRef, children, className, contentClassName, contentRef, contentStyle, fade = false, hideDelay, onEdgeChange, onScroll, orientation, overscroll, railInset, ref, render, size, style, tabIndex, trigger, ...props }) {
|
|
38
|
+
const fadeRef = useRef(null);
|
|
39
|
+
const { containerProps, guttersProps } = useScrollAreaContainer({
|
|
40
|
+
actionsRef,
|
|
41
|
+
containerRef: ref,
|
|
42
|
+
contentRef,
|
|
43
|
+
fade,
|
|
44
|
+
fadeRef,
|
|
45
|
+
hideDelay,
|
|
46
|
+
onEdgeChange,
|
|
33
47
|
orientation,
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
overscroll,
|
|
49
|
+
railInset,
|
|
50
|
+
size,
|
|
51
|
+
trigger
|
|
36
52
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
className:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
const ownStyle = useMemo(() => ({
|
|
54
|
+
...containerProps.style,
|
|
55
|
+
...style
|
|
56
|
+
}), [containerProps.style, style]);
|
|
57
|
+
const content = contentClassName !== void 0 || contentStyle !== void 0 || fade ? /* @__PURE__ */ jsx(ScrollAreaContent, {
|
|
58
|
+
className: contentClassName,
|
|
59
|
+
ref: fadeRef,
|
|
60
|
+
style: contentStyle,
|
|
61
|
+
children
|
|
62
|
+
}) : children;
|
|
63
|
+
return useRender({
|
|
64
|
+
defaultTagName: "div",
|
|
65
|
+
render,
|
|
66
|
+
props: mergeProps({ "data-slot": "scroll-area" }, props, {
|
|
67
|
+
children: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ScrollAreaGutters, { ...guttersProps }), content] }),
|
|
68
|
+
className: cn(containerProps.className, "r-scroll-area__viewport", className),
|
|
69
|
+
onScroll,
|
|
70
|
+
ref: containerProps.ref,
|
|
71
|
+
style: ownStyle,
|
|
72
|
+
tabIndex: tabIndex ?? containerProps.tabIndex
|
|
73
|
+
})
|
|
52
74
|
});
|
|
53
75
|
}
|
|
54
76
|
//#endregion
|
|
55
|
-
export { ScrollArea
|
|
77
|
+
export { ScrollArea, ScrollAreaContent };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//#region src/components/scroll-area/scroll-geometry.ts
|
|
2
|
+
/**
|
|
3
|
+
* Where the thumb of one axis goes, or `null` when that axis has nothing to
|
|
4
|
+
* scroll. One pixel of overflow is ignored: it is usually rounding.
|
|
5
|
+
*/
|
|
6
|
+
function thumbPlacement(metrics, axis) {
|
|
7
|
+
const client = axis === "y" ? metrics.clientHeight : metrics.clientWidth;
|
|
8
|
+
const scroll = axis === "y" ? metrics.scrollHeight : metrics.scrollWidth;
|
|
9
|
+
const offset = axis === "y" ? metrics.scrollTop : metrics.scrollLeft;
|
|
10
|
+
const overflow = scroll - client;
|
|
11
|
+
if (client <= 0 || overflow <= 1) return null;
|
|
12
|
+
return {
|
|
13
|
+
progress: Math.min(1, Math.max(0, offset / overflow)),
|
|
14
|
+
ratio: client / scroll
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** The thumb's length in px: its share of the rail, at least `min`, and never longer than the rail. */
|
|
18
|
+
function thumbLength(ratio, rail, min) {
|
|
19
|
+
return Math.max(0, Math.min(rail, Math.max(min, ratio * rail)));
|
|
20
|
+
}
|
|
21
|
+
/** How far down (or along) its rail a thumb of `length` px starts, in px. */
|
|
22
|
+
function thumbOffset(progress, rail, length) {
|
|
23
|
+
return Math.max(0, rail - length) * progress;
|
|
24
|
+
}
|
|
25
|
+
/** The px left past each edge of the axes that have a thumb (`thumbPlacement` isn't `null`). */
|
|
26
|
+
function scrollOverflow(metrics, axes) {
|
|
27
|
+
const [yStart, yEnd] = axes.y ? remaining(metrics.scrollTop, metrics.scrollHeight - metrics.clientHeight) : [0, 0];
|
|
28
|
+
const [xStart, xEnd] = axes.x ? remaining(metrics.scrollLeft, metrics.scrollWidth - metrics.clientWidth) : [0, 0];
|
|
29
|
+
return {
|
|
30
|
+
xEnd,
|
|
31
|
+
xStart,
|
|
32
|
+
yEnd,
|
|
33
|
+
yStart
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function remaining(offset, overflow) {
|
|
37
|
+
const start = Math.min(overflow, Math.max(0, offset));
|
|
38
|
+
return [start, overflow - start];
|
|
39
|
+
}
|
|
40
|
+
/** An edge has more content past it once at least a pixel is left; less is rounding. */
|
|
41
|
+
function overflows(remainingPx) {
|
|
42
|
+
return remainingPx >= 1;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* How far a click on the rail pages: most of a viewport, keeping a strip of
|
|
46
|
+
* the previous page in view, as browsers do for Page Down.
|
|
47
|
+
*/
|
|
48
|
+
function pageDistance(client) {
|
|
49
|
+
return Math.round(client * .875);
|
|
50
|
+
}
|
|
51
|
+
/** -1 when the pointer is before the thumb, 1 after it, 0 on it. */
|
|
52
|
+
function pageDirection(pointer, thumbStart, thumbEnd) {
|
|
53
|
+
if (pointer < thumbStart) return -1;
|
|
54
|
+
if (pointer > thumbEnd) return 1;
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { overflows, pageDirection, pageDistance, scrollOverflow, thumbLength, thumbOffset, thumbPlacement };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//#region src/components/scroll-area/scroll-to-element.ts
|
|
2
|
+
/**
|
|
3
|
+
* The scroll offset on one axis that brings an element into view, clamped to
|
|
4
|
+
* what the container can scroll. `nearest` doesn't move an element that is
|
|
5
|
+
* already in view (inside the `offset` margins), and otherwise lines it up
|
|
6
|
+
* with the closer edge. Works on physical coordinates, so it holds for a
|
|
7
|
+
* negative `scrollLeft` in right-to-left layouts too.
|
|
8
|
+
*/
|
|
9
|
+
function alignedScrollOffset({ align, itemSize, itemStart, max, min, offset, scroll, viewSize, viewStart }) {
|
|
10
|
+
const position = scroll + itemStart - viewStart;
|
|
11
|
+
const atStart = position - offset;
|
|
12
|
+
const atEnd = position + itemSize - viewSize + offset;
|
|
13
|
+
let target;
|
|
14
|
+
if (align === "start") target = atStart;
|
|
15
|
+
else if (align === "end") target = atEnd;
|
|
16
|
+
else if (align === "center") target = position - (viewSize - itemSize) / 2;
|
|
17
|
+
else if (atStart < scroll) target = atStart;
|
|
18
|
+
else if (atEnd > scroll) target = Math.min(atEnd, atStart);
|
|
19
|
+
else target = scroll;
|
|
20
|
+
return Math.min(max, Math.max(min, target));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Scrolls `container`, and nothing around it, so that `element` is in view.
|
|
24
|
+
* `block` and `offset` apply to the axis the area scrolls in: vertical,
|
|
25
|
+
* unless the area is horizontal only (where `start` is the inline start, so
|
|
26
|
+
* the right edge in right-to-left layouts). With `orientation="both"` the
|
|
27
|
+
* horizontal axis just brings the element into view.
|
|
28
|
+
*/
|
|
29
|
+
function scrollToElement(container, element, orientation, { behavior, block = "start", offset = 0 } = {}) {
|
|
30
|
+
const view = container.getBoundingClientRect();
|
|
31
|
+
const item = element.getBoundingClientRect();
|
|
32
|
+
const target = {};
|
|
33
|
+
if (behavior) target.behavior = behavior;
|
|
34
|
+
if (orientation !== "horizontal") target.top = alignedScrollOffset({
|
|
35
|
+
align: block,
|
|
36
|
+
itemSize: item.height,
|
|
37
|
+
itemStart: item.top,
|
|
38
|
+
max: container.scrollHeight - container.clientHeight,
|
|
39
|
+
min: 0,
|
|
40
|
+
offset,
|
|
41
|
+
scroll: container.scrollTop,
|
|
42
|
+
viewSize: container.clientHeight,
|
|
43
|
+
viewStart: view.top + container.clientTop
|
|
44
|
+
});
|
|
45
|
+
if (orientation !== "vertical") {
|
|
46
|
+
const rtl = getComputedStyle(container).direction === "rtl";
|
|
47
|
+
const max = container.scrollWidth - container.clientWidth;
|
|
48
|
+
const inline = orientation === "horizontal";
|
|
49
|
+
let align = inline ? block : "nearest";
|
|
50
|
+
if (rtl && align === "start") align = "end";
|
|
51
|
+
else if (rtl && align === "end") align = "start";
|
|
52
|
+
target.left = alignedScrollOffset({
|
|
53
|
+
align,
|
|
54
|
+
itemSize: item.width,
|
|
55
|
+
itemStart: item.left,
|
|
56
|
+
max: rtl ? 0 : max,
|
|
57
|
+
min: rtl ? -max : 0,
|
|
58
|
+
offset: inline ? offset : 0,
|
|
59
|
+
scroll: container.scrollLeft,
|
|
60
|
+
viewSize: container.clientWidth,
|
|
61
|
+
viewStart: view.left + container.clientLeft
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
container.scrollTo(target);
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { alignedScrollOffset, scrollToElement };
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { CSSProperties, ComponentPropsWithRef, ReactNode, Ref, RefCallback, RefObject, UIEventHandler } from "react";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
//#region src/components/scroll-area/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `hover` shows the scrollbars while the pointer is over the area or it
|
|
6
|
+
* scrolls; `scroll` only while it scrolls; `none` always shows them.
|
|
7
|
+
*/
|
|
8
|
+
export type ScrollAreaTrigger = "hover" | "scroll" | "none";
|
|
9
|
+
export type ScrollAreaSize = "small" | "medium";
|
|
10
|
+
/** The directions the area scrolls in, each with its own scrollbar. */
|
|
11
|
+
export type ScrollAreaOrientation = "vertical" | "horizontal" | "both";
|
|
12
|
+
/**
|
|
13
|
+
* What a scroll does at the end of the area: `none` stops there, with no bounce; `contain` stops
|
|
14
|
+
* there but lets Mac trackpads and iOS bounce the area; `auto` carries on to the page behind.
|
|
15
|
+
*/
|
|
16
|
+
export type ScrollAreaOverscroll = "none" | "contain" | "auto";
|
|
17
|
+
/** Which edges the area is scrolled to: `true` when there is nothing more past that edge. */
|
|
18
|
+
export type ScrollAreaEdges = {
|
|
19
|
+
top: boolean;
|
|
20
|
+
bottom: boolean;
|
|
21
|
+
left: boolean;
|
|
22
|
+
right: boolean;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Room in px the scrollbars keep clear at each end, such as for a sticky
|
|
26
|
+
* header or footer inside the area. `top` and `bottom` shorten the vertical
|
|
27
|
+
* scrollbar, `left` and `right` the horizontal one.
|
|
28
|
+
*/
|
|
29
|
+
export type ScrollAreaRailInset = {
|
|
30
|
+
top?: number;
|
|
31
|
+
bottom?: number;
|
|
32
|
+
left?: number;
|
|
33
|
+
right?: number;
|
|
34
|
+
};
|
|
35
|
+
export type ScrollAreaScrollToElementOptions = {
|
|
36
|
+
/** Where the element lines up, as in `scrollIntoView`. */
|
|
37
|
+
block?: "start" | "center" | "end" | "nearest";
|
|
38
|
+
/** Room in px kept between the element and that edge, such as a sticky header's height. `center` ignores it. */
|
|
39
|
+
offset?: number;
|
|
40
|
+
behavior?: ScrollBehavior;
|
|
41
|
+
};
|
|
42
|
+
/** Methods on `actionsRef`. */
|
|
43
|
+
export type ScrollAreaActions = {
|
|
44
|
+
/** Scrolls the container, and only the container (never the page), so that `element` inside it is in view. */
|
|
45
|
+
scrollToElement: (element: Element, options?: ScrollAreaScrollToElementOptions) => void;
|
|
46
|
+
/** Measures the container again now, for a size change nothing observed reports. */
|
|
47
|
+
measure: () => void;
|
|
48
|
+
};
|
|
49
|
+
/** How the scrollbars look and behave: set on `ScrollArea`, or passed to `useScrollAreaContainer`. */
|
|
50
|
+
export type ScrollAreaOptions = {
|
|
51
|
+
/** Methods to scroll from code that the container doesn't have natively. */
|
|
52
|
+
actionsRef?: Ref<ScrollAreaActions>;
|
|
53
|
+
/** Fade the content out toward an edge that has more past it. The scrollbars don't fade. */
|
|
54
|
+
fade?: boolean;
|
|
55
|
+
/** How long the scrollbars stay after scrolling stops, in ms. At least 100 with `trigger="scroll"`. */
|
|
56
|
+
hideDelay?: number;
|
|
57
|
+
/** The directions the area scrolls in. Content too wide for a vertical area is cut off. */
|
|
58
|
+
orientation?: ScrollAreaOrientation;
|
|
59
|
+
/**
|
|
60
|
+
* What a scroll that reaches the end does: stop there with no bounce (`none`, the default), stop
|
|
61
|
+
* there but let Mac trackpads and iOS bounce the area (`contain`), or carry on to the page behind
|
|
62
|
+
* (`auto`, which leaves the container's own `overscroll-behavior` alone). Only in the directions
|
|
63
|
+
* the area overflows right now.
|
|
64
|
+
*/
|
|
65
|
+
overscroll?: ScrollAreaOverscroll;
|
|
66
|
+
/**
|
|
67
|
+
* Room in px the scrollbars keep clear at each end, such as a sticky header's height, so
|
|
68
|
+
* they don't run over it. Also settable in CSS, on the root:
|
|
69
|
+
* `--r-scroll-area-rail-inset-{top,bottom,left,right}`, which don't inherit.
|
|
70
|
+
*/
|
|
71
|
+
railInset?: ScrollAreaRailInset;
|
|
72
|
+
/** Thumb thickness: `small` is 5px, `medium` 7px; both grow while pointed at or dragged. */
|
|
73
|
+
size?: ScrollAreaSize;
|
|
74
|
+
/** When the scrollbars show. They always show while scrolling or dragging. */
|
|
75
|
+
trigger?: ScrollAreaTrigger;
|
|
76
|
+
/** Called when the area reaches or leaves an edge, and after mounting when the content overflows. */
|
|
77
|
+
onEdgeChange?: (edges: ScrollAreaEdges) => void;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Where the gutters (the scrollbars) go. `sticky`: inside the container, as its first child, a
|
|
81
|
+
* zero-height layer stuck to the top of the viewport; the container is the root, with no frame
|
|
82
|
+
* around it. `absolute`: beside the container, a layer laid over a positioned frame (the root)
|
|
83
|
+
* that holds both; for a container that can't hold children (a textarea).
|
|
84
|
+
*/
|
|
85
|
+
export type ScrollAreaGuttersPosition = "sticky" | "absolute";
|
|
86
|
+
/** What `useScrollAreaContainer` takes: ScrollArea's options, where the gutters go, and your refs. */
|
|
87
|
+
export type ScrollAreaContainerOptions<E extends HTMLElement = HTMLElement> = ScrollAreaOptions & {
|
|
88
|
+
/** Your ref to the element that scrolls: `containerProps.ref` forwards to it. */
|
|
89
|
+
containerRef?: Ref<E> | undefined;
|
|
90
|
+
/** With absolute gutters, your ref to the frame: `frameProps.ref` forwards to it. */
|
|
91
|
+
frameRef?: Ref<HTMLDivElement> | undefined;
|
|
92
|
+
/** Where the gutters go: inside the container (`sticky`, the default) or beside it in a frame (`absolute`). */
|
|
93
|
+
guttersPosition?: ScrollAreaGuttersPosition;
|
|
94
|
+
/**
|
|
95
|
+
* An element whose size is the content's, watched for size changes (such as a hidden mirror of a
|
|
96
|
+
* textarea's text). Without it each element child of the container is watched, and children
|
|
97
|
+
* added or removed.
|
|
98
|
+
*/
|
|
99
|
+
contentRef?: RefObject<HTMLElement | null>;
|
|
100
|
+
/**
|
|
101
|
+
* With `fade` and sticky gutters, the element the fade masks: a content element inside the
|
|
102
|
+
* container (`ScrollAreaContent`), since the mask can't sit on a container that holds the
|
|
103
|
+
* gutters. With absolute gutters the container itself is masked and this isn't read.
|
|
104
|
+
*/
|
|
105
|
+
fadeRef?: RefObject<HTMLElement | null>;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* What to spread on the element that scrolls (the container), merged with your own `className`
|
|
109
|
+
* and `style`, so the area measures it and its gutters follow it.
|
|
110
|
+
*/
|
|
111
|
+
export type ScrollAreaContainerProps<E extends HTMLElement = HTMLElement> = {
|
|
112
|
+
/**
|
|
113
|
+
* Put it on the element that scrolls (it fits any element's `ref` and VirtualList's `domRef`).
|
|
114
|
+
* It forwards to the `containerRef` you pass, so there is nothing to merge.
|
|
115
|
+
*/
|
|
116
|
+
ref: RefCallback<E>;
|
|
117
|
+
/**
|
|
118
|
+
* Before your own classes. With sticky gutters the container is the root, so these are the
|
|
119
|
+
* root's classes and modifiers; with absolute gutters, `r-scroll-area__container`.
|
|
120
|
+
*/
|
|
121
|
+
className: string;
|
|
122
|
+
/** With sticky gutters, the root's variables (`railInset`, the timelines); merge your style after it. */
|
|
123
|
+
style: CSSProperties | undefined;
|
|
124
|
+
/** `0` while there is something to scroll, so the container is a tab stop; else `undefined`. */
|
|
125
|
+
tabIndex: 0 | undefined;
|
|
126
|
+
};
|
|
127
|
+
/** With absolute gutters, what to spread on the positioned frame (the root) around the container and the gutters. */
|
|
128
|
+
export type ScrollAreaFrameProps = {
|
|
129
|
+
/** It forwards to the `frameRef` you pass. */
|
|
130
|
+
ref: RefCallback<HTMLDivElement>;
|
|
131
|
+
/** The root's classes and modifiers, before your own (size and frame the area there). */
|
|
132
|
+
className: string;
|
|
133
|
+
/** The root's variables (`railInset`, the timelines); merge your style after it. */
|
|
134
|
+
style: CSSProperties;
|
|
135
|
+
};
|
|
136
|
+
declare const guttersBinding: unique symbol;
|
|
137
|
+
/**
|
|
138
|
+
* How `<ScrollAreaGutters />` is wired to its container: opaque, made only by
|
|
139
|
+
* `useScrollAreaContainer`, so how the gutters are wired can change without breaking a caller.
|
|
140
|
+
*/
|
|
141
|
+
export type ScrollAreaGuttersBinding = {
|
|
142
|
+
readonly [guttersBinding]: true;
|
|
143
|
+
};
|
|
144
|
+
/** What `<ScrollAreaGutters />` takes: spread `useScrollAreaContainer`'s `guttersProps`, never built by hand. */
|
|
145
|
+
export type ScrollAreaGuttersProps = {
|
|
146
|
+
/** The binding to its container, from `useScrollAreaContainer`. */
|
|
147
|
+
binding: ScrollAreaGuttersBinding;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* What `useScrollAreaContainer` returns: props to spread on the elements you render (the
|
|
151
|
+
* container, the frame with absolute gutters, `<ScrollAreaGutters />`), and the area's methods.
|
|
152
|
+
*/
|
|
153
|
+
export type ScrollAreaContainer<E extends HTMLElement = HTMLElement> = {
|
|
154
|
+
/** Spread on the element that scrolls. */
|
|
155
|
+
containerProps: ScrollAreaContainerProps<E>;
|
|
156
|
+
/** With absolute gutters, spread on the frame around the container and the gutters; `undefined` with sticky gutters. */
|
|
157
|
+
frameProps: ScrollAreaFrameProps | undefined;
|
|
158
|
+
/** Spread on `<ScrollAreaGutters />`. */
|
|
159
|
+
guttersProps: ScrollAreaGuttersProps;
|
|
160
|
+
/** Where to put the gutters: the container's first child (`sticky`) or right after it (`absolute`). */
|
|
161
|
+
guttersPosition: ScrollAreaGuttersPosition;
|
|
162
|
+
/** Measures again now, for a size change nothing observed reports (a textarea's input). */
|
|
163
|
+
measure: () => void;
|
|
164
|
+
/** Scrolls the container, and only the container, so that `element` inside it is in view. */
|
|
165
|
+
scrollToElement: (element: Element, options?: ScrollAreaScrollToElementOptions) => void;
|
|
166
|
+
/** Whether either axis has something to scroll now. */
|
|
167
|
+
scrollable: boolean;
|
|
168
|
+
};
|
|
169
|
+
export type ScrollAreaOwnProps = ScrollAreaOptions & {
|
|
170
|
+
/** The scrolling content, in ScrollArea's own container. */
|
|
171
|
+
children?: ReactNode;
|
|
172
|
+
/**
|
|
173
|
+
* An element whose size is the content's, watched for size changes (such as a hidden mirror of a
|
|
174
|
+
* textarea's text). Without it each element child of the container is watched, and children
|
|
175
|
+
* added or removed.
|
|
176
|
+
*/
|
|
177
|
+
contentRef?: RefObject<HTMLElement | null>;
|
|
178
|
+
/** Class of the area, the element that scrolls: size and frame it here. */
|
|
179
|
+
className?: string;
|
|
180
|
+
/** Style of the area. */
|
|
181
|
+
style?: CSSProperties;
|
|
182
|
+
/**
|
|
183
|
+
* Class of a content element around `children`, inside the container: put padding and the
|
|
184
|
+
* content's own layout (grid, flex, `gap`, `stack-y-*`) here, so the scrollbars stay at the edges
|
|
185
|
+
* and the gutters layer is never one of the laid-out children. Only with it, `contentStyle` or
|
|
186
|
+
* `fade` is there one.
|
|
187
|
+
*/
|
|
188
|
+
contentClassName?: string;
|
|
189
|
+
/** Style of the content element; renders it, as `contentClassName` does. */
|
|
190
|
+
contentStyle?: CSSProperties;
|
|
191
|
+
/** The container's scroll events. */
|
|
192
|
+
onScroll?: UIEventHandler<HTMLDivElement>;
|
|
193
|
+
/** The container's: by default `0` while there is something to scroll. */
|
|
194
|
+
tabIndex?: number;
|
|
195
|
+
/**
|
|
196
|
+
* Another element in place of ScrollArea's own `<div>`, as the container (an element or a
|
|
197
|
+
* function that gets the props). It must hold children: the gutters are its first child.
|
|
198
|
+
*/
|
|
199
|
+
render?: useRender.RenderProp | undefined;
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* `ref`, `className`, `style` and the native props go to the area: one element, the container,
|
|
203
|
+
* the element that scrolls, with the scrollbars inside it (sticky gutters).
|
|
204
|
+
*/
|
|
205
|
+
export type ScrollAreaProps = ScrollAreaOwnProps & Omit<ComponentPropsWithRef<"div">, keyof ScrollAreaOwnProps>;
|
|
206
|
+
/** An optional box around the content, inside the container: native div props and `ref`. */
|
|
207
|
+
export type ScrollAreaContentProps = ComponentPropsWithRef<"div">;
|
|
208
|
+
export type ScrollAreaTextareaOwnProps = {
|
|
209
|
+
/** How long the scrollbar stays after scrolling stops, as on ScrollArea. */
|
|
210
|
+
hideDelay?: number;
|
|
211
|
+
/** Whether a scroll that reaches the end carries on to the page, as on ScrollArea. */
|
|
212
|
+
overscroll?: ScrollAreaOverscroll;
|
|
213
|
+
/** Thumb thickness, as on ScrollArea. */
|
|
214
|
+
size?: ScrollAreaSize;
|
|
215
|
+
/** When the scrollbar shows, as on ScrollArea. */
|
|
216
|
+
trigger?: ScrollAreaTrigger;
|
|
217
|
+
/** Class of the root that holds the textarea and its scrollbar. */
|
|
218
|
+
wrapperClassName?: string;
|
|
219
|
+
/** Style of the root; size the textarea itself with `className` / `style`. */
|
|
220
|
+
wrapperStyle?: CSSProperties;
|
|
221
|
+
};
|
|
222
|
+
/** Native textarea props; `ref`, `className`, `style` and `onScroll` go to the textarea. */
|
|
223
|
+
export type ScrollAreaTextareaProps = ScrollAreaTextareaOwnProps & Omit<ComponentPropsWithRef<"textarea">, keyof ScrollAreaTextareaOwnProps>;
|
|
224
|
+
//#endregion
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { pageDirection, pageDistance } from "./scroll-geometry.mjs";
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
//#region src/components/scroll-area/use-rail-pointer.ts
|
|
4
|
+
const reducedMotion = () => typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
5
|
+
/**
|
|
6
|
+
* Mouse and pen input on one scrollbar: dragging the thumb scrolls the
|
|
7
|
+
* container in proportion, a press on the rail beside the thumb pages
|
|
8
|
+
* toward it. Wheel, touch and keyboard scrolling stay native.
|
|
9
|
+
*
|
|
10
|
+
* `scrollLeft` is negative in right-to-left layouts, and both a drag delta
|
|
11
|
+
* and a page step are physical (left is negative), so neither needs a
|
|
12
|
+
* direction check.
|
|
13
|
+
*/
|
|
14
|
+
function useRailPointer({ axis, containerRef, onDraggingChange }) {
|
|
15
|
+
const drag = useRef(null);
|
|
16
|
+
const [dragging, setDragging] = useState(false);
|
|
17
|
+
const changeDragging = (next) => {
|
|
18
|
+
setDragging(next);
|
|
19
|
+
onDraggingChange(next);
|
|
20
|
+
};
|
|
21
|
+
const notifyRef = useRef(onDraggingChange);
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
notifyRef.current = onDraggingChange;
|
|
24
|
+
});
|
|
25
|
+
useLayoutEffect(() => () => {
|
|
26
|
+
const current = drag.current;
|
|
27
|
+
if (!current) return;
|
|
28
|
+
drag.current = null;
|
|
29
|
+
try {
|
|
30
|
+
if (current.thumb.hasPointerCapture(current.pointerId)) current.thumb.releasePointerCapture(current.pointerId);
|
|
31
|
+
} catch {}
|
|
32
|
+
notifyRef.current(false);
|
|
33
|
+
}, []);
|
|
34
|
+
const vertical = axis === "y";
|
|
35
|
+
const along = (event) => vertical ? event.clientY : event.clientX;
|
|
36
|
+
const onRailPointerDown = (event) => {
|
|
37
|
+
const container = containerRef.current;
|
|
38
|
+
const thumb = event.currentTarget.firstElementChild;
|
|
39
|
+
if (!container || !thumb || event.button !== 0 || event.pointerType === "touch") return;
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
const rect = thumb.getBoundingClientRect();
|
|
42
|
+
const distance = (vertical ? pageDirection(event.clientY, rect.top, rect.bottom) : pageDirection(event.clientX, rect.left, rect.right)) * pageDistance(vertical ? container.clientHeight : container.clientWidth);
|
|
43
|
+
container.scrollBy({
|
|
44
|
+
behavior: reducedMotion() ? "instant" : "smooth",
|
|
45
|
+
[vertical ? "top" : "left"]: distance
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const onThumbPointerDown = (event) => {
|
|
49
|
+
const container = containerRef.current;
|
|
50
|
+
const thumb = event.currentTarget;
|
|
51
|
+
const rail = thumb.parentElement;
|
|
52
|
+
if (!container || !rail || event.button !== 0) return;
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
event.stopPropagation();
|
|
55
|
+
const travel = vertical ? rail.clientHeight - thumb.offsetHeight : rail.clientWidth - thumb.offsetWidth;
|
|
56
|
+
const overflow = vertical ? container.scrollHeight - container.clientHeight : container.scrollWidth - container.clientWidth;
|
|
57
|
+
drag.current = {
|
|
58
|
+
pointerId: event.pointerId,
|
|
59
|
+
thumb,
|
|
60
|
+
scale: travel > 0 ? overflow / travel : 0,
|
|
61
|
+
scrollStart: vertical ? container.scrollTop : container.scrollLeft,
|
|
62
|
+
start: along(event)
|
|
63
|
+
};
|
|
64
|
+
thumb.setPointerCapture(event.pointerId);
|
|
65
|
+
changeDragging(true);
|
|
66
|
+
};
|
|
67
|
+
const onThumbPointerMove = (event) => {
|
|
68
|
+
const container = containerRef.current;
|
|
69
|
+
const current = drag.current;
|
|
70
|
+
if (!container || current?.pointerId !== event.pointerId) return;
|
|
71
|
+
const scroll = current.scrollStart + (along(event) - current.start) * current.scale;
|
|
72
|
+
if (vertical) container.scrollTop = scroll;
|
|
73
|
+
else container.scrollLeft = scroll;
|
|
74
|
+
};
|
|
75
|
+
const endDrag = (event) => {
|
|
76
|
+
if (drag.current?.pointerId !== event.pointerId) return;
|
|
77
|
+
drag.current = null;
|
|
78
|
+
changeDragging(false);
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
dragging,
|
|
82
|
+
railHandlers: { onPointerDown: onRailPointerDown },
|
|
83
|
+
thumbHandlers: {
|
|
84
|
+
onLostPointerCapture: endDrag,
|
|
85
|
+
onPointerCancel: endDrag,
|
|
86
|
+
onPointerDown: onThumbPointerDown,
|
|
87
|
+
onPointerMove: onThumbPointerMove,
|
|
88
|
+
onPointerUp: endDrag
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { useRailPointer };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ScrollAreaContainer, ScrollAreaContainerOptions, ScrollAreaFrameProps } from "./types.mjs";
|
|
2
|
+
//#region src/components/scroll-area/use-scroll-area-container.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* ScrollArea's scrollbars for an element you render yourself: the demo's `useContainer`, as a
|
|
5
|
+
* hook in your own component. It measures the element that scrolls (the container), listens to
|
|
6
|
+
* its scroll, and keeps the state; you render the elements.
|
|
7
|
+
*
|
|
8
|
+
* Spread `containerProps` on the element that scrolls (a `<ul>`, a `<textarea>`, VirtualList's
|
|
9
|
+
* scroller through `domRef`), merging your own `className` and `style` with them (pass your own
|
|
10
|
+
* refs as `containerRef` and `frameRef`: the returned `ref`s already forward to them), and render
|
|
11
|
+
* `<ScrollAreaGutters {...guttersProps} />` where `guttersPosition` says:
|
|
12
|
+
*
|
|
13
|
+
* - `sticky` (the default): as the container's first child. The container is the root and
|
|
14
|
+
* carries its classes; `frameProps` is `undefined`.
|
|
15
|
+
* - `absolute`: right after the container, both inside a frame that gets `frameProps` (the
|
|
16
|
+
* root, positioned, sized and bordered by you).
|
|
17
|
+
*
|
|
18
|
+
* `<ScrollArea>` is this hook with a `<div>` of its own.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useScrollAreaContainer<E extends HTMLElement = HTMLElement>(options: ScrollAreaContainerOptions<E> & {
|
|
21
|
+
guttersPosition: "absolute";
|
|
22
|
+
}): ScrollAreaContainer<E> & {
|
|
23
|
+
frameProps: ScrollAreaFrameProps;
|
|
24
|
+
};
|
|
25
|
+
export declare function useScrollAreaContainer<E extends HTMLElement = HTMLElement>(options?: ScrollAreaContainerOptions<E>): ScrollAreaContainer<E>;
|
|
26
|
+
//#endregion
|