@lattice-ui/textarea 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 ADDED
@@ -0,0 +1,18 @@
1
+ # @lattice-ui/textarea
2
+
3
+ Headless multi-line text input primitives for Roblox UI with optional auto-resize behavior.
4
+
5
+ ## Exports
6
+
7
+ - `Textarea`
8
+ - `Textarea.Root`
9
+ - `Textarea.Input`
10
+ - `Textarea.Label`
11
+ - `Textarea.Description`
12
+ - `Textarea.Message`
13
+
14
+ ## Notes
15
+
16
+ - Supports controlled/uncontrolled `value`.
17
+ - `autoResize` is enabled by default and respects `minRows`/`maxRows`.
18
+ - `onValueCommit` fires on `FocusLost` commit moments.
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { TextareaDescriptionProps } from "./types";
3
+ export declare function TextareaDescription(props: TextareaDescriptionProps): React.JSX.Element;
@@ -0,0 +1,31 @@
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 useTextareaContext = TS.import(script, script.Parent, "context").useTextareaContext
7
+ local function TextareaDescription(props)
8
+ local textareaContext = useTextareaContext()
9
+ if props.asChild then
10
+ local child = props.children
11
+ if not child then
12
+ error("[TextareaDescription] `asChild` requires a child element.")
13
+ end
14
+ return React.createElement(Slot, {
15
+ Name = "TextareaDescription",
16
+ Text = "Description",
17
+ }, child)
18
+ end
19
+ return React.createElement("textlabel", {
20
+ BackgroundTransparency = 1,
21
+ BorderSizePixel = 0,
22
+ Size = UDim2.fromOffset(300, 20),
23
+ Text = "Description",
24
+ TextColor3 = if textareaContext.disabled then Color3.fromRGB(132, 139, 154) else Color3.fromRGB(170, 179, 195),
25
+ TextSize = 13,
26
+ TextXAlignment = Enum.TextXAlignment.Left,
27
+ })
28
+ end
29
+ return {
30
+ TextareaDescription = TextareaDescription,
31
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { TextareaInputProps } from "./types";
3
+ export declare function TextareaInput(props: TextareaInputProps): React.JSX.Element;
@@ -0,0 +1,109 @@
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 resolveTextareaHeight = TS.import(script, script.Parent, "autoResize").resolveTextareaHeight
7
+ local useTextareaContext = TS.import(script, script.Parent, "context").useTextareaContext
8
+ local function toTextBox(instance)
9
+ if not instance or not instance:IsA("TextBox") then
10
+ return nil
11
+ end
12
+ return instance
13
+ end
14
+ local function TextareaInput(props)
15
+ local textareaContext = useTextareaContext()
16
+ local disabled = textareaContext.disabled or props.disabled == true
17
+ local readOnly = textareaContext.readOnly or props.readOnly == true
18
+ local _condition = props.lineHeight
19
+ if _condition == nil then
20
+ _condition = 18
21
+ end
22
+ local lineHeight = _condition
23
+ local setInputRef = React.useCallback(function(instance)
24
+ textareaContext.inputRef.current = toTextBox(instance)
25
+ end, { textareaContext.inputRef })
26
+ local applyAutoResize = React.useCallback(function(textBox)
27
+ if not textareaContext.autoResize then
28
+ return nil
29
+ end
30
+ local height = resolveTextareaHeight(textBox.Text, {
31
+ lineHeight = lineHeight,
32
+ minRows = textareaContext.minRows,
33
+ maxRows = textareaContext.maxRows,
34
+ verticalPadding = 14,
35
+ })
36
+ local currentSize = textBox.Size
37
+ if currentSize.Y.Offset ~= height or currentSize.Y.Scale ~= 0 then
38
+ textBox.Size = UDim2.fromOffset(currentSize.X.Offset, height)
39
+ end
40
+ end, { lineHeight, textareaContext.autoResize, textareaContext.maxRows, textareaContext.minRows })
41
+ local handleTextChanged = React.useCallback(function(textBox)
42
+ if disabled or readOnly then
43
+ if textBox.Text ~= textareaContext.value then
44
+ textBox.Text = textareaContext.value
45
+ end
46
+ applyAutoResize(textBox)
47
+ return nil
48
+ end
49
+ textareaContext.setValue(textBox.Text)
50
+ applyAutoResize(textBox)
51
+ end, { applyAutoResize, disabled, readOnly, textareaContext })
52
+ local handleFocusLost = React.useCallback(function(textBox)
53
+ if disabled then
54
+ return nil
55
+ end
56
+ textareaContext.commitValue(textBox.Text)
57
+ end, { disabled, textareaContext })
58
+ React.useEffect(function()
59
+ local input = textareaContext.inputRef.current
60
+ if not input then
61
+ return nil
62
+ end
63
+ applyAutoResize(input)
64
+ end, { applyAutoResize, textareaContext.inputRef, textareaContext.value })
65
+ local sharedProps = {
66
+ Active = not disabled,
67
+ ClearTextOnFocus = false,
68
+ MultiLine = true,
69
+ Selectable = not disabled,
70
+ Text = textareaContext.value,
71
+ TextEditable = not disabled and not readOnly,
72
+ TextWrapped = true,
73
+ Change = {
74
+ Text = handleTextChanged,
75
+ },
76
+ Event = {
77
+ FocusLost = handleFocusLost,
78
+ },
79
+ ref = setInputRef,
80
+ }
81
+ if props.asChild then
82
+ local child = props.children
83
+ if not child then
84
+ error("[TextareaInput] `asChild` requires a child element.")
85
+ end
86
+ local _attributes = table.clone(sharedProps)
87
+ setmetatable(_attributes, nil)
88
+ return React.createElement(Slot, _attributes, child)
89
+ end
90
+ local _attributes = table.clone(sharedProps)
91
+ setmetatable(_attributes, nil)
92
+ _attributes.BackgroundColor3 = Color3.fromRGB(39, 46, 61)
93
+ _attributes.BorderSizePixel = 0
94
+ _attributes.PlaceholderText = "Type..."
95
+ _attributes.Size = UDim2.fromOffset(240, 68)
96
+ _attributes.TextColor3 = if disabled then Color3.fromRGB(137, 145, 162) else Color3.fromRGB(235, 240, 248)
97
+ _attributes.TextSize = 15
98
+ _attributes.TextXAlignment = Enum.TextXAlignment.Left
99
+ _attributes.TextYAlignment = Enum.TextYAlignment.Top
100
+ return React.createElement("textbox", _attributes, React.createElement("uipadding", {
101
+ PaddingBottom = UDim.new(0, 7),
102
+ PaddingLeft = UDim.new(0, 10),
103
+ PaddingRight = UDim.new(0, 10),
104
+ PaddingTop = UDim.new(0, 7),
105
+ }))
106
+ end
107
+ return {
108
+ TextareaInput = TextareaInput,
109
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { TextareaLabelProps } from "./types";
3
+ export declare function TextareaLabel(props: TextareaLabelProps): React.JSX.Element;
@@ -0,0 +1,49 @@
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 useTextareaContext = TS.import(script, script.Parent, "context").useTextareaContext
7
+ local function TextareaLabel(props)
8
+ local textareaContext = useTextareaContext()
9
+ local disabled = textareaContext.disabled
10
+ local handleActivated = React.useCallback(function()
11
+ if disabled then
12
+ return nil
13
+ end
14
+ local _result = textareaContext.inputRef.current
15
+ if _result ~= nil then
16
+ _result:CaptureFocus()
17
+ end
18
+ end, { disabled, textareaContext.inputRef })
19
+ local sharedProps = {
20
+ Active = not disabled,
21
+ Selectable = not disabled,
22
+ Text = "Label",
23
+ Event = {
24
+ Activated = handleActivated,
25
+ },
26
+ }
27
+ if props.asChild then
28
+ local child = props.children
29
+ if not child then
30
+ error("[TextareaLabel] `asChild` requires a child element.")
31
+ end
32
+ local _attributes = table.clone(sharedProps)
33
+ setmetatable(_attributes, nil)
34
+ return React.createElement(Slot, _attributes, child)
35
+ end
36
+ local _attributes = table.clone(sharedProps)
37
+ setmetatable(_attributes, nil)
38
+ _attributes.AutoButtonColor = false
39
+ _attributes.BackgroundTransparency = 1
40
+ _attributes.BorderSizePixel = 0
41
+ _attributes.Size = UDim2.fromOffset(240, 22)
42
+ _attributes.TextColor3 = if disabled then Color3.fromRGB(149, 157, 173) else Color3.fromRGB(225, 231, 241)
43
+ _attributes.TextSize = 14
44
+ _attributes.TextXAlignment = Enum.TextXAlignment.Left
45
+ return React.createElement("textbutton", _attributes)
46
+ end
47
+ return {
48
+ TextareaLabel = TextareaLabel,
49
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { TextareaMessageProps } from "./types";
3
+ export declare function TextareaMessage(props: TextareaMessageProps): React.JSX.Element;
@@ -0,0 +1,31 @@
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 useTextareaContext = TS.import(script, script.Parent, "context").useTextareaContext
7
+ local function TextareaMessage(props)
8
+ local textareaContext = useTextareaContext()
9
+ if props.asChild then
10
+ local child = props.children
11
+ if not child then
12
+ error("[TextareaMessage] `asChild` requires a child element.")
13
+ end
14
+ return React.createElement(Slot, {
15
+ Name = "TextareaMessage",
16
+ Text = "Message",
17
+ }, child)
18
+ end
19
+ return React.createElement("textlabel", {
20
+ BackgroundTransparency = 1,
21
+ BorderSizePixel = 0,
22
+ Size = UDim2.fromOffset(300, 20),
23
+ Text = "Message",
24
+ TextColor3 = if textareaContext.invalid == true then Color3.fromRGB(255, 128, 128) elseif textareaContext.disabled then Color3.fromRGB(132, 139, 154) else Color3.fromRGB(170, 179, 195),
25
+ TextSize = 13,
26
+ TextXAlignment = Enum.TextXAlignment.Left,
27
+ })
28
+ end
29
+ return {
30
+ TextareaMessage = TextareaMessage,
31
+ }
@@ -0,0 +1,4 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { TextareaProps } from "./types";
3
+ export declare function TextareaRoot(props: TextareaProps): React.JSX.Element;
4
+ export { TextareaRoot as Textarea };
@@ -0,0 +1,72 @@
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 useControllableState = _core.useControllableState
6
+ local TextareaContextProvider = TS.import(script, script.Parent, "context").TextareaContextProvider
7
+ local function TextareaRoot(props)
8
+ local _object = {
9
+ value = props.value,
10
+ }
11
+ local _left = "defaultValue"
12
+ local _condition = props.defaultValue
13
+ if _condition == nil then
14
+ _condition = ""
15
+ end
16
+ _object[_left] = _condition
17
+ _object.onChange = props.onValueChange
18
+ local _binding = useControllableState(_object)
19
+ local value = _binding[1]
20
+ local setValueState = _binding[2]
21
+ local disabled = props.disabled == true
22
+ local readOnly = props.readOnly == true
23
+ local required = props.required == true
24
+ local invalid = props.invalid == true
25
+ local _condition_1 = props.autoResize
26
+ if _condition_1 == nil then
27
+ _condition_1 = true
28
+ end
29
+ local autoResize = _condition_1
30
+ local _condition_2 = props.minRows
31
+ if _condition_2 == nil then
32
+ _condition_2 = 3
33
+ end
34
+ local minRows = math.max(1, _condition_2)
35
+ local maxRows = if props.maxRows ~= nil then math.max(minRows, props.maxRows) else nil
36
+ local inputRef = React.useRef()
37
+ local setValue = React.useCallback(function(nextValue)
38
+ if disabled or readOnly then
39
+ return nil
40
+ end
41
+ setValueState(nextValue)
42
+ end, { disabled, readOnly, setValueState })
43
+ local commitValue = React.useCallback(function(nextValue)
44
+ local _result = props.onValueCommit
45
+ if _result ~= nil then
46
+ _result(nextValue)
47
+ end
48
+ end, { props.onValueCommit })
49
+ local contextValue = React.useMemo(function()
50
+ return {
51
+ value = value,
52
+ setValue = setValue,
53
+ commitValue = commitValue,
54
+ disabled = disabled,
55
+ readOnly = readOnly,
56
+ required = required,
57
+ invalid = invalid,
58
+ name = props.name,
59
+ autoResize = autoResize,
60
+ minRows = minRows,
61
+ maxRows = maxRows,
62
+ inputRef = inputRef,
63
+ }
64
+ end, { autoResize, commitValue, disabled, invalid, maxRows, minRows, props.name, readOnly, required, setValue, value })
65
+ return React.createElement(TextareaContextProvider, {
66
+ value = contextValue,
67
+ }, props.children)
68
+ end
69
+ return {
70
+ TextareaRoot = TextareaRoot,
71
+ Textarea = TextareaRoot,
72
+ }
@@ -0,0 +1,7 @@
1
+ export type TextareaAutoResizeOptions = {
2
+ minRows: number;
3
+ maxRows?: number;
4
+ lineHeight: number;
5
+ verticalPadding?: number;
6
+ };
7
+ export declare function resolveTextareaHeight(text: string, options: TextareaAutoResizeOptions): number;
@@ -0,0 +1,23 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local function countLines(text)
3
+ if #text == 0 then
4
+ return 1
5
+ end
6
+ return #string.split(text, "\n")
7
+ end
8
+ local function resolveTextareaHeight(text, options)
9
+ local minRows = math.max(1, math.floor(options.minRows))
10
+ local maxRows = if options.maxRows ~= nil then math.max(minRows, math.floor(options.maxRows)) else nil
11
+ local lineHeight = math.max(1, options.lineHeight)
12
+ local _condition = options.verticalPadding
13
+ if _condition == nil then
14
+ _condition = 0
15
+ end
16
+ local verticalPadding = math.max(0, _condition)
17
+ local naturalRows = countLines(text)
18
+ local clampedRows = if maxRows ~= nil then math.clamp(naturalRows, minRows, maxRows) else math.max(minRows, naturalRows)
19
+ return clampedRows * lineHeight + verticalPadding
20
+ end
21
+ return {
22
+ resolveTextareaHeight = resolveTextareaHeight,
23
+ }
@@ -0,0 +1,3 @@
1
+ import type { TextareaContextValue } from "./types";
2
+ declare const TextareaContextProvider: import("@rbxts/react").Provider<TextareaContextValue | undefined>, useTextareaContext: () => TextareaContextValue;
3
+ export { TextareaContextProvider, useTextareaContext };
@@ -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("Textarea")
5
+ local TextareaContextProvider = _binding[1]
6
+ local useTextareaContext = _binding[2]
7
+ return {
8
+ TextareaContextProvider = TextareaContextProvider,
9
+ useTextareaContext = useTextareaContext,
10
+ }
@@ -0,0 +1,51 @@
1
+ import type React from "@rbxts/react";
2
+ export type TextareaSetValue = (value: string) => void;
3
+ export type TextareaCommitValue = (value: string) => void;
4
+ export type TextareaContextValue = {
5
+ value: string;
6
+ setValue: TextareaSetValue;
7
+ commitValue: TextareaCommitValue;
8
+ disabled: boolean;
9
+ readOnly: boolean;
10
+ required: boolean;
11
+ invalid: boolean;
12
+ name?: string;
13
+ autoResize: boolean;
14
+ minRows: number;
15
+ maxRows?: number;
16
+ inputRef: React.MutableRefObject<TextBox | undefined>;
17
+ };
18
+ export type TextareaProps = {
19
+ value?: string;
20
+ defaultValue?: string;
21
+ onValueChange?: (value: string) => void;
22
+ onValueCommit?: (value: string) => void;
23
+ disabled?: boolean;
24
+ readOnly?: boolean;
25
+ required?: boolean;
26
+ invalid?: boolean;
27
+ name?: string;
28
+ autoResize?: boolean;
29
+ minRows?: number;
30
+ maxRows?: number;
31
+ children?: React.ReactNode;
32
+ };
33
+ export type TextareaInputProps = {
34
+ asChild?: boolean;
35
+ disabled?: boolean;
36
+ readOnly?: boolean;
37
+ lineHeight?: number;
38
+ children?: React.ReactElement;
39
+ };
40
+ export type TextareaLabelProps = {
41
+ asChild?: boolean;
42
+ children?: React.ReactElement;
43
+ };
44
+ export type TextareaDescriptionProps = {
45
+ asChild?: boolean;
46
+ children?: React.ReactElement;
47
+ };
48
+ export type TextareaMessageProps = {
49
+ asChild?: boolean;
50
+ children?: React.ReactElement;
51
+ };
@@ -0,0 +1,2 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ return nil
package/out/index.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { TextareaDescription } from "./Textarea/TextareaDescription";
2
+ import { TextareaInput } from "./Textarea/TextareaInput";
3
+ import { TextareaLabel } from "./Textarea/TextareaLabel";
4
+ import { TextareaMessage } from "./Textarea/TextareaMessage";
5
+ import { TextareaRoot } from "./Textarea/TextareaRoot";
6
+ export declare const Textarea: {
7
+ readonly Root: typeof TextareaRoot;
8
+ readonly Input: typeof TextareaInput;
9
+ readonly Label: typeof TextareaLabel;
10
+ readonly Description: typeof TextareaDescription;
11
+ readonly Message: typeof TextareaMessage;
12
+ };
13
+ export { resolveTextareaHeight } from "./Textarea/autoResize";
14
+ export type { TextareaCommitValue, TextareaContextValue, TextareaDescriptionProps, TextareaInputProps, TextareaLabelProps, TextareaMessageProps, TextareaProps, TextareaSetValue, } from "./Textarea/types";
package/out/init.luau ADDED
@@ -0,0 +1,18 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local exports = {}
4
+ local TextareaDescription = TS.import(script, script, "Textarea", "TextareaDescription").TextareaDescription
5
+ local TextareaInput = TS.import(script, script, "Textarea", "TextareaInput").TextareaInput
6
+ local TextareaLabel = TS.import(script, script, "Textarea", "TextareaLabel").TextareaLabel
7
+ local TextareaMessage = TS.import(script, script, "Textarea", "TextareaMessage").TextareaMessage
8
+ local TextareaRoot = TS.import(script, script, "Textarea", "TextareaRoot").TextareaRoot
9
+ local Textarea = {
10
+ Root = TextareaRoot,
11
+ Input = TextareaInput,
12
+ Label = TextareaLabel,
13
+ Description = TextareaDescription,
14
+ Message = TextareaMessage,
15
+ }
16
+ exports.resolveTextareaHeight = TS.import(script, script, "Textarea", "autoResize").resolveTextareaHeight
17
+ exports.Textarea = Textarea
18
+ return exports
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@lattice-ui/textarea",
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,32 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { useTextareaContext } from "./context";
3
+ import type { TextareaDescriptionProps } from "./types";
4
+
5
+ export function TextareaDescription(props: TextareaDescriptionProps) {
6
+ const textareaContext = useTextareaContext();
7
+
8
+ if (props.asChild) {
9
+ const child = props.children;
10
+ if (!child) {
11
+ error("[TextareaDescription] `asChild` requires a child element.");
12
+ }
13
+
14
+ return (
15
+ <Slot Name="TextareaDescription" Text="Description">
16
+ {child}
17
+ </Slot>
18
+ );
19
+ }
20
+
21
+ return (
22
+ <textlabel
23
+ BackgroundTransparency={1}
24
+ BorderSizePixel={0}
25
+ Size={UDim2.fromOffset(300, 20)}
26
+ Text="Description"
27
+ TextColor3={textareaContext.disabled ? Color3.fromRGB(132, 139, 154) : Color3.fromRGB(170, 179, 195)}
28
+ TextSize={13}
29
+ TextXAlignment={Enum.TextXAlignment.Left}
30
+ />
31
+ );
32
+ }
@@ -0,0 +1,131 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { resolveTextareaHeight } from "./autoResize";
3
+ import { useTextareaContext } from "./context";
4
+ import type { TextareaInputProps } from "./types";
5
+
6
+ function toTextBox(instance: Instance | undefined) {
7
+ if (!instance || !instance.IsA("TextBox")) {
8
+ return undefined;
9
+ }
10
+
11
+ return instance;
12
+ }
13
+
14
+ export function TextareaInput(props: TextareaInputProps) {
15
+ const textareaContext = useTextareaContext();
16
+ const disabled = textareaContext.disabled || props.disabled === true;
17
+ const readOnly = textareaContext.readOnly || props.readOnly === true;
18
+ const lineHeight = props.lineHeight ?? 18;
19
+
20
+ const setInputRef = React.useCallback(
21
+ (instance: Instance | undefined) => {
22
+ textareaContext.inputRef.current = toTextBox(instance);
23
+ },
24
+ [textareaContext.inputRef],
25
+ );
26
+
27
+ const applyAutoResize = React.useCallback(
28
+ (textBox: TextBox) => {
29
+ if (!textareaContext.autoResize) {
30
+ return;
31
+ }
32
+
33
+ const height = resolveTextareaHeight(textBox.Text, {
34
+ lineHeight,
35
+ minRows: textareaContext.minRows,
36
+ maxRows: textareaContext.maxRows,
37
+ verticalPadding: 14,
38
+ });
39
+
40
+ const currentSize = textBox.Size;
41
+ if (currentSize.Y.Offset !== height || currentSize.Y.Scale !== 0) {
42
+ textBox.Size = UDim2.fromOffset(currentSize.X.Offset, height);
43
+ }
44
+ },
45
+ [lineHeight, textareaContext.autoResize, textareaContext.maxRows, textareaContext.minRows],
46
+ );
47
+
48
+ const handleTextChanged = React.useCallback(
49
+ (textBox: TextBox) => {
50
+ if (disabled || readOnly) {
51
+ if (textBox.Text !== textareaContext.value) {
52
+ textBox.Text = textareaContext.value;
53
+ }
54
+
55
+ applyAutoResize(textBox);
56
+ return;
57
+ }
58
+
59
+ textareaContext.setValue(textBox.Text);
60
+ applyAutoResize(textBox);
61
+ },
62
+ [applyAutoResize, disabled, readOnly, textareaContext],
63
+ );
64
+
65
+ const handleFocusLost = React.useCallback(
66
+ (textBox: TextBox) => {
67
+ if (disabled) {
68
+ return;
69
+ }
70
+
71
+ textareaContext.commitValue(textBox.Text);
72
+ },
73
+ [disabled, textareaContext],
74
+ );
75
+
76
+ React.useEffect(() => {
77
+ const input = textareaContext.inputRef.current;
78
+ if (!input) {
79
+ return;
80
+ }
81
+
82
+ applyAutoResize(input);
83
+ }, [applyAutoResize, textareaContext.inputRef, textareaContext.value]);
84
+
85
+ const sharedProps = {
86
+ Active: !disabled,
87
+ ClearTextOnFocus: false,
88
+ MultiLine: true,
89
+ Selectable: !disabled,
90
+ Text: textareaContext.value,
91
+ TextEditable: !disabled && !readOnly,
92
+ TextWrapped: true,
93
+ Change: {
94
+ Text: handleTextChanged,
95
+ },
96
+ Event: {
97
+ FocusLost: handleFocusLost,
98
+ },
99
+ ref: setInputRef,
100
+ };
101
+
102
+ if (props.asChild) {
103
+ const child = props.children;
104
+ if (!child) {
105
+ error("[TextareaInput] `asChild` requires a child element.");
106
+ }
107
+
108
+ return <Slot {...sharedProps}>{child}</Slot>;
109
+ }
110
+
111
+ return (
112
+ <textbox
113
+ {...sharedProps}
114
+ BackgroundColor3={Color3.fromRGB(39, 46, 61)}
115
+ BorderSizePixel={0}
116
+ PlaceholderText="Type..."
117
+ Size={UDim2.fromOffset(240, 68)}
118
+ TextColor3={disabled ? Color3.fromRGB(137, 145, 162) : Color3.fromRGB(235, 240, 248)}
119
+ TextSize={15}
120
+ TextXAlignment={Enum.TextXAlignment.Left}
121
+ TextYAlignment={Enum.TextYAlignment.Top}
122
+ >
123
+ <uipadding
124
+ PaddingBottom={new UDim(0, 7)}
125
+ PaddingLeft={new UDim(0, 10)}
126
+ PaddingRight={new UDim(0, 10)}
127
+ PaddingTop={new UDim(0, 7)}
128
+ />
129
+ </textbox>
130
+ );
131
+ }
@@ -0,0 +1,47 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { useTextareaContext } from "./context";
3
+ import type { TextareaLabelProps } from "./types";
4
+
5
+ export function TextareaLabel(props: TextareaLabelProps) {
6
+ const textareaContext = useTextareaContext();
7
+ const disabled = textareaContext.disabled;
8
+
9
+ const handleActivated = React.useCallback(() => {
10
+ if (disabled) {
11
+ return;
12
+ }
13
+
14
+ textareaContext.inputRef.current?.CaptureFocus();
15
+ }, [disabled, textareaContext.inputRef]);
16
+
17
+ const sharedProps = {
18
+ Active: !disabled,
19
+ Selectable: !disabled,
20
+ Text: "Label",
21
+ Event: {
22
+ Activated: handleActivated,
23
+ },
24
+ };
25
+
26
+ if (props.asChild) {
27
+ const child = props.children;
28
+ if (!child) {
29
+ error("[TextareaLabel] `asChild` requires a child element.");
30
+ }
31
+
32
+ return <Slot {...sharedProps}>{child}</Slot>;
33
+ }
34
+
35
+ return (
36
+ <textbutton
37
+ {...sharedProps}
38
+ AutoButtonColor={false}
39
+ BackgroundTransparency={1}
40
+ BorderSizePixel={0}
41
+ Size={UDim2.fromOffset(240, 22)}
42
+ TextColor3={disabled ? Color3.fromRGB(149, 157, 173) : Color3.fromRGB(225, 231, 241)}
43
+ TextSize={14}
44
+ TextXAlignment={Enum.TextXAlignment.Left}
45
+ />
46
+ );
47
+ }
@@ -0,0 +1,38 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { useTextareaContext } from "./context";
3
+ import type { TextareaMessageProps } from "./types";
4
+
5
+ export function TextareaMessage(props: TextareaMessageProps) {
6
+ const textareaContext = useTextareaContext();
7
+
8
+ if (props.asChild) {
9
+ const child = props.children;
10
+ if (!child) {
11
+ error("[TextareaMessage] `asChild` requires a child element.");
12
+ }
13
+
14
+ return (
15
+ <Slot Name="TextareaMessage" Text="Message">
16
+ {child}
17
+ </Slot>
18
+ );
19
+ }
20
+
21
+ return (
22
+ <textlabel
23
+ BackgroundTransparency={1}
24
+ BorderSizePixel={0}
25
+ Size={UDim2.fromOffset(300, 20)}
26
+ Text="Message"
27
+ TextColor3={
28
+ textareaContext.invalid === true
29
+ ? Color3.fromRGB(255, 128, 128)
30
+ : textareaContext.disabled
31
+ ? Color3.fromRGB(132, 139, 154)
32
+ : Color3.fromRGB(170, 179, 195)
33
+ }
34
+ TextSize={13}
35
+ TextXAlignment={Enum.TextXAlignment.Left}
36
+ />
37
+ );
38
+ }
@@ -0,0 +1,61 @@
1
+ import { React, useControllableState } from "@lattice-ui/core";
2
+ import { TextareaContextProvider } from "./context";
3
+ import type { TextareaProps } from "./types";
4
+
5
+ export function TextareaRoot(props: TextareaProps) {
6
+ const [value, setValueState] = useControllableState<string>({
7
+ value: props.value,
8
+ defaultValue: props.defaultValue ?? "",
9
+ onChange: props.onValueChange,
10
+ });
11
+
12
+ const disabled = props.disabled === true;
13
+ const readOnly = props.readOnly === true;
14
+ const required = props.required === true;
15
+ const invalid = props.invalid === true;
16
+ const autoResize = props.autoResize ?? true;
17
+ const minRows = math.max(1, props.minRows ?? 3);
18
+ const maxRows = props.maxRows !== undefined ? math.max(minRows, props.maxRows) : undefined;
19
+
20
+ const inputRef = React.useRef<TextBox>();
21
+
22
+ const setValue = React.useCallback(
23
+ (nextValue: string) => {
24
+ if (disabled || readOnly) {
25
+ return;
26
+ }
27
+
28
+ setValueState(nextValue);
29
+ },
30
+ [disabled, readOnly, setValueState],
31
+ );
32
+
33
+ const commitValue = React.useCallback(
34
+ (nextValue: string) => {
35
+ props.onValueCommit?.(nextValue);
36
+ },
37
+ [props.onValueCommit],
38
+ );
39
+
40
+ const contextValue = React.useMemo(
41
+ () => ({
42
+ value,
43
+ setValue,
44
+ commitValue,
45
+ disabled,
46
+ readOnly,
47
+ required,
48
+ invalid,
49
+ name: props.name,
50
+ autoResize,
51
+ minRows,
52
+ maxRows,
53
+ inputRef,
54
+ }),
55
+ [autoResize, commitValue, disabled, invalid, maxRows, minRows, props.name, readOnly, required, setValue, value],
56
+ );
57
+
58
+ return <TextareaContextProvider value={contextValue}>{props.children}</TextareaContextProvider>;
59
+ }
60
+
61
+ export { TextareaRoot as Textarea };
@@ -0,0 +1,27 @@
1
+ export type TextareaAutoResizeOptions = {
2
+ minRows: number;
3
+ maxRows?: number;
4
+ lineHeight: number;
5
+ verticalPadding?: number;
6
+ };
7
+
8
+ function countLines(text: string) {
9
+ if (text.size() === 0) {
10
+ return 1;
11
+ }
12
+
13
+ return text.split("\n").size();
14
+ }
15
+
16
+ export function resolveTextareaHeight(text: string, options: TextareaAutoResizeOptions) {
17
+ const minRows = math.max(1, math.floor(options.minRows));
18
+ const maxRows = options.maxRows !== undefined ? math.max(minRows, math.floor(options.maxRows)) : undefined;
19
+ const lineHeight = math.max(1, options.lineHeight);
20
+ const verticalPadding = math.max(0, options.verticalPadding ?? 0);
21
+
22
+ const naturalRows = countLines(text);
23
+ const clampedRows =
24
+ maxRows !== undefined ? math.clamp(naturalRows, minRows, maxRows) : math.max(minRows, naturalRows);
25
+
26
+ return clampedRows * lineHeight + verticalPadding;
27
+ }
@@ -0,0 +1,6 @@
1
+ import { createStrictContext } from "@lattice-ui/core";
2
+ import type { TextareaContextValue } from "./types";
3
+
4
+ const [TextareaContextProvider, useTextareaContext] = createStrictContext<TextareaContextValue>("Textarea");
5
+
6
+ export { TextareaContextProvider, useTextareaContext };
@@ -0,0 +1,58 @@
1
+ import type React from "@rbxts/react";
2
+
3
+ export type TextareaSetValue = (value: string) => void;
4
+ export type TextareaCommitValue = (value: string) => void;
5
+
6
+ export type TextareaContextValue = {
7
+ value: string;
8
+ setValue: TextareaSetValue;
9
+ commitValue: TextareaCommitValue;
10
+ disabled: boolean;
11
+ readOnly: boolean;
12
+ required: boolean;
13
+ invalid: boolean;
14
+ name?: string;
15
+ autoResize: boolean;
16
+ minRows: number;
17
+ maxRows?: number;
18
+ inputRef: React.MutableRefObject<TextBox | undefined>;
19
+ };
20
+
21
+ export type TextareaProps = {
22
+ value?: string;
23
+ defaultValue?: string;
24
+ onValueChange?: (value: string) => void;
25
+ onValueCommit?: (value: string) => void;
26
+ disabled?: boolean;
27
+ readOnly?: boolean;
28
+ required?: boolean;
29
+ invalid?: boolean;
30
+ name?: string;
31
+ autoResize?: boolean;
32
+ minRows?: number;
33
+ maxRows?: number;
34
+ children?: React.ReactNode;
35
+ };
36
+
37
+ export type TextareaInputProps = {
38
+ asChild?: boolean;
39
+ disabled?: boolean;
40
+ readOnly?: boolean;
41
+ lineHeight?: number;
42
+ children?: React.ReactElement;
43
+ };
44
+
45
+ export type TextareaLabelProps = {
46
+ asChild?: boolean;
47
+ children?: React.ReactElement;
48
+ };
49
+
50
+ export type TextareaDescriptionProps = {
51
+ asChild?: boolean;
52
+ children?: React.ReactElement;
53
+ };
54
+
55
+ export type TextareaMessageProps = {
56
+ asChild?: boolean;
57
+ children?: React.ReactElement;
58
+ };
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { TextareaDescription } from "./Textarea/TextareaDescription";
2
+ import { TextareaInput } from "./Textarea/TextareaInput";
3
+ import { TextareaLabel } from "./Textarea/TextareaLabel";
4
+ import { TextareaMessage } from "./Textarea/TextareaMessage";
5
+ import { TextareaRoot } from "./Textarea/TextareaRoot";
6
+
7
+ export const Textarea = {
8
+ Root: TextareaRoot,
9
+ Input: TextareaInput,
10
+ Label: TextareaLabel,
11
+ Description: TextareaDescription,
12
+ Message: TextareaMessage,
13
+ } as const;
14
+
15
+ export { resolveTextareaHeight } from "./Textarea/autoResize";
16
+ export type {
17
+ TextareaCommitValue,
18
+ TextareaContextValue,
19
+ TextareaDescriptionProps,
20
+ TextareaInputProps,
21
+ TextareaLabelProps,
22
+ TextareaMessageProps,
23
+ TextareaProps,
24
+ TextareaSetValue,
25
+ } from "./Textarea/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
+ }