rigid-ui 0.0.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/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/scroll-area/index.d.ts +41 -0
- package/dist/scroll-area/index.js +850 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 fdemb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# rigid-ui
|
|
2
|
+
|
|
3
|
+
Unstyled UI components for SolidJS. Based on [Base UI](https://base-ui.com/).
|
|
4
|
+
|
|
5
|
+
## Components ported
|
|
6
|
+
|
|
7
|
+
- **ScrollArea** — a container with native scroll and stylable scrollbars.
|
|
8
|
+
|
|
9
|
+
## Components not ported
|
|
10
|
+
|
|
11
|
+
- Anything else from BaseUI. I just needed the ScrollArea, but maybe I'll add more later.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { ScrollArea } from "rigid-ui/scroll-area";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Notes
|
|
20
|
+
|
|
21
|
+
- I didn't port the `useRender` utility for polymorphic components. The scroll area components are just divs.
|
|
22
|
+
|
|
23
|
+
## Credits
|
|
24
|
+
|
|
25
|
+
This project tries to port some of great code from [Base UI](https://base-ui.com/), an unstyled React component library.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ParentProps, JSX } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
interface ScrollAreaRootProps extends ParentProps<JSX.HTMLAttributes<HTMLDivElement>> {
|
|
4
|
+
overflowEdgeThreshold?: number | Partial<{
|
|
5
|
+
xStart: number;
|
|
6
|
+
xEnd: number;
|
|
7
|
+
yStart: number;
|
|
8
|
+
yEnd: number;
|
|
9
|
+
}>;
|
|
10
|
+
ref?: HTMLDivElement | ((el: HTMLDivElement) => void);
|
|
11
|
+
}
|
|
12
|
+
declare function ScrollAreaRoot(props: ScrollAreaRootProps): JSX.Element;
|
|
13
|
+
|
|
14
|
+
interface ScrollAreaViewportProps extends ParentProps<JSX.HTMLAttributes<HTMLDivElement>> {
|
|
15
|
+
ref?: HTMLDivElement | ((el: HTMLDivElement) => void);
|
|
16
|
+
}
|
|
17
|
+
declare function ScrollAreaViewport(props: ScrollAreaViewportProps): JSX.Element;
|
|
18
|
+
|
|
19
|
+
interface ScrollAreaScrollbarProps extends ParentProps<JSX.HTMLAttributes<HTMLDivElement>> {
|
|
20
|
+
orientation?: "vertical" | "horizontal";
|
|
21
|
+
keepMounted?: boolean;
|
|
22
|
+
ref?: HTMLDivElement | ((el: HTMLDivElement) => void);
|
|
23
|
+
}
|
|
24
|
+
declare function ScrollAreaScrollbar(props: ScrollAreaScrollbarProps): JSX.Element;
|
|
25
|
+
|
|
26
|
+
interface ScrollAreaThumbProps extends ParentProps<JSX.HTMLAttributes<HTMLDivElement>> {
|
|
27
|
+
ref?: HTMLDivElement | ((el: HTMLDivElement) => void);
|
|
28
|
+
}
|
|
29
|
+
declare function ScrollAreaThumb(props: ScrollAreaThumbProps): JSX.Element;
|
|
30
|
+
|
|
31
|
+
interface ScrollAreaContentProps extends ParentProps<JSX.HTMLAttributes<HTMLDivElement>> {
|
|
32
|
+
ref?: HTMLDivElement | ((el: HTMLDivElement) => void);
|
|
33
|
+
}
|
|
34
|
+
declare function ScrollAreaContent(props: ScrollAreaContentProps): JSX.Element;
|
|
35
|
+
|
|
36
|
+
interface ScrollAreaCornerProps extends ParentProps<JSX.HTMLAttributes<HTMLDivElement>> {
|
|
37
|
+
ref?: HTMLDivElement | ((el: HTMLDivElement) => void);
|
|
38
|
+
}
|
|
39
|
+
declare function ScrollAreaCorner(props: ScrollAreaCornerProps): JSX.Element;
|
|
40
|
+
|
|
41
|
+
export { ScrollAreaContent, type ScrollAreaContentProps, ScrollAreaCorner, type ScrollAreaCornerProps, ScrollAreaRoot, type ScrollAreaRootProps, ScrollAreaScrollbar, type ScrollAreaScrollbarProps, ScrollAreaThumb, type ScrollAreaThumbProps, ScrollAreaViewport, type ScrollAreaViewportProps };
|
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import { createContext, splitProps, createSignal, onCleanup, onMount, createEffect, Show, useContext } from 'solid-js';
|
|
2
|
+
|
|
3
|
+
// src/scroll-area/root/ScrollAreaRoot.tsx
|
|
4
|
+
var ScrollAreaRootContext = createContext();
|
|
5
|
+
function useScrollAreaRootContext() {
|
|
6
|
+
const context = useContext(ScrollAreaRootContext);
|
|
7
|
+
if (context === void 0) {
|
|
8
|
+
throw new Error("rigid-ui: ScrollArea parts must be placed within <ScrollAreaRoot>.");
|
|
9
|
+
}
|
|
10
|
+
return context;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/scroll-area/constants.ts
|
|
14
|
+
var SCROLL_TIMEOUT = 500;
|
|
15
|
+
var MIN_THUMB_SIZE = 16;
|
|
16
|
+
|
|
17
|
+
// src/utils/getOffset.ts
|
|
18
|
+
function getOffset(element, prop, axis) {
|
|
19
|
+
if (!element) {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
const styles = getComputedStyle(element);
|
|
23
|
+
const propAxis = axis === "x" ? "Inline" : "Block";
|
|
24
|
+
if (axis === "x" && prop === "margin") {
|
|
25
|
+
return parseFloat(styles[`${prop}InlineStart`]) * 2;
|
|
26
|
+
}
|
|
27
|
+
return parseFloat(styles[`${prop}${propAxis}Start`]) + parseFloat(styles[`${prop}${propAxis}End`]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/utils/styles.ts
|
|
31
|
+
var DISABLE_SCROLLBAR_CLASS_NAME = "base-ui-disable-scrollbar";
|
|
32
|
+
var STYLE_CONTENT = `.${DISABLE_SCROLLBAR_CLASS_NAME}{scrollbar-width:none}.${DISABLE_SCROLLBAR_CLASS_NAME}::-webkit-scrollbar{display:none}`;
|
|
33
|
+
var styleInjected = false;
|
|
34
|
+
var styleDisableScrollbar = {
|
|
35
|
+
className: DISABLE_SCROLLBAR_CLASS_NAME,
|
|
36
|
+
inject() {
|
|
37
|
+
if (styleInjected || typeof document === "undefined") {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const style = document.createElement("style");
|
|
41
|
+
style.textContent = STYLE_CONTENT;
|
|
42
|
+
document.head.appendChild(style);
|
|
43
|
+
styleInjected = true;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/utils/contains.ts
|
|
48
|
+
function isShadowRoot(node) {
|
|
49
|
+
return node instanceof ShadowRoot;
|
|
50
|
+
}
|
|
51
|
+
function contains(parent, child) {
|
|
52
|
+
if (!parent || !child) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const rootNode = child.getRootNode?.();
|
|
56
|
+
if (parent.contains(child)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (rootNode && isShadowRoot(rootNode)) {
|
|
60
|
+
let next = child;
|
|
61
|
+
while (next) {
|
|
62
|
+
if (parent === next) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
next = next.parentNode || next.host;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
var Timeout = class {
|
|
71
|
+
constructor() {
|
|
72
|
+
this.id = null;
|
|
73
|
+
}
|
|
74
|
+
start(delay, callback) {
|
|
75
|
+
this.clear();
|
|
76
|
+
this.id = setTimeout(() => {
|
|
77
|
+
this.id = null;
|
|
78
|
+
callback();
|
|
79
|
+
}, delay);
|
|
80
|
+
}
|
|
81
|
+
clear() {
|
|
82
|
+
if (this.id !== null) {
|
|
83
|
+
clearTimeout(this.id);
|
|
84
|
+
this.id = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
function useTimeout() {
|
|
89
|
+
const timeout = new Timeout();
|
|
90
|
+
onCleanup(() => timeout.clear());
|
|
91
|
+
return timeout;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/scroll-area/root/ScrollAreaRoot.tsx
|
|
95
|
+
var DEFAULT_SIZE = { width: 0, height: 0 };
|
|
96
|
+
var DEFAULT_OVERFLOW_EDGES = {
|
|
97
|
+
xStart: false,
|
|
98
|
+
xEnd: false,
|
|
99
|
+
yStart: false,
|
|
100
|
+
yEnd: false
|
|
101
|
+
};
|
|
102
|
+
var DEFAULT_HIDDEN_STATE = { x: false, y: false, corner: false };
|
|
103
|
+
function normalizeOverflowEdgeThreshold(threshold) {
|
|
104
|
+
if (typeof threshold === "number") {
|
|
105
|
+
const value = Math.max(0, threshold);
|
|
106
|
+
return { xStart: value, xEnd: value, yStart: value, yEnd: value };
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
xStart: Math.max(0, threshold?.xStart || 0),
|
|
110
|
+
xEnd: Math.max(0, threshold?.xEnd || 0),
|
|
111
|
+
yStart: Math.max(0, threshold?.yStart || 0),
|
|
112
|
+
yEnd: Math.max(0, threshold?.yEnd || 0)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function ScrollAreaRoot(props) {
|
|
116
|
+
const [local, others] = splitProps(props, ["children", "overflowEdgeThreshold", "ref", "style"]);
|
|
117
|
+
const overflowEdgeThreshold = normalizeOverflowEdgeThreshold(local.overflowEdgeThreshold);
|
|
118
|
+
const scrollYTimeout = useTimeout();
|
|
119
|
+
const scrollXTimeout = useTimeout();
|
|
120
|
+
const [hovering, setHovering] = createSignal(false);
|
|
121
|
+
const [scrollingX, setScrollingX] = createSignal(false);
|
|
122
|
+
const [scrollingY, setScrollingY] = createSignal(false);
|
|
123
|
+
const [cornerSize, setCornerSize] = createSignal(DEFAULT_SIZE);
|
|
124
|
+
const [thumbSize, setThumbSize] = createSignal(DEFAULT_SIZE);
|
|
125
|
+
const [overflowEdges, setOverflowEdges] = createSignal(DEFAULT_OVERFLOW_EDGES);
|
|
126
|
+
const [hiddenState, setHiddenState] = createSignal(DEFAULT_HIDDEN_STATE);
|
|
127
|
+
let rootRef;
|
|
128
|
+
let scrollPosition = { x: 0, y: 0 };
|
|
129
|
+
let thumbDragging = false;
|
|
130
|
+
let startY = 0;
|
|
131
|
+
let startX = 0;
|
|
132
|
+
let startScrollTop = 0;
|
|
133
|
+
let startScrollLeft = 0;
|
|
134
|
+
let currentOrientation = "vertical";
|
|
135
|
+
styleDisableScrollbar.inject();
|
|
136
|
+
const refs = {
|
|
137
|
+
viewportRef: void 0,
|
|
138
|
+
scrollbarYRef: void 0,
|
|
139
|
+
scrollbarXRef: void 0,
|
|
140
|
+
thumbYRef: void 0,
|
|
141
|
+
thumbXRef: void 0,
|
|
142
|
+
cornerRef: void 0
|
|
143
|
+
};
|
|
144
|
+
function handleScroll(pos) {
|
|
145
|
+
const offsetX = pos.x - scrollPosition.x;
|
|
146
|
+
const offsetY = pos.y - scrollPosition.y;
|
|
147
|
+
scrollPosition = pos;
|
|
148
|
+
if (offsetY !== 0) {
|
|
149
|
+
setScrollingY(true);
|
|
150
|
+
scrollYTimeout.start(SCROLL_TIMEOUT, () => setScrollingY(false));
|
|
151
|
+
}
|
|
152
|
+
if (offsetX !== 0) {
|
|
153
|
+
setScrollingX(true);
|
|
154
|
+
scrollXTimeout.start(SCROLL_TIMEOUT, () => setScrollingX(false));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function handlePointerDown(event) {
|
|
158
|
+
if (event.button !== 0) return;
|
|
159
|
+
thumbDragging = true;
|
|
160
|
+
startY = event.clientY;
|
|
161
|
+
startX = event.clientX;
|
|
162
|
+
currentOrientation = event.currentTarget.getAttribute("data-orientation");
|
|
163
|
+
if (refs.viewportRef) {
|
|
164
|
+
startScrollTop = refs.viewportRef.scrollTop;
|
|
165
|
+
startScrollLeft = refs.viewportRef.scrollLeft;
|
|
166
|
+
}
|
|
167
|
+
if (refs.thumbYRef && currentOrientation === "vertical") {
|
|
168
|
+
refs.thumbYRef.setPointerCapture(event.pointerId);
|
|
169
|
+
}
|
|
170
|
+
if (refs.thumbXRef && currentOrientation === "horizontal") {
|
|
171
|
+
refs.thumbXRef.setPointerCapture(event.pointerId);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function handlePointerMove(event) {
|
|
175
|
+
if (!thumbDragging) return;
|
|
176
|
+
const deltaY = event.clientY - startY;
|
|
177
|
+
const deltaX = event.clientX - startX;
|
|
178
|
+
const vp = refs.viewportRef;
|
|
179
|
+
if (!vp) return;
|
|
180
|
+
const scrollableH = vp.scrollHeight;
|
|
181
|
+
const vpH = vp.clientHeight;
|
|
182
|
+
const scrollableW = vp.scrollWidth;
|
|
183
|
+
const vpW = vp.clientWidth;
|
|
184
|
+
if (refs.thumbYRef && refs.scrollbarYRef && currentOrientation === "vertical") {
|
|
185
|
+
const sbOffset = getOffset(refs.scrollbarYRef, "padding", "y");
|
|
186
|
+
const thOffset = getOffset(refs.thumbYRef, "margin", "y");
|
|
187
|
+
const thH = refs.thumbYRef.offsetHeight;
|
|
188
|
+
const maxOffset = refs.scrollbarYRef.offsetHeight - thH - sbOffset - thOffset;
|
|
189
|
+
vp.scrollTop = startScrollTop + deltaY / maxOffset * (scrollableH - vpH);
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
setScrollingY(true);
|
|
192
|
+
scrollYTimeout.start(SCROLL_TIMEOUT, () => setScrollingY(false));
|
|
193
|
+
}
|
|
194
|
+
if (refs.thumbXRef && refs.scrollbarXRef && currentOrientation === "horizontal") {
|
|
195
|
+
const sbOffset = getOffset(refs.scrollbarXRef, "padding", "x");
|
|
196
|
+
const thOffset = getOffset(refs.thumbXRef, "margin", "x");
|
|
197
|
+
const thW = refs.thumbXRef.offsetWidth;
|
|
198
|
+
const maxOffset = refs.scrollbarXRef.offsetWidth - thW - sbOffset - thOffset;
|
|
199
|
+
vp.scrollLeft = startScrollLeft + deltaX / maxOffset * (scrollableW - vpW);
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
setScrollingX(true);
|
|
202
|
+
scrollXTimeout.start(SCROLL_TIMEOUT, () => setScrollingX(false));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function handlePointerUp(event) {
|
|
206
|
+
thumbDragging = false;
|
|
207
|
+
if (refs.thumbYRef && currentOrientation === "vertical") {
|
|
208
|
+
refs.thumbYRef.releasePointerCapture(event.pointerId);
|
|
209
|
+
}
|
|
210
|
+
if (refs.thumbXRef && currentOrientation === "horizontal") {
|
|
211
|
+
refs.thumbXRef.releasePointerCapture(event.pointerId);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function handlePointerEnterOrMove(event) {
|
|
215
|
+
if (event.pointerType !== "touch") {
|
|
216
|
+
setHovering(contains(rootRef, event.target));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const contextValue = {
|
|
220
|
+
cornerSize,
|
|
221
|
+
setCornerSize,
|
|
222
|
+
thumbSize,
|
|
223
|
+
setThumbSize,
|
|
224
|
+
hovering,
|
|
225
|
+
setHovering,
|
|
226
|
+
scrollingX,
|
|
227
|
+
setScrollingX,
|
|
228
|
+
scrollingY,
|
|
229
|
+
setScrollingY,
|
|
230
|
+
hiddenState,
|
|
231
|
+
setHiddenState,
|
|
232
|
+
overflowEdges,
|
|
233
|
+
setOverflowEdges,
|
|
234
|
+
overflowEdgeThreshold,
|
|
235
|
+
// Refs are read/written directly by child components
|
|
236
|
+
get viewportRef() {
|
|
237
|
+
return refs.viewportRef;
|
|
238
|
+
},
|
|
239
|
+
set viewportRef(el) {
|
|
240
|
+
refs.viewportRef = el;
|
|
241
|
+
},
|
|
242
|
+
get scrollbarYRef() {
|
|
243
|
+
return refs.scrollbarYRef;
|
|
244
|
+
},
|
|
245
|
+
set scrollbarYRef(el) {
|
|
246
|
+
refs.scrollbarYRef = el;
|
|
247
|
+
},
|
|
248
|
+
get scrollbarXRef() {
|
|
249
|
+
return refs.scrollbarXRef;
|
|
250
|
+
},
|
|
251
|
+
set scrollbarXRef(el) {
|
|
252
|
+
refs.scrollbarXRef = el;
|
|
253
|
+
},
|
|
254
|
+
get thumbYRef() {
|
|
255
|
+
return refs.thumbYRef;
|
|
256
|
+
},
|
|
257
|
+
set thumbYRef(el) {
|
|
258
|
+
refs.thumbYRef = el;
|
|
259
|
+
},
|
|
260
|
+
get thumbXRef() {
|
|
261
|
+
return refs.thumbXRef;
|
|
262
|
+
},
|
|
263
|
+
set thumbXRef(el) {
|
|
264
|
+
refs.thumbXRef = el;
|
|
265
|
+
},
|
|
266
|
+
get cornerRef() {
|
|
267
|
+
return refs.cornerRef;
|
|
268
|
+
},
|
|
269
|
+
set cornerRef(el) {
|
|
270
|
+
refs.cornerRef = el;
|
|
271
|
+
},
|
|
272
|
+
handlePointerDown,
|
|
273
|
+
handlePointerMove,
|
|
274
|
+
handlePointerUp,
|
|
275
|
+
handleScroll
|
|
276
|
+
};
|
|
277
|
+
const mergedStyle = () => {
|
|
278
|
+
const base = {
|
|
279
|
+
position: "relative",
|
|
280
|
+
overflow: "hidden",
|
|
281
|
+
["--scroll-area-corner-height" /* scrollAreaCornerHeight */]: `${cornerSize().height}px`,
|
|
282
|
+
["--scroll-area-corner-width" /* scrollAreaCornerWidth */]: `${cornerSize().width}px`
|
|
283
|
+
};
|
|
284
|
+
if (typeof local.style === "object" && local.style) {
|
|
285
|
+
return { ...base, ...local.style };
|
|
286
|
+
}
|
|
287
|
+
return base;
|
|
288
|
+
};
|
|
289
|
+
return /* @__PURE__ */ React.createElement(ScrollAreaRootContext.Provider, { value: contextValue }, /* @__PURE__ */ React.createElement(
|
|
290
|
+
"div",
|
|
291
|
+
{
|
|
292
|
+
ref: (el) => {
|
|
293
|
+
rootRef = el;
|
|
294
|
+
if (typeof local.ref === "function") local.ref(el);
|
|
295
|
+
},
|
|
296
|
+
role: "presentation",
|
|
297
|
+
onPointerEnter: handlePointerEnterOrMove,
|
|
298
|
+
onPointerMove: handlePointerEnterOrMove,
|
|
299
|
+
onPointerLeave: () => setHovering(false),
|
|
300
|
+
style: mergedStyle(),
|
|
301
|
+
"data-scrolling": scrollingX() || scrollingY() ? "" : void 0,
|
|
302
|
+
"data-has-overflow-x": !hiddenState().x ? "" : void 0,
|
|
303
|
+
"data-has-overflow-y": !hiddenState().y ? "" : void 0,
|
|
304
|
+
"data-overflow-x-start": overflowEdges().xStart ? "" : void 0,
|
|
305
|
+
"data-overflow-x-end": overflowEdges().xEnd ? "" : void 0,
|
|
306
|
+
"data-overflow-y-start": overflowEdges().yStart ? "" : void 0,
|
|
307
|
+
"data-overflow-y-end": overflowEdges().yEnd ? "" : void 0,
|
|
308
|
+
...others
|
|
309
|
+
},
|
|
310
|
+
local.children
|
|
311
|
+
));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/utils/detectBrowser.ts
|
|
315
|
+
var isWebKit = typeof CSS === "undefined" || !CSS.supports ? false : CSS.supports("-webkit-backdrop-filter:none");
|
|
316
|
+
var ScrollAreaViewportContext = createContext();
|
|
317
|
+
function useScrollAreaViewportContext() {
|
|
318
|
+
const context = useContext(ScrollAreaViewportContext);
|
|
319
|
+
if (context === void 0) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
"rigid-ui: ScrollAreaViewportContext missing. ScrollAreaViewport parts must be placed within <ScrollArea.Viewport>."
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return context;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/utils/clamp.ts
|
|
328
|
+
function clamp(val, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
|
|
329
|
+
return Math.max(min, Math.min(val, max));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/utils/onVisible.ts
|
|
333
|
+
function onVisible(element, callback) {
|
|
334
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
335
|
+
return () => {
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const observer = new IntersectionObserver((entries) => {
|
|
339
|
+
entries.forEach((entry) => {
|
|
340
|
+
if (entry.intersectionRatio > 0) {
|
|
341
|
+
callback();
|
|
342
|
+
observer.disconnect();
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
observer.observe(element);
|
|
347
|
+
return () => {
|
|
348
|
+
observer.disconnect();
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/scroll-area/viewport/ScrollAreaViewport.tsx
|
|
353
|
+
var scrollAreaOverflowVarsRegistered = false;
|
|
354
|
+
function removeCSSVariableInheritance() {
|
|
355
|
+
if (scrollAreaOverflowVarsRegistered || isWebKit) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (typeof CSS !== "undefined" && "registerProperty" in CSS) {
|
|
359
|
+
[
|
|
360
|
+
"--scroll-area-overflow-x-start" /* scrollAreaOverflowXStart */,
|
|
361
|
+
"--scroll-area-overflow-x-end" /* scrollAreaOverflowXEnd */,
|
|
362
|
+
"--scroll-area-overflow-y-start" /* scrollAreaOverflowYStart */,
|
|
363
|
+
"--scroll-area-overflow-y-end" /* scrollAreaOverflowYEnd */
|
|
364
|
+
].forEach((name) => {
|
|
365
|
+
try {
|
|
366
|
+
CSS.registerProperty({
|
|
367
|
+
name,
|
|
368
|
+
syntax: "<length>",
|
|
369
|
+
inherits: false,
|
|
370
|
+
initialValue: "0px"
|
|
371
|
+
});
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
scrollAreaOverflowVarsRegistered = true;
|
|
377
|
+
}
|
|
378
|
+
function ScrollAreaViewport(props) {
|
|
379
|
+
const [local, others] = splitProps(props, ["children", "ref", "class", "style"]);
|
|
380
|
+
const ctx = useScrollAreaRootContext();
|
|
381
|
+
let programmaticScroll = true;
|
|
382
|
+
const scrollEndTimeout = new Timeout();
|
|
383
|
+
const waitForAnimationsTimeout = new Timeout();
|
|
384
|
+
onCleanup(() => {
|
|
385
|
+
scrollEndTimeout.clear();
|
|
386
|
+
waitForAnimationsTimeout.clear();
|
|
387
|
+
});
|
|
388
|
+
function computeThumbPosition() {
|
|
389
|
+
const viewportEl = ctx.viewportRef;
|
|
390
|
+
const scrollbarYEl = ctx.scrollbarYRef;
|
|
391
|
+
const scrollbarXEl = ctx.scrollbarXRef;
|
|
392
|
+
const thumbYEl = ctx.thumbYRef;
|
|
393
|
+
const thumbXEl = ctx.thumbXRef;
|
|
394
|
+
const cornerEl = ctx.cornerRef;
|
|
395
|
+
if (!viewportEl) return;
|
|
396
|
+
const scrollableContentHeight = viewportEl.scrollHeight;
|
|
397
|
+
const scrollableContentWidth = viewportEl.scrollWidth;
|
|
398
|
+
const viewportHeight = viewportEl.clientHeight;
|
|
399
|
+
const viewportWidth = viewportEl.clientWidth;
|
|
400
|
+
const scrollTop = viewportEl.scrollTop;
|
|
401
|
+
const scrollLeft = viewportEl.scrollLeft;
|
|
402
|
+
if (scrollableContentHeight === 0 || scrollableContentWidth === 0) return;
|
|
403
|
+
const scrollbarYHidden = viewportHeight >= scrollableContentHeight;
|
|
404
|
+
const scrollbarXHidden = viewportWidth >= scrollableContentWidth;
|
|
405
|
+
const ratioX = viewportWidth / scrollableContentWidth;
|
|
406
|
+
const ratioY = viewportHeight / scrollableContentHeight;
|
|
407
|
+
const maxScrollLeft = Math.max(0, scrollableContentWidth - viewportWidth);
|
|
408
|
+
const maxScrollTop = Math.max(0, scrollableContentHeight - viewportHeight);
|
|
409
|
+
let scrollLeftFromStart = 0;
|
|
410
|
+
let scrollLeftFromEnd = 0;
|
|
411
|
+
if (!scrollbarXHidden) {
|
|
412
|
+
{
|
|
413
|
+
scrollLeftFromStart = clamp(scrollLeft, 0, maxScrollLeft);
|
|
414
|
+
}
|
|
415
|
+
scrollLeftFromEnd = maxScrollLeft - scrollLeftFromStart;
|
|
416
|
+
}
|
|
417
|
+
const scrollTopFromStart = !scrollbarYHidden ? clamp(scrollTop, 0, maxScrollTop) : 0;
|
|
418
|
+
const scrollTopFromEnd = !scrollbarYHidden ? maxScrollTop - scrollTopFromStart : 0;
|
|
419
|
+
const nextWidth = scrollbarXHidden ? 0 : viewportWidth;
|
|
420
|
+
const nextHeight = scrollbarYHidden ? 0 : viewportHeight;
|
|
421
|
+
let nextCornerWidth = 0;
|
|
422
|
+
let nextCornerHeight = 0;
|
|
423
|
+
if (!scrollbarXHidden && !scrollbarYHidden) {
|
|
424
|
+
nextCornerWidth = scrollbarYEl?.offsetWidth || 0;
|
|
425
|
+
nextCornerHeight = scrollbarXEl?.offsetHeight || 0;
|
|
426
|
+
}
|
|
427
|
+
const cs = ctx.cornerSize();
|
|
428
|
+
const cornerNotYetSized = cs.width === 0 && cs.height === 0;
|
|
429
|
+
const cornerWidthOffset = cornerNotYetSized ? nextCornerWidth : 0;
|
|
430
|
+
const cornerHeightOffset = cornerNotYetSized ? nextCornerHeight : 0;
|
|
431
|
+
const scrollbarXOffset = getOffset(scrollbarXEl ?? null, "padding", "x");
|
|
432
|
+
const scrollbarYOffset = getOffset(scrollbarYEl ?? null, "padding", "y");
|
|
433
|
+
const thumbXOffset = getOffset(thumbXEl ?? null, "margin", "x");
|
|
434
|
+
const thumbYOffset = getOffset(thumbYEl ?? null, "margin", "y");
|
|
435
|
+
const idealNextWidth = nextWidth - scrollbarXOffset - thumbXOffset;
|
|
436
|
+
const idealNextHeight = nextHeight - scrollbarYOffset - thumbYOffset;
|
|
437
|
+
const maxNextWidth = scrollbarXEl ? Math.min(scrollbarXEl.offsetWidth - cornerWidthOffset, idealNextWidth) : idealNextWidth;
|
|
438
|
+
const maxNextHeight = scrollbarYEl ? Math.min(scrollbarYEl.offsetHeight - cornerHeightOffset, idealNextHeight) : idealNextHeight;
|
|
439
|
+
const clampedNextWidth = Math.max(MIN_THUMB_SIZE, maxNextWidth * ratioX);
|
|
440
|
+
const clampedNextHeight = Math.max(MIN_THUMB_SIZE, maxNextHeight * ratioY);
|
|
441
|
+
ctx.setThumbSize((prevSize) => {
|
|
442
|
+
if (prevSize.height === clampedNextHeight && prevSize.width === clampedNextWidth) {
|
|
443
|
+
return prevSize;
|
|
444
|
+
}
|
|
445
|
+
return { width: clampedNextWidth, height: clampedNextHeight };
|
|
446
|
+
});
|
|
447
|
+
if (scrollbarYEl && thumbYEl) {
|
|
448
|
+
const maxThumbOffsetY = scrollbarYEl.offsetHeight - clampedNextHeight - scrollbarYOffset - thumbYOffset;
|
|
449
|
+
const scrollRangeY = scrollableContentHeight - viewportHeight;
|
|
450
|
+
const scrollRatioY = scrollRangeY === 0 ? 0 : scrollTop / scrollRangeY;
|
|
451
|
+
const thumbOffsetY = Math.min(maxThumbOffsetY, Math.max(0, scrollRatioY * maxThumbOffsetY));
|
|
452
|
+
thumbYEl.style.transform = `translate3d(0,${thumbOffsetY}px,0)`;
|
|
453
|
+
}
|
|
454
|
+
if (scrollbarXEl && thumbXEl) {
|
|
455
|
+
const maxThumbOffsetX = scrollbarXEl.offsetWidth - clampedNextWidth - scrollbarXOffset - thumbXOffset;
|
|
456
|
+
const scrollRangeX = scrollableContentWidth - viewportWidth;
|
|
457
|
+
const scrollRatioX = scrollRangeX === 0 ? 0 : scrollLeft / scrollRangeX;
|
|
458
|
+
const thumbOffsetX = clamp(scrollRatioX * maxThumbOffsetX, 0, maxThumbOffsetX);
|
|
459
|
+
thumbXEl.style.transform = `translate3d(${thumbOffsetX}px,0,0)`;
|
|
460
|
+
}
|
|
461
|
+
const clampedScrollLeftStart = clamp(scrollLeftFromStart, 0, maxScrollLeft);
|
|
462
|
+
const clampedScrollLeftEnd = clamp(scrollLeftFromEnd, 0, maxScrollLeft);
|
|
463
|
+
const clampedScrollTopStart = clamp(scrollTopFromStart, 0, maxScrollTop);
|
|
464
|
+
const clampedScrollTopEnd = clamp(scrollTopFromEnd, 0, maxScrollTop);
|
|
465
|
+
const overflowMetricsPx = [
|
|
466
|
+
["--scroll-area-overflow-x-start" /* scrollAreaOverflowXStart */, clampedScrollLeftStart],
|
|
467
|
+
["--scroll-area-overflow-x-end" /* scrollAreaOverflowXEnd */, clampedScrollLeftEnd],
|
|
468
|
+
["--scroll-area-overflow-y-start" /* scrollAreaOverflowYStart */, clampedScrollTopStart],
|
|
469
|
+
["--scroll-area-overflow-y-end" /* scrollAreaOverflowYEnd */, clampedScrollTopEnd]
|
|
470
|
+
];
|
|
471
|
+
for (const [cssVar, value] of overflowMetricsPx) {
|
|
472
|
+
viewportEl.style.setProperty(cssVar, `${value}px`);
|
|
473
|
+
}
|
|
474
|
+
if (cornerEl) {
|
|
475
|
+
if (scrollbarXHidden || scrollbarYHidden) {
|
|
476
|
+
ctx.setCornerSize({ width: 0, height: 0 });
|
|
477
|
+
} else if (!scrollbarXHidden && !scrollbarYHidden) {
|
|
478
|
+
ctx.setCornerSize({ width: nextCornerWidth, height: nextCornerHeight });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
ctx.setHiddenState((prevState) => {
|
|
482
|
+
const cornerHidden = scrollbarYHidden || scrollbarXHidden;
|
|
483
|
+
if (prevState.y === scrollbarYHidden && prevState.x === scrollbarXHidden && prevState.corner === cornerHidden) {
|
|
484
|
+
return prevState;
|
|
485
|
+
}
|
|
486
|
+
return { y: scrollbarYHidden, x: scrollbarXHidden, corner: cornerHidden };
|
|
487
|
+
});
|
|
488
|
+
const nextOverflowEdges = {
|
|
489
|
+
xStart: !scrollbarXHidden && clampedScrollLeftStart > ctx.overflowEdgeThreshold.xStart,
|
|
490
|
+
xEnd: !scrollbarXHidden && clampedScrollLeftEnd > ctx.overflowEdgeThreshold.xEnd,
|
|
491
|
+
yStart: !scrollbarYHidden && clampedScrollTopStart > ctx.overflowEdgeThreshold.yStart,
|
|
492
|
+
yEnd: !scrollbarYHidden && clampedScrollTopEnd > ctx.overflowEdgeThreshold.yEnd
|
|
493
|
+
};
|
|
494
|
+
ctx.setOverflowEdges((prev) => {
|
|
495
|
+
if (prev.xStart === nextOverflowEdges.xStart && prev.xEnd === nextOverflowEdges.xEnd && prev.yStart === nextOverflowEdges.yStart && prev.yEnd === nextOverflowEdges.yEnd) {
|
|
496
|
+
return prev;
|
|
497
|
+
}
|
|
498
|
+
return nextOverflowEdges;
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
onMount(() => {
|
|
502
|
+
const viewportEl = ctx.viewportRef;
|
|
503
|
+
if (!viewportEl) return;
|
|
504
|
+
removeCSSVariableInheritance();
|
|
505
|
+
if (viewportEl.matches(":hover")) {
|
|
506
|
+
ctx.setHovering(true);
|
|
507
|
+
}
|
|
508
|
+
queueMicrotask(computeThumbPosition);
|
|
509
|
+
let hasInitialized = false;
|
|
510
|
+
const cleanupVisible = onVisible(viewportEl, () => {
|
|
511
|
+
if (!hasInitialized) {
|
|
512
|
+
hasInitialized = true;
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
computeThumbPosition();
|
|
516
|
+
});
|
|
517
|
+
onCleanup(cleanupVisible);
|
|
518
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
519
|
+
let roInitialized = false;
|
|
520
|
+
const ro = new ResizeObserver(() => {
|
|
521
|
+
if (!roInitialized) {
|
|
522
|
+
roInitialized = true;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
computeThumbPosition();
|
|
526
|
+
});
|
|
527
|
+
ro.observe(viewportEl);
|
|
528
|
+
waitForAnimationsTimeout.start(0, () => {
|
|
529
|
+
const animations = viewportEl.getAnimations({ subtree: true });
|
|
530
|
+
if (animations.length === 0) return;
|
|
531
|
+
Promise.all(animations.map((a) => a.finished)).then(computeThumbPosition).catch(() => {
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
onCleanup(() => ro.disconnect());
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
createEffect(() => {
|
|
538
|
+
ctx.hiddenState();
|
|
539
|
+
queueMicrotask(computeThumbPosition);
|
|
540
|
+
});
|
|
541
|
+
function handleUserInteraction() {
|
|
542
|
+
programmaticScroll = false;
|
|
543
|
+
}
|
|
544
|
+
const hs = () => ctx.hiddenState();
|
|
545
|
+
const mergedClass = () => {
|
|
546
|
+
const base = styleDisableScrollbar.className;
|
|
547
|
+
return local.class ? `${base} ${local.class}` : base;
|
|
548
|
+
};
|
|
549
|
+
const mergedStyle = () => {
|
|
550
|
+
const base = {
|
|
551
|
+
overflow: "scroll",
|
|
552
|
+
height: "100%"
|
|
553
|
+
};
|
|
554
|
+
if (typeof local.style === "object" && local.style) {
|
|
555
|
+
return { ...base, ...local.style };
|
|
556
|
+
}
|
|
557
|
+
return base;
|
|
558
|
+
};
|
|
559
|
+
return /* @__PURE__ */ React.createElement(ScrollAreaViewportContext.Provider, { value: { computeThumbPosition } }, /* @__PURE__ */ React.createElement(
|
|
560
|
+
"div",
|
|
561
|
+
{
|
|
562
|
+
ref: (el) => {
|
|
563
|
+
ctx.viewportRef = el;
|
|
564
|
+
if (typeof local.ref === "function") local.ref(el);
|
|
565
|
+
},
|
|
566
|
+
role: "presentation",
|
|
567
|
+
tabIndex: !hs().x || !hs().y ? 0 : void 0,
|
|
568
|
+
class: mergedClass(),
|
|
569
|
+
onScroll: () => {
|
|
570
|
+
const viewportEl = ctx.viewportRef;
|
|
571
|
+
if (!viewportEl) return;
|
|
572
|
+
computeThumbPosition();
|
|
573
|
+
if (!programmaticScroll) {
|
|
574
|
+
ctx.handleScroll({
|
|
575
|
+
x: viewportEl.scrollLeft,
|
|
576
|
+
y: viewportEl.scrollTop
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
scrollEndTimeout.start(100, () => {
|
|
580
|
+
programmaticScroll = true;
|
|
581
|
+
});
|
|
582
|
+
},
|
|
583
|
+
onWheel: handleUserInteraction,
|
|
584
|
+
onTouchMove: handleUserInteraction,
|
|
585
|
+
onPointerMove: handleUserInteraction,
|
|
586
|
+
onPointerEnter: handleUserInteraction,
|
|
587
|
+
onKeyDown: handleUserInteraction,
|
|
588
|
+
style: mergedStyle(),
|
|
589
|
+
"data-scrolling": ctx.scrollingX() || ctx.scrollingY() ? "" : void 0,
|
|
590
|
+
"data-has-overflow-x": !ctx.hiddenState().x ? "" : void 0,
|
|
591
|
+
"data-has-overflow-y": !ctx.hiddenState().y ? "" : void 0,
|
|
592
|
+
"data-overflow-x-start": ctx.overflowEdges().xStart ? "" : void 0,
|
|
593
|
+
"data-overflow-x-end": ctx.overflowEdges().xEnd ? "" : void 0,
|
|
594
|
+
"data-overflow-y-start": ctx.overflowEdges().yStart ? "" : void 0,
|
|
595
|
+
"data-overflow-y-end": ctx.overflowEdges().yEnd ? "" : void 0,
|
|
596
|
+
...others
|
|
597
|
+
},
|
|
598
|
+
local.children
|
|
599
|
+
));
|
|
600
|
+
}
|
|
601
|
+
var ScrollAreaScrollbarContext = createContext();
|
|
602
|
+
function useScrollAreaScrollbarContext() {
|
|
603
|
+
const context = useContext(ScrollAreaScrollbarContext);
|
|
604
|
+
if (context === void 0) {
|
|
605
|
+
throw new Error(
|
|
606
|
+
"rigid-ui: ScrollAreaScrollbarContext is missing. ScrollAreaScrollbar parts must be placed within <ScrollArea.Scrollbar>."
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
return context;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/scroll-area/scrollbar/ScrollAreaScrollbar.tsx
|
|
613
|
+
function ScrollAreaScrollbar(props) {
|
|
614
|
+
const [local, others] = splitProps(props, [
|
|
615
|
+
"children",
|
|
616
|
+
"orientation",
|
|
617
|
+
"keepMounted",
|
|
618
|
+
"ref",
|
|
619
|
+
"style"
|
|
620
|
+
]);
|
|
621
|
+
const orientation = () => local.orientation ?? "vertical";
|
|
622
|
+
const keepMounted = () => local.keepMounted ?? false;
|
|
623
|
+
const ctx = useScrollAreaRootContext();
|
|
624
|
+
createEffect(() => {
|
|
625
|
+
const orient = orientation();
|
|
626
|
+
const scrollbarEl = orient === "vertical" ? ctx.scrollbarYRef : ctx.scrollbarXRef;
|
|
627
|
+
const viewportEl = ctx.viewportRef;
|
|
628
|
+
if (!scrollbarEl) return;
|
|
629
|
+
function handleWheel(event) {
|
|
630
|
+
if (!viewportEl || !scrollbarEl || event.ctrlKey) return;
|
|
631
|
+
event.preventDefault();
|
|
632
|
+
if (orient === "vertical") {
|
|
633
|
+
if (viewportEl.scrollTop === 0 && event.deltaY < 0) return;
|
|
634
|
+
if (viewportEl.scrollTop === viewportEl.scrollHeight - viewportEl.clientHeight && event.deltaY > 0)
|
|
635
|
+
return;
|
|
636
|
+
viewportEl.scrollTop += event.deltaY;
|
|
637
|
+
} else {
|
|
638
|
+
if (viewportEl.scrollLeft === 0 && event.deltaX < 0) return;
|
|
639
|
+
if (viewportEl.scrollLeft === viewportEl.scrollWidth - viewportEl.clientWidth && event.deltaX > 0)
|
|
640
|
+
return;
|
|
641
|
+
viewportEl.scrollLeft += event.deltaX;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
scrollbarEl.addEventListener("wheel", handleWheel, { passive: false });
|
|
645
|
+
onCleanup(() => scrollbarEl.removeEventListener("wheel", handleWheel));
|
|
646
|
+
});
|
|
647
|
+
function handleScrollbarPointerDown(event) {
|
|
648
|
+
if (event.button !== 0) return;
|
|
649
|
+
if (event.currentTarget !== event.target) return;
|
|
650
|
+
const viewportEl = ctx.viewportRef;
|
|
651
|
+
if (!viewportEl) return;
|
|
652
|
+
const orient = orientation();
|
|
653
|
+
if (ctx.thumbYRef && ctx.scrollbarYRef && orient === "vertical") {
|
|
654
|
+
const thumbYOffset = getOffset(ctx.thumbYRef, "margin", "y");
|
|
655
|
+
const scrollbarYOffset = getOffset(ctx.scrollbarYRef, "padding", "y");
|
|
656
|
+
const thumbHeight = ctx.thumbYRef.offsetHeight;
|
|
657
|
+
const trackRectY = ctx.scrollbarYRef.getBoundingClientRect();
|
|
658
|
+
const clickY = event.clientY - trackRectY.top - thumbHeight / 2 - scrollbarYOffset + thumbYOffset / 2;
|
|
659
|
+
const scrollableContentHeight = viewportEl.scrollHeight;
|
|
660
|
+
const viewportHeight = viewportEl.clientHeight;
|
|
661
|
+
const maxThumbOffsetY = ctx.scrollbarYRef.offsetHeight - thumbHeight - scrollbarYOffset - thumbYOffset;
|
|
662
|
+
const scrollRatioY = clickY / maxThumbOffsetY;
|
|
663
|
+
viewportEl.scrollTop = scrollRatioY * (scrollableContentHeight - viewportHeight);
|
|
664
|
+
}
|
|
665
|
+
if (ctx.thumbXRef && ctx.scrollbarXRef && orient === "horizontal") {
|
|
666
|
+
const thumbXOffset = getOffset(ctx.thumbXRef, "margin", "x");
|
|
667
|
+
const scrollbarXOffset = getOffset(ctx.scrollbarXRef, "padding", "x");
|
|
668
|
+
const thumbWidth = ctx.thumbXRef.offsetWidth;
|
|
669
|
+
const trackRectX = ctx.scrollbarXRef.getBoundingClientRect();
|
|
670
|
+
const clickX = event.clientX - trackRectX.left - thumbWidth / 2 - scrollbarXOffset + thumbXOffset / 2;
|
|
671
|
+
const scrollableContentWidth = viewportEl.scrollWidth;
|
|
672
|
+
const viewportWidth = viewportEl.clientWidth;
|
|
673
|
+
const maxThumbOffsetX = ctx.scrollbarXRef.offsetWidth - thumbWidth - scrollbarXOffset - thumbXOffset;
|
|
674
|
+
const scrollRatioX = clickX / maxThumbOffsetX;
|
|
675
|
+
let newScrollLeft;
|
|
676
|
+
{
|
|
677
|
+
newScrollLeft = scrollRatioX * (scrollableContentWidth - viewportWidth);
|
|
678
|
+
}
|
|
679
|
+
viewportEl.scrollLeft = newScrollLeft;
|
|
680
|
+
}
|
|
681
|
+
ctx.handlePointerDown(event);
|
|
682
|
+
}
|
|
683
|
+
const isHidden = () => orientation() === "vertical" ? ctx.hiddenState().y : ctx.hiddenState().x;
|
|
684
|
+
const shouldRender = () => keepMounted() || !isHidden();
|
|
685
|
+
const mergedStyle = () => {
|
|
686
|
+
const base = {
|
|
687
|
+
position: "absolute",
|
|
688
|
+
"touch-action": "none",
|
|
689
|
+
"-webkit-user-select": "none",
|
|
690
|
+
"user-select": "none"
|
|
691
|
+
};
|
|
692
|
+
if (orientation() === "vertical") {
|
|
693
|
+
Object.assign(base, {
|
|
694
|
+
top: "0",
|
|
695
|
+
bottom: `var(${"--scroll-area-corner-height" /* scrollAreaCornerHeight */})`,
|
|
696
|
+
"inset-inline-end": "0",
|
|
697
|
+
["--scroll-area-thumb-height" /* scrollAreaThumbHeight */]: `${ctx.thumbSize().height}px`
|
|
698
|
+
});
|
|
699
|
+
} else {
|
|
700
|
+
Object.assign(base, {
|
|
701
|
+
"inset-inline-start": "0",
|
|
702
|
+
"inset-inline-end": `var(${"--scroll-area-corner-width" /* scrollAreaCornerWidth */})`,
|
|
703
|
+
bottom: "0",
|
|
704
|
+
["--scroll-area-thumb-width" /* scrollAreaThumbWidth */]: `${ctx.thumbSize().width}px`
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
if (typeof local.style === "object" && local.style) {
|
|
708
|
+
return { ...base, ...local.style };
|
|
709
|
+
}
|
|
710
|
+
return base;
|
|
711
|
+
};
|
|
712
|
+
return /* @__PURE__ */ React.createElement(Show, { when: shouldRender() }, /* @__PURE__ */ React.createElement(ScrollAreaScrollbarContext.Provider, { value: { orientation: orientation() } }, /* @__PURE__ */ React.createElement(
|
|
713
|
+
"div",
|
|
714
|
+
{
|
|
715
|
+
ref: (el) => {
|
|
716
|
+
if (orientation() === "vertical") {
|
|
717
|
+
ctx.scrollbarYRef = el;
|
|
718
|
+
} else {
|
|
719
|
+
ctx.scrollbarXRef = el;
|
|
720
|
+
}
|
|
721
|
+
if (typeof local.ref === "function") local.ref(el);
|
|
722
|
+
},
|
|
723
|
+
"data-orientation": orientation(),
|
|
724
|
+
"data-hovering": ctx.hovering() ? "" : void 0,
|
|
725
|
+
"data-scrolling": ctx.scrollingX() || ctx.scrollingY() ? "" : void 0,
|
|
726
|
+
onPointerDown: handleScrollbarPointerDown,
|
|
727
|
+
onPointerUp: (e) => ctx.handlePointerUp(e),
|
|
728
|
+
style: mergedStyle(),
|
|
729
|
+
...others
|
|
730
|
+
},
|
|
731
|
+
local.children
|
|
732
|
+
)));
|
|
733
|
+
}
|
|
734
|
+
function ScrollAreaThumb(props) {
|
|
735
|
+
const [local, others] = splitProps(props, ["children", "ref", "style"]);
|
|
736
|
+
const ctx = useScrollAreaRootContext();
|
|
737
|
+
const scrollbarCtx = useScrollAreaScrollbarContext();
|
|
738
|
+
const mergedStyle = () => {
|
|
739
|
+
const base = scrollbarCtx.orientation === "vertical" ? { height: `var(${"--scroll-area-thumb-height" /* scrollAreaThumbHeight */})` } : { width: `var(${"--scroll-area-thumb-width" /* scrollAreaThumbWidth */})` };
|
|
740
|
+
if (typeof local.style === "object" && local.style) {
|
|
741
|
+
return { ...base, ...local.style };
|
|
742
|
+
}
|
|
743
|
+
return base;
|
|
744
|
+
};
|
|
745
|
+
return /* @__PURE__ */ React.createElement(
|
|
746
|
+
"div",
|
|
747
|
+
{
|
|
748
|
+
ref: (el) => {
|
|
749
|
+
if (scrollbarCtx.orientation === "vertical") {
|
|
750
|
+
ctx.thumbYRef = el;
|
|
751
|
+
} else {
|
|
752
|
+
ctx.thumbXRef = el;
|
|
753
|
+
}
|
|
754
|
+
if (typeof local.ref === "function") local.ref(el);
|
|
755
|
+
},
|
|
756
|
+
"data-orientation": scrollbarCtx.orientation,
|
|
757
|
+
onPointerDown: (e) => ctx.handlePointerDown(e),
|
|
758
|
+
onPointerMove: (e) => ctx.handlePointerMove(e),
|
|
759
|
+
onPointerUp: (e) => {
|
|
760
|
+
if (scrollbarCtx.orientation === "vertical") {
|
|
761
|
+
ctx.setScrollingY(false);
|
|
762
|
+
}
|
|
763
|
+
if (scrollbarCtx.orientation === "horizontal") {
|
|
764
|
+
ctx.setScrollingX(false);
|
|
765
|
+
}
|
|
766
|
+
ctx.handlePointerUp(e);
|
|
767
|
+
},
|
|
768
|
+
style: mergedStyle(),
|
|
769
|
+
...others
|
|
770
|
+
},
|
|
771
|
+
local.children
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
function ScrollAreaContent(props) {
|
|
775
|
+
const [local, others] = splitProps(props, ["children", "ref", "style"]);
|
|
776
|
+
const { computeThumbPosition } = useScrollAreaViewportContext();
|
|
777
|
+
const ctx = useScrollAreaRootContext();
|
|
778
|
+
let contentRef;
|
|
779
|
+
onMount(() => {
|
|
780
|
+
if (typeof ResizeObserver === "undefined" || !contentRef) return;
|
|
781
|
+
let hasInitialized = false;
|
|
782
|
+
const ro = new ResizeObserver(() => {
|
|
783
|
+
if (!hasInitialized) {
|
|
784
|
+
hasInitialized = true;
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
computeThumbPosition();
|
|
788
|
+
});
|
|
789
|
+
ro.observe(contentRef);
|
|
790
|
+
onCleanup(() => ro.disconnect());
|
|
791
|
+
});
|
|
792
|
+
const mergedStyle = () => {
|
|
793
|
+
const base = { "min-width": "fit-content" };
|
|
794
|
+
if (typeof local.style === "object" && local.style) {
|
|
795
|
+
return { ...base, ...local.style };
|
|
796
|
+
}
|
|
797
|
+
return base;
|
|
798
|
+
};
|
|
799
|
+
return /* @__PURE__ */ React.createElement(
|
|
800
|
+
"div",
|
|
801
|
+
{
|
|
802
|
+
ref: (el) => {
|
|
803
|
+
contentRef = el;
|
|
804
|
+
if (typeof local.ref === "function") local.ref(el);
|
|
805
|
+
},
|
|
806
|
+
role: "presentation",
|
|
807
|
+
style: mergedStyle(),
|
|
808
|
+
"data-scrolling": ctx.scrollingX() || ctx.scrollingY() ? "" : void 0,
|
|
809
|
+
"data-has-overflow-x": !ctx.hiddenState().x ? "" : void 0,
|
|
810
|
+
"data-has-overflow-y": !ctx.hiddenState().y ? "" : void 0,
|
|
811
|
+
"data-overflow-x-start": ctx.overflowEdges().xStart ? "" : void 0,
|
|
812
|
+
"data-overflow-x-end": ctx.overflowEdges().xEnd ? "" : void 0,
|
|
813
|
+
"data-overflow-y-start": ctx.overflowEdges().yStart ? "" : void 0,
|
|
814
|
+
"data-overflow-y-end": ctx.overflowEdges().yEnd ? "" : void 0,
|
|
815
|
+
...others
|
|
816
|
+
},
|
|
817
|
+
local.children
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
function ScrollAreaCorner(props) {
|
|
821
|
+
const [local, others] = splitProps(props, ["children", "ref", "style"]);
|
|
822
|
+
const ctx = useScrollAreaRootContext();
|
|
823
|
+
const mergedStyle = () => {
|
|
824
|
+
const base = {
|
|
825
|
+
position: "absolute",
|
|
826
|
+
bottom: "0",
|
|
827
|
+
"inset-inline-end": "0",
|
|
828
|
+
width: `${ctx.cornerSize().width}px`,
|
|
829
|
+
height: `${ctx.cornerSize().height}px`
|
|
830
|
+
};
|
|
831
|
+
if (typeof local.style === "object" && local.style) {
|
|
832
|
+
return { ...base, ...local.style };
|
|
833
|
+
}
|
|
834
|
+
return base;
|
|
835
|
+
};
|
|
836
|
+
return /* @__PURE__ */ React.createElement(Show, { when: !ctx.hiddenState().corner }, /* @__PURE__ */ React.createElement(
|
|
837
|
+
"div",
|
|
838
|
+
{
|
|
839
|
+
ref: (el) => {
|
|
840
|
+
ctx.cornerRef = el;
|
|
841
|
+
if (typeof local.ref === "function") local.ref(el);
|
|
842
|
+
},
|
|
843
|
+
style: mergedStyle(),
|
|
844
|
+
...others
|
|
845
|
+
},
|
|
846
|
+
local.children
|
|
847
|
+
));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export { ScrollAreaContent, ScrollAreaCorner, ScrollAreaRoot, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rigid-ui",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Unstyled UI components for SolidJS — a port of Base UI",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"components",
|
|
7
|
+
"headless",
|
|
8
|
+
"scroll-area",
|
|
9
|
+
"solid",
|
|
10
|
+
"solidjs",
|
|
11
|
+
"ui",
|
|
12
|
+
"unstyled"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "fdemb",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/fdemb/rigid-ui"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
"./scroll-area": {
|
|
26
|
+
"types": "./dist/scroll-area/index.d.ts",
|
|
27
|
+
"import": "./dist/scroll-area/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"dev": "vite",
|
|
32
|
+
"build": "tsup",
|
|
33
|
+
"format": "oxfmt ."
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"oxfmt": "^0.28.0",
|
|
37
|
+
"solid-js": "^1.9.11",
|
|
38
|
+
"tsup": "^8.4.0",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"vite": "^7.3.1",
|
|
41
|
+
"vite-plugin-solid": "^2.11.10"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"solid-js": "^1.9.0"
|
|
45
|
+
}
|
|
46
|
+
}
|