@lattice-ui/scroll-area 0.3.0
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/README.md +18 -0
- package/out/ScrollArea/ScrollAreaCorner.d.ts +3 -0
- package/out/ScrollArea/ScrollAreaCorner.luau +29 -0
- package/out/ScrollArea/ScrollAreaRoot.d.ts +4 -0
- package/out/ScrollArea/ScrollAreaRoot.luau +75 -0
- package/out/ScrollArea/ScrollAreaScrollbar.d.ts +3 -0
- package/out/ScrollArea/ScrollAreaScrollbar.luau +30 -0
- package/out/ScrollArea/ScrollAreaThumb.d.ts +3 -0
- package/out/ScrollArea/ScrollAreaThumb.luau +40 -0
- package/out/ScrollArea/ScrollAreaViewport.d.ts +3 -0
- package/out/ScrollArea/ScrollAreaViewport.luau +74 -0
- package/out/ScrollArea/context.d.ts +3 -0
- package/out/ScrollArea/context.luau +10 -0
- package/out/ScrollArea/scrollMath.d.ts +3 -0
- package/out/ScrollArea/scrollMath.luau +36 -0
- package/out/ScrollArea/types.d.ts +46 -0
- package/out/ScrollArea/types.luau +2 -0
- package/out/index.d.ts +14 -0
- package/out/init.luau +21 -0
- package/package.json +23 -0
- package/src/ScrollArea/ScrollAreaCorner.tsx +29 -0
- package/src/ScrollArea/ScrollAreaRoot.tsx +99 -0
- package/src/ScrollArea/ScrollAreaScrollbar.tsx +31 -0
- package/src/ScrollArea/ScrollAreaThumb.tsx +51 -0
- package/src/ScrollArea/ScrollAreaViewport.tsx +86 -0
- package/src/ScrollArea/context.ts +6 -0
- package/src/ScrollArea/scrollMath.ts +45 -0
- package/src/ScrollArea/types.ts +51 -0
- package/src/index.ts +26 -0
- package/tsconfig.json +16 -0
- package/tsconfig.typecheck.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @lattice-ui/scroll-area
|
|
2
|
+
|
|
3
|
+
Headless scroll area primitives for Roblox UI with custom scrollbars and thumbs.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
- `ScrollArea`
|
|
8
|
+
- `ScrollArea.Root`
|
|
9
|
+
- `ScrollArea.Viewport`
|
|
10
|
+
- `ScrollArea.Scrollbar`
|
|
11
|
+
- `ScrollArea.Thumb`
|
|
12
|
+
- `ScrollArea.Corner`
|
|
13
|
+
|
|
14
|
+
## Notes
|
|
15
|
+
|
|
16
|
+
- Supports vertical and horizontal scrollbar primitives.
|
|
17
|
+
- `type` supports `auto`, `always`, and `scroll` visibility policies.
|
|
18
|
+
- Thumb/canvas math helpers are exported for unit testing.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
|
|
4
|
+
local React = _core.React
|
|
5
|
+
local Slot = _core.Slot
|
|
6
|
+
local useScrollAreaContext = TS.import(script, script.Parent, "context").useScrollAreaContext
|
|
7
|
+
local function ScrollAreaCorner(props)
|
|
8
|
+
local scrollAreaContext = useScrollAreaContext()
|
|
9
|
+
local visible = scrollAreaContext.showHorizontalScrollbar and scrollAreaContext.showVerticalScrollbar
|
|
10
|
+
if props.asChild then
|
|
11
|
+
local child = props.children
|
|
12
|
+
if not child then
|
|
13
|
+
error("[ScrollAreaCorner] `asChild` requires a child element.")
|
|
14
|
+
end
|
|
15
|
+
return React.createElement(Slot, {
|
|
16
|
+
Visible = visible,
|
|
17
|
+
}, child)
|
|
18
|
+
end
|
|
19
|
+
return React.createElement("frame", {
|
|
20
|
+
BackgroundColor3 = Color3.fromRGB(44, 52, 67),
|
|
21
|
+
BorderSizePixel = 0,
|
|
22
|
+
Position = UDim2.fromScale(1, 1),
|
|
23
|
+
Size = UDim2.fromOffset(8, 8),
|
|
24
|
+
Visible = visible,
|
|
25
|
+
}, props.children)
|
|
26
|
+
end
|
|
27
|
+
return {
|
|
28
|
+
ScrollAreaCorner = ScrollAreaCorner,
|
|
29
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local React = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).React
|
|
4
|
+
local ScrollAreaContextProvider = TS.import(script, script.Parent, "context").ScrollAreaContextProvider
|
|
5
|
+
local function createAxisMetrics()
|
|
6
|
+
return {
|
|
7
|
+
viewportSize = 0,
|
|
8
|
+
contentSize = 0,
|
|
9
|
+
scrollPosition = 0,
|
|
10
|
+
}
|
|
11
|
+
end
|
|
12
|
+
local function ScrollAreaRoot(props)
|
|
13
|
+
local scrollType = props.type or "auto"
|
|
14
|
+
local _condition = props.scrollHideDelayMs
|
|
15
|
+
if _condition == nil then
|
|
16
|
+
_condition = 600
|
|
17
|
+
end
|
|
18
|
+
local scrollHideDelayMs = math.max(0, _condition)
|
|
19
|
+
local viewportRef = React.useRef()
|
|
20
|
+
local metrics, setMetrics = React.useState(function()
|
|
21
|
+
return {
|
|
22
|
+
vertical = createAxisMetrics(),
|
|
23
|
+
horizontal = createAxisMetrics(),
|
|
24
|
+
}
|
|
25
|
+
end)
|
|
26
|
+
local showScrollbarsFromActivity, setShowScrollbarsFromActivity = React.useState(scrollType ~= "scroll")
|
|
27
|
+
local activitySequenceRef = React.useRef(0)
|
|
28
|
+
React.useEffect(function()
|
|
29
|
+
if scrollType ~= "scroll" then
|
|
30
|
+
setShowScrollbarsFromActivity(true)
|
|
31
|
+
end
|
|
32
|
+
end, { scrollType })
|
|
33
|
+
local setViewport = React.useCallback(function(instance)
|
|
34
|
+
viewportRef.current = instance
|
|
35
|
+
end, {})
|
|
36
|
+
local notifyScrollActivity = React.useCallback(function()
|
|
37
|
+
if scrollType ~= "scroll" then
|
|
38
|
+
return nil
|
|
39
|
+
end
|
|
40
|
+
activitySequenceRef.current += 1
|
|
41
|
+
local sequence = activitySequenceRef.current
|
|
42
|
+
setShowScrollbarsFromActivity(true)
|
|
43
|
+
task.delay(scrollHideDelayMs / 1000, function()
|
|
44
|
+
if sequence ~= activitySequenceRef.current then
|
|
45
|
+
return nil
|
|
46
|
+
end
|
|
47
|
+
setShowScrollbarsFromActivity(false)
|
|
48
|
+
end)
|
|
49
|
+
end, { scrollHideDelayMs, scrollType })
|
|
50
|
+
local hasVerticalOverflow = metrics.vertical.contentSize > metrics.vertical.viewportSize + 1
|
|
51
|
+
local hasHorizontalOverflow = metrics.horizontal.contentSize > metrics.horizontal.viewportSize + 1
|
|
52
|
+
local showVerticalScrollbar = if scrollType == "always" then hasVerticalOverflow elseif scrollType == "scroll" then hasVerticalOverflow and showScrollbarsFromActivity else hasVerticalOverflow
|
|
53
|
+
local showHorizontalScrollbar = if scrollType == "always" then hasHorizontalOverflow elseif scrollType == "scroll" then hasHorizontalOverflow and showScrollbarsFromActivity else hasHorizontalOverflow
|
|
54
|
+
local contextValue = React.useMemo(function()
|
|
55
|
+
return {
|
|
56
|
+
type = scrollType,
|
|
57
|
+
scrollHideDelayMs = scrollHideDelayMs,
|
|
58
|
+
viewportRef = viewportRef,
|
|
59
|
+
setViewport = setViewport,
|
|
60
|
+
vertical = metrics.vertical,
|
|
61
|
+
horizontal = metrics.horizontal,
|
|
62
|
+
setMetrics = setMetrics,
|
|
63
|
+
notifyScrollActivity = notifyScrollActivity,
|
|
64
|
+
showVerticalScrollbar = showVerticalScrollbar,
|
|
65
|
+
showHorizontalScrollbar = showHorizontalScrollbar,
|
|
66
|
+
}
|
|
67
|
+
end, { metrics.horizontal, metrics.vertical, notifyScrollActivity, scrollHideDelayMs, setViewport, showHorizontalScrollbar, showVerticalScrollbar, scrollType })
|
|
68
|
+
return React.createElement(ScrollAreaContextProvider, {
|
|
69
|
+
value = contextValue,
|
|
70
|
+
}, props.children)
|
|
71
|
+
end
|
|
72
|
+
return {
|
|
73
|
+
ScrollAreaRoot = ScrollAreaRoot,
|
|
74
|
+
ScrollArea = ScrollAreaRoot,
|
|
75
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
|
|
4
|
+
local React = _core.React
|
|
5
|
+
local Slot = _core.Slot
|
|
6
|
+
local useScrollAreaContext = TS.import(script, script.Parent, "context").useScrollAreaContext
|
|
7
|
+
local function ScrollAreaScrollbar(props)
|
|
8
|
+
local scrollAreaContext = useScrollAreaContext()
|
|
9
|
+
local vertical = props.orientation == "vertical"
|
|
10
|
+
local visible = if vertical then scrollAreaContext.showVerticalScrollbar else scrollAreaContext.showHorizontalScrollbar
|
|
11
|
+
if props.asChild then
|
|
12
|
+
local child = props.children
|
|
13
|
+
if not child then
|
|
14
|
+
error("[ScrollAreaScrollbar] `asChild` requires a child element.")
|
|
15
|
+
end
|
|
16
|
+
return React.createElement(Slot, {
|
|
17
|
+
Visible = visible,
|
|
18
|
+
}, child)
|
|
19
|
+
end
|
|
20
|
+
return React.createElement("frame", {
|
|
21
|
+
BackgroundColor3 = Color3.fromRGB(44, 52, 67),
|
|
22
|
+
BorderSizePixel = 0,
|
|
23
|
+
Position = if vertical then UDim2.fromScale(1, 0) else UDim2.fromScale(0, 1),
|
|
24
|
+
Size = if vertical then UDim2.fromOffset(8, 160) else UDim2.fromOffset(260, 8),
|
|
25
|
+
Visible = visible,
|
|
26
|
+
}, props.children)
|
|
27
|
+
end
|
|
28
|
+
return {
|
|
29
|
+
ScrollAreaScrollbar = ScrollAreaScrollbar,
|
|
30
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
|
|
4
|
+
local React = _core.React
|
|
5
|
+
local Slot = _core.Slot
|
|
6
|
+
local useScrollAreaContext = TS.import(script, script.Parent, "context").useScrollAreaContext
|
|
7
|
+
local _scrollMath = TS.import(script, script.Parent, "scrollMath")
|
|
8
|
+
local resolveThumbOffset = _scrollMath.resolveThumbOffset
|
|
9
|
+
local resolveThumbSize = _scrollMath.resolveThumbSize
|
|
10
|
+
local function ScrollAreaThumb(props)
|
|
11
|
+
local scrollAreaContext = useScrollAreaContext()
|
|
12
|
+
local vertical = props.orientation == "vertical"
|
|
13
|
+
local axisMetrics = if vertical then scrollAreaContext.vertical else scrollAreaContext.horizontal
|
|
14
|
+
local trackSize = math.max(1, axisMetrics.viewportSize)
|
|
15
|
+
local thumbSize = resolveThumbSize(axisMetrics.viewportSize, axisMetrics.contentSize, trackSize)
|
|
16
|
+
local thumbOffset = resolveThumbOffset(axisMetrics.scrollPosition, axisMetrics.viewportSize, axisMetrics.contentSize, trackSize, thumbSize)
|
|
17
|
+
local sizeScale = if trackSize > 0 then thumbSize / trackSize else 1
|
|
18
|
+
local offsetScale = if trackSize > 0 then thumbOffset / trackSize else 0
|
|
19
|
+
if props.asChild then
|
|
20
|
+
local child = props.children
|
|
21
|
+
if not child then
|
|
22
|
+
error("[ScrollAreaThumb] `asChild` requires a child element.")
|
|
23
|
+
end
|
|
24
|
+
return React.createElement(Slot, {
|
|
25
|
+
Position = if vertical then UDim2.fromScale(0, offsetScale) else UDim2.fromScale(offsetScale, 0),
|
|
26
|
+
Size = if vertical then UDim2.fromScale(1, sizeScale) else UDim2.fromScale(sizeScale, 1),
|
|
27
|
+
}, child)
|
|
28
|
+
end
|
|
29
|
+
return React.createElement("frame", {
|
|
30
|
+
BackgroundColor3 = Color3.fromRGB(118, 128, 149),
|
|
31
|
+
BorderSizePixel = 0,
|
|
32
|
+
Position = if vertical then UDim2.fromScale(0, offsetScale) else UDim2.fromScale(offsetScale, 0),
|
|
33
|
+
Size = if vertical then UDim2.fromScale(1, sizeScale) else UDim2.fromScale(sizeScale, 1),
|
|
34
|
+
}, React.createElement("uicorner", {
|
|
35
|
+
CornerRadius = UDim.new(1, 0),
|
|
36
|
+
}), props.children)
|
|
37
|
+
end
|
|
38
|
+
return {
|
|
39
|
+
ScrollAreaThumb = ScrollAreaThumb,
|
|
40
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
|
|
4
|
+
local React = _core.React
|
|
5
|
+
local Slot = _core.Slot
|
|
6
|
+
local useScrollAreaContext = TS.import(script, script.Parent, "context").useScrollAreaContext
|
|
7
|
+
local function toScrollingFrame(instance)
|
|
8
|
+
if not instance or not instance:IsA("ScrollingFrame") then
|
|
9
|
+
return nil
|
|
10
|
+
end
|
|
11
|
+
return instance
|
|
12
|
+
end
|
|
13
|
+
local function ScrollAreaViewport(props)
|
|
14
|
+
local scrollAreaContext = useScrollAreaContext()
|
|
15
|
+
local setViewportRef = React.useCallback(function(instance)
|
|
16
|
+
scrollAreaContext.setViewport(toScrollingFrame(instance))
|
|
17
|
+
end, { scrollAreaContext })
|
|
18
|
+
React.useEffect(function()
|
|
19
|
+
local viewport = scrollAreaContext.viewportRef.current
|
|
20
|
+
if not viewport then
|
|
21
|
+
return nil
|
|
22
|
+
end
|
|
23
|
+
local updateMetrics = function()
|
|
24
|
+
scrollAreaContext.setMetrics({
|
|
25
|
+
vertical = {
|
|
26
|
+
viewportSize = viewport.AbsoluteWindowSize.Y,
|
|
27
|
+
contentSize = viewport.AbsoluteCanvasSize.Y,
|
|
28
|
+
scrollPosition = viewport.CanvasPosition.Y,
|
|
29
|
+
},
|
|
30
|
+
horizontal = {
|
|
31
|
+
viewportSize = viewport.AbsoluteWindowSize.X,
|
|
32
|
+
contentSize = viewport.AbsoluteCanvasSize.X,
|
|
33
|
+
scrollPosition = viewport.CanvasPosition.X,
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
end
|
|
37
|
+
updateMetrics()
|
|
38
|
+
local canvasConnection = viewport:GetPropertyChangedSignal("CanvasPosition"):Connect(function()
|
|
39
|
+
updateMetrics()
|
|
40
|
+
scrollAreaContext.notifyScrollActivity()
|
|
41
|
+
end)
|
|
42
|
+
local absoluteCanvasConnection = viewport:GetPropertyChangedSignal("AbsoluteCanvasSize"):Connect(updateMetrics)
|
|
43
|
+
local absoluteWindowConnection = viewport:GetPropertyChangedSignal("AbsoluteWindowSize"):Connect(updateMetrics)
|
|
44
|
+
return function()
|
|
45
|
+
canvasConnection:Disconnect()
|
|
46
|
+
absoluteCanvasConnection:Disconnect()
|
|
47
|
+
absoluteWindowConnection:Disconnect()
|
|
48
|
+
end
|
|
49
|
+
end, { scrollAreaContext })
|
|
50
|
+
if props.asChild then
|
|
51
|
+
local child = props.children
|
|
52
|
+
if not child then
|
|
53
|
+
error("[ScrollAreaViewport] `asChild` requires a child element.")
|
|
54
|
+
end
|
|
55
|
+
return React.createElement(Slot, {
|
|
56
|
+
ref = setViewportRef,
|
|
57
|
+
}, child)
|
|
58
|
+
end
|
|
59
|
+
return React.createElement("scrollingframe", {
|
|
60
|
+
Active = true,
|
|
61
|
+
AutomaticCanvasSize = Enum.AutomaticSize.XY,
|
|
62
|
+
BackgroundTransparency = 1,
|
|
63
|
+
BorderSizePixel = 0,
|
|
64
|
+
CanvasSize = UDim2.fromScale(0, 0),
|
|
65
|
+
ScrollBarImageTransparency = 1,
|
|
66
|
+
ScrollBarThickness = 0,
|
|
67
|
+
ScrollingDirection = Enum.ScrollingDirection.XY,
|
|
68
|
+
Size = UDim2.fromOffset(260, 160),
|
|
69
|
+
ref = setViewportRef,
|
|
70
|
+
}, props.children)
|
|
71
|
+
end
|
|
72
|
+
return {
|
|
73
|
+
ScrollAreaViewport = ScrollAreaViewport,
|
|
74
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ScrollAreaContextValue } from "./types";
|
|
2
|
+
declare const ScrollAreaContextProvider: import("@rbxts/react").Provider<ScrollAreaContextValue | undefined>, useScrollAreaContext: () => ScrollAreaContextValue;
|
|
3
|
+
export { ScrollAreaContextProvider, useScrollAreaContext };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local createStrictContext = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).createStrictContext
|
|
4
|
+
local _binding = createStrictContext("ScrollArea")
|
|
5
|
+
local ScrollAreaContextProvider = _binding[1]
|
|
6
|
+
local useScrollAreaContext = _binding[2]
|
|
7
|
+
return {
|
|
8
|
+
ScrollAreaContextProvider = ScrollAreaContextProvider,
|
|
9
|
+
useScrollAreaContext = useScrollAreaContext,
|
|
10
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function resolveThumbSize(viewportSize: number, contentSize: number, trackSize: number, minimumThumbSize?: number): number;
|
|
2
|
+
export declare function resolveThumbOffset(scrollPosition: number, viewportSize: number, contentSize: number, trackSize: number, thumbSize: number): number;
|
|
3
|
+
export declare function resolveCanvasPositionFromThumbOffset(thumbOffset: number, viewportSize: number, contentSize: number, trackSize: number, thumbSize: number): number;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local function resolveThumbSize(viewportSize, contentSize, trackSize, minimumThumbSize)
|
|
3
|
+
if minimumThumbSize == nil then
|
|
4
|
+
minimumThumbSize = 18
|
|
5
|
+
end
|
|
6
|
+
if viewportSize <= 0 or contentSize <= 0 or trackSize <= 0 then
|
|
7
|
+
return minimumThumbSize
|
|
8
|
+
end
|
|
9
|
+
if contentSize <= viewportSize then
|
|
10
|
+
return trackSize
|
|
11
|
+
end
|
|
12
|
+
local proportionalSize = (viewportSize / contentSize) * trackSize
|
|
13
|
+
return math.clamp(proportionalSize, minimumThumbSize, trackSize)
|
|
14
|
+
end
|
|
15
|
+
local function resolveThumbOffset(scrollPosition, viewportSize, contentSize, trackSize, thumbSize)
|
|
16
|
+
if contentSize <= viewportSize or trackSize <= thumbSize then
|
|
17
|
+
return 0
|
|
18
|
+
end
|
|
19
|
+
local maxScroll = math.max(1, contentSize - viewportSize)
|
|
20
|
+
local maxThumbOffset = trackSize - thumbSize
|
|
21
|
+
return math.clamp((scrollPosition / maxScroll) * maxThumbOffset, 0, maxThumbOffset)
|
|
22
|
+
end
|
|
23
|
+
local function resolveCanvasPositionFromThumbOffset(thumbOffset, viewportSize, contentSize, trackSize, thumbSize)
|
|
24
|
+
if contentSize <= viewportSize or trackSize <= thumbSize then
|
|
25
|
+
return 0
|
|
26
|
+
end
|
|
27
|
+
local maxScroll = math.max(1, contentSize - viewportSize)
|
|
28
|
+
local maxThumbOffset = trackSize - thumbSize
|
|
29
|
+
local ratio = math.clamp(thumbOffset / maxThumbOffset, 0, 1)
|
|
30
|
+
return ratio * maxScroll
|
|
31
|
+
end
|
|
32
|
+
return {
|
|
33
|
+
resolveThumbSize = resolveThumbSize,
|
|
34
|
+
resolveThumbOffset = resolveThumbOffset,
|
|
35
|
+
resolveCanvasPositionFromThumbOffset = resolveCanvasPositionFromThumbOffset,
|
|
36
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type React from "@rbxts/react";
|
|
2
|
+
export type ScrollAreaType = "auto" | "always" | "scroll";
|
|
3
|
+
export type ScrollAreaOrientation = "vertical" | "horizontal";
|
|
4
|
+
export type ScrollAxisMetrics = {
|
|
5
|
+
viewportSize: number;
|
|
6
|
+
contentSize: number;
|
|
7
|
+
scrollPosition: number;
|
|
8
|
+
};
|
|
9
|
+
export type ScrollAreaContextValue = {
|
|
10
|
+
type: ScrollAreaType;
|
|
11
|
+
scrollHideDelayMs: number;
|
|
12
|
+
viewportRef: React.MutableRefObject<ScrollingFrame | undefined>;
|
|
13
|
+
setViewport: (instance: ScrollingFrame | undefined) => void;
|
|
14
|
+
vertical: ScrollAxisMetrics;
|
|
15
|
+
horizontal: ScrollAxisMetrics;
|
|
16
|
+
setMetrics: (metrics: {
|
|
17
|
+
vertical: ScrollAxisMetrics;
|
|
18
|
+
horizontal: ScrollAxisMetrics;
|
|
19
|
+
}) => void;
|
|
20
|
+
notifyScrollActivity: () => void;
|
|
21
|
+
showVerticalScrollbar: boolean;
|
|
22
|
+
showHorizontalScrollbar: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type ScrollAreaProps = {
|
|
25
|
+
type?: ScrollAreaType;
|
|
26
|
+
scrollHideDelayMs?: number;
|
|
27
|
+
children?: React.ReactNode;
|
|
28
|
+
};
|
|
29
|
+
export type ScrollAreaViewportProps = {
|
|
30
|
+
asChild?: boolean;
|
|
31
|
+
children?: React.ReactElement;
|
|
32
|
+
};
|
|
33
|
+
export type ScrollAreaScrollbarProps = {
|
|
34
|
+
orientation: ScrollAreaOrientation;
|
|
35
|
+
asChild?: boolean;
|
|
36
|
+
children?: React.ReactElement;
|
|
37
|
+
};
|
|
38
|
+
export type ScrollAreaThumbProps = {
|
|
39
|
+
orientation: ScrollAreaOrientation;
|
|
40
|
+
asChild?: boolean;
|
|
41
|
+
children?: React.ReactElement;
|
|
42
|
+
};
|
|
43
|
+
export type ScrollAreaCornerProps = {
|
|
44
|
+
asChild?: boolean;
|
|
45
|
+
children?: React.ReactElement;
|
|
46
|
+
};
|
package/out/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ScrollAreaCorner } from "./ScrollArea/ScrollAreaCorner";
|
|
2
|
+
import { ScrollAreaRoot } from "./ScrollArea/ScrollAreaRoot";
|
|
3
|
+
import { ScrollAreaScrollbar } from "./ScrollArea/ScrollAreaScrollbar";
|
|
4
|
+
import { ScrollAreaThumb } from "./ScrollArea/ScrollAreaThumb";
|
|
5
|
+
import { ScrollAreaViewport } from "./ScrollArea/ScrollAreaViewport";
|
|
6
|
+
export declare const ScrollArea: {
|
|
7
|
+
readonly Root: typeof ScrollAreaRoot;
|
|
8
|
+
readonly Viewport: typeof ScrollAreaViewport;
|
|
9
|
+
readonly Scrollbar: typeof ScrollAreaScrollbar;
|
|
10
|
+
readonly Thumb: typeof ScrollAreaThumb;
|
|
11
|
+
readonly Corner: typeof ScrollAreaCorner;
|
|
12
|
+
};
|
|
13
|
+
export { resolveCanvasPositionFromThumbOffset, resolveThumbOffset, resolveThumbSize } from "./ScrollArea/scrollMath";
|
|
14
|
+
export type { ScrollAreaContextValue, ScrollAreaCornerProps, ScrollAreaOrientation, ScrollAreaProps, ScrollAreaScrollbarProps, ScrollAreaThumbProps, ScrollAreaType, ScrollAreaViewportProps, ScrollAxisMetrics, } from "./ScrollArea/types";
|
package/out/init.luau
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local exports = {}
|
|
4
|
+
local ScrollAreaCorner = TS.import(script, script, "ScrollArea", "ScrollAreaCorner").ScrollAreaCorner
|
|
5
|
+
local ScrollAreaRoot = TS.import(script, script, "ScrollArea", "ScrollAreaRoot").ScrollAreaRoot
|
|
6
|
+
local ScrollAreaScrollbar = TS.import(script, script, "ScrollArea", "ScrollAreaScrollbar").ScrollAreaScrollbar
|
|
7
|
+
local ScrollAreaThumb = TS.import(script, script, "ScrollArea", "ScrollAreaThumb").ScrollAreaThumb
|
|
8
|
+
local ScrollAreaViewport = TS.import(script, script, "ScrollArea", "ScrollAreaViewport").ScrollAreaViewport
|
|
9
|
+
local ScrollArea = {
|
|
10
|
+
Root = ScrollAreaRoot,
|
|
11
|
+
Viewport = ScrollAreaViewport,
|
|
12
|
+
Scrollbar = ScrollAreaScrollbar,
|
|
13
|
+
Thumb = ScrollAreaThumb,
|
|
14
|
+
Corner = ScrollAreaCorner,
|
|
15
|
+
}
|
|
16
|
+
local _scrollMath = TS.import(script, script, "ScrollArea", "scrollMath")
|
|
17
|
+
exports.resolveCanvasPositionFromThumbOffset = _scrollMath.resolveCanvasPositionFromThumbOffset
|
|
18
|
+
exports.resolveThumbOffset = _scrollMath.resolveThumbOffset
|
|
19
|
+
exports.resolveThumbSize = _scrollMath.resolveThumbSize
|
|
20
|
+
exports.ScrollArea = ScrollArea
|
|
21
|
+
return exports
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lattice-ui/scroll-area",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"main": "out/init.luau",
|
|
6
|
+
"types": "out/index.d.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@lattice-ui/core": "0.3.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@rbxts/react": "17.3.7-ts.1",
|
|
12
|
+
"@rbxts/react-roblox": "17.3.7-ts.1"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@rbxts/react": "^17",
|
|
16
|
+
"@rbxts/react-roblox": "^17"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "rbxtsc -p tsconfig.json",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.typecheck.json",
|
|
21
|
+
"watch": "rbxtsc -p tsconfig.json -w"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import { useScrollAreaContext } from "./context";
|
|
3
|
+
import type { ScrollAreaCornerProps } from "./types";
|
|
4
|
+
|
|
5
|
+
export function ScrollAreaCorner(props: ScrollAreaCornerProps) {
|
|
6
|
+
const scrollAreaContext = useScrollAreaContext();
|
|
7
|
+
const visible = scrollAreaContext.showHorizontalScrollbar && scrollAreaContext.showVerticalScrollbar;
|
|
8
|
+
|
|
9
|
+
if (props.asChild) {
|
|
10
|
+
const child = props.children;
|
|
11
|
+
if (!child) {
|
|
12
|
+
error("[ScrollAreaCorner] `asChild` requires a child element.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return <Slot Visible={visible}>{child}</Slot>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<frame
|
|
20
|
+
BackgroundColor3={Color3.fromRGB(44, 52, 67)}
|
|
21
|
+
BorderSizePixel={0}
|
|
22
|
+
Position={UDim2.fromScale(1, 1)}
|
|
23
|
+
Size={UDim2.fromOffset(8, 8)}
|
|
24
|
+
Visible={visible}
|
|
25
|
+
>
|
|
26
|
+
{props.children}
|
|
27
|
+
</frame>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { React } from "@lattice-ui/core";
|
|
2
|
+
import { ScrollAreaContextProvider } from "./context";
|
|
3
|
+
import type { ScrollAreaProps, ScrollAxisMetrics } from "./types";
|
|
4
|
+
|
|
5
|
+
function createAxisMetrics(): ScrollAxisMetrics {
|
|
6
|
+
return {
|
|
7
|
+
viewportSize: 0,
|
|
8
|
+
contentSize: 0,
|
|
9
|
+
scrollPosition: 0,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ScrollAreaRoot(props: ScrollAreaProps) {
|
|
14
|
+
const scrollType = props.type ?? "auto";
|
|
15
|
+
const scrollHideDelayMs = math.max(0, props.scrollHideDelayMs ?? 600);
|
|
16
|
+
|
|
17
|
+
const viewportRef = React.useRef<ScrollingFrame>();
|
|
18
|
+
const [metrics, setMetrics] = React.useState(() => ({
|
|
19
|
+
vertical: createAxisMetrics(),
|
|
20
|
+
horizontal: createAxisMetrics(),
|
|
21
|
+
}));
|
|
22
|
+
const [showScrollbarsFromActivity, setShowScrollbarsFromActivity] = React.useState(scrollType !== "scroll");
|
|
23
|
+
const activitySequenceRef = React.useRef(0);
|
|
24
|
+
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
if (scrollType !== "scroll") {
|
|
27
|
+
setShowScrollbarsFromActivity(true);
|
|
28
|
+
}
|
|
29
|
+
}, [scrollType]);
|
|
30
|
+
|
|
31
|
+
const setViewport = React.useCallback((instance: ScrollingFrame | undefined) => {
|
|
32
|
+
viewportRef.current = instance;
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const notifyScrollActivity = React.useCallback(() => {
|
|
36
|
+
if (scrollType !== "scroll") {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
activitySequenceRef.current += 1;
|
|
41
|
+
const sequence = activitySequenceRef.current;
|
|
42
|
+
|
|
43
|
+
setShowScrollbarsFromActivity(true);
|
|
44
|
+
|
|
45
|
+
task.delay(scrollHideDelayMs / 1000, () => {
|
|
46
|
+
if (sequence !== activitySequenceRef.current) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setShowScrollbarsFromActivity(false);
|
|
51
|
+
});
|
|
52
|
+
}, [scrollHideDelayMs, scrollType]);
|
|
53
|
+
|
|
54
|
+
const hasVerticalOverflow = metrics.vertical.contentSize > metrics.vertical.viewportSize + 1;
|
|
55
|
+
const hasHorizontalOverflow = metrics.horizontal.contentSize > metrics.horizontal.viewportSize + 1;
|
|
56
|
+
|
|
57
|
+
const showVerticalScrollbar =
|
|
58
|
+
scrollType === "always"
|
|
59
|
+
? hasVerticalOverflow
|
|
60
|
+
: scrollType === "scroll"
|
|
61
|
+
? hasVerticalOverflow && showScrollbarsFromActivity
|
|
62
|
+
: hasVerticalOverflow;
|
|
63
|
+
|
|
64
|
+
const showHorizontalScrollbar =
|
|
65
|
+
scrollType === "always"
|
|
66
|
+
? hasHorizontalOverflow
|
|
67
|
+
: scrollType === "scroll"
|
|
68
|
+
? hasHorizontalOverflow && showScrollbarsFromActivity
|
|
69
|
+
: hasHorizontalOverflow;
|
|
70
|
+
|
|
71
|
+
const contextValue = React.useMemo(
|
|
72
|
+
() => ({
|
|
73
|
+
type: scrollType,
|
|
74
|
+
scrollHideDelayMs,
|
|
75
|
+
viewportRef,
|
|
76
|
+
setViewport,
|
|
77
|
+
vertical: metrics.vertical,
|
|
78
|
+
horizontal: metrics.horizontal,
|
|
79
|
+
setMetrics,
|
|
80
|
+
notifyScrollActivity,
|
|
81
|
+
showVerticalScrollbar,
|
|
82
|
+
showHorizontalScrollbar,
|
|
83
|
+
}),
|
|
84
|
+
[
|
|
85
|
+
metrics.horizontal,
|
|
86
|
+
metrics.vertical,
|
|
87
|
+
notifyScrollActivity,
|
|
88
|
+
scrollHideDelayMs,
|
|
89
|
+
setViewport,
|
|
90
|
+
showHorizontalScrollbar,
|
|
91
|
+
showVerticalScrollbar,
|
|
92
|
+
scrollType,
|
|
93
|
+
],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return <ScrollAreaContextProvider value={contextValue}>{props.children}</ScrollAreaContextProvider>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { ScrollAreaRoot as ScrollArea };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import { useScrollAreaContext } from "./context";
|
|
3
|
+
import type { ScrollAreaScrollbarProps } from "./types";
|
|
4
|
+
|
|
5
|
+
export function ScrollAreaScrollbar(props: ScrollAreaScrollbarProps) {
|
|
6
|
+
const scrollAreaContext = useScrollAreaContext();
|
|
7
|
+
|
|
8
|
+
const vertical = props.orientation === "vertical";
|
|
9
|
+
const visible = vertical ? scrollAreaContext.showVerticalScrollbar : scrollAreaContext.showHorizontalScrollbar;
|
|
10
|
+
|
|
11
|
+
if (props.asChild) {
|
|
12
|
+
const child = props.children;
|
|
13
|
+
if (!child) {
|
|
14
|
+
error("[ScrollAreaScrollbar] `asChild` requires a child element.");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return <Slot Visible={visible}>{child}</Slot>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<frame
|
|
22
|
+
BackgroundColor3={Color3.fromRGB(44, 52, 67)}
|
|
23
|
+
BorderSizePixel={0}
|
|
24
|
+
Position={vertical ? UDim2.fromScale(1, 0) : UDim2.fromScale(0, 1)}
|
|
25
|
+
Size={vertical ? UDim2.fromOffset(8, 160) : UDim2.fromOffset(260, 8)}
|
|
26
|
+
Visible={visible}
|
|
27
|
+
>
|
|
28
|
+
{props.children}
|
|
29
|
+
</frame>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import { useScrollAreaContext } from "./context";
|
|
3
|
+
import { resolveThumbOffset, resolveThumbSize } from "./scrollMath";
|
|
4
|
+
import type { ScrollAreaThumbProps } from "./types";
|
|
5
|
+
|
|
6
|
+
export function ScrollAreaThumb(props: ScrollAreaThumbProps) {
|
|
7
|
+
const scrollAreaContext = useScrollAreaContext();
|
|
8
|
+
const vertical = props.orientation === "vertical";
|
|
9
|
+
|
|
10
|
+
const axisMetrics = vertical ? scrollAreaContext.vertical : scrollAreaContext.horizontal;
|
|
11
|
+
const trackSize = math.max(1, axisMetrics.viewportSize);
|
|
12
|
+
const thumbSize = resolveThumbSize(axisMetrics.viewportSize, axisMetrics.contentSize, trackSize);
|
|
13
|
+
const thumbOffset = resolveThumbOffset(
|
|
14
|
+
axisMetrics.scrollPosition,
|
|
15
|
+
axisMetrics.viewportSize,
|
|
16
|
+
axisMetrics.contentSize,
|
|
17
|
+
trackSize,
|
|
18
|
+
thumbSize,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const sizeScale = trackSize > 0 ? thumbSize / trackSize : 1;
|
|
22
|
+
const offsetScale = trackSize > 0 ? thumbOffset / trackSize : 0;
|
|
23
|
+
|
|
24
|
+
if (props.asChild) {
|
|
25
|
+
const child = props.children;
|
|
26
|
+
if (!child) {
|
|
27
|
+
error("[ScrollAreaThumb] `asChild` requires a child element.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Slot
|
|
32
|
+
Position={vertical ? UDim2.fromScale(0, offsetScale) : UDim2.fromScale(offsetScale, 0)}
|
|
33
|
+
Size={vertical ? UDim2.fromScale(1, sizeScale) : UDim2.fromScale(sizeScale, 1)}
|
|
34
|
+
>
|
|
35
|
+
{child}
|
|
36
|
+
</Slot>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<frame
|
|
42
|
+
BackgroundColor3={Color3.fromRGB(118, 128, 149)}
|
|
43
|
+
BorderSizePixel={0}
|
|
44
|
+
Position={vertical ? UDim2.fromScale(0, offsetScale) : UDim2.fromScale(offsetScale, 0)}
|
|
45
|
+
Size={vertical ? UDim2.fromScale(1, sizeScale) : UDim2.fromScale(sizeScale, 1)}
|
|
46
|
+
>
|
|
47
|
+
<uicorner CornerRadius={new UDim(1, 0)} />
|
|
48
|
+
{props.children}
|
|
49
|
+
</frame>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import { useScrollAreaContext } from "./context";
|
|
3
|
+
import type { ScrollAreaViewportProps } from "./types";
|
|
4
|
+
|
|
5
|
+
function toScrollingFrame(instance: Instance | undefined) {
|
|
6
|
+
if (!instance || !instance.IsA("ScrollingFrame")) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return instance;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ScrollAreaViewport(props: ScrollAreaViewportProps) {
|
|
14
|
+
const scrollAreaContext = useScrollAreaContext();
|
|
15
|
+
|
|
16
|
+
const setViewportRef = React.useCallback(
|
|
17
|
+
(instance: Instance | undefined) => {
|
|
18
|
+
scrollAreaContext.setViewport(toScrollingFrame(instance));
|
|
19
|
+
},
|
|
20
|
+
[scrollAreaContext],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
const viewport = scrollAreaContext.viewportRef.current;
|
|
25
|
+
if (!viewport) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const updateMetrics = () => {
|
|
30
|
+
scrollAreaContext.setMetrics({
|
|
31
|
+
vertical: {
|
|
32
|
+
viewportSize: viewport.AbsoluteWindowSize.Y,
|
|
33
|
+
contentSize: viewport.AbsoluteCanvasSize.Y,
|
|
34
|
+
scrollPosition: viewport.CanvasPosition.Y,
|
|
35
|
+
},
|
|
36
|
+
horizontal: {
|
|
37
|
+
viewportSize: viewport.AbsoluteWindowSize.X,
|
|
38
|
+
contentSize: viewport.AbsoluteCanvasSize.X,
|
|
39
|
+
scrollPosition: viewport.CanvasPosition.X,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
updateMetrics();
|
|
45
|
+
|
|
46
|
+
const canvasConnection = viewport.GetPropertyChangedSignal("CanvasPosition").Connect(() => {
|
|
47
|
+
updateMetrics();
|
|
48
|
+
scrollAreaContext.notifyScrollActivity();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const absoluteCanvasConnection = viewport.GetPropertyChangedSignal("AbsoluteCanvasSize").Connect(updateMetrics);
|
|
52
|
+
const absoluteWindowConnection = viewport.GetPropertyChangedSignal("AbsoluteWindowSize").Connect(updateMetrics);
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
canvasConnection.Disconnect();
|
|
56
|
+
absoluteCanvasConnection.Disconnect();
|
|
57
|
+
absoluteWindowConnection.Disconnect();
|
|
58
|
+
};
|
|
59
|
+
}, [scrollAreaContext]);
|
|
60
|
+
|
|
61
|
+
if (props.asChild) {
|
|
62
|
+
const child = props.children;
|
|
63
|
+
if (!child) {
|
|
64
|
+
error("[ScrollAreaViewport] `asChild` requires a child element.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return <Slot ref={setViewportRef}>{child}</Slot>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<scrollingframe
|
|
72
|
+
Active
|
|
73
|
+
AutomaticCanvasSize={Enum.AutomaticSize.XY}
|
|
74
|
+
BackgroundTransparency={1}
|
|
75
|
+
BorderSizePixel={0}
|
|
76
|
+
CanvasSize={UDim2.fromScale(0, 0)}
|
|
77
|
+
ScrollBarImageTransparency={1}
|
|
78
|
+
ScrollBarThickness={0}
|
|
79
|
+
ScrollingDirection={Enum.ScrollingDirection.XY}
|
|
80
|
+
Size={UDim2.fromOffset(260, 160)}
|
|
81
|
+
ref={setViewportRef}
|
|
82
|
+
>
|
|
83
|
+
{props.children}
|
|
84
|
+
</scrollingframe>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createStrictContext } from "@lattice-ui/core";
|
|
2
|
+
import type { ScrollAreaContextValue } from "./types";
|
|
3
|
+
|
|
4
|
+
const [ScrollAreaContextProvider, useScrollAreaContext] = createStrictContext<ScrollAreaContextValue>("ScrollArea");
|
|
5
|
+
|
|
6
|
+
export { ScrollAreaContextProvider, useScrollAreaContext };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function resolveThumbSize(viewportSize: number, contentSize: number, trackSize: number, minimumThumbSize = 18) {
|
|
2
|
+
if (viewportSize <= 0 || contentSize <= 0 || trackSize <= 0) {
|
|
3
|
+
return minimumThumbSize;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (contentSize <= viewportSize) {
|
|
7
|
+
return trackSize;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const proportionalSize = (viewportSize / contentSize) * trackSize;
|
|
11
|
+
return math.clamp(proportionalSize, minimumThumbSize, trackSize);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveThumbOffset(
|
|
15
|
+
scrollPosition: number,
|
|
16
|
+
viewportSize: number,
|
|
17
|
+
contentSize: number,
|
|
18
|
+
trackSize: number,
|
|
19
|
+
thumbSize: number,
|
|
20
|
+
) {
|
|
21
|
+
if (contentSize <= viewportSize || trackSize <= thumbSize) {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const maxScroll = math.max(1, contentSize - viewportSize);
|
|
26
|
+
const maxThumbOffset = trackSize - thumbSize;
|
|
27
|
+
return math.clamp((scrollPosition / maxScroll) * maxThumbOffset, 0, maxThumbOffset);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveCanvasPositionFromThumbOffset(
|
|
31
|
+
thumbOffset: number,
|
|
32
|
+
viewportSize: number,
|
|
33
|
+
contentSize: number,
|
|
34
|
+
trackSize: number,
|
|
35
|
+
thumbSize: number,
|
|
36
|
+
) {
|
|
37
|
+
if (contentSize <= viewportSize || trackSize <= thumbSize) {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const maxScroll = math.max(1, contentSize - viewportSize);
|
|
42
|
+
const maxThumbOffset = trackSize - thumbSize;
|
|
43
|
+
const ratio = math.clamp(thumbOffset / maxThumbOffset, 0, 1);
|
|
44
|
+
return ratio * maxScroll;
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type React from "@rbxts/react";
|
|
2
|
+
|
|
3
|
+
export type ScrollAreaType = "auto" | "always" | "scroll";
|
|
4
|
+
export type ScrollAreaOrientation = "vertical" | "horizontal";
|
|
5
|
+
|
|
6
|
+
export type ScrollAxisMetrics = {
|
|
7
|
+
viewportSize: number;
|
|
8
|
+
contentSize: number;
|
|
9
|
+
scrollPosition: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ScrollAreaContextValue = {
|
|
13
|
+
type: ScrollAreaType;
|
|
14
|
+
scrollHideDelayMs: number;
|
|
15
|
+
viewportRef: React.MutableRefObject<ScrollingFrame | undefined>;
|
|
16
|
+
setViewport: (instance: ScrollingFrame | undefined) => void;
|
|
17
|
+
vertical: ScrollAxisMetrics;
|
|
18
|
+
horizontal: ScrollAxisMetrics;
|
|
19
|
+
setMetrics: (metrics: { vertical: ScrollAxisMetrics; horizontal: ScrollAxisMetrics }) => void;
|
|
20
|
+
notifyScrollActivity: () => void;
|
|
21
|
+
showVerticalScrollbar: boolean;
|
|
22
|
+
showHorizontalScrollbar: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ScrollAreaProps = {
|
|
26
|
+
type?: ScrollAreaType;
|
|
27
|
+
scrollHideDelayMs?: number;
|
|
28
|
+
children?: React.ReactNode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ScrollAreaViewportProps = {
|
|
32
|
+
asChild?: boolean;
|
|
33
|
+
children?: React.ReactElement;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ScrollAreaScrollbarProps = {
|
|
37
|
+
orientation: ScrollAreaOrientation;
|
|
38
|
+
asChild?: boolean;
|
|
39
|
+
children?: React.ReactElement;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type ScrollAreaThumbProps = {
|
|
43
|
+
orientation: ScrollAreaOrientation;
|
|
44
|
+
asChild?: boolean;
|
|
45
|
+
children?: React.ReactElement;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type ScrollAreaCornerProps = {
|
|
49
|
+
asChild?: boolean;
|
|
50
|
+
children?: React.ReactElement;
|
|
51
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ScrollAreaCorner } from "./ScrollArea/ScrollAreaCorner";
|
|
2
|
+
import { ScrollAreaRoot } from "./ScrollArea/ScrollAreaRoot";
|
|
3
|
+
import { ScrollAreaScrollbar } from "./ScrollArea/ScrollAreaScrollbar";
|
|
4
|
+
import { ScrollAreaThumb } from "./ScrollArea/ScrollAreaThumb";
|
|
5
|
+
import { ScrollAreaViewport } from "./ScrollArea/ScrollAreaViewport";
|
|
6
|
+
|
|
7
|
+
export const ScrollArea = {
|
|
8
|
+
Root: ScrollAreaRoot,
|
|
9
|
+
Viewport: ScrollAreaViewport,
|
|
10
|
+
Scrollbar: ScrollAreaScrollbar,
|
|
11
|
+
Thumb: ScrollAreaThumb,
|
|
12
|
+
Corner: ScrollAreaCorner,
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export { resolveCanvasPositionFromThumbOffset, resolveThumbOffset, resolveThumbSize } from "./ScrollArea/scrollMath";
|
|
16
|
+
export type {
|
|
17
|
+
ScrollAreaContextValue,
|
|
18
|
+
ScrollAreaCornerProps,
|
|
19
|
+
ScrollAreaOrientation,
|
|
20
|
+
ScrollAreaProps,
|
|
21
|
+
ScrollAreaScrollbarProps,
|
|
22
|
+
ScrollAreaThumbProps,
|
|
23
|
+
ScrollAreaType,
|
|
24
|
+
ScrollAreaViewportProps,
|
|
25
|
+
ScrollAxisMetrics,
|
|
26
|
+
} from "./ScrollArea/types";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "out",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"typeRoots": [
|
|
8
|
+
"./node_modules/@rbxts",
|
|
9
|
+
"../../node_modules/@rbxts",
|
|
10
|
+
"./node_modules/@lattice-ui",
|
|
11
|
+
"../../node_modules/@lattice-ui"
|
|
12
|
+
],
|
|
13
|
+
"types": ["types", "compiler-types"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"baseUrl": "..",
|
|
6
|
+
"rootDir": "..",
|
|
7
|
+
"paths": {
|
|
8
|
+
"@lattice-ui/accordion": ["accordion/src/index.ts"],
|
|
9
|
+
"@lattice-ui/avatar": ["avatar/src/index.ts"],
|
|
10
|
+
"@lattice-ui/checkbox": ["checkbox/src/index.ts"],
|
|
11
|
+
"@lattice-ui/combobox": ["combobox/src/index.ts"],
|
|
12
|
+
"@lattice-ui/core": ["core/src/index.ts"],
|
|
13
|
+
"@lattice-ui/dialog": ["dialog/src/index.ts"],
|
|
14
|
+
"@lattice-ui/focus": ["focus/src/index.ts"],
|
|
15
|
+
"@lattice-ui/layer": ["layer/src/index.ts"],
|
|
16
|
+
"@lattice-ui/menu": ["menu/src/index.ts"],
|
|
17
|
+
"@lattice-ui/popover": ["popover/src/index.ts"],
|
|
18
|
+
"@lattice-ui/popper": ["popper/src/index.ts"],
|
|
19
|
+
"@lattice-ui/progress": ["progress/src/index.ts"],
|
|
20
|
+
"@lattice-ui/radio-group": ["radio-group/src/index.ts"],
|
|
21
|
+
"@lattice-ui/scroll-area": ["scroll-area/src/index.ts"],
|
|
22
|
+
"@lattice-ui/select": ["select/src/index.ts"],
|
|
23
|
+
"@lattice-ui/slider": ["slider/src/index.ts"],
|
|
24
|
+
"@lattice-ui/style": ["style/src/index.ts"],
|
|
25
|
+
"@lattice-ui/switch": ["switch/src/index.ts"],
|
|
26
|
+
"@lattice-ui/system": ["system/src/index.ts"],
|
|
27
|
+
"@lattice-ui/tabs": ["tabs/src/index.ts"],
|
|
28
|
+
"@lattice-ui/text-field": ["text-field/src/index.ts"],
|
|
29
|
+
"@lattice-ui/textarea": ["textarea/src/index.ts"],
|
|
30
|
+
"@lattice-ui/toast": ["toast/src/index.ts"],
|
|
31
|
+
"@lattice-ui/toggle-group": ["toggle-group/src/index.ts"],
|
|
32
|
+
"@lattice-ui/tooltip": ["tooltip/src/index.ts"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|