@butternutbox/pawprint-native 0.0.1 → 0.1.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.
Files changed (182) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/CHANGELOG.md +16 -0
  3. package/COMPONENT_GUIDELINES.md +111 -4
  4. package/dist/index.cjs +12370 -1455
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +1110 -11
  7. package/dist/index.d.ts +1110 -11
  8. package/dist/index.js +12324 -1455
  9. package/dist/index.js.map +1 -1
  10. package/package.json +28 -9
  11. package/src/__mocks__/asset-stub.ts +1 -0
  12. package/src/__mocks__/emotion-native.tsx +18 -0
  13. package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
  14. package/src/__mocks__/react-native-reanimated.tsx +79 -0
  15. package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
  16. package/src/__mocks__/react-native-svg.tsx +27 -0
  17. package/src/__mocks__/react-native-worklets.tsx +11 -0
  18. package/src/__mocks__/react-native.tsx +338 -0
  19. package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
  20. package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
  21. package/src/__mocks__/rn-primitives/select.tsx +116 -0
  22. package/src/__mocks__/rn-primitives/slider.tsx +40 -0
  23. package/src/__mocks__/rn-primitives/slot.tsx +30 -0
  24. package/src/__mocks__/rn-primitives/switch.tsx +24 -0
  25. package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
  26. package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
  27. package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
  28. package/src/components/atoms/Avatar/Avatar.tsx +68 -22
  29. package/src/components/atoms/Avatar/index.ts +1 -6
  30. package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
  31. package/src/components/atoms/Badge/Badge.test.tsx +90 -0
  32. package/src/components/atoms/Button/Button.test.tsx +123 -0
  33. package/src/components/atoms/Button/Button.tsx +1 -1
  34. package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
  35. package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
  36. package/src/components/atoms/CarouselControls/index.ts +2 -0
  37. package/src/components/atoms/Hint/Hint.test.tsx +36 -0
  38. package/src/components/atoms/Icon/Icon.test.tsx +98 -0
  39. package/src/components/atoms/Icon/Icon.tsx +5 -1
  40. package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
  41. package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
  42. package/src/components/atoms/Input/Input.stories.tsx +129 -86
  43. package/src/components/atoms/Input/Input.test.tsx +306 -0
  44. package/src/components/atoms/Input/Input.tsx +9 -1
  45. package/src/components/atoms/Input/InputField.tsx +226 -74
  46. package/src/components/atoms/Link/Link.test.tsx +89 -0
  47. package/src/components/atoms/Logo/Logo.registry.ts +30 -5
  48. package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
  49. package/src/components/atoms/Logo/Logo.test.tsx +56 -0
  50. package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
  51. package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
  52. package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
  53. package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
  54. package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
  55. package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
  56. package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
  57. package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
  58. package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
  59. package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
  60. package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
  61. package/src/components/atoms/Logo/assets/index.ts +11 -0
  62. package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
  63. package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
  64. package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
  65. package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
  66. package/src/components/atoms/NumberInput/index.ts +4 -0
  67. package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
  68. package/src/components/atoms/Spinner/Spinner.tsx +14 -5
  69. package/src/components/atoms/Switch/Switch.test.tsx +92 -0
  70. package/src/components/atoms/Switch/Switch.tsx +16 -13
  71. package/src/components/atoms/Tag/Tag.test.tsx +70 -0
  72. package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
  73. package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
  74. package/src/components/atoms/TextArea/TextArea.tsx +171 -0
  75. package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
  76. package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
  77. package/src/components/atoms/TextArea/index.ts +6 -0
  78. package/src/components/atoms/Typography/Typography.test.tsx +94 -0
  79. package/src/components/atoms/index.ts +3 -0
  80. package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
  81. package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
  82. package/src/components/molecules/Accordion/Accordion.tsx +284 -0
  83. package/src/components/molecules/Accordion/index.ts +6 -0
  84. package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
  85. package/src/components/molecules/Animated/Animated.tsx +283 -0
  86. package/src/components/molecules/Animated/index.ts +10 -0
  87. package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
  88. package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +8 -14
  89. package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
  90. package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
  91. package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
  92. package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
  93. package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
  94. package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
  95. package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
  96. package/src/components/molecules/CopyField/CopyField.tsx +156 -0
  97. package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
  98. package/src/components/molecules/CopyField/hooks/index.ts +1 -0
  99. package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
  100. package/src/components/molecules/CopyField/index.ts +4 -0
  101. package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
  102. package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
  103. package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
  104. package/src/components/molecules/DatePicker/index.ts +2 -0
  105. package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
  106. package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
  107. package/src/components/molecules/Drawer/Drawer.tsx +187 -0
  108. package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
  109. package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
  110. package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
  111. package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
  112. package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
  113. package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
  114. package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
  115. package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
  116. package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
  117. package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
  118. package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
  119. package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
  120. package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
  121. package/src/components/molecules/Drawer/index.ts +12 -0
  122. package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
  123. package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
  124. package/src/components/molecules/FilterTab/index.ts +2 -0
  125. package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
  126. package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
  127. package/src/components/molecules/MessageCard/index.ts +10 -0
  128. package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
  129. package/src/components/molecules/Notification/Notification.tsx +426 -0
  130. package/src/components/molecules/Notification/index.ts +2 -0
  131. package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
  132. package/src/components/molecules/NumberField/NumberField.tsx +186 -0
  133. package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
  134. package/src/components/molecules/NumberField/index.ts +2 -0
  135. package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
  136. package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
  137. package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
  138. package/src/components/molecules/PasswordField/PasswordFieldError.tsx +52 -0
  139. package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
  140. package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +92 -0
  141. package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
  142. package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
  143. package/src/components/molecules/PasswordField/index.ts +10 -0
  144. package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +243 -0
  145. package/src/components/molecules/PictureSelector/PictureSelector.tsx +313 -0
  146. package/src/components/molecules/PictureSelector/index.ts +5 -0
  147. package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
  148. package/src/components/molecules/Progress/Progress.tsx +184 -0
  149. package/src/components/molecules/Progress/index.ts +2 -0
  150. package/src/components/molecules/Radio/Radio.test.tsx +104 -0
  151. package/src/components/molecules/Radio/Radio.tsx +1 -2
  152. package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
  153. package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
  154. package/src/components/molecules/SearchField/SearchField.tsx +143 -0
  155. package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
  156. package/src/components/molecules/SearchField/hooks/index.ts +1 -0
  157. package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
  158. package/src/components/molecules/SearchField/index.ts +4 -0
  159. package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
  160. package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
  161. package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
  162. package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
  163. package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
  164. package/src/components/molecules/SelectField/SelectField.tsx +236 -0
  165. package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
  166. package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
  167. package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
  168. package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
  169. package/src/components/molecules/SelectField/hooks/index.ts +2 -0
  170. package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
  171. package/src/components/molecules/SelectField/index.ts +10 -0
  172. package/src/components/molecules/Slider/Slider.test.tsx +102 -0
  173. package/src/components/molecules/Slider/Slider.tsx +293 -180
  174. package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
  175. package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
  176. package/src/components/molecules/Tooltip/index.ts +2 -0
  177. package/src/components/molecules/index.ts +15 -0
  178. package/src/test-utils.tsx +20 -0
  179. package/tsconfig.json +1 -1
  180. package/tsup.config.ts +16 -2
  181. package/vitest.config.ts +114 -0
  182. package/vitest.setup.ts +16 -0
@@ -1,8 +1,10 @@
1
- import React, { useState } from "react"
1
+ import React from "react"
2
2
  import { View, StyleSheet } from "react-native"
3
3
  import { Input } from "./Input"
4
4
  import type { InputProps } from "./Input"
5
- import { Typography } from "../Typography"
5
+ import type { InputState } from "./InputField"
6
+ import { Icon } from "../Icon"
7
+ import { Search, Info } from "@butternutbox/pawprint-icons/core"
6
8
 
7
9
  export default {
8
10
  title: "Atoms/Input",
@@ -12,10 +14,6 @@ export default {
12
14
  control: { type: "text" },
13
15
  description: "Label text"
14
16
  },
15
- placeholder: {
16
- control: { type: "text" },
17
- description: "Placeholder text"
18
- },
19
17
  description: {
20
18
  control: { type: "text" },
21
19
  description: "Help text below input"
@@ -24,107 +22,156 @@ export default {
24
22
  control: { type: "text" },
25
23
  description: "Error message"
26
24
  },
27
- state: {
28
- control: { type: "select" },
29
- options: ["default", "error", "success"],
30
- description: "Visual state of the input"
31
- },
32
25
  optionalText: {
33
26
  control: { type: "text" },
34
- description: "Optional indicator next to label"
27
+ description: "Optional text to display next to label"
35
28
  },
36
29
  disabled: {
37
30
  control: { type: "boolean" },
38
- description: "Prevents interaction"
31
+ description: "Disables the input"
39
32
  }
40
33
  }
41
34
  }
42
35
 
43
36
  export const Default = (args: InputProps) => <Input {...args} />
44
37
  Default.args = {
45
- label: "Email",
46
- placeholder: "you@butternutbox.com",
47
- description: "We'll never share your email",
48
- state: "default"
38
+ label: "Label",
39
+ placeholder: "Placeholder",
40
+ description: "Help text"
49
41
  }
50
42
 
43
+ export const WithIcons = () => (
44
+ <View style={styles.column}>
45
+ <Input
46
+ label="Leading Icon"
47
+ placeholder="Placeholder"
48
+ leadingIcon={<Icon icon={Search} size="md" />}
49
+ description="Help text"
50
+ />
51
+ <Input
52
+ label="Trailing Icon"
53
+ placeholder="Placeholder"
54
+ trailingIcon={<Icon icon={Search} size="md" />}
55
+ description="Help text"
56
+ />
57
+ <Input
58
+ label="Both Icons"
59
+ placeholder="Placeholder"
60
+ leadingIcon={<Icon icon={Search} size="md" />}
61
+ trailingIcon={<Icon icon={Info} size="md" />}
62
+ description="Help text"
63
+ />
64
+ </View>
65
+ )
66
+
51
67
  export const States = () => (
52
68
  <View style={styles.column}>
53
- <View style={styles.section}>
54
- <Typography size="sm" weight="semiBold" color="tertiary">
55
- Default
56
- </Typography>
57
- <Input
58
- label="Email"
59
- placeholder="you@butternutbox.com"
60
- description="We'll never share your email"
61
- state="default"
62
- />
63
- </View>
64
- <View style={styles.section}>
65
- <Typography size="sm" weight="semiBold" color="tertiary">
66
- Error
67
- </Typography>
68
- <Input
69
- label="Email"
70
- placeholder="you@butternutbox.com"
71
- error="Please enter a valid email address"
72
- state="error"
73
- />
74
- </View>
75
- <View style={styles.section}>
76
- <Typography size="sm" weight="semiBold" color="tertiary">
77
- Success
78
- </Typography>
79
- <Input
80
- label="Email"
81
- placeholder="you@butternutbox.com"
82
- description="Email is valid"
83
- state="success"
84
- />
85
- </View>
86
- <View style={styles.section}>
87
- <Typography size="sm" weight="semiBold" color="tertiary">
88
- Disabled
89
- </Typography>
90
- <Input
91
- label="Email"
92
- placeholder="you@butternutbox.com"
93
- editable={false}
94
- state="default"
95
- />
96
- </View>
97
- <View style={styles.section}>
98
- <Typography size="sm" weight="semiBold" color="tertiary">
99
- With optional text
100
- </Typography>
69
+ <Input
70
+ label="Default State"
71
+ placeholder="Enter text"
72
+ description="Normal input state"
73
+ />
74
+ <Input
75
+ label="Error State"
76
+ placeholder="Enter text"
77
+ state="error"
78
+ description="Manually set to error state"
79
+ />
80
+ <Input
81
+ label="Error with Custom Message"
82
+ placeholder="Enter text"
83
+ state="error"
84
+ description="Manually set to error state"
85
+ error="Custom error message"
86
+ />
87
+ <Input
88
+ label="Success State"
89
+ placeholder="Enter text"
90
+ state="success"
91
+ description="Manually set to success state"
92
+ />
93
+ <Input
94
+ label="Disabled"
95
+ placeholder="Enter text"
96
+ description="Help text"
97
+ editable={false}
98
+ />
99
+ </View>
100
+ )
101
+
102
+ export const KeyboardTypes = () => (
103
+ <View style={styles.column}>
104
+ <Input label="Default" keyboardType="default" placeholder="Enter text" />
105
+ <Input
106
+ label="Email"
107
+ keyboardType="email-address"
108
+ placeholder="you@butternutbox.com"
109
+ />
110
+ <Input label="Numeric" keyboardType="numeric" placeholder="Enter number" />
111
+ <Input
112
+ label="Phone"
113
+ keyboardType="phone-pad"
114
+ placeholder="+44 20 1234 5678"
115
+ />
116
+ <Input
117
+ label="URL"
118
+ keyboardType="url"
119
+ placeholder="https://butternutbox.com"
120
+ />
121
+ </View>
122
+ )
123
+
124
+ export const CustomStateValidationWithSuccess = () => {
125
+ const [username, setUsername] = React.useState("")
126
+ const [usernameState, setUsernameState] =
127
+ React.useState<InputState>("default")
128
+
129
+ const validateUsername = (value: string) => {
130
+ if (!value) {
131
+ setUsernameState("default")
132
+ return
133
+ }
134
+
135
+ const hasMinLength = value.length >= 3
136
+ const hasMaxLength = value.length <= 20
137
+ const isValidFormat = /^[a-z0-9_]+$/.test(value)
138
+
139
+ setUsernameState(
140
+ hasMinLength && hasMaxLength && isValidFormat ? "success" : "error"
141
+ )
142
+ }
143
+
144
+ const handleChange = (newValue: string) => {
145
+ setUsername(newValue)
146
+ validateUsername(newValue)
147
+ }
148
+
149
+ return (
150
+ <View style={styles.column}>
101
151
  <Input
102
- label="Phone"
103
- placeholder="07123 456789"
104
- optionalText="(optional)"
105
- state="default"
152
+ label="Username"
153
+ placeholder="Enter username"
154
+ value={username}
155
+ onValueChange={handleChange}
156
+ state={usernameState}
157
+ description="3-20 chars, lowercase letters, numbers, and underscores"
158
+ error="Custom validation error"
106
159
  />
107
160
  </View>
108
- </View>
109
- )
161
+ )
162
+ }
110
163
 
111
164
  export const Controlled = () => {
112
- const [value, setValue] = useState("")
113
- const hasError = value.length > 0 && !value.includes("@")
165
+ const [value, setValue] = React.useState("")
114
166
 
115
167
  return (
116
168
  <View style={styles.column}>
117
- <Typography size="sm" weight="semiBold" color="tertiary">
118
- Controlled: {value || "(empty)"}
119
- </Typography>
120
169
  <Input
121
- label="Email"
122
- placeholder="you@butternutbox.com"
170
+ label="Controlled Input"
123
171
  value={value}
124
- onChangeText={setValue}
125
- state={hasError ? "error" : "default"}
126
- error={hasError ? "Must contain @" : undefined}
127
- description="Type to see validation"
172
+ onValueChange={(newValue) => setValue(newValue)}
173
+ placeholder="Type something..."
174
+ description={`You typed: ${value.length} characters`}
128
175
  />
129
176
  </View>
130
177
  )
@@ -134,9 +181,5 @@ const styles = StyleSheet.create({
134
181
  column: {
135
182
  flexDirection: "column",
136
183
  gap: 24
137
- },
138
- section: {
139
- flexDirection: "column",
140
- gap: 8
141
184
  }
142
185
  })
@@ -0,0 +1,306 @@
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 { Input } from "./Input"
8
+
9
+ describe("Input", () => {
10
+ describe("when using simple props API", () => {
11
+ it("renders label when provided", () => {
12
+ renderWithTheme(<Input label="Email" placeholder="Enter email" />)
13
+ expect(screen.getByText("Email")).toBeInTheDocument()
14
+ })
15
+
16
+ it("renders description when provided", () => {
17
+ renderWithTheme(
18
+ <Input
19
+ label="Email"
20
+ placeholder="Enter email"
21
+ description="We'll never share your email"
22
+ />
23
+ )
24
+ expect(
25
+ screen.getByText("We'll never share your email")
26
+ ).toBeInTheDocument()
27
+ })
28
+
29
+ it("renders error message when error prop and state='error' are provided", () => {
30
+ renderWithTheme(
31
+ <Input
32
+ label="Email"
33
+ placeholder="Enter email"
34
+ state="error"
35
+ error="Invalid email address"
36
+ />
37
+ )
38
+ expect(screen.getByText("Invalid email address")).toBeInTheDocument()
39
+ })
40
+
41
+ it("does not render error message when error prop is provided without state='error'", () => {
42
+ renderWithTheme(
43
+ <Input
44
+ label="Email"
45
+ placeholder="Enter email"
46
+ error="Invalid email address"
47
+ />
48
+ )
49
+ expect(
50
+ screen.queryByText("Invalid email address")
51
+ ).not.toBeInTheDocument()
52
+ })
53
+
54
+ it("shows optionalText when provided", () => {
55
+ renderWithTheme(
56
+ <Input
57
+ label="Email"
58
+ placeholder="Enter email"
59
+ optionalText="(optional)"
60
+ />
61
+ )
62
+ expect(screen.getByText("(optional)")).toBeInTheDocument()
63
+ })
64
+
65
+ it("renders input with placeholder", () => {
66
+ renderWithTheme(<Input label="Email" placeholder="Enter email" />)
67
+ expect(screen.getByPlaceholderText("Enter email")).toBeInTheDocument()
68
+ })
69
+
70
+ it("accepts user input", async () => {
71
+ const user = userEvent.setup()
72
+ renderWithTheme(<Input label="Email" placeholder="Enter email" />)
73
+
74
+ const input = screen.getByPlaceholderText("Enter email")
75
+ await user.type(input, "test@example.com")
76
+ expect(input).toHaveValue("test@example.com")
77
+ })
78
+ })
79
+
80
+ describe("when using controlled mode", () => {
81
+ it("updates value when parent updates", async () => {
82
+ const user = userEvent.setup()
83
+
84
+ function ControlledInput() {
85
+ const [value, setValue] = React.useState("")
86
+ return (
87
+ <Input
88
+ label="Email"
89
+ value={value}
90
+ onValueChange={(newValue) => setValue(newValue)}
91
+ placeholder="Enter email"
92
+ />
93
+ )
94
+ }
95
+
96
+ renderWithTheme(<ControlledInput />)
97
+
98
+ const input = screen.getByPlaceholderText("Enter email")
99
+ await user.type(input, "test@example.com")
100
+ expect(input).toHaveValue("test@example.com")
101
+ })
102
+
103
+ it("calls onValueChange when value changes", async () => {
104
+ const user = userEvent.setup()
105
+ const onValueChange = vi.fn()
106
+
107
+ renderWithTheme(
108
+ <Input
109
+ label="Email"
110
+ value=""
111
+ onValueChange={onValueChange}
112
+ placeholder="Enter email"
113
+ />
114
+ )
115
+
116
+ const input = screen.getByPlaceholderText("Enter email")
117
+ await user.type(input, "a")
118
+ expect(onValueChange).toHaveBeenCalledWith("a")
119
+ })
120
+ })
121
+
122
+ describe("when disabled", () => {
123
+ it("disables the input", () => {
124
+ renderWithTheme(
125
+ <Input label="Email" placeholder="Enter email" editable={false} />
126
+ )
127
+ expect(screen.getByPlaceholderText("Enter email")).toBeDisabled()
128
+ })
129
+
130
+ it("does not accept user input when disabled", async () => {
131
+ const user = userEvent.setup()
132
+ renderWithTheme(
133
+ <Input label="Email" placeholder="Enter email" editable={false} />
134
+ )
135
+
136
+ const input = screen.getByPlaceholderText("Enter email")
137
+ await user.type(input, "test")
138
+ expect(input).toHaveValue("")
139
+ })
140
+ })
141
+
142
+ describe("when using compound component API", () => {
143
+ it("renders compound components", () => {
144
+ renderWithTheme(
145
+ <Input.Root>
146
+ <Input.Label>Email</Input.Label>
147
+ <Input.Field placeholder="Enter email" />
148
+ <Input.Description>Help text</Input.Description>
149
+ </Input.Root>
150
+ )
151
+
152
+ expect(screen.getByText("Email")).toBeInTheDocument()
153
+ expect(screen.getByPlaceholderText("Enter email")).toBeInTheDocument()
154
+ expect(screen.getByText("Help text")).toBeInTheDocument()
155
+ })
156
+
157
+ it("renders error in compound mode", () => {
158
+ renderWithTheme(
159
+ <Input.Root>
160
+ <Input.Label state="error">Email</Input.Label>
161
+ <Input.Field placeholder="Enter email" state="error" />
162
+ <Input.Error>Invalid email</Input.Error>
163
+ </Input.Root>
164
+ )
165
+ expect(screen.getByText("Invalid email")).toBeInTheDocument()
166
+ })
167
+ })
168
+
169
+ describe("keyboard types", () => {
170
+ it("renders with email keyboard type", () => {
171
+ renderWithTheme(
172
+ <Input
173
+ label="Email"
174
+ keyboardType="email-address"
175
+ placeholder="Enter email"
176
+ />
177
+ )
178
+ expect(screen.getByPlaceholderText("Enter email")).toBeInTheDocument()
179
+ })
180
+
181
+ it("renders with numeric keyboard type", () => {
182
+ renderWithTheme(
183
+ <Input
184
+ label="Amount"
185
+ keyboardType="numeric"
186
+ placeholder="Enter amount"
187
+ />
188
+ )
189
+ expect(screen.getByPlaceholderText("Enter amount")).toBeInTheDocument()
190
+ })
191
+
192
+ it("renders with phone keyboard type", () => {
193
+ renderWithTheme(
194
+ <Input
195
+ label="Phone"
196
+ keyboardType="phone-pad"
197
+ placeholder="Enter phone"
198
+ />
199
+ )
200
+ expect(screen.getByPlaceholderText("Enter phone")).toBeInTheDocument()
201
+ })
202
+ })
203
+
204
+ describe("validation states", () => {
205
+ it.each(["default", "error", "success"] as const)(
206
+ "renders %s state without errors",
207
+ (state) => {
208
+ renderWithTheme(
209
+ <Input label="Field" state={state} placeholder="Enter value" />
210
+ )
211
+ expect(screen.getByPlaceholderText("Enter value")).toBeInTheDocument()
212
+ }
213
+ )
214
+
215
+ it("shows error message with error state", () => {
216
+ renderWithTheme(
217
+ <Input
218
+ label="Email"
219
+ placeholder="Enter email"
220
+ state="error"
221
+ error="Invalid email address"
222
+ />
223
+ )
224
+ expect(screen.getByText("Invalid email address")).toBeInTheDocument()
225
+ })
226
+ })
227
+
228
+ describe("when rendering icons", () => {
229
+ it("renders leading icon", () => {
230
+ renderWithTheme(
231
+ <Input.Root>
232
+ <Input.Field
233
+ placeholder="Search"
234
+ leadingIcon={<span data-testid="leading-icon">icon</span>}
235
+ />
236
+ </Input.Root>
237
+ )
238
+ expect(screen.getByTestId("leading-icon")).toBeInTheDocument()
239
+ })
240
+
241
+ it("renders trailing icon", () => {
242
+ renderWithTheme(
243
+ <Input.Root>
244
+ <Input.Field
245
+ placeholder="Search"
246
+ trailingIcon={<span data-testid="trailing-icon">icon</span>}
247
+ />
248
+ </Input.Root>
249
+ )
250
+ expect(screen.getByTestId("trailing-icon")).toBeInTheDocument()
251
+ })
252
+
253
+ it("renders action icon", () => {
254
+ renderWithTheme(
255
+ <Input.Root>
256
+ <Input.Field
257
+ placeholder="Search"
258
+ actionIcon={<span data-testid="action-icon">X</span>}
259
+ />
260
+ </Input.Root>
261
+ )
262
+ expect(screen.getByTestId("action-icon")).toBeInTheDocument()
263
+ })
264
+
265
+ it("hides state icons when hideStateIcons is true", () => {
266
+ const { container } = renderWithTheme(
267
+ <Input.Root>
268
+ <Input.Field placeholder="Email" state="error" hideStateIcons />
269
+ </Input.Root>
270
+ )
271
+ expect(container.querySelectorAll("svg")).toHaveLength(0)
272
+ })
273
+ })
274
+
275
+ describe("compound component API with states", () => {
276
+ it("renders error state in compound mode", () => {
277
+ renderWithTheme(
278
+ <Input.Root>
279
+ <Input.Label state="error">Email</Input.Label>
280
+ <Input.Field placeholder="Enter email" state="error" />
281
+ <Input.Error>Invalid email</Input.Error>
282
+ </Input.Root>
283
+ )
284
+ expect(screen.getByText("Invalid email")).toBeInTheDocument()
285
+ })
286
+
287
+ it("renders success state in compound mode", () => {
288
+ renderWithTheme(
289
+ <Input.Root>
290
+ <Input.Label state="success">Email</Input.Label>
291
+ <Input.Field placeholder="Enter email" state="success" />
292
+ <Input.Description state="success">Email verified</Input.Description>
293
+ </Input.Root>
294
+ )
295
+ expect(screen.getByText("Email verified")).toBeInTheDocument()
296
+ })
297
+ })
298
+
299
+ describe("when using with ref", () => {
300
+ it("forwards ref to input element", () => {
301
+ const ref = React.createRef<TextInput>()
302
+ renderWithTheme(<Input ref={ref} label="Test" />)
303
+ expect(ref.current).toBeTruthy()
304
+ })
305
+ })
306
+ })
@@ -12,6 +12,7 @@ type InputOwnProps = {
12
12
  error?: string
13
13
  state?: InputState
14
14
  optionalText?: string
15
+ onValueChange?: (value: string) => void
15
16
  }
16
17
 
17
18
  export type InputProps = InputOwnProps &
@@ -41,6 +42,8 @@ const StyledRoot = styled(View)(({ theme }) => {
41
42
  * error="Invalid email address"
42
43
  * state="error"
43
44
  * optionalText="(optional)"
45
+ * leadingIcon={<Icon icon={Search} size="md" />}
46
+ * onValueChange={(value) => setValue(value)}
44
47
  * />
45
48
  *
46
49
  * **Compound Component API:**
@@ -60,6 +63,7 @@ const InputRoot = React.forwardRef<View, InputProps>(
60
63
  error,
61
64
  state = "default",
62
65
  optionalText,
66
+ onValueChange,
63
67
  children,
64
68
  // InputField props
65
69
  ...inputFieldProps
@@ -79,7 +83,11 @@ const InputRoot = React.forwardRef<View, InputProps>(
79
83
  {label}
80
84
  </InputLabel>
81
85
  )}
82
- <InputField state={state} {...inputFieldProps} />
86
+ <InputField
87
+ state={state}
88
+ onChangeText={onValueChange}
89
+ {...inputFieldProps}
90
+ />
83
91
  {description && (
84
92
  <InputDescription state={state}>{description}</InputDescription>
85
93
  )}