@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,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
|
+
})
|
|
@@ -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
|
+
})
|