@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,369 @@
|
|
|
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 { PasswordField } from "./PasswordField"
|
|
8
|
+
|
|
9
|
+
describe("PasswordField", () => {
|
|
10
|
+
describe("when using simple props API", () => {
|
|
11
|
+
it("renders label when provided", () => {
|
|
12
|
+
renderWithTheme(
|
|
13
|
+
<PasswordField label="Password" placeholder="Enter password" />
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
expect(screen.getByText("Password")).toBeInTheDocument()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("renders description when provided", () => {
|
|
20
|
+
renderWithTheme(
|
|
21
|
+
<PasswordField
|
|
22
|
+
label="Password"
|
|
23
|
+
placeholder="Enter password"
|
|
24
|
+
description="Choose a strong password"
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
expect(screen.getByText("Choose a strong password")).toBeInTheDocument()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("renders error message when error prop and state='error' are provided", () => {
|
|
32
|
+
renderWithTheme(
|
|
33
|
+
<PasswordField
|
|
34
|
+
label="Password"
|
|
35
|
+
placeholder="Enter password"
|
|
36
|
+
state="error"
|
|
37
|
+
error="Password is too weak"
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
expect(screen.getByText("Password is too weak")).toBeInTheDocument()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("does not render error message when error prop is provided without state='error'", () => {
|
|
45
|
+
renderWithTheme(
|
|
46
|
+
<PasswordField
|
|
47
|
+
label="Password"
|
|
48
|
+
placeholder="Enter password"
|
|
49
|
+
error="Password is too weak"
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
expect(screen.queryByText("Password is too weak")).not.toBeInTheDocument()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("shows (optional) when optionalText is provided", () => {
|
|
57
|
+
renderWithTheme(
|
|
58
|
+
<PasswordField
|
|
59
|
+
label="Password"
|
|
60
|
+
placeholder="Enter password"
|
|
61
|
+
optionalText="(optional)"
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
expect(screen.getByText("(optional)")).toBeInTheDocument()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("renders input with placeholder", () => {
|
|
69
|
+
renderWithTheme(
|
|
70
|
+
<PasswordField label="Password" placeholder="Enter password" />
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
expect(screen.getByPlaceholderText("Enter password")).toBeInTheDocument()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("accepts user input", async () => {
|
|
77
|
+
const user = userEvent.setup()
|
|
78
|
+
|
|
79
|
+
renderWithTheme(
|
|
80
|
+
<PasswordField label="Password" placeholder="Enter password" />
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const input = screen.getByPlaceholderText("Enter password")
|
|
84
|
+
await user.type(input, "mypassword")
|
|
85
|
+
|
|
86
|
+
expect(input).toHaveValue("mypassword")
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("renders requirements list when showRequirements is true", () => {
|
|
90
|
+
const requirements = ["At least 8 characters", "One uppercase letter"]
|
|
91
|
+
|
|
92
|
+
renderWithTheme(
|
|
93
|
+
<PasswordField
|
|
94
|
+
label="Password"
|
|
95
|
+
placeholder="Enter password"
|
|
96
|
+
requirements={requirements}
|
|
97
|
+
showRequirements={true}
|
|
98
|
+
/>
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
expect(screen.getByText("At least 8 characters")).toBeInTheDocument()
|
|
102
|
+
expect(screen.getByText("One uppercase letter")).toBeInTheDocument()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("does not render requirements list when showRequirements is false", () => {
|
|
106
|
+
const requirements = ["At least 8 characters", "One uppercase letter"]
|
|
107
|
+
|
|
108
|
+
renderWithTheme(
|
|
109
|
+
<PasswordField
|
|
110
|
+
label="Password"
|
|
111
|
+
placeholder="Enter password"
|
|
112
|
+
requirements={requirements}
|
|
113
|
+
showRequirements={false}
|
|
114
|
+
/>
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
expect(
|
|
118
|
+
screen.queryByText("At least 8 characters")
|
|
119
|
+
).not.toBeInTheDocument()
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe("when using controlled mode", () => {
|
|
124
|
+
it("updates value when parent updates", async () => {
|
|
125
|
+
const user = userEvent.setup()
|
|
126
|
+
|
|
127
|
+
function ControlledPasswordField() {
|
|
128
|
+
const [value, setValue] = React.useState("")
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<PasswordField
|
|
132
|
+
label="Password"
|
|
133
|
+
value={value}
|
|
134
|
+
onValueChange={(newValue) => setValue(newValue)}
|
|
135
|
+
placeholder="Enter password"
|
|
136
|
+
/>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
renderWithTheme(<ControlledPasswordField />)
|
|
141
|
+
|
|
142
|
+
const input = screen.getByPlaceholderText("Enter password")
|
|
143
|
+
await user.type(input, "mypassword")
|
|
144
|
+
|
|
145
|
+
expect(input).toHaveValue("mypassword")
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("calls onValueChange when value changes", async () => {
|
|
149
|
+
const user = userEvent.setup()
|
|
150
|
+
const onValueChange = vi.fn()
|
|
151
|
+
|
|
152
|
+
renderWithTheme(
|
|
153
|
+
<PasswordField
|
|
154
|
+
label="Password"
|
|
155
|
+
value=""
|
|
156
|
+
onValueChange={onValueChange}
|
|
157
|
+
placeholder="Enter password"
|
|
158
|
+
/>
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
const input = screen.getByPlaceholderText("Enter password")
|
|
162
|
+
await user.type(input, "a")
|
|
163
|
+
|
|
164
|
+
expect(onValueChange).toHaveBeenCalledWith("a")
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe("when disabled", () => {
|
|
169
|
+
it("disables the input", () => {
|
|
170
|
+
renderWithTheme(
|
|
171
|
+
<PasswordField
|
|
172
|
+
label="Password"
|
|
173
|
+
placeholder="Enter password"
|
|
174
|
+
editable={false}
|
|
175
|
+
/>
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
expect(screen.getByPlaceholderText("Enter password")).toBeDisabled()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it("does not accept user input when disabled", async () => {
|
|
182
|
+
const user = userEvent.setup()
|
|
183
|
+
|
|
184
|
+
renderWithTheme(
|
|
185
|
+
<PasswordField
|
|
186
|
+
label="Password"
|
|
187
|
+
placeholder="Enter password"
|
|
188
|
+
editable={false}
|
|
189
|
+
/>
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
const input = screen.getByPlaceholderText("Enter password")
|
|
193
|
+
await user.type(input, "test")
|
|
194
|
+
|
|
195
|
+
expect(input).toHaveValue("")
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe("password visibility toggle", () => {
|
|
200
|
+
it("shows toggle button", () => {
|
|
201
|
+
renderWithTheme(<PasswordField placeholder="Enter password" />)
|
|
202
|
+
|
|
203
|
+
expect(screen.getByLabelText("Show password")).toBeInTheDocument()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it("toggles password visibility when button is clicked", async () => {
|
|
207
|
+
const user = userEvent.setup()
|
|
208
|
+
|
|
209
|
+
renderWithTheme(<PasswordField placeholder="Enter password" />)
|
|
210
|
+
|
|
211
|
+
expect(screen.getByLabelText("Show password")).toBeInTheDocument()
|
|
212
|
+
|
|
213
|
+
const toggleButton = screen.getByLabelText("Show password")
|
|
214
|
+
await user.click(toggleButton)
|
|
215
|
+
|
|
216
|
+
expect(screen.getByLabelText("Hide password")).toBeInTheDocument()
|
|
217
|
+
|
|
218
|
+
await user.click(screen.getByLabelText("Hide password"))
|
|
219
|
+
expect(screen.getByLabelText("Show password")).toBeInTheDocument()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it("does not toggle when disabled", async () => {
|
|
223
|
+
const user = userEvent.setup()
|
|
224
|
+
|
|
225
|
+
renderWithTheme(
|
|
226
|
+
<PasswordField placeholder="Enter password" editable={false} />
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
expect(screen.getByLabelText("Show password")).toBeInTheDocument()
|
|
230
|
+
|
|
231
|
+
const toggleButton = screen.getByLabelText("Show password")
|
|
232
|
+
await user.click(toggleButton)
|
|
233
|
+
|
|
234
|
+
// Button label should not change when disabled
|
|
235
|
+
expect(screen.getByLabelText("Show password")).toBeInTheDocument()
|
|
236
|
+
expect(screen.queryByLabelText("Hide password")).not.toBeInTheDocument()
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe("when using compound component API", () => {
|
|
241
|
+
it("renders compound components", () => {
|
|
242
|
+
renderWithTheme(
|
|
243
|
+
<PasswordField.Root>
|
|
244
|
+
<PasswordField.Label>Password</PasswordField.Label>
|
|
245
|
+
<PasswordField.Field placeholder="Enter password" />
|
|
246
|
+
<PasswordField.Description>
|
|
247
|
+
Choose a strong password
|
|
248
|
+
</PasswordField.Description>
|
|
249
|
+
</PasswordField.Root>
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
expect(screen.getByText("Password")).toBeInTheDocument()
|
|
253
|
+
expect(screen.getByPlaceholderText("Enter password")).toBeInTheDocument()
|
|
254
|
+
expect(screen.getByText("Choose a strong password")).toBeInTheDocument()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it("renders error in compound mode", () => {
|
|
258
|
+
renderWithTheme(
|
|
259
|
+
<PasswordField.Root>
|
|
260
|
+
<PasswordField.Label state="error">Password</PasswordField.Label>
|
|
261
|
+
<PasswordField.Field placeholder="Enter password" state="error" />
|
|
262
|
+
<PasswordField.Error>Invalid password</PasswordField.Error>
|
|
263
|
+
</PasswordField.Root>
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
expect(screen.getByText("Invalid password")).toBeInTheDocument()
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it("renders requirements in compound mode", () => {
|
|
270
|
+
const requirements = ["At least 8 characters", "One number"]
|
|
271
|
+
|
|
272
|
+
renderWithTheme(
|
|
273
|
+
<PasswordField.Root>
|
|
274
|
+
<PasswordField.Label>Password</PasswordField.Label>
|
|
275
|
+
<PasswordField.Field placeholder="Enter password" />
|
|
276
|
+
<PasswordField.Requirements requirements={requirements} />
|
|
277
|
+
</PasswordField.Root>
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
expect(screen.getByText("At least 8 characters")).toBeInTheDocument()
|
|
281
|
+
expect(screen.getByText("One number")).toBeInTheDocument()
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
describe("validation states", () => {
|
|
286
|
+
it.each(["default", "error", "success"] as const)(
|
|
287
|
+
"renders %s state without errors",
|
|
288
|
+
(state) => {
|
|
289
|
+
renderWithTheme(
|
|
290
|
+
<PasswordField
|
|
291
|
+
label="Password"
|
|
292
|
+
state={state}
|
|
293
|
+
placeholder="Enter password"
|
|
294
|
+
/>
|
|
295
|
+
)
|
|
296
|
+
expect(
|
|
297
|
+
screen.getByPlaceholderText("Enter password")
|
|
298
|
+
).toBeInTheDocument()
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
it("shows error state with error message", () => {
|
|
303
|
+
renderWithTheme(
|
|
304
|
+
<PasswordField
|
|
305
|
+
label="Password"
|
|
306
|
+
placeholder="Enter password"
|
|
307
|
+
state="error"
|
|
308
|
+
error="Password is too weak"
|
|
309
|
+
/>
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
expect(screen.getByText("Password is too weak")).toBeInTheDocument()
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it("hides requirements when state is success", () => {
|
|
316
|
+
const requirements = ["At least 8 characters", "One uppercase letter"]
|
|
317
|
+
|
|
318
|
+
renderWithTheme(
|
|
319
|
+
<PasswordField
|
|
320
|
+
label="Password"
|
|
321
|
+
placeholder="Enter password"
|
|
322
|
+
requirements={requirements}
|
|
323
|
+
showRequirements={true}
|
|
324
|
+
state="success"
|
|
325
|
+
/>
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
expect(
|
|
329
|
+
screen.queryByText("At least 8 characters")
|
|
330
|
+
).not.toBeInTheDocument()
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
describe("compound component API with states", () => {
|
|
335
|
+
it("renders error state in compound mode", () => {
|
|
336
|
+
renderWithTheme(
|
|
337
|
+
<PasswordField.Root>
|
|
338
|
+
<PasswordField.Label state="error">Password</PasswordField.Label>
|
|
339
|
+
<PasswordField.Field placeholder="Enter password" state="error" />
|
|
340
|
+
<PasswordField.Error>Invalid password</PasswordField.Error>
|
|
341
|
+
</PasswordField.Root>
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
expect(screen.getByText("Invalid password")).toBeInTheDocument()
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it("renders success state in compound mode", () => {
|
|
348
|
+
renderWithTheme(
|
|
349
|
+
<PasswordField.Root>
|
|
350
|
+
<PasswordField.Label state="success">Password</PasswordField.Label>
|
|
351
|
+
<PasswordField.Field placeholder="Enter password" state="success" />
|
|
352
|
+
<PasswordField.Description state="success">
|
|
353
|
+
Password verified
|
|
354
|
+
</PasswordField.Description>
|
|
355
|
+
</PasswordField.Root>
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
expect(screen.getByText("Password verified")).toBeInTheDocument()
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
describe("when using with ref", () => {
|
|
363
|
+
it("forwards ref to input element", () => {
|
|
364
|
+
const ref = React.createRef<TextInput>()
|
|
365
|
+
renderWithTheme(<PasswordField ref={ref} label="Test" />)
|
|
366
|
+
expect(ref.current).toBeTruthy()
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
})
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import type { InputState, InputFieldProps } from "../../atoms/Input/InputField"
|
|
5
|
+
import { InputLabel } from "../../atoms/Input/InputLabel"
|
|
6
|
+
import { InputDescription } from "../../atoms/Input/InputDescription"
|
|
7
|
+
import { PasswordFieldInput } from "./PasswordFieldInput"
|
|
8
|
+
import { PasswordFieldError } from "./PasswordFieldError"
|
|
9
|
+
import {
|
|
10
|
+
PasswordFieldRequirements,
|
|
11
|
+
type PasswordRequirement
|
|
12
|
+
} from "./PasswordFieldRequirements"
|
|
13
|
+
|
|
14
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
15
|
+
|
|
16
|
+
const StyledRoot = styled(View)(({ theme }) => {
|
|
17
|
+
const { spacing } = theme.tokens.components.inputs
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
gap: parseTokenValue(spacing.gap)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
type PasswordFieldOwnProps = {
|
|
25
|
+
label?: string
|
|
26
|
+
description?: string
|
|
27
|
+
error?: string | string[]
|
|
28
|
+
state?: InputState
|
|
29
|
+
optionalText?: string
|
|
30
|
+
requirements?: string[] | PasswordRequirement[]
|
|
31
|
+
showRequirements?: boolean
|
|
32
|
+
onValueChange?: (value: string) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type PasswordFieldProps = PasswordFieldOwnProps &
|
|
36
|
+
Omit<InputFieldProps, keyof PasswordFieldOwnProps> &
|
|
37
|
+
Omit<ViewProps, keyof PasswordFieldOwnProps>
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Password field component with visibility toggle and optional requirements list.
|
|
41
|
+
*
|
|
42
|
+
* **Recommended: Use with usePasswordField hook:**
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* import { PasswordField, usePasswordField } from '@butternutbox/pawprint-native'
|
|
46
|
+
*
|
|
47
|
+
* const passwordProps = usePasswordField({
|
|
48
|
+
* validationRules: [
|
|
49
|
+
* { test: (v) => v.length >= 8, message: "At least 8 characters" },
|
|
50
|
+
* { test: (v) => /[A-Z]/.test(v), message: "One uppercase letter" },
|
|
51
|
+
* { test: (v) => /[a-z]/.test(v), message: "One lowercase letter" },
|
|
52
|
+
* { test: (v) => /\d/.test(v), message: "One number" }
|
|
53
|
+
* ]
|
|
54
|
+
* })
|
|
55
|
+
*
|
|
56
|
+
* <PasswordField
|
|
57
|
+
* {...passwordProps}
|
|
58
|
+
* label="Create Password"
|
|
59
|
+
* placeholder="Enter password"
|
|
60
|
+
* />
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* **Simple Props API (without hook):**
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* const [password, setPassword] = useState("")
|
|
67
|
+
* const [state, setState] = useState<InputState>("default")
|
|
68
|
+
*
|
|
69
|
+
* const requirements: PasswordRequirement[] = [
|
|
70
|
+
* { text: "At least 8 characters", satisfied: password.length >= 8 },
|
|
71
|
+
* { text: "One uppercase letter", satisfied: /[A-Z]/.test(password) }
|
|
72
|
+
* ]
|
|
73
|
+
*
|
|
74
|
+
* <PasswordField
|
|
75
|
+
* label="Password"
|
|
76
|
+
* placeholder="Enter password"
|
|
77
|
+
* value={password}
|
|
78
|
+
* onValueChange={setPassword}
|
|
79
|
+
* state={state}
|
|
80
|
+
* requirements={requirements}
|
|
81
|
+
* showRequirements={password.length > 0}
|
|
82
|
+
* />
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* **Compound Component API:**
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* const [password, setPassword] = useState("")
|
|
89
|
+
* const [state, setState] = useState<InputState>("default")
|
|
90
|
+
*
|
|
91
|
+
* <PasswordField.Root>
|
|
92
|
+
* <PasswordField.Label state={state}>Password</PasswordField.Label>
|
|
93
|
+
* <PasswordField.Field
|
|
94
|
+
* placeholder="Enter password"
|
|
95
|
+
* value={password}
|
|
96
|
+
* onValueChange={setPassword}
|
|
97
|
+
* state={state}
|
|
98
|
+
* />
|
|
99
|
+
* <PasswordField.Description state={state}>
|
|
100
|
+
* Choose a strong password
|
|
101
|
+
* </PasswordField.Description>
|
|
102
|
+
* <PasswordField.Requirements requirements={requirements} />
|
|
103
|
+
* <PasswordField.Error>Password is required</PasswordField.Error>
|
|
104
|
+
* </PasswordField.Root>
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* @param {string} [label] - Label text (props API)
|
|
108
|
+
* @param {string} [description] - Description/help text (props API)
|
|
109
|
+
* @param {string | string[]} [error] - Error message(s) to display. Pass a string for a single error or an array for multiple. Does not affect visual state - use state prop for that.
|
|
110
|
+
* @param {InputState} [state] - Visual state of the input: 'default', 'error', or 'success' (props API)
|
|
111
|
+
* @param {string} [optionalText] - Optional text to display next to label (props API)
|
|
112
|
+
* @param {string[] | PasswordRequirement[]} [requirements] - List of password requirements to display. Can be simple strings or objects with text and satisfied status (props API)
|
|
113
|
+
* @param {boolean} [showRequirements] - Whether to show requirements list. Requirements auto-hide when state is 'success' (props API)
|
|
114
|
+
* @param {function} [onValueChange] - Value change handler (receives string value)
|
|
115
|
+
* @param {string} [value] - Controlled value
|
|
116
|
+
* @param {string} [defaultValue] - Default value for uncontrolled mode
|
|
117
|
+
* @param {function} [onChangeText] - Change event handler
|
|
118
|
+
* @param {boolean} [editable] - Controls whether the input is editable
|
|
119
|
+
*/
|
|
120
|
+
const PasswordFieldRoot = React.forwardRef<View, PasswordFieldProps>(
|
|
121
|
+
(
|
|
122
|
+
{
|
|
123
|
+
label,
|
|
124
|
+
description,
|
|
125
|
+
error,
|
|
126
|
+
state = "default",
|
|
127
|
+
optionalText,
|
|
128
|
+
requirements,
|
|
129
|
+
showRequirements = false,
|
|
130
|
+
onValueChange,
|
|
131
|
+
value,
|
|
132
|
+
children,
|
|
133
|
+
...inputFieldProps
|
|
134
|
+
},
|
|
135
|
+
ref
|
|
136
|
+
) => {
|
|
137
|
+
if (children) {
|
|
138
|
+
return <StyledRoot ref={ref}>{children}</StyledRoot>
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<StyledRoot ref={ref}>
|
|
143
|
+
{label && (
|
|
144
|
+
<InputLabel optionalText={optionalText} state={state}>
|
|
145
|
+
{label}
|
|
146
|
+
</InputLabel>
|
|
147
|
+
)}
|
|
148
|
+
<PasswordFieldInput
|
|
149
|
+
state={state}
|
|
150
|
+
value={value}
|
|
151
|
+
onValueChange={onValueChange}
|
|
152
|
+
{...inputFieldProps}
|
|
153
|
+
/>
|
|
154
|
+
{description && (
|
|
155
|
+
<InputDescription state={state}>{description}</InputDescription>
|
|
156
|
+
)}
|
|
157
|
+
{state === "error" &&
|
|
158
|
+
(Array.isArray(error) ? error : error ? [error] : []).map(
|
|
159
|
+
(message, index) => (
|
|
160
|
+
<PasswordFieldError key={index}>{message}</PasswordFieldError>
|
|
161
|
+
)
|
|
162
|
+
)}
|
|
163
|
+
{showRequirements &&
|
|
164
|
+
requirements &&
|
|
165
|
+
requirements.length > 0 &&
|
|
166
|
+
state !== "success" && (
|
|
167
|
+
<PasswordFieldRequirements requirements={requirements} />
|
|
168
|
+
)}
|
|
169
|
+
</StyledRoot>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
PasswordFieldRoot.displayName = "PasswordField"
|
|
175
|
+
|
|
176
|
+
type PasswordFieldComponent = React.ForwardRefExoticComponent<
|
|
177
|
+
PasswordFieldProps & React.RefAttributes<View>
|
|
178
|
+
> & {
|
|
179
|
+
Root: typeof StyledRoot
|
|
180
|
+
Label: typeof InputLabel
|
|
181
|
+
Field: typeof PasswordFieldInput
|
|
182
|
+
Description: typeof InputDescription
|
|
183
|
+
Requirements: typeof PasswordFieldRequirements
|
|
184
|
+
Error: typeof PasswordFieldError
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const PasswordField = Object.assign(PasswordFieldRoot, {
|
|
188
|
+
Root: StyledRoot,
|
|
189
|
+
Label: InputLabel,
|
|
190
|
+
Field: PasswordFieldInput,
|
|
191
|
+
Description: InputDescription,
|
|
192
|
+
Requirements: PasswordFieldRequirements,
|
|
193
|
+
Error: PasswordFieldError
|
|
194
|
+
}) as PasswordFieldComponent
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
import { Typography } from "../../atoms/Typography"
|
|
6
|
+
import { Icon } from "../../atoms/Icon"
|
|
7
|
+
import { Cancel } from "@butternutbox/pawprint-icons/core"
|
|
8
|
+
|
|
9
|
+
export type PasswordFieldErrorProps = {
|
|
10
|
+
children?: React.ReactNode
|
|
11
|
+
} & ViewProps
|
|
12
|
+
|
|
13
|
+
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
14
|
+
|
|
15
|
+
const StyledErrorRow = styled(View)<{ gap: number }>(({ gap }) => ({
|
|
16
|
+
flexDirection: "row",
|
|
17
|
+
alignItems: "center",
|
|
18
|
+
gap
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Error message component for PasswordField.
|
|
23
|
+
* Displays the error text with a RemoveCircle icon to match the design system
|
|
24
|
+
* password validation styling.
|
|
25
|
+
*
|
|
26
|
+
* @param children - Error message text
|
|
27
|
+
*/
|
|
28
|
+
export const PasswordFieldError = React.forwardRef<
|
|
29
|
+
View,
|
|
30
|
+
PasswordFieldErrorProps
|
|
31
|
+
>(({ children, ...rest }, ref) => {
|
|
32
|
+
const theme = useTheme()
|
|
33
|
+
const { listItem } = theme.tokens.components.validationList
|
|
34
|
+
const { typography } = theme.tokens.semantics
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<StyledErrorRow
|
|
38
|
+
ref={ref}
|
|
39
|
+
gap={parseTokenValue(listItem.spacing.gap)}
|
|
40
|
+
{...rest}
|
|
41
|
+
>
|
|
42
|
+
<Icon icon={Cancel} size="xs" customColour={listItem.colour.icon.error} />
|
|
43
|
+
<Typography
|
|
44
|
+
token={typography.body.medium.md}
|
|
45
|
+
color={listItem.colour.text.default}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</Typography>
|
|
49
|
+
</StyledErrorRow>
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
PasswordFieldError.displayName = "PasswordField.Error"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { Pressable, TextInput } from "react-native"
|
|
3
|
+
import { InputField } from "../../atoms/Input/InputField"
|
|
4
|
+
import { Icon } from "../../atoms/Icon"
|
|
5
|
+
import { Visibility, VisibilityOff } from "@butternutbox/pawprint-icons/core"
|
|
6
|
+
import type { PasswordFieldProps } from "./PasswordField"
|
|
7
|
+
|
|
8
|
+
const HIT_SLOP = 12
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Input field component for PasswordField.
|
|
12
|
+
* Wraps InputField with password visibility toggle.
|
|
13
|
+
*
|
|
14
|
+
* @param onValueChange - Value change handler
|
|
15
|
+
* @param value - Controlled value
|
|
16
|
+
* @param state - Visual state of the input
|
|
17
|
+
* @param editable - Controls whether the input is editable
|
|
18
|
+
*/
|
|
19
|
+
export const PasswordFieldInput = React.forwardRef<
|
|
20
|
+
TextInput,
|
|
21
|
+
Omit<
|
|
22
|
+
PasswordFieldProps,
|
|
23
|
+
| "label"
|
|
24
|
+
| "description"
|
|
25
|
+
| "error"
|
|
26
|
+
| "optionalText"
|
|
27
|
+
| "requirements"
|
|
28
|
+
| "showRequirements"
|
|
29
|
+
>
|
|
30
|
+
>(({ onValueChange, value, state, editable, ...inputFieldProps }, ref) => {
|
|
31
|
+
const [showPassword, setShowPassword] = React.useState(false)
|
|
32
|
+
|
|
33
|
+
const handleValueChange = (newValue: string) => {
|
|
34
|
+
onValueChange?.(newValue)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const handleToggleVisibility = () => {
|
|
38
|
+
if (editable === false) return
|
|
39
|
+
setShowPassword((prev) => !prev)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const toggleButton = (
|
|
43
|
+
<Pressable
|
|
44
|
+
onPress={handleToggleVisibility}
|
|
45
|
+
accessibilityLabel={showPassword ? "Hide password" : "Show password"}
|
|
46
|
+
accessibilityRole="button"
|
|
47
|
+
disabled={editable === false}
|
|
48
|
+
hitSlop={{
|
|
49
|
+
top: HIT_SLOP,
|
|
50
|
+
bottom: HIT_SLOP,
|
|
51
|
+
left: HIT_SLOP,
|
|
52
|
+
right: HIT_SLOP
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<Icon icon={showPassword ? VisibilityOff : Visibility} size="md" />
|
|
56
|
+
</Pressable>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<InputField
|
|
61
|
+
ref={ref}
|
|
62
|
+
{...inputFieldProps}
|
|
63
|
+
state={state}
|
|
64
|
+
value={value}
|
|
65
|
+
onChangeText={handleValueChange}
|
|
66
|
+
actionIcon={toggleButton}
|
|
67
|
+
secureTextEntry={!showPassword}
|
|
68
|
+
editable={editable}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
PasswordFieldInput.displayName = "PasswordField.Field"
|