@lattice-ui/radio-group 0.4.4 → 0.5.0-next.2
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/default.project.json +9 -0
- package/out/RadioGroup/RadioGroupIndicator.d.ts +1 -1
- package/out/RadioGroup/RadioGroupIndicator.luau +46 -7
- package/out/RadioGroup/RadioGroupItem.luau +16 -7
- package/out/RadioGroup/RadioGroupRoot.luau +4 -3
- package/out/RadioGroup/types.d.ts +3 -0
- package/package.json +14 -2
- package/src/RadioGroup/RadioGroupIndicator.tsx +95 -0
- package/src/RadioGroup/RadioGroupItem.tsx +167 -0
- package/src/RadioGroup/RadioGroupRoot.tsx +80 -0
- package/src/RadioGroup/context.ts +8 -0
- package/src/RadioGroup/types.ts +53 -0
- package/src/index.ts +25 -0
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { React } from "@lattice-ui/core";
|
|
2
2
|
import type { RadioGroupIndicatorProps } from "./types";
|
|
3
|
-
export declare function RadioGroupIndicator(props: RadioGroupIndicatorProps): React.JSX.Element
|
|
3
|
+
export declare function RadioGroupIndicator(props: RadioGroupIndicatorProps): React.JSX.Element;
|
|
@@ -3,21 +3,35 @@ local TS = _G[script]
|
|
|
3
3
|
local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
|
|
4
4
|
local React = _core.React
|
|
5
5
|
local Slot = _core.Slot
|
|
6
|
+
local Presence = TS.import(script, TS.getModule(script, "@lattice-ui", "layer").out).Presence
|
|
7
|
+
local _motion = TS.import(script, TS.getModule(script, "@lattice-ui", "motion").out)
|
|
8
|
+
local createIndicatorRevealRecipe = _motion.createIndicatorRevealRecipe
|
|
9
|
+
local usePresenceMotionController = _motion.usePresenceMotionController
|
|
6
10
|
local useRadioGroupItemContext = TS.import(script, script.Parent, "context").useRadioGroupItemContext
|
|
7
|
-
local function
|
|
8
|
-
local
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
local function RadioGroupIndicatorImpl(props)
|
|
12
|
+
local defaultTransition = React.useMemo(function()
|
|
13
|
+
return createIndicatorRevealRecipe(UDim2.fromOffset(10, 10))
|
|
14
|
+
end, {})
|
|
15
|
+
local config = props.transition or defaultTransition
|
|
16
|
+
local motion = usePresenceMotionController({
|
|
17
|
+
present = props.present,
|
|
18
|
+
forceMount = props.forceMount,
|
|
19
|
+
config = config,
|
|
20
|
+
onExitComplete = props.onExitComplete,
|
|
21
|
+
})
|
|
22
|
+
local mounted = motion.mounted
|
|
23
|
+
local visible = mounted and (motion.present or motion.phase ~= "exited")
|
|
24
|
+
if not mounted then
|
|
12
25
|
return nil
|
|
13
26
|
end
|
|
14
|
-
local child = props.children
|
|
15
27
|
if props.asChild then
|
|
28
|
+
local child = props.children
|
|
16
29
|
if not React.isValidElement(child) then
|
|
17
30
|
error("[RadioGroupIndicator] `asChild` requires a child element.")
|
|
18
31
|
end
|
|
19
32
|
return React.createElement(Slot, {
|
|
20
33
|
Visible = visible,
|
|
34
|
+
ref = motion.ref,
|
|
21
35
|
}, child)
|
|
22
36
|
end
|
|
23
37
|
return React.createElement("frame", {
|
|
@@ -25,7 +39,32 @@ local function RadioGroupIndicator(props)
|
|
|
25
39
|
BorderSizePixel = 0,
|
|
26
40
|
Size = UDim2.fromOffset(10, 10),
|
|
27
41
|
Visible = visible,
|
|
28
|
-
|
|
42
|
+
ref = motion.ref,
|
|
43
|
+
}, props.children)
|
|
44
|
+
end
|
|
45
|
+
local function RadioGroupIndicator(props)
|
|
46
|
+
local radioGroupItemContext = useRadioGroupItemContext()
|
|
47
|
+
local visible = radioGroupItemContext.checked
|
|
48
|
+
local forceMount = props.forceMount == true
|
|
49
|
+
if forceMount then
|
|
50
|
+
return React.createElement(RadioGroupIndicatorImpl, {
|
|
51
|
+
asChild = props.asChild,
|
|
52
|
+
forceMount = true,
|
|
53
|
+
present = visible,
|
|
54
|
+
transition = props.transition,
|
|
55
|
+
}, props.children)
|
|
56
|
+
end
|
|
57
|
+
return React.createElement(Presence, {
|
|
58
|
+
present = visible,
|
|
59
|
+
render = function(state)
|
|
60
|
+
return React.createElement(RadioGroupIndicatorImpl, {
|
|
61
|
+
asChild = props.asChild,
|
|
62
|
+
onExitComplete = state.onExitComplete,
|
|
63
|
+
present = state.isPresent,
|
|
64
|
+
transition = props.transition,
|
|
65
|
+
}, props.children)
|
|
66
|
+
end,
|
|
67
|
+
})
|
|
29
68
|
end
|
|
30
69
|
return {
|
|
31
70
|
RadioGroupIndicator = RadioGroupIndicator,
|
|
@@ -3,7 +3,10 @@ local TS = _G[script]
|
|
|
3
3
|
local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
|
|
4
4
|
local React = _core.React
|
|
5
5
|
local Slot = _core.Slot
|
|
6
|
-
local useFocusNode =
|
|
6
|
+
local useFocusNode = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).useFocusNode
|
|
7
|
+
local _motion = TS.import(script, TS.getModule(script, "@lattice-ui", "motion").out)
|
|
8
|
+
local createSelectionResponseRecipe = _motion.createSelectionResponseRecipe
|
|
9
|
+
local useResponseMotion = _motion.useResponseMotion
|
|
7
10
|
local _context = TS.import(script, script.Parent, "context")
|
|
8
11
|
local RadioGroupItemContextProvider = _context.RadioGroupItemContextProvider
|
|
9
12
|
local useRadioGroupContext = _context.useRadioGroupContext
|
|
@@ -45,13 +48,19 @@ local function RadioGroupItem(props)
|
|
|
45
48
|
return disabledRef.current
|
|
46
49
|
end,
|
|
47
50
|
})
|
|
51
|
+
local motionRef = useResponseMotion(checked, {
|
|
52
|
+
active = {
|
|
53
|
+
BackgroundColor3 = Color3.fromRGB(88, 142, 255),
|
|
54
|
+
},
|
|
55
|
+
inactive = {
|
|
56
|
+
BackgroundColor3 = Color3.fromRGB(47, 53, 68),
|
|
57
|
+
},
|
|
58
|
+
}, props.transition or createSelectionResponseRecipe())
|
|
48
59
|
local setItemRef = React.useCallback(function(instance)
|
|
49
|
-
if not instance or not instance:IsA("GuiObject") then
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
itemRef.current = instance
|
|
54
|
-
end, {})
|
|
60
|
+
local nextItem = if not instance or not instance:IsA("GuiObject") then nil else instance
|
|
61
|
+
itemRef.current = nextItem
|
|
62
|
+
motionRef.current = nextItem
|
|
63
|
+
end, { motionRef })
|
|
55
64
|
local handleSelect = React.useCallback(function()
|
|
56
65
|
if disabled then
|
|
57
66
|
return nil
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
-- Compiled with roblox-ts v3.0.0
|
|
2
2
|
local TS = _G[script]
|
|
3
3
|
local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
|
|
4
|
-
local findOrderedSelectionEntry = _core.findOrderedSelectionEntry
|
|
5
|
-
local focusOrderedSelectionEntry = _core.focusOrderedSelectionEntry
|
|
6
|
-
local getRelativeOrderedSelectionEntry = _core.getRelativeOrderedSelectionEntry
|
|
7
4
|
local React = _core.React
|
|
8
5
|
local useControllableState = _core.useControllableState
|
|
6
|
+
local _focus = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out)
|
|
7
|
+
local findOrderedSelectionEntry = _focus.findOrderedSelectionEntry
|
|
8
|
+
local focusOrderedSelectionEntry = _focus.focusOrderedSelectionEntry
|
|
9
|
+
local getRelativeOrderedSelectionEntry = _focus.getRelativeOrderedSelectionEntry
|
|
9
10
|
local RadioGroupContextProvider = TS.import(script, script.Parent, "context").RadioGroupContextProvider
|
|
10
11
|
local function RadioGroupRoot(props)
|
|
11
12
|
local orientation = props.orientation or "vertical"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PresenceMotionConfig, ResponseMotionConfig } from "@lattice-ui/motion";
|
|
1
2
|
import type React from "@rbxts/react";
|
|
2
3
|
export type RadioGroupSetValue = (value: string) => void;
|
|
3
4
|
export type RadioGroupOrientation = "horizontal" | "vertical";
|
|
@@ -31,12 +32,14 @@ export type RadioGroupProps = {
|
|
|
31
32
|
children?: React.ReactNode;
|
|
32
33
|
};
|
|
33
34
|
export type RadioGroupItemProps = {
|
|
35
|
+
transition?: ResponseMotionConfig;
|
|
34
36
|
value: string;
|
|
35
37
|
disabled?: boolean;
|
|
36
38
|
asChild?: boolean;
|
|
37
39
|
children?: React.ReactElement;
|
|
38
40
|
};
|
|
39
41
|
export type RadioGroupIndicatorProps = {
|
|
42
|
+
transition?: PresenceMotionConfig;
|
|
40
43
|
forceMount?: boolean;
|
|
41
44
|
asChild?: boolean;
|
|
42
45
|
children?: React.ReactNode;
|
package/package.json
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lattice-ui/radio-group",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-next.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "out/init.luau",
|
|
6
6
|
"types": "out/index.d.ts",
|
|
7
|
+
"source": "src/index.ts",
|
|
7
8
|
"files": [
|
|
9
|
+
"default.project.json",
|
|
8
10
|
"out",
|
|
11
|
+
"src",
|
|
9
12
|
"README.md"
|
|
10
13
|
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/astra-void/lattice-ui.git"
|
|
17
|
+
},
|
|
11
18
|
"dependencies": {
|
|
12
|
-
"@lattice-ui/core": "0.
|
|
19
|
+
"@lattice-ui/core": "0.5.0-next.2",
|
|
20
|
+
"@lattice-ui/layer": "0.5.0-next.2",
|
|
21
|
+
"@lattice-ui/focus": "0.5.0-next.2",
|
|
22
|
+
"@lattice-ui/motion": "0.5.0-next.2"
|
|
13
23
|
},
|
|
14
24
|
"devDependencies": {
|
|
15
25
|
"@rbxts/react": "17.3.7-ts.1",
|
|
@@ -21,6 +31,8 @@
|
|
|
21
31
|
},
|
|
22
32
|
"scripts": {
|
|
23
33
|
"build": "rbxtsc -p tsconfig.json",
|
|
34
|
+
"lint": "eslint .",
|
|
35
|
+
"lint:fix": "eslint . --fix",
|
|
24
36
|
"typecheck": "tsc -p tsconfig.typecheck.json",
|
|
25
37
|
"watch": "rbxtsc -p tsconfig.json -w"
|
|
26
38
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import { Presence } from "@lattice-ui/layer";
|
|
3
|
+
import {
|
|
4
|
+
createIndicatorRevealRecipe,
|
|
5
|
+
type PresenceMotionConfig,
|
|
6
|
+
usePresenceMotionController,
|
|
7
|
+
} from "@lattice-ui/motion";
|
|
8
|
+
import { useRadioGroupItemContext } from "./context";
|
|
9
|
+
import type { RadioGroupIndicatorProps } from "./types";
|
|
10
|
+
|
|
11
|
+
function RadioGroupIndicatorImpl(props: {
|
|
12
|
+
present: boolean;
|
|
13
|
+
forceMount?: boolean;
|
|
14
|
+
transition?: PresenceMotionConfig;
|
|
15
|
+
onExitComplete?: () => void;
|
|
16
|
+
asChild?: boolean;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
}) {
|
|
19
|
+
const defaultTransition = React.useMemo(() => createIndicatorRevealRecipe(UDim2.fromOffset(10, 10)), []);
|
|
20
|
+
const config = props.transition ?? defaultTransition;
|
|
21
|
+
|
|
22
|
+
const motion = usePresenceMotionController<Frame>({
|
|
23
|
+
present: props.present,
|
|
24
|
+
forceMount: props.forceMount,
|
|
25
|
+
config,
|
|
26
|
+
onExitComplete: props.onExitComplete,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const mounted = motion.mounted;
|
|
30
|
+
const visible = mounted && (motion.present || motion.phase !== "exited");
|
|
31
|
+
|
|
32
|
+
if (!mounted) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (props.asChild) {
|
|
37
|
+
const child = props.children;
|
|
38
|
+
if (!React.isValidElement(child)) {
|
|
39
|
+
error("[RadioGroupIndicator] `asChild` requires a child element.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Slot Visible={visible} ref={motion.ref}>
|
|
44
|
+
{child}
|
|
45
|
+
</Slot>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<frame
|
|
51
|
+
BackgroundColor3={Color3.fromRGB(240, 244, 252)}
|
|
52
|
+
BorderSizePixel={0}
|
|
53
|
+
Size={UDim2.fromOffset(10, 10)}
|
|
54
|
+
Visible={visible}
|
|
55
|
+
ref={motion.ref}
|
|
56
|
+
>
|
|
57
|
+
{props.children}
|
|
58
|
+
</frame>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function RadioGroupIndicator(props: RadioGroupIndicatorProps) {
|
|
63
|
+
const radioGroupItemContext = useRadioGroupItemContext();
|
|
64
|
+
const visible = radioGroupItemContext.checked;
|
|
65
|
+
const forceMount = props.forceMount === true;
|
|
66
|
+
|
|
67
|
+
if (forceMount) {
|
|
68
|
+
return (
|
|
69
|
+
<RadioGroupIndicatorImpl
|
|
70
|
+
asChild={props.asChild}
|
|
71
|
+
forceMount={true}
|
|
72
|
+
present={visible}
|
|
73
|
+
transition={props.transition}
|
|
74
|
+
>
|
|
75
|
+
{props.children}
|
|
76
|
+
</RadioGroupIndicatorImpl>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Presence
|
|
82
|
+
present={visible}
|
|
83
|
+
render={(state) => (
|
|
84
|
+
<RadioGroupIndicatorImpl
|
|
85
|
+
asChild={props.asChild}
|
|
86
|
+
onExitComplete={state.onExitComplete}
|
|
87
|
+
present={state.isPresent}
|
|
88
|
+
transition={props.transition}
|
|
89
|
+
>
|
|
90
|
+
{props.children}
|
|
91
|
+
</RadioGroupIndicatorImpl>
|
|
92
|
+
)}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import { useFocusNode } from "@lattice-ui/focus";
|
|
3
|
+
import { createSelectionResponseRecipe, useResponseMotion } from "@lattice-ui/motion";
|
|
4
|
+
import { RadioGroupItemContextProvider, useRadioGroupContext } from "./context";
|
|
5
|
+
import type { RadioGroupItemProps } from "./types";
|
|
6
|
+
|
|
7
|
+
let nextItemId = 0;
|
|
8
|
+
let nextItemOrder = 0;
|
|
9
|
+
|
|
10
|
+
export function RadioGroupItem(props: RadioGroupItemProps) {
|
|
11
|
+
const radioGroupContext = useRadioGroupContext();
|
|
12
|
+
const disabled = radioGroupContext.disabled || props.disabled === true;
|
|
13
|
+
const checked = radioGroupContext.value === props.value;
|
|
14
|
+
const itemRef = React.useRef<GuiObject>();
|
|
15
|
+
const disabledRef = React.useRef(disabled);
|
|
16
|
+
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
disabledRef.current = disabled;
|
|
19
|
+
}, [disabled]);
|
|
20
|
+
|
|
21
|
+
const itemIdRef = React.useRef(0);
|
|
22
|
+
if (itemIdRef.current === 0) {
|
|
23
|
+
nextItemId += 1;
|
|
24
|
+
itemIdRef.current = nextItemId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const itemOrderRef = React.useRef(0);
|
|
28
|
+
if (itemOrderRef.current === 0) {
|
|
29
|
+
nextItemOrder += 1;
|
|
30
|
+
itemOrderRef.current = nextItemOrder;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
return radioGroupContext.registerItem({
|
|
35
|
+
id: itemIdRef.current,
|
|
36
|
+
value: props.value,
|
|
37
|
+
order: itemOrderRef.current,
|
|
38
|
+
ref: itemRef,
|
|
39
|
+
getDisabled: () => disabledRef.current,
|
|
40
|
+
});
|
|
41
|
+
}, [props.value, radioGroupContext]);
|
|
42
|
+
|
|
43
|
+
useFocusNode({
|
|
44
|
+
ref: itemRef,
|
|
45
|
+
getDisabled: () => disabledRef.current,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const motionRef = useResponseMotion<GuiObject>(
|
|
49
|
+
checked,
|
|
50
|
+
{
|
|
51
|
+
active: { BackgroundColor3: Color3.fromRGB(88, 142, 255) },
|
|
52
|
+
inactive: { BackgroundColor3: Color3.fromRGB(47, 53, 68) },
|
|
53
|
+
},
|
|
54
|
+
props.transition ?? createSelectionResponseRecipe(),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const setItemRef = React.useCallback(
|
|
58
|
+
(instance: Instance | undefined) => {
|
|
59
|
+
const nextItem = !instance || !instance.IsA("GuiObject") ? undefined : instance;
|
|
60
|
+
itemRef.current = nextItem;
|
|
61
|
+
motionRef.current = nextItem;
|
|
62
|
+
},
|
|
63
|
+
[motionRef],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const handleSelect = React.useCallback(() => {
|
|
67
|
+
if (disabled) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
radioGroupContext.setValue(props.value);
|
|
72
|
+
}, [disabled, props.value, radioGroupContext]);
|
|
73
|
+
|
|
74
|
+
const handleSelectionGained = React.useCallback(() => {
|
|
75
|
+
if (disabled) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
radioGroupContext.setValue(props.value);
|
|
80
|
+
}, [disabled, props.value, radioGroupContext]);
|
|
81
|
+
|
|
82
|
+
const handleInputBegan = React.useCallback(
|
|
83
|
+
(_rbx: GuiObject, inputObject: InputObject) => {
|
|
84
|
+
if (disabled) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const keyCode = inputObject.KeyCode;
|
|
89
|
+
const direction =
|
|
90
|
+
radioGroupContext.orientation === "horizontal"
|
|
91
|
+
? keyCode === Enum.KeyCode.Left
|
|
92
|
+
? -1
|
|
93
|
+
: keyCode === Enum.KeyCode.Right
|
|
94
|
+
? 1
|
|
95
|
+
: undefined
|
|
96
|
+
: keyCode === Enum.KeyCode.Up
|
|
97
|
+
? -1
|
|
98
|
+
: keyCode === Enum.KeyCode.Down
|
|
99
|
+
? 1
|
|
100
|
+
: undefined;
|
|
101
|
+
|
|
102
|
+
if (direction !== undefined) {
|
|
103
|
+
radioGroupContext.moveSelection(props.value, direction);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (keyCode !== Enum.KeyCode.Return && keyCode !== Enum.KeyCode.Space) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
radioGroupContext.setValue(props.value);
|
|
112
|
+
},
|
|
113
|
+
[disabled, props.value, radioGroupContext],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const eventHandlers = React.useMemo(
|
|
117
|
+
() => ({
|
|
118
|
+
Activated: handleSelect,
|
|
119
|
+
InputBegan: handleInputBegan,
|
|
120
|
+
SelectionGained: handleSelectionGained,
|
|
121
|
+
}),
|
|
122
|
+
[handleInputBegan, handleSelect, handleSelectionGained],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const itemContextValue = React.useMemo(
|
|
126
|
+
() => ({
|
|
127
|
+
checked,
|
|
128
|
+
disabled,
|
|
129
|
+
}),
|
|
130
|
+
[checked, disabled],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<RadioGroupItemContextProvider value={itemContextValue}>
|
|
135
|
+
{props.asChild ? (
|
|
136
|
+
(() => {
|
|
137
|
+
const child = props.children;
|
|
138
|
+
if (!child) {
|
|
139
|
+
error("[RadioGroupItem] `asChild` requires a child element.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<Slot Active={!disabled} Event={eventHandlers} Selectable={!disabled} ref={setItemRef}>
|
|
144
|
+
{child}
|
|
145
|
+
</Slot>
|
|
146
|
+
);
|
|
147
|
+
})()
|
|
148
|
+
) : (
|
|
149
|
+
<textbutton
|
|
150
|
+
Active={!disabled}
|
|
151
|
+
AutoButtonColor={false}
|
|
152
|
+
BackgroundColor3={checked ? Color3.fromRGB(88, 142, 255) : Color3.fromRGB(47, 53, 68)}
|
|
153
|
+
BorderSizePixel={0}
|
|
154
|
+
Event={eventHandlers}
|
|
155
|
+
Selectable={!disabled}
|
|
156
|
+
Size={UDim2.fromOffset(170, 34)}
|
|
157
|
+
Text={props.value}
|
|
158
|
+
TextColor3={disabled ? Color3.fromRGB(139, 146, 160) : Color3.fromRGB(236, 241, 249)}
|
|
159
|
+
TextSize={15}
|
|
160
|
+
ref={setItemRef}
|
|
161
|
+
>
|
|
162
|
+
{props.children}
|
|
163
|
+
</textbutton>
|
|
164
|
+
)}
|
|
165
|
+
</RadioGroupItemContextProvider>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { React, useControllableState } from "@lattice-ui/core";
|
|
2
|
+
import {
|
|
3
|
+
findOrderedSelectionEntry,
|
|
4
|
+
focusOrderedSelectionEntry,
|
|
5
|
+
getRelativeOrderedSelectionEntry,
|
|
6
|
+
} from "@lattice-ui/focus";
|
|
7
|
+
import { RadioGroupContextProvider } from "./context";
|
|
8
|
+
import type { RadioGroupItemRegistration, RadioGroupProps } from "./types";
|
|
9
|
+
|
|
10
|
+
export function RadioGroupRoot(props: RadioGroupProps) {
|
|
11
|
+
const orientation = props.orientation ?? "vertical";
|
|
12
|
+
const [value, setValueState] = useControllableState<string | undefined>({
|
|
13
|
+
value: props.value,
|
|
14
|
+
defaultValue: props.defaultValue,
|
|
15
|
+
onChange: (nextValue) => {
|
|
16
|
+
if (nextValue !== undefined) {
|
|
17
|
+
props.onValueChange?.(nextValue);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const disabled = props.disabled === true;
|
|
23
|
+
const required = props.required === true;
|
|
24
|
+
const itemEntriesRef = React.useRef<Array<RadioGroupItemRegistration>>([]);
|
|
25
|
+
const [, setRegistryRevision] = React.useState(0);
|
|
26
|
+
|
|
27
|
+
const setValue = React.useCallback(
|
|
28
|
+
(nextValue: string) => {
|
|
29
|
+
if (disabled) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setValueState(nextValue);
|
|
34
|
+
},
|
|
35
|
+
[disabled, setValueState],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const registerItem = React.useCallback((item: RadioGroupItemRegistration) => {
|
|
39
|
+
itemEntriesRef.current.push(item);
|
|
40
|
+
setRegistryRevision((revision) => revision + 1);
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
const index = itemEntriesRef.current.findIndex((entry) => entry.id === item.id);
|
|
44
|
+
if (index >= 0) {
|
|
45
|
+
itemEntriesRef.current.remove(index);
|
|
46
|
+
setRegistryRevision((revision) => revision + 1);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const moveSelection = React.useCallback(
|
|
52
|
+
(fromValue: string, direction: -1 | 1) => {
|
|
53
|
+
const currentItem =
|
|
54
|
+
findOrderedSelectionEntry(itemEntriesRef.current, (item) => item.value === fromValue) ?? undefined;
|
|
55
|
+
const nextItem = getRelativeOrderedSelectionEntry(itemEntriesRef.current, currentItem?.id, direction);
|
|
56
|
+
if (!nextItem) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
focusOrderedSelectionEntry(nextItem);
|
|
61
|
+
setValue(nextItem.value);
|
|
62
|
+
},
|
|
63
|
+
[setValue],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const contextValue = React.useMemo(
|
|
67
|
+
() => ({
|
|
68
|
+
value,
|
|
69
|
+
setValue,
|
|
70
|
+
disabled,
|
|
71
|
+
required,
|
|
72
|
+
orientation,
|
|
73
|
+
registerItem,
|
|
74
|
+
moveSelection,
|
|
75
|
+
}),
|
|
76
|
+
[disabled, moveSelection, orientation, registerItem, required, setValue, value],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return <RadioGroupContextProvider value={contextValue}>{props.children}</RadioGroupContextProvider>;
|
|
80
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createStrictContext } from "@lattice-ui/core";
|
|
2
|
+
import type { RadioGroupContextValue, RadioGroupItemContextValue } from "./types";
|
|
3
|
+
|
|
4
|
+
const [RadioGroupContextProvider, useRadioGroupContext] = createStrictContext<RadioGroupContextValue>("RadioGroup");
|
|
5
|
+
const [RadioGroupItemContextProvider, useRadioGroupItemContext] =
|
|
6
|
+
createStrictContext<RadioGroupItemContextValue>("RadioGroupItem");
|
|
7
|
+
|
|
8
|
+
export { RadioGroupContextProvider, RadioGroupItemContextProvider, useRadioGroupContext, useRadioGroupItemContext };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PresenceMotionConfig, ResponseMotionConfig } from "@lattice-ui/motion";
|
|
2
|
+
import type React from "@rbxts/react";
|
|
3
|
+
|
|
4
|
+
export type RadioGroupSetValue = (value: string) => void;
|
|
5
|
+
export type RadioGroupOrientation = "horizontal" | "vertical";
|
|
6
|
+
|
|
7
|
+
export type RadioGroupItemRegistration = {
|
|
8
|
+
id: number;
|
|
9
|
+
value: string;
|
|
10
|
+
order: number;
|
|
11
|
+
ref: React.MutableRefObject<GuiObject | undefined>;
|
|
12
|
+
getDisabled: () => boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type RadioGroupContextValue = {
|
|
16
|
+
value?: string;
|
|
17
|
+
setValue: RadioGroupSetValue;
|
|
18
|
+
disabled: boolean;
|
|
19
|
+
required: boolean;
|
|
20
|
+
orientation: RadioGroupOrientation;
|
|
21
|
+
registerItem: (item: RadioGroupItemRegistration) => () => void;
|
|
22
|
+
moveSelection: (fromValue: string, direction: -1 | 1) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type RadioGroupItemContextValue = {
|
|
26
|
+
checked: boolean;
|
|
27
|
+
disabled: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RadioGroupProps = {
|
|
31
|
+
value?: string;
|
|
32
|
+
defaultValue?: string;
|
|
33
|
+
onValueChange?: (value: string) => void;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
required?: boolean;
|
|
36
|
+
orientation?: RadioGroupOrientation;
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type RadioGroupItemProps = {
|
|
41
|
+
transition?: ResponseMotionConfig;
|
|
42
|
+
value: string;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
asChild?: boolean;
|
|
45
|
+
children?: React.ReactElement;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type RadioGroupIndicatorProps = {
|
|
49
|
+
transition?: PresenceMotionConfig;
|
|
50
|
+
forceMount?: boolean;
|
|
51
|
+
asChild?: boolean;
|
|
52
|
+
children?: React.ReactNode;
|
|
53
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { RadioGroupIndicator } from "./RadioGroup/RadioGroupIndicator";
|
|
2
|
+
import { RadioGroupItem } from "./RadioGroup/RadioGroupItem";
|
|
3
|
+
import { RadioGroupRoot } from "./RadioGroup/RadioGroupRoot";
|
|
4
|
+
|
|
5
|
+
export const RadioGroup = {
|
|
6
|
+
Root: RadioGroupRoot,
|
|
7
|
+
Item: RadioGroupItem,
|
|
8
|
+
Indicator: RadioGroupIndicator,
|
|
9
|
+
} as const satisfies {
|
|
10
|
+
Root: typeof RadioGroupRoot;
|
|
11
|
+
Item: typeof RadioGroupItem;
|
|
12
|
+
Indicator: typeof RadioGroupIndicator;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
RadioGroupContextValue,
|
|
17
|
+
RadioGroupIndicatorProps,
|
|
18
|
+
RadioGroupItemContextValue,
|
|
19
|
+
RadioGroupItemProps,
|
|
20
|
+
RadioGroupItemRegistration,
|
|
21
|
+
RadioGroupOrientation,
|
|
22
|
+
RadioGroupProps,
|
|
23
|
+
RadioGroupSetValue,
|
|
24
|
+
} from "./RadioGroup/types";
|
|
25
|
+
export { RadioGroupIndicator, RadioGroupItem, RadioGroupRoot };
|