@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.
Files changed (187) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/CHANGELOG.md +30 -0
  3. package/COMPONENT_GUIDELINES.md +111 -4
  4. package/dist/index.cjs +12413 -1459
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +1111 -13
  7. package/dist/index.d.ts +1111 -13
  8. package/dist/index.js +12365 -1457
  9. package/dist/index.js.map +1 -1
  10. package/package.json +29 -11
  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.stories.tsx +2 -2
  42. package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
  43. package/src/components/atoms/Illustration/Illustration.tsx +3 -3
  44. package/src/components/atoms/Input/Input.stories.tsx +129 -86
  45. package/src/components/atoms/Input/Input.test.tsx +306 -0
  46. package/src/components/atoms/Input/Input.tsx +9 -1
  47. package/src/components/atoms/Input/InputField.tsx +226 -74
  48. package/src/components/atoms/Link/Link.test.tsx +89 -0
  49. package/src/components/atoms/Link/Link.tsx +7 -6
  50. package/src/components/atoms/Logo/Logo.registry.ts +30 -5
  51. package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
  52. package/src/components/atoms/Logo/Logo.test.tsx +56 -0
  53. package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
  54. package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
  55. package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
  56. package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
  57. package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
  58. package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
  59. package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
  60. package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
  61. package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
  62. package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
  63. package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
  64. package/src/components/atoms/Logo/assets/index.ts +11 -0
  65. package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
  66. package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
  67. package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
  68. package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
  69. package/src/components/atoms/NumberInput/index.ts +4 -0
  70. package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
  71. package/src/components/atoms/Spinner/Spinner.tsx +14 -5
  72. package/src/components/atoms/Switch/Switch.test.tsx +92 -0
  73. package/src/components/atoms/Switch/Switch.tsx +28 -17
  74. package/src/components/atoms/Tag/Tag.test.tsx +70 -0
  75. package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
  76. package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
  77. package/src/components/atoms/TextArea/TextArea.tsx +171 -0
  78. package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
  79. package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
  80. package/src/components/atoms/TextArea/index.ts +6 -0
  81. package/src/components/atoms/Typography/Typography.test.tsx +94 -0
  82. package/src/components/atoms/index.ts +3 -0
  83. package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
  84. package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
  85. package/src/components/molecules/Accordion/Accordion.tsx +284 -0
  86. package/src/components/molecules/Accordion/index.ts +6 -0
  87. package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
  88. package/src/components/molecules/Animated/Animated.tsx +283 -0
  89. package/src/components/molecules/Animated/index.ts +10 -0
  90. package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +44 -25
  91. package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
  92. package/src/components/molecules/ButtonDock/ButtonDock.tsx +16 -13
  93. package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +48 -29
  94. package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
  95. package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
  96. package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
  97. package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
  98. package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
  99. package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
  100. package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
  101. package/src/components/molecules/CopyField/CopyField.tsx +156 -0
  102. package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
  103. package/src/components/molecules/CopyField/hooks/index.ts +1 -0
  104. package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
  105. package/src/components/molecules/CopyField/index.ts +4 -0
  106. package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
  107. package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
  108. package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
  109. package/src/components/molecules/DatePicker/index.ts +2 -0
  110. package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
  111. package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
  112. package/src/components/molecules/Drawer/Drawer.tsx +187 -0
  113. package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
  114. package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
  115. package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
  116. package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
  117. package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
  118. package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
  119. package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
  120. package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
  121. package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
  122. package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
  123. package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
  124. package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
  125. package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
  126. package/src/components/molecules/Drawer/index.ts +12 -0
  127. package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
  128. package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
  129. package/src/components/molecules/FilterTab/index.ts +2 -0
  130. package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
  131. package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
  132. package/src/components/molecules/MessageCard/index.ts +10 -0
  133. package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
  134. package/src/components/molecules/Notification/Notification.tsx +426 -0
  135. package/src/components/molecules/Notification/index.ts +2 -0
  136. package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
  137. package/src/components/molecules/NumberField/NumberField.tsx +186 -0
  138. package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
  139. package/src/components/molecules/NumberField/index.ts +2 -0
  140. package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
  141. package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
  142. package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
  143. package/src/components/molecules/PasswordField/PasswordFieldError.tsx +53 -0
  144. package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
  145. package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +95 -0
  146. package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
  147. package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
  148. package/src/components/molecules/PasswordField/index.ts +10 -0
  149. package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +204 -0
  150. package/src/components/molecules/PictureSelector/PictureSelector.tsx +335 -0
  151. package/src/components/molecules/PictureSelector/index.ts +5 -0
  152. package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
  153. package/src/components/molecules/Progress/Progress.tsx +184 -0
  154. package/src/components/molecules/Progress/index.ts +2 -0
  155. package/src/components/molecules/Radio/Radio.test.tsx +104 -0
  156. package/src/components/molecules/Radio/Radio.tsx +1 -2
  157. package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
  158. package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
  159. package/src/components/molecules/SearchField/SearchField.tsx +143 -0
  160. package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
  161. package/src/components/molecules/SearchField/hooks/index.ts +1 -0
  162. package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
  163. package/src/components/molecules/SearchField/index.ts +4 -0
  164. package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
  165. package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
  166. package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
  167. package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
  168. package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
  169. package/src/components/molecules/SelectField/SelectField.tsx +236 -0
  170. package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
  171. package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
  172. package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
  173. package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
  174. package/src/components/molecules/SelectField/hooks/index.ts +2 -0
  175. package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
  176. package/src/components/molecules/SelectField/index.ts +10 -0
  177. package/src/components/molecules/Slider/Slider.test.tsx +102 -0
  178. package/src/components/molecules/Slider/Slider.tsx +293 -180
  179. package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
  180. package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
  181. package/src/components/molecules/Tooltip/index.ts +2 -0
  182. package/src/components/molecules/index.ts +15 -0
  183. package/src/test-utils.tsx +20 -0
  184. package/tsconfig.json +1 -1
  185. package/tsup.config.ts +16 -2
  186. package/vitest.config.ts +114 -0
  187. package/vitest.setup.ts +16 -0
@@ -0,0 +1,104 @@
1
+ import React from "react"
2
+ import { screen } from "@testing-library/react"
3
+ import userEvent from "@testing-library/user-event"
4
+ import { describe, it, expect, vi } from "vitest"
5
+ import { renderWithTheme } from "../../../test-utils"
6
+ import { Radio } from "./Radio"
7
+
8
+ describe("Radio", () => {
9
+ describe("when component is rendering", () => {
10
+ it("renders label text", () => {
11
+ renderWithTheme(<Radio value="chicken" label="Chicken" />)
12
+ expect(screen.getByText("Chicken")).toBeInTheDocument()
13
+ })
14
+
15
+ it("renders label and subText", () => {
16
+ renderWithTheme(
17
+ <Radio value="chicken" label="Chicken" subText="Tender & tasty" />
18
+ )
19
+ expect(screen.getByText("Chicken")).toBeInTheDocument()
20
+ expect(screen.getByText("Tender & tasty")).toBeInTheDocument()
21
+ })
22
+
23
+ it("renders without label", () => {
24
+ const { container } = renderWithTheme(<Radio value="plain" />)
25
+ expect(container.firstChild).toBeTruthy()
26
+ })
27
+ })
28
+
29
+ describe("when rendering variants", () => {
30
+ it("renders standalone variant", () => {
31
+ renderWithTheme(
32
+ <Radio value="val" variant="standalone" label="Standalone" />
33
+ )
34
+ expect(screen.getByText("Standalone")).toBeInTheDocument()
35
+ })
36
+
37
+ it("renders tile variant", () => {
38
+ renderWithTheme(<Radio value="val" variant="tile" label="Tile" />)
39
+ expect(screen.getByText("Tile")).toBeInTheDocument()
40
+ })
41
+ })
42
+
43
+ describe("when component is interactive", () => {
44
+ it("calls onSelect when pressed", async () => {
45
+ const user = userEvent.setup()
46
+ const onSelect = vi.fn()
47
+ renderWithTheme(
48
+ <Radio value="chicken" label="Chicken" onSelect={onSelect} />
49
+ )
50
+
51
+ await user.click(screen.getByRole("radio"))
52
+ expect(onSelect).toHaveBeenCalledWith("chicken")
53
+ })
54
+
55
+ it("does not call onSelect when disabled", async () => {
56
+ const user = userEvent.setup()
57
+ const onSelect = vi.fn()
58
+ renderWithTheme(
59
+ <Radio value="chicken" label="Chicken" onSelect={onSelect} disabled />
60
+ )
61
+
62
+ await user.click(screen.getByRole("radio"))
63
+ expect(onSelect).not.toHaveBeenCalled()
64
+ })
65
+ })
66
+
67
+ describe("when selected", () => {
68
+ it("shows indicator when selected", () => {
69
+ renderWithTheme(<Radio value="chicken" label="Chicken" selected />)
70
+ expect(screen.getByRole("radio")).toHaveAttribute("aria-selected", "true")
71
+ })
72
+
73
+ it("does not show indicator when not selected", () => {
74
+ renderWithTheme(
75
+ <Radio value="chicken" label="Chicken" selected={false} />
76
+ )
77
+ // Not selected by default
78
+ expect(screen.getByRole("radio")).not.toHaveAttribute(
79
+ "aria-selected",
80
+ "true"
81
+ )
82
+ })
83
+ })
84
+
85
+ describe("accessibility", () => {
86
+ it("has radio role", () => {
87
+ renderWithTheme(<Radio value="val" label="Option" />)
88
+ expect(screen.getByRole("radio")).toBeInTheDocument()
89
+ })
90
+
91
+ it("sets disabled accessibility state", () => {
92
+ renderWithTheme(<Radio value="val" label="Option" disabled />)
93
+ expect(screen.getByRole("radio")).toBeDisabled()
94
+ })
95
+ })
96
+
97
+ describe("when using with ref", () => {
98
+ it("forwards ref", () => {
99
+ const ref = React.createRef<any>()
100
+ renderWithTheme(<Radio ref={ref} value="val" label="Ref test" />)
101
+ expect(ref.current).toBeTruthy()
102
+ })
103
+ })
104
+ })
@@ -95,8 +95,7 @@ const StyledTextContent = styled(View)<{
95
95
  textContentGap: number
96
96
  }>(({ textContentGap }) => ({
97
97
  flexDirection: "column",
98
- gap: textContentGap,
99
- marginTop: 2
98
+ gap: textContentGap
100
99
  }))
101
100
 
102
101
  const StyledAssetWrapper = styled(View)({
@@ -0,0 +1,242 @@
1
+ import React, { useState } from "react"
2
+ import { View, StyleSheet } from "react-native"
3
+ import { SearchField } from "./SearchField"
4
+ import type { SearchFieldProps } from "./SearchField"
5
+ import type { InputState } from "../../atoms/Input/InputField"
6
+ import { Typography } from "../../atoms/Typography"
7
+
8
+ export default {
9
+ title: "Molecules/SearchField",
10
+ component: SearchField,
11
+ argTypes: {
12
+ label: {
13
+ control: { type: "text" },
14
+ description: "Label text"
15
+ },
16
+ placeholder: {
17
+ control: { type: "text" },
18
+ description: "Placeholder text"
19
+ },
20
+ description: {
21
+ control: { type: "text" },
22
+ description: "Help text below input"
23
+ },
24
+ error: {
25
+ control: { type: "text" },
26
+ description: "Error message"
27
+ },
28
+ state: {
29
+ control: { type: "select" },
30
+ options: ["default", "error", "success"],
31
+ description: "Visual state of the input"
32
+ },
33
+ optionalText: {
34
+ control: { type: "text" },
35
+ description: "Optional text to display next to label"
36
+ },
37
+ editable: {
38
+ control: { type: "boolean" },
39
+ description: "Controls whether the input is editable"
40
+ }
41
+ }
42
+ }
43
+
44
+ export const Default = (args: SearchFieldProps) => (
45
+ <View style={styles.container}>
46
+ <SearchField {...args} />
47
+ </View>
48
+ )
49
+ Default.args = {
50
+ label: "Search",
51
+ placeholder: "Search...",
52
+ description: "Search for items"
53
+ }
54
+
55
+ export const States = () => {
56
+ const [defaultValue, setDefaultValue] = useState("")
57
+ const [errorValue, setErrorValue] = useState("search query")
58
+ const [successValue, setSuccessValue] = useState("search query")
59
+
60
+ return (
61
+ <View style={styles.column}>
62
+ <View style={styles.section}>
63
+ <Typography size="sm" weight="semiBold" color="tertiary">
64
+ Default State (Uncontrolled)
65
+ </Typography>
66
+ <SearchField
67
+ label="Search"
68
+ placeholder="Search..."
69
+ description="Uncontrolled - clear button shows when focused with text"
70
+ />
71
+ </View>
72
+ <View style={styles.section}>
73
+ <Typography size="sm" weight="semiBold" color="tertiary">
74
+ Default State (Controlled)
75
+ </Typography>
76
+ <SearchField
77
+ label="Search"
78
+ placeholder="Search..."
79
+ value={defaultValue}
80
+ description="Controlled - clear button shows when focused with text"
81
+ onValueChange={setDefaultValue}
82
+ onClear={() => setDefaultValue("")}
83
+ />
84
+ </View>
85
+ <View style={styles.section}>
86
+ <Typography size="sm" weight="semiBold" color="tertiary">
87
+ Error State with Text
88
+ </Typography>
89
+ <SearchField
90
+ label="Search"
91
+ placeholder="Search..."
92
+ value={errorValue}
93
+ state="error"
94
+ error="No results found"
95
+ description="Error state with text (clear button visible and functional)"
96
+ onValueChange={setErrorValue}
97
+ onClear={() => setErrorValue("")}
98
+ />
99
+ </View>
100
+ <View style={styles.section}>
101
+ <Typography size="sm" weight="semiBold" color="tertiary">
102
+ Error State Empty
103
+ </Typography>
104
+ <SearchField
105
+ label="Search"
106
+ placeholder="Search..."
107
+ state="error"
108
+ error="Search query required"
109
+ description="Error state without text"
110
+ />
111
+ </View>
112
+ <View style={styles.section}>
113
+ <Typography size="sm" weight="semiBold" color="tertiary">
114
+ Success State
115
+ </Typography>
116
+ <SearchField
117
+ label="Search"
118
+ placeholder="Search..."
119
+ value={successValue}
120
+ state="success"
121
+ description="Success state (clear button visible when focused)"
122
+ onValueChange={setSuccessValue}
123
+ onClear={() => setSuccessValue("")}
124
+ />
125
+ </View>
126
+ <View style={styles.section}>
127
+ <Typography size="sm" weight="semiBold" color="tertiary">
128
+ Disabled
129
+ </Typography>
130
+ <SearchField
131
+ label="Search"
132
+ placeholder="Search..."
133
+ description="Disabled search field"
134
+ editable={false}
135
+ />
136
+ </View>
137
+ </View>
138
+ )
139
+ }
140
+
141
+ export const WithOptionalText = () => (
142
+ <View style={styles.column}>
143
+ <View style={styles.section}>
144
+ <Typography size="sm" weight="semiBold" color="tertiary">
145
+ With Optional Text
146
+ </Typography>
147
+ <SearchField
148
+ label="Search"
149
+ optionalText="(optional)"
150
+ placeholder="Search..."
151
+ description="Optional search field"
152
+ />
153
+ </View>
154
+ </View>
155
+ )
156
+
157
+ export const Controlled = () => {
158
+ const [value, setValue] = useState("")
159
+
160
+ return (
161
+ <View style={styles.column}>
162
+ <View style={styles.section}>
163
+ <Typography size="sm" weight="semiBold" color="tertiary">
164
+ Controlled: {value || "(empty)"}
165
+ </Typography>
166
+ <SearchField
167
+ label="Search"
168
+ placeholder="Search..."
169
+ value={value}
170
+ onValueChange={setValue}
171
+ onClear={() => setValue("")}
172
+ description={`Current value: "${value || "empty"}"`}
173
+ />
174
+ </View>
175
+ </View>
176
+ )
177
+ }
178
+
179
+ export const CustomStateValidationWithSuccess = () => {
180
+ const [search, setSearch] = useState("")
181
+ const [searchState, setSearchState] = useState<InputState>("default")
182
+
183
+ const validateSearch = (value: string) => {
184
+ if (!value) {
185
+ setSearchState("default")
186
+ return
187
+ }
188
+
189
+ const hasMinLength = value.length >= 2
190
+ const hasMaxLength = value.length <= 50
191
+ const hasValidChars = /^[a-zA-Z0-9\s-]+$/.test(value)
192
+
193
+ const allRequirementsMet = hasMinLength && hasMaxLength && hasValidChars
194
+
195
+ setSearchState(allRequirementsMet ? "success" : "error")
196
+ }
197
+
198
+ const handleChange = (newValue: string) => {
199
+ setSearch(newValue)
200
+ validateSearch(newValue)
201
+ }
202
+
203
+ const handleClear = () => {
204
+ setSearch("")
205
+ setSearchState("default")
206
+ }
207
+
208
+ return (
209
+ <View style={styles.column}>
210
+ <View style={styles.section}>
211
+ <Typography size="sm" weight="semiBold" color="tertiary">
212
+ Custom State Validation with Success
213
+ </Typography>
214
+ <SearchField
215
+ label="Product Search"
216
+ placeholder="Search products..."
217
+ value={search}
218
+ onValueChange={handleChange}
219
+ onClear={handleClear}
220
+ state={searchState}
221
+ description="2-50 chars, letters, numbers, spaces, and hyphens only"
222
+ error="Invalid search query"
223
+ />
224
+ </View>
225
+ </View>
226
+ )
227
+ }
228
+
229
+ const styles = StyleSheet.create({
230
+ container: {
231
+ width: 320
232
+ },
233
+ column: {
234
+ flexDirection: "column",
235
+ gap: 24,
236
+ width: 320
237
+ },
238
+ section: {
239
+ flexDirection: "column",
240
+ gap: 8
241
+ }
242
+ })
@@ -0,0 +1,318 @@
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 { SearchField } from "./SearchField"
8
+
9
+ describe("SearchField", () => {
10
+ describe("when using simple props API", () => {
11
+ it("renders label when provided", () => {
12
+ renderWithTheme(<SearchField label="Search" placeholder="Search..." />)
13
+ expect(screen.getByText("Search")).toBeInTheDocument()
14
+ })
15
+
16
+ it("renders description when provided", () => {
17
+ renderWithTheme(
18
+ <SearchField
19
+ label="Search"
20
+ placeholder="Search..."
21
+ description="Search for items in the list"
22
+ />
23
+ )
24
+ expect(
25
+ screen.getByText("Search for items in the list")
26
+ ).toBeInTheDocument()
27
+ })
28
+
29
+ it("renders error message when error prop and state='error' are provided", () => {
30
+ renderWithTheme(
31
+ <SearchField
32
+ label="Search"
33
+ placeholder="Search..."
34
+ state="error"
35
+ error="No results found"
36
+ />
37
+ )
38
+ expect(screen.getByText("No results found")).toBeInTheDocument()
39
+ })
40
+
41
+ it("does not render error message when error prop is provided without state='error'", () => {
42
+ renderWithTheme(
43
+ <SearchField
44
+ label="Search"
45
+ placeholder="Search..."
46
+ error="No results found"
47
+ />
48
+ )
49
+ expect(screen.queryByText("No results found")).not.toBeInTheDocument()
50
+ })
51
+
52
+ it("shows (optional) when optionalText is provided", () => {
53
+ renderWithTheme(
54
+ <SearchField
55
+ label="Search"
56
+ placeholder="Search..."
57
+ optionalText="(optional)"
58
+ />
59
+ )
60
+ expect(screen.getByText("(optional)")).toBeInTheDocument()
61
+ })
62
+
63
+ it("renders input with placeholder", () => {
64
+ renderWithTheme(<SearchField label="Search" placeholder="Search..." />)
65
+ expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument()
66
+ })
67
+
68
+ it("renders search icon", () => {
69
+ renderWithTheme(<SearchField placeholder="Search..." />)
70
+ expect(screen.getByLabelText("Search")).toBeInTheDocument()
71
+ })
72
+
73
+ it("accepts user input", async () => {
74
+ const user = userEvent.setup()
75
+ renderWithTheme(<SearchField label="Search" placeholder="Search..." />)
76
+
77
+ const input = screen.getByPlaceholderText("Search...")
78
+ await user.type(input, "test query")
79
+ expect(input).toHaveValue("test query")
80
+ })
81
+ })
82
+
83
+ describe("when using controlled mode", () => {
84
+ it("updates value when parent updates", async () => {
85
+ const user = userEvent.setup()
86
+
87
+ function ControlledSearchField() {
88
+ const [value, setValue] = React.useState("")
89
+ return (
90
+ <SearchField
91
+ label="Search"
92
+ value={value}
93
+ onValueChange={(newValue) => setValue(newValue)}
94
+ onClear={() => setValue("")}
95
+ placeholder="Search..."
96
+ />
97
+ )
98
+ }
99
+
100
+ renderWithTheme(<ControlledSearchField />)
101
+
102
+ const input = screen.getByPlaceholderText("Search...")
103
+ await user.type(input, "test query")
104
+ expect(input).toHaveValue("test query")
105
+ })
106
+
107
+ it("calls onValueChange when value changes", async () => {
108
+ const user = userEvent.setup()
109
+ const onValueChange = vi.fn()
110
+
111
+ renderWithTheme(
112
+ <SearchField
113
+ label="Search"
114
+ value=""
115
+ onValueChange={onValueChange}
116
+ onClear={() => {}}
117
+ placeholder="Search..."
118
+ />
119
+ )
120
+
121
+ const input = screen.getByPlaceholderText("Search...")
122
+ await user.type(input, "a")
123
+ expect(onValueChange).toHaveBeenCalledWith("a")
124
+ })
125
+
126
+ it("calls onClear when clear button is pressed", async () => {
127
+ const user = userEvent.setup()
128
+ const onClear = vi.fn()
129
+
130
+ renderWithTheme(
131
+ <SearchField
132
+ placeholder="Search..."
133
+ value="test"
134
+ state="error"
135
+ onValueChange={() => {}}
136
+ onClear={onClear}
137
+ />
138
+ )
139
+
140
+ const clearButton = screen.getByLabelText("Clear search")
141
+ await user.click(clearButton)
142
+
143
+ expect(onClear).toHaveBeenCalledTimes(1)
144
+ })
145
+ })
146
+
147
+ describe("when disabled", () => {
148
+ it("disables the input", () => {
149
+ renderWithTheme(
150
+ <SearchField label="Search" placeholder="Search..." editable={false} />
151
+ )
152
+ expect(screen.getByPlaceholderText("Search...")).toBeDisabled()
153
+ })
154
+
155
+ it("does not accept user input when disabled", async () => {
156
+ const user = userEvent.setup()
157
+ renderWithTheme(
158
+ <SearchField label="Search" placeholder="Search..." editable={false} />
159
+ )
160
+
161
+ const input = screen.getByPlaceholderText("Search...")
162
+ await user.type(input, "test")
163
+ expect(input).toHaveValue("")
164
+ })
165
+ })
166
+
167
+ describe("clear button behavior", () => {
168
+ it("does not show clear button when field is empty", () => {
169
+ renderWithTheme(<SearchField placeholder="Search..." />)
170
+ expect(screen.queryByLabelText("Clear search")).not.toBeInTheDocument()
171
+ })
172
+
173
+ it("shows clear button when field has text and is focused", () => {
174
+ renderWithTheme(
175
+ <SearchField
176
+ placeholder="Search..."
177
+ value="test"
178
+ state="error"
179
+ onValueChange={() => {}}
180
+ onClear={() => {}}
181
+ />
182
+ )
183
+
184
+ expect(screen.getByLabelText("Clear search")).toBeInTheDocument()
185
+ })
186
+
187
+ it("shows clear button when field has text and is in error state", () => {
188
+ renderWithTheme(
189
+ <SearchField
190
+ placeholder="Search..."
191
+ value="test"
192
+ state="error"
193
+ onValueChange={() => {}}
194
+ onClear={() => {}}
195
+ />
196
+ )
197
+ expect(screen.getByLabelText("Clear search")).toBeInTheDocument()
198
+ })
199
+
200
+ it("clears the input when clear button is pressed", async () => {
201
+ const user = userEvent.setup()
202
+ const onClear = vi.fn()
203
+ const onValueChange = vi.fn()
204
+
205
+ renderWithTheme(
206
+ <SearchField
207
+ placeholder="Search..."
208
+ value="test"
209
+ state="error"
210
+ onValueChange={onValueChange}
211
+ onClear={onClear}
212
+ />
213
+ )
214
+
215
+ const clearButton = screen.getByLabelText("Clear search")
216
+ await user.click(clearButton)
217
+
218
+ expect(onClear).toHaveBeenCalledTimes(1)
219
+ })
220
+ })
221
+
222
+ describe("when using compound component API", () => {
223
+ it("renders compound components", () => {
224
+ renderWithTheme(
225
+ <SearchField.Root>
226
+ <SearchField.Label>Search Products</SearchField.Label>
227
+ <SearchField.Field placeholder="Search..." onClear={() => {}} />
228
+ <SearchField.Description>Find products</SearchField.Description>
229
+ </SearchField.Root>
230
+ )
231
+
232
+ expect(screen.getByText("Search Products")).toBeInTheDocument()
233
+ expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument()
234
+ expect(screen.getByText("Find products")).toBeInTheDocument()
235
+ })
236
+
237
+ it("renders error in compound mode", () => {
238
+ renderWithTheme(
239
+ <SearchField.Root>
240
+ <SearchField.Label state="error">Search</SearchField.Label>
241
+ <SearchField.Field
242
+ placeholder="Search..."
243
+ state="error"
244
+ onClear={() => {}}
245
+ />
246
+ <SearchField.Error>Invalid search</SearchField.Error>
247
+ </SearchField.Root>
248
+ )
249
+ expect(screen.getByText("Invalid search")).toBeInTheDocument()
250
+ })
251
+ })
252
+
253
+ describe("validation states", () => {
254
+ it.each(["default", "error", "success"] as const)(
255
+ "renders %s state without errors",
256
+ (state) => {
257
+ renderWithTheme(
258
+ <SearchField label="Search" state={state} placeholder="Search..." />
259
+ )
260
+ expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument()
261
+ }
262
+ )
263
+
264
+ it("shows error state with error message", () => {
265
+ renderWithTheme(
266
+ <SearchField
267
+ label="Search"
268
+ placeholder="Search..."
269
+ state="error"
270
+ error="No results found"
271
+ />
272
+ )
273
+ expect(screen.getByText("No results found")).toBeInTheDocument()
274
+ })
275
+ })
276
+
277
+ describe("compound component API with states", () => {
278
+ it("renders error state in compound mode", () => {
279
+ renderWithTheme(
280
+ <SearchField.Root>
281
+ <SearchField.Label state="error">Search</SearchField.Label>
282
+ <SearchField.Field
283
+ placeholder="Search..."
284
+ state="error"
285
+ onClear={() => {}}
286
+ />
287
+ <SearchField.Error>Invalid search</SearchField.Error>
288
+ </SearchField.Root>
289
+ )
290
+ expect(screen.getByText("Invalid search")).toBeInTheDocument()
291
+ })
292
+
293
+ it("renders success state in compound mode", () => {
294
+ renderWithTheme(
295
+ <SearchField.Root>
296
+ <SearchField.Label state="success">Search</SearchField.Label>
297
+ <SearchField.Field
298
+ placeholder="Search..."
299
+ state="success"
300
+ onClear={() => {}}
301
+ />
302
+ <SearchField.Description state="success">
303
+ Search verified
304
+ </SearchField.Description>
305
+ </SearchField.Root>
306
+ )
307
+ expect(screen.getByText("Search verified")).toBeInTheDocument()
308
+ })
309
+ })
310
+
311
+ describe("when using with ref", () => {
312
+ it("forwards ref to input element", () => {
313
+ const ref = React.createRef<TextInput>()
314
+ renderWithTheme(<SearchField ref={ref} label="Test" />)
315
+ expect(ref.current).toBeTruthy()
316
+ })
317
+ })
318
+ })