@butternutbox/pawprint-native 0.0.1 → 0.2.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/.turbo/turbo-build.log +15 -15
- package/CHANGELOG.md +30 -0
- package/COMPONENT_GUIDELINES.md +111 -4
- package/dist/index.cjs +12413 -1459
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1111 -13
- package/dist/index.d.ts +1111 -13
- package/dist/index.js +12365 -1457
- package/dist/index.js.map +1 -1
- package/package.json +29 -11
- package/src/__mocks__/asset-stub.ts +1 -0
- package/src/__mocks__/emotion-native.tsx +18 -0
- package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
- package/src/__mocks__/react-native-reanimated.tsx +79 -0
- package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
- package/src/__mocks__/react-native-svg.tsx +27 -0
- package/src/__mocks__/react-native-worklets.tsx +11 -0
- package/src/__mocks__/react-native.tsx +338 -0
- package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
- package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
- package/src/__mocks__/rn-primitives/select.tsx +116 -0
- package/src/__mocks__/rn-primitives/slider.tsx +40 -0
- package/src/__mocks__/rn-primitives/slot.tsx +30 -0
- package/src/__mocks__/rn-primitives/switch.tsx +24 -0
- package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
- package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
- package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
- package/src/components/atoms/Avatar/Avatar.tsx +68 -22
- package/src/components/atoms/Avatar/index.ts +1 -6
- package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
- package/src/components/atoms/Badge/Badge.test.tsx +90 -0
- package/src/components/atoms/Button/Button.test.tsx +123 -0
- package/src/components/atoms/Button/Button.tsx +1 -1
- package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
- package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
- package/src/components/atoms/CarouselControls/index.ts +2 -0
- package/src/components/atoms/Hint/Hint.test.tsx +36 -0
- package/src/components/atoms/Icon/Icon.test.tsx +98 -0
- package/src/components/atoms/Icon/Icon.tsx +5 -1
- package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
- package/src/components/atoms/Illustration/Illustration.stories.tsx +2 -2
- package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
- package/src/components/atoms/Illustration/Illustration.tsx +3 -3
- package/src/components/atoms/Input/Input.stories.tsx +129 -86
- package/src/components/atoms/Input/Input.test.tsx +306 -0
- package/src/components/atoms/Input/Input.tsx +9 -1
- package/src/components/atoms/Input/InputField.tsx +226 -74
- package/src/components/atoms/Link/Link.test.tsx +89 -0
- package/src/components/atoms/Link/Link.tsx +7 -6
- package/src/components/atoms/Logo/Logo.registry.ts +30 -5
- package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
- package/src/components/atoms/Logo/Logo.test.tsx +56 -0
- package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
- package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
- package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
- package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
- package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
- package/src/components/atoms/Logo/assets/index.ts +11 -0
- package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
- package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
- package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
- package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
- package/src/components/atoms/NumberInput/index.ts +4 -0
- package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
- package/src/components/atoms/Spinner/Spinner.tsx +14 -5
- package/src/components/atoms/Switch/Switch.test.tsx +92 -0
- package/src/components/atoms/Switch/Switch.tsx +28 -17
- package/src/components/atoms/Tag/Tag.test.tsx +70 -0
- package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
- package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
- package/src/components/atoms/TextArea/TextArea.tsx +171 -0
- package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
- package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
- package/src/components/atoms/TextArea/index.ts +6 -0
- package/src/components/atoms/Typography/Typography.test.tsx +94 -0
- package/src/components/atoms/index.ts +3 -0
- package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
- package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
- package/src/components/molecules/Accordion/Accordion.tsx +284 -0
- package/src/components/molecules/Accordion/index.ts +6 -0
- package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
- package/src/components/molecules/Animated/Animated.tsx +283 -0
- package/src/components/molecules/Animated/index.ts +10 -0
- package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +44 -25
- package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
- package/src/components/molecules/ButtonDock/ButtonDock.tsx +16 -13
- package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +48 -29
- package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
- package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
- package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
- package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
- package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
- package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
- package/src/components/molecules/CopyField/CopyField.tsx +156 -0
- package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
- package/src/components/molecules/CopyField/hooks/index.ts +1 -0
- package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
- package/src/components/molecules/CopyField/index.ts +4 -0
- package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
- package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
- package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
- package/src/components/molecules/DatePicker/index.ts +2 -0
- package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
- package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
- package/src/components/molecules/Drawer/Drawer.tsx +187 -0
- package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
- package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
- package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
- package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
- package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
- package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
- package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
- package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
- package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
- package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
- package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
- package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
- package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
- package/src/components/molecules/Drawer/index.ts +12 -0
- package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
- package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
- package/src/components/molecules/FilterTab/index.ts +2 -0
- package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
- package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
- package/src/components/molecules/MessageCard/index.ts +10 -0
- package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
- package/src/components/molecules/Notification/Notification.tsx +426 -0
- package/src/components/molecules/Notification/index.ts +2 -0
- package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
- package/src/components/molecules/NumberField/NumberField.tsx +186 -0
- package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
- package/src/components/molecules/NumberField/index.ts +2 -0
- package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
- package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
- package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
- package/src/components/molecules/PasswordField/PasswordFieldError.tsx +53 -0
- package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
- package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +95 -0
- package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
- package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
- package/src/components/molecules/PasswordField/index.ts +10 -0
- package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +204 -0
- package/src/components/molecules/PictureSelector/PictureSelector.tsx +335 -0
- package/src/components/molecules/PictureSelector/index.ts +5 -0
- package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
- package/src/components/molecules/Progress/Progress.tsx +184 -0
- package/src/components/molecules/Progress/index.ts +2 -0
- package/src/components/molecules/Radio/Radio.test.tsx +104 -0
- package/src/components/molecules/Radio/Radio.tsx +1 -2
- package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
- package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
- package/src/components/molecules/SearchField/SearchField.tsx +143 -0
- package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
- package/src/components/molecules/SearchField/hooks/index.ts +1 -0
- package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
- package/src/components/molecules/SearchField/index.ts +4 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
- package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
- package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
- package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
- package/src/components/molecules/SelectField/SelectField.tsx +236 -0
- package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
- package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
- package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
- package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
- package/src/components/molecules/SelectField/hooks/index.ts +2 -0
- package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
- package/src/components/molecules/SelectField/index.ts +10 -0
- package/src/components/molecules/Slider/Slider.test.tsx +102 -0
- package/src/components/molecules/Slider/Slider.tsx +293 -180
- package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
- package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
- package/src/components/molecules/Tooltip/index.ts +2 -0
- package/src/components/molecules/index.ts +15 -0
- package/src/test-utils.tsx +20 -0
- package/tsconfig.json +1 -1
- package/tsup.config.ts +16 -2
- package/vitest.config.ts +114 -0
- package/vitest.setup.ts +16 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { TextInput } from "react-native"
|
|
3
|
+
import { screen } from "@testing-library/react"
|
|
4
|
+
import userEvent from "@testing-library/user-event"
|
|
5
|
+
import { describe, it, expect, vi } from "vitest"
|
|
6
|
+
import { renderWithTheme } from "../../../test-utils"
|
|
7
|
+
import { TextArea } from "./TextArea"
|
|
8
|
+
|
|
9
|
+
describe("TextArea", () => {
|
|
10
|
+
describe("when using simple props API", () => {
|
|
11
|
+
it("renders label when provided", () => {
|
|
12
|
+
renderWithTheme(
|
|
13
|
+
<TextArea label="Description" placeholder="Enter text..." />
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
expect(screen.getByText("Description")).toBeInTheDocument()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("renders description when provided", () => {
|
|
20
|
+
renderWithTheme(
|
|
21
|
+
<TextArea
|
|
22
|
+
label="Description"
|
|
23
|
+
placeholder="Enter text..."
|
|
24
|
+
description="Please provide details"
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
expect(screen.getByText("Please provide details")).toBeInTheDocument()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("renders error message when error prop and state='error' are provided", () => {
|
|
32
|
+
renderWithTheme(
|
|
33
|
+
<TextArea
|
|
34
|
+
label="Description"
|
|
35
|
+
placeholder="Enter text..."
|
|
36
|
+
state="error"
|
|
37
|
+
error="Description is required"
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
expect(screen.getByText("Description is required")).toBeInTheDocument()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("does not render error message when error prop is provided without state='error'", () => {
|
|
45
|
+
renderWithTheme(
|
|
46
|
+
<TextArea
|
|
47
|
+
label="Description"
|
|
48
|
+
placeholder="Enter text..."
|
|
49
|
+
error="Description is required"
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
expect(
|
|
54
|
+
screen.queryByText("Description is required")
|
|
55
|
+
).not.toBeInTheDocument()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("shows (optional) when optionalText is provided", () => {
|
|
59
|
+
renderWithTheme(
|
|
60
|
+
<TextArea
|
|
61
|
+
label="Description"
|
|
62
|
+
placeholder="Enter text..."
|
|
63
|
+
optionalText="(optional)"
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect(screen.getByText("(optional)")).toBeInTheDocument()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("renders textarea with placeholder", () => {
|
|
71
|
+
renderWithTheme(
|
|
72
|
+
<TextArea label="Description" placeholder="Enter text..." />
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
expect(screen.getByPlaceholderText("Enter text...")).toBeInTheDocument()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("accepts user input", async () => {
|
|
79
|
+
const user = userEvent.setup()
|
|
80
|
+
|
|
81
|
+
renderWithTheme(
|
|
82
|
+
<TextArea label="Description" placeholder="Enter text..." />
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const textarea = screen.getByPlaceholderText("Enter text...")
|
|
86
|
+
await user.type(textarea, "Test description")
|
|
87
|
+
|
|
88
|
+
expect(textarea).toHaveValue("Test description")
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("shows character count when maxLength is provided", () => {
|
|
92
|
+
renderWithTheme(
|
|
93
|
+
<TextArea
|
|
94
|
+
label="Description"
|
|
95
|
+
placeholder="Enter text..."
|
|
96
|
+
maxLength={100}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
expect(screen.getByText("0/100")).toBeInTheDocument()
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe("when using controlled mode", () => {
|
|
105
|
+
it("updates value when parent updates", async () => {
|
|
106
|
+
const user = userEvent.setup()
|
|
107
|
+
|
|
108
|
+
function ControlledTextArea() {
|
|
109
|
+
const [value, setValue] = React.useState("")
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<TextArea
|
|
113
|
+
label="Description"
|
|
114
|
+
value={value}
|
|
115
|
+
onValueChange={setValue}
|
|
116
|
+
placeholder="Enter text..."
|
|
117
|
+
/>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
renderWithTheme(<ControlledTextArea />)
|
|
122
|
+
|
|
123
|
+
const textarea = screen.getByPlaceholderText("Enter text...")
|
|
124
|
+
await user.type(textarea, "Test description")
|
|
125
|
+
|
|
126
|
+
expect(textarea).toHaveValue("Test description")
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("calls onValueChange when value changes", async () => {
|
|
130
|
+
const user = userEvent.setup()
|
|
131
|
+
const onValueChange = vi.fn()
|
|
132
|
+
|
|
133
|
+
renderWithTheme(
|
|
134
|
+
<TextArea
|
|
135
|
+
label="Description"
|
|
136
|
+
value=""
|
|
137
|
+
onValueChange={onValueChange}
|
|
138
|
+
placeholder="Enter text..."
|
|
139
|
+
/>
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const textarea = screen.getByPlaceholderText("Enter text...")
|
|
143
|
+
await user.type(textarea, "a")
|
|
144
|
+
|
|
145
|
+
expect(onValueChange).toHaveBeenCalled()
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe("when disabled", () => {
|
|
150
|
+
it("disables the textarea", () => {
|
|
151
|
+
renderWithTheme(
|
|
152
|
+
<TextArea
|
|
153
|
+
label="Description"
|
|
154
|
+
placeholder="Enter text..."
|
|
155
|
+
editable={false}
|
|
156
|
+
/>
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
expect(screen.getByPlaceholderText("Enter text...")).toBeDisabled()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it("does not accept user input when disabled", async () => {
|
|
163
|
+
const user = userEvent.setup()
|
|
164
|
+
|
|
165
|
+
renderWithTheme(
|
|
166
|
+
<TextArea
|
|
167
|
+
label="Description"
|
|
168
|
+
placeholder="Enter text..."
|
|
169
|
+
editable={false}
|
|
170
|
+
/>
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const textarea = screen.getByPlaceholderText("Enter text...")
|
|
174
|
+
await user.type(textarea, "test")
|
|
175
|
+
|
|
176
|
+
expect(textarea).toHaveValue("")
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe("character count", () => {
|
|
181
|
+
it("updates character count as user types", async () => {
|
|
182
|
+
const user = userEvent.setup()
|
|
183
|
+
|
|
184
|
+
renderWithTheme(
|
|
185
|
+
<TextArea
|
|
186
|
+
label="Description"
|
|
187
|
+
placeholder="Enter text..."
|
|
188
|
+
maxLength={100}
|
|
189
|
+
/>
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
const textarea = screen.getByPlaceholderText("Enter text...")
|
|
193
|
+
await user.type(textarea, "Hello")
|
|
194
|
+
|
|
195
|
+
expect(screen.getByText("5/100")).toBeInTheDocument()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it("shows character count in controlled mode", async () => {
|
|
199
|
+
const user = userEvent.setup()
|
|
200
|
+
|
|
201
|
+
function ControlledTextArea() {
|
|
202
|
+
const [value, setValue] = React.useState("")
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<TextArea
|
|
206
|
+
label="Description"
|
|
207
|
+
value={value}
|
|
208
|
+
onValueChange={setValue}
|
|
209
|
+
placeholder="Enter text..."
|
|
210
|
+
maxLength={50}
|
|
211
|
+
/>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
renderWithTheme(<ControlledTextArea />)
|
|
216
|
+
|
|
217
|
+
const textarea = screen.getByPlaceholderText("Enter text...")
|
|
218
|
+
await user.type(textarea, "Test")
|
|
219
|
+
|
|
220
|
+
expect(screen.getByText("4/50")).toBeInTheDocument()
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
describe("when using compound component API", () => {
|
|
225
|
+
it("renders compound components", () => {
|
|
226
|
+
renderWithTheme(
|
|
227
|
+
<TextArea.Root>
|
|
228
|
+
<TextArea.Label>Description</TextArea.Label>
|
|
229
|
+
<TextArea.Field placeholder="Enter text..." />
|
|
230
|
+
<TextArea.Description>Provide details</TextArea.Description>
|
|
231
|
+
</TextArea.Root>
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
expect(screen.getByText("Description")).toBeInTheDocument()
|
|
235
|
+
expect(screen.getByPlaceholderText("Enter text...")).toBeInTheDocument()
|
|
236
|
+
expect(screen.getByText("Provide details")).toBeInTheDocument()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it("renders error in compound mode", () => {
|
|
240
|
+
renderWithTheme(
|
|
241
|
+
<TextArea.Root>
|
|
242
|
+
<TextArea.Label state="error">Description</TextArea.Label>
|
|
243
|
+
<TextArea.Field placeholder="Enter text..." state="error" />
|
|
244
|
+
<TextArea.Error>Invalid description</TextArea.Error>
|
|
245
|
+
</TextArea.Root>
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
expect(screen.getByText("Invalid description")).toBeInTheDocument()
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe("validation states", () => {
|
|
253
|
+
describe("manual state control", () => {
|
|
254
|
+
it("renders error state when state='error'", () => {
|
|
255
|
+
renderWithTheme(
|
|
256
|
+
<TextArea
|
|
257
|
+
label="Description"
|
|
258
|
+
placeholder="Enter text..."
|
|
259
|
+
state="error"
|
|
260
|
+
/>
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
const textarea = screen.getByPlaceholderText("Enter text...")
|
|
264
|
+
expect(textarea).toBeInTheDocument()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it("renders success state when state='success'", () => {
|
|
268
|
+
renderWithTheme(
|
|
269
|
+
<TextArea
|
|
270
|
+
label="Description"
|
|
271
|
+
placeholder="Enter text..."
|
|
272
|
+
state="success"
|
|
273
|
+
/>
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
const textarea = screen.getByPlaceholderText("Enter text...")
|
|
277
|
+
expect(textarea).toBeInTheDocument()
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it("renders default state when state='default'", () => {
|
|
281
|
+
renderWithTheme(
|
|
282
|
+
<TextArea
|
|
283
|
+
label="Description"
|
|
284
|
+
placeholder="Enter text..."
|
|
285
|
+
state="default"
|
|
286
|
+
/>
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
const textarea = screen.getByPlaceholderText("Enter text...")
|
|
290
|
+
expect(textarea).toBeInTheDocument()
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it("shows error state with error message", () => {
|
|
294
|
+
renderWithTheme(
|
|
295
|
+
<TextArea
|
|
296
|
+
label="Description"
|
|
297
|
+
placeholder="Enter text..."
|
|
298
|
+
state="error"
|
|
299
|
+
error="Description is required"
|
|
300
|
+
/>
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
expect(screen.getByText("Description is required")).toBeInTheDocument()
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
describe("compound component API with states", () => {
|
|
309
|
+
it("renders error state in compound mode", () => {
|
|
310
|
+
renderWithTheme(
|
|
311
|
+
<TextArea.Root>
|
|
312
|
+
<TextArea.Label state="error">Description</TextArea.Label>
|
|
313
|
+
<TextArea.Field placeholder="Enter text..." state="error" />
|
|
314
|
+
<TextArea.Error>Invalid description</TextArea.Error>
|
|
315
|
+
</TextArea.Root>
|
|
316
|
+
)
|
|
317
|
+
expect(screen.getByText("Invalid description")).toBeInTheDocument()
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it("renders success state in compound mode", () => {
|
|
321
|
+
renderWithTheme(
|
|
322
|
+
<TextArea.Root>
|
|
323
|
+
<TextArea.Label state="success">Description</TextArea.Label>
|
|
324
|
+
<TextArea.Field placeholder="Enter text..." state="success" />
|
|
325
|
+
<TextArea.Description state="success">
|
|
326
|
+
Description verified
|
|
327
|
+
</TextArea.Description>
|
|
328
|
+
</TextArea.Root>
|
|
329
|
+
)
|
|
330
|
+
expect(screen.getByText("Description verified")).toBeInTheDocument()
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
describe("rows prop", () => {
|
|
335
|
+
it("renders with default rows", () => {
|
|
336
|
+
renderWithTheme(
|
|
337
|
+
<TextArea label="Description" placeholder="Enter text..." />
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
const textarea = screen.getByPlaceholderText("Enter text...")
|
|
341
|
+
expect(textarea).toBeInTheDocument()
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it("renders with custom rows", () => {
|
|
345
|
+
renderWithTheme(
|
|
346
|
+
<TextArea label="Description" placeholder="Enter text..." rows={5} />
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
const textarea = screen.getByPlaceholderText("Enter text...")
|
|
350
|
+
expect(textarea).toBeInTheDocument()
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
describe("when rendering icons", () => {
|
|
355
|
+
it("renders leading icon", () => {
|
|
356
|
+
renderWithTheme(
|
|
357
|
+
<TextArea.Root>
|
|
358
|
+
<TextArea.Field
|
|
359
|
+
placeholder="Search"
|
|
360
|
+
leadingIcon={<span data-testid="leading-icon">icon</span>}
|
|
361
|
+
/>
|
|
362
|
+
</TextArea.Root>
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
expect(screen.getByTestId("leading-icon")).toBeInTheDocument()
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it("renders trailing icon", () => {
|
|
369
|
+
renderWithTheme(
|
|
370
|
+
<TextArea.Root>
|
|
371
|
+
<TextArea.Field
|
|
372
|
+
placeholder="Search"
|
|
373
|
+
trailingIcon={<span data-testid="trailing-icon">icon</span>}
|
|
374
|
+
/>
|
|
375
|
+
</TextArea.Root>
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
expect(screen.getByTestId("trailing-icon")).toBeInTheDocument()
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it("renders both leading and trailing icons", () => {
|
|
382
|
+
renderWithTheme(
|
|
383
|
+
<TextArea.Root>
|
|
384
|
+
<TextArea.Field
|
|
385
|
+
placeholder="Search"
|
|
386
|
+
leadingIcon={<span data-testid="leading-icon">icon</span>}
|
|
387
|
+
trailingIcon={<span data-testid="trailing-icon">icon</span>}
|
|
388
|
+
/>
|
|
389
|
+
</TextArea.Root>
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
expect(screen.getByTestId("leading-icon")).toBeInTheDocument()
|
|
393
|
+
expect(screen.getByTestId("trailing-icon")).toBeInTheDocument()
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it("hides state icons when hideStateIcons is true", () => {
|
|
397
|
+
const { container } = renderWithTheme(
|
|
398
|
+
<TextArea.Root>
|
|
399
|
+
<TextArea.Field placeholder="Text" state="error" hideStateIcons />
|
|
400
|
+
</TextArea.Root>
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
expect(container.querySelectorAll("svg")).toHaveLength(0)
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
describe("when using with ref", () => {
|
|
408
|
+
it("forwards ref to input element", () => {
|
|
409
|
+
const ref = React.createRef<TextInput>()
|
|
410
|
+
|
|
411
|
+
renderWithTheme(<TextArea ref={ref} label="Test" />)
|
|
412
|
+
|
|
413
|
+
expect(ref.current).toBeTruthy()
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
})
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { TextAreaField, type TextAreaFieldProps } from "./TextAreaField"
|
|
5
|
+
import { InputError } from "../Input/InputError"
|
|
6
|
+
import type { InputState } from "../Input/InputField"
|
|
7
|
+
import { TextAreaLabel } from "./TextAreaLabel"
|
|
8
|
+
import { InputDescription } from "../Input/InputDescription"
|
|
9
|
+
|
|
10
|
+
type TextAreaOwnProps = {
|
|
11
|
+
label?: string
|
|
12
|
+
description?: string
|
|
13
|
+
error?: string
|
|
14
|
+
state?: InputState
|
|
15
|
+
optionalText?: string
|
|
16
|
+
onValueChange?: (value: string) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type TextAreaProps = TextAreaOwnProps &
|
|
20
|
+
Omit<TextAreaFieldProps, keyof TextAreaOwnProps> &
|
|
21
|
+
Omit<ViewProps, keyof TextAreaOwnProps>
|
|
22
|
+
|
|
23
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
24
|
+
|
|
25
|
+
const StyledRoot = styled(View)(({ theme }) => {
|
|
26
|
+
const { spacing } = theme.tokens.components.inputs
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
gap: parseTokenValue(spacing.gap)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Multi-line text input component for collecting longer user input.
|
|
35
|
+
* Supports both a simple props API and a flexible compound component API.
|
|
36
|
+
*
|
|
37
|
+
* **Simple Props API:**
|
|
38
|
+
* @example
|
|
39
|
+
* <TextArea
|
|
40
|
+
* label="Description"
|
|
41
|
+
* placeholder="Enter your description..."
|
|
42
|
+
* description="Provide a detailed description"
|
|
43
|
+
* error="Description is required"
|
|
44
|
+
* optionalText="(optional)"
|
|
45
|
+
* maxLength={500}
|
|
46
|
+
* rows={5}
|
|
47
|
+
* />
|
|
48
|
+
*
|
|
49
|
+
* **Compound Component API:**
|
|
50
|
+
* @example
|
|
51
|
+
* <TextArea.Root>
|
|
52
|
+
* <TextArea.Label optionalText="(optional)" maxLength={500} currentLength={text.length}>
|
|
53
|
+
* Description
|
|
54
|
+
* </TextArea.Label>
|
|
55
|
+
* <TextArea.Field
|
|
56
|
+
* placeholder="Enter your description..."
|
|
57
|
+
* maxLength={500}
|
|
58
|
+
* rows={5}
|
|
59
|
+
* />
|
|
60
|
+
* <TextArea.Description>Provide a detailed description</TextArea.Description>
|
|
61
|
+
* <TextArea.Error>Description is required</TextArea.Error>
|
|
62
|
+
* </TextArea.Root>
|
|
63
|
+
*
|
|
64
|
+
* @param {string} [label] - Label text (props API)
|
|
65
|
+
* @param {string} [description] - Description/help text (props API)
|
|
66
|
+
* @param {string} [error] - Error message to display (props API, does not affect visual state - use state prop for that)
|
|
67
|
+
* @param {InputState} [state] - Visual state of the textarea: 'default', 'error', or 'success' (props API)
|
|
68
|
+
* @param {string} [optionalText] - Optional text to display next to label (props API)
|
|
69
|
+
* @param {React.ReactNode} [leadingIcon] - Icon to display at the start of the textarea
|
|
70
|
+
* @param {React.ReactNode} [trailingIcon] - Icon to display at the end of the textarea (overrides automatic state icons)
|
|
71
|
+
* @param {boolean} [hideStateIcons] - Hide automatic error/success icons
|
|
72
|
+
* @param {string} [placeholder] - Placeholder text
|
|
73
|
+
* @param {string} [value] - Controlled value
|
|
74
|
+
* @param {string} [defaultValue] - Default value for uncontrolled mode
|
|
75
|
+
* @param {function} [onChangeText] - Change event handler
|
|
76
|
+
* @param {number} [maxLength] - Maximum character length (shows character counter when provided)
|
|
77
|
+
* @param {number} [rows=2] - Number of visible text rows
|
|
78
|
+
* @param {boolean} [editable] - Controls whether the textarea is editable
|
|
79
|
+
*
|
|
80
|
+
* Also supports all native TextInput props.
|
|
81
|
+
*/
|
|
82
|
+
const TextAreaRoot = React.forwardRef<View, TextAreaProps>(
|
|
83
|
+
(
|
|
84
|
+
{
|
|
85
|
+
label,
|
|
86
|
+
description,
|
|
87
|
+
error,
|
|
88
|
+
state = "default",
|
|
89
|
+
optionalText,
|
|
90
|
+
onValueChange,
|
|
91
|
+
children,
|
|
92
|
+
maxLength,
|
|
93
|
+
value,
|
|
94
|
+
defaultValue,
|
|
95
|
+
onChangeText,
|
|
96
|
+
...textAreaFieldProps
|
|
97
|
+
},
|
|
98
|
+
ref
|
|
99
|
+
) => {
|
|
100
|
+
const [charCount, setCharCount] = React.useState(0)
|
|
101
|
+
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
if (value !== undefined) {
|
|
104
|
+
setCharCount(String(value).length)
|
|
105
|
+
} else if (defaultValue !== undefined) {
|
|
106
|
+
setCharCount(String(defaultValue).length)
|
|
107
|
+
}
|
|
108
|
+
}, [value, defaultValue])
|
|
109
|
+
|
|
110
|
+
const handleChangeText = (text: string) => {
|
|
111
|
+
setCharCount(text.length)
|
|
112
|
+
if (onChangeText) {
|
|
113
|
+
onChangeText(text)
|
|
114
|
+
}
|
|
115
|
+
if (onValueChange) {
|
|
116
|
+
onValueChange(text)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (children) {
|
|
121
|
+
return <StyledRoot ref={ref}>{children}</StyledRoot>
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<StyledRoot ref={ref}>
|
|
126
|
+
{label && (
|
|
127
|
+
<TextAreaLabel
|
|
128
|
+
optionalText={optionalText}
|
|
129
|
+
state={state}
|
|
130
|
+
maxLength={maxLength}
|
|
131
|
+
currentLength={charCount}
|
|
132
|
+
>
|
|
133
|
+
{label}
|
|
134
|
+
</TextAreaLabel>
|
|
135
|
+
)}
|
|
136
|
+
<TextAreaField
|
|
137
|
+
state={state}
|
|
138
|
+
maxLength={maxLength}
|
|
139
|
+
value={value}
|
|
140
|
+
defaultValue={defaultValue}
|
|
141
|
+
onChangeText={handleChangeText}
|
|
142
|
+
{...textAreaFieldProps}
|
|
143
|
+
/>
|
|
144
|
+
{description && (
|
|
145
|
+
<InputDescription state={state}>{description}</InputDescription>
|
|
146
|
+
)}
|
|
147
|
+
{error && state === "error" && <InputError>{error}</InputError>}
|
|
148
|
+
</StyledRoot>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
TextAreaRoot.displayName = "TextArea"
|
|
154
|
+
|
|
155
|
+
type TextAreaComponent = React.ForwardRefExoticComponent<
|
|
156
|
+
TextAreaProps & React.RefAttributes<View>
|
|
157
|
+
> & {
|
|
158
|
+
Root: typeof StyledRoot
|
|
159
|
+
Label: typeof TextAreaLabel
|
|
160
|
+
Field: typeof TextAreaField
|
|
161
|
+
Description: typeof InputDescription
|
|
162
|
+
Error: typeof InputError
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const TextArea = Object.assign(TextAreaRoot, {
|
|
166
|
+
Root: StyledRoot,
|
|
167
|
+
Label: TextAreaLabel,
|
|
168
|
+
Field: TextAreaField,
|
|
169
|
+
Description: InputDescription,
|
|
170
|
+
Error: InputError
|
|
171
|
+
}) as TextAreaComponent
|