@butternutbox/pawprint-native 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +15 -15
- package/CHANGELOG.md +16 -0
- package/COMPONENT_GUIDELINES.md +111 -4
- package/dist/index.cjs +12370 -1455
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1110 -11
- package/dist/index.d.ts +1110 -11
- package/dist/index.js +12324 -1455
- package/dist/index.js.map +1 -1
- package/package.json +28 -9
- 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.test.tsx +55 -0
- 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/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 +16 -13
- 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.test.tsx +83 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +8 -14
- 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 +52 -0
- package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
- package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +92 -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 +243 -0
- package/src/components/molecules/PictureSelector/PictureSelector.tsx +313 -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
|
@@ -38,23 +38,32 @@ const Spinner = React.forwardRef<View, SpinnerProps>(
|
|
|
38
38
|
|
|
39
39
|
const { size: sizeTokens, colour } = theme.tokens.components.spinner
|
|
40
40
|
const borderWidth = parseTokenValue(
|
|
41
|
-
theme.tokens.semantics.dimensions.borderWidth.
|
|
41
|
+
theme.tokens.semantics.dimensions.borderWidth.lg
|
|
42
42
|
)
|
|
43
43
|
const baseColor = colour.background.base[variant]
|
|
44
44
|
const progressColor = colour.background.progress[variant]
|
|
45
45
|
const dimension = parseTokenValue(sizeTokens[size])
|
|
46
46
|
|
|
47
47
|
useEffect(() => {
|
|
48
|
-
|
|
48
|
+
let stopped = false
|
|
49
|
+
const animate = () => {
|
|
50
|
+
spinAnim.setValue(0)
|
|
49
51
|
Animated.timing(spinAnim, {
|
|
50
52
|
toValue: 1,
|
|
51
53
|
duration: 600,
|
|
52
54
|
easing: Easing.linear,
|
|
53
55
|
useNativeDriver: true
|
|
56
|
+
}).start(({ finished }) => {
|
|
57
|
+
if (finished && !stopped) {
|
|
58
|
+
animate()
|
|
59
|
+
}
|
|
54
60
|
})
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return () =>
|
|
61
|
+
}
|
|
62
|
+
animate()
|
|
63
|
+
return () => {
|
|
64
|
+
stopped = true
|
|
65
|
+
spinAnim.stopAnimation()
|
|
66
|
+
}
|
|
58
67
|
}, [spinAnim])
|
|
59
68
|
|
|
60
69
|
const spin = spinAnim.interpolate({
|
|
@@ -0,0 +1,92 @@
|
|
|
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 { Switch } from "./Switch"
|
|
7
|
+
|
|
8
|
+
describe("Switch", () => {
|
|
9
|
+
describe("when component is rendering", () => {
|
|
10
|
+
it("renders label", () => {
|
|
11
|
+
renderWithTheme(<Switch label="Notifications" />)
|
|
12
|
+
expect(screen.getByText("Notifications")).toBeInTheDocument()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("renders label and subText", () => {
|
|
16
|
+
renderWithTheme(
|
|
17
|
+
<Switch label="SMS Notifications" subText="Receive SMS updates" />
|
|
18
|
+
)
|
|
19
|
+
expect(screen.getByText("SMS Notifications")).toBeInTheDocument()
|
|
20
|
+
expect(screen.getByText("Receive SMS updates")).toBeInTheDocument()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("renders without label or subText", () => {
|
|
24
|
+
const { container } = renderWithTheme(<Switch />)
|
|
25
|
+
expect(container.firstChild).toBeTruthy()
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe("when component is uncontrolled", () => {
|
|
30
|
+
it("toggles checked state when clicked", async () => {
|
|
31
|
+
const user = userEvent.setup()
|
|
32
|
+
renderWithTheme(<Switch label="Toggle" />)
|
|
33
|
+
|
|
34
|
+
const switchEl = screen.getByRole("switch")
|
|
35
|
+
expect(switchEl).toHaveAttribute("aria-checked", "false")
|
|
36
|
+
|
|
37
|
+
await user.click(switchEl)
|
|
38
|
+
expect(switchEl).toHaveAttribute("aria-checked", "true")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("does not toggle when disabled", async () => {
|
|
42
|
+
const user = userEvent.setup()
|
|
43
|
+
renderWithTheme(<Switch label="Toggle" disabled />)
|
|
44
|
+
|
|
45
|
+
const switchEl = screen.getByRole("switch")
|
|
46
|
+
expect(switchEl).toBeDisabled()
|
|
47
|
+
|
|
48
|
+
await user.click(switchEl)
|
|
49
|
+
expect(switchEl).toHaveAttribute("aria-checked", "false")
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("can be initially checked via defaultChecked", () => {
|
|
53
|
+
renderWithTheme(<Switch label="Toggle" defaultChecked />)
|
|
54
|
+
|
|
55
|
+
const switchEl = screen.getByRole("switch")
|
|
56
|
+
expect(switchEl).toHaveAttribute("aria-checked", "true")
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("when component is controlled", () => {
|
|
61
|
+
it("calls onCheckedChange when clicked", async () => {
|
|
62
|
+
const user = userEvent.setup()
|
|
63
|
+
const onCheckedChange = vi.fn()
|
|
64
|
+
renderWithTheme(
|
|
65
|
+
<Switch
|
|
66
|
+
label="Toggle"
|
|
67
|
+
checked={false}
|
|
68
|
+
onCheckedChange={onCheckedChange}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
await user.click(screen.getByRole("switch"))
|
|
73
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("reflects controlled checked state", () => {
|
|
77
|
+
renderWithTheme(
|
|
78
|
+
<Switch label="Toggle" checked={true} onCheckedChange={() => {}} />
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
expect(screen.getByRole("switch")).toHaveAttribute("aria-checked", "true")
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe("when using with ref", () => {
|
|
86
|
+
it("forwards ref", () => {
|
|
87
|
+
const ref = React.createRef<any>()
|
|
88
|
+
renderWithTheme(<Switch ref={ref} label="Ref test" />)
|
|
89
|
+
expect(ref.current).toBeTruthy()
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -28,7 +28,7 @@ const StyledContainer = styled(View)<{
|
|
|
28
28
|
opacity: switchOpacity
|
|
29
29
|
}))
|
|
30
30
|
|
|
31
|
-
const
|
|
31
|
+
const StyledControlTrack = styled(View)<{
|
|
32
32
|
switchChecked: boolean
|
|
33
33
|
controlWidth: number
|
|
34
34
|
controlHeight: number
|
|
@@ -129,7 +129,7 @@ export const Switch = React.forwardRef<View, SwitchProps>(
|
|
|
129
129
|
|
|
130
130
|
const thumbTranslateX = animValue.interpolate({
|
|
131
131
|
inputRange: [0, 1],
|
|
132
|
-
outputRange: [inset, inset + translateX]
|
|
132
|
+
outputRange: [inset, inset + translateX + 3]
|
|
133
133
|
})
|
|
134
134
|
|
|
135
135
|
const handleCheckedChange = (checked: boolean) => {
|
|
@@ -146,19 +146,22 @@ export const Switch = React.forwardRef<View, SwitchProps>(
|
|
|
146
146
|
switchOpacity={disabled ? parseFloat(opacity.disabled) : 1}
|
|
147
147
|
{...rest}
|
|
148
148
|
>
|
|
149
|
-
<
|
|
149
|
+
<SwitchPrimitive.Root
|
|
150
150
|
checked={isChecked}
|
|
151
151
|
onCheckedChange={handleCheckedChange}
|
|
152
152
|
disabled={disabled}
|
|
153
|
-
|
|
154
|
-
controlWidth={controlWidth}
|
|
155
|
-
controlHeight={controlHeight}
|
|
156
|
-
controlBorderWidth={borderWidthValue}
|
|
157
|
-
controlBorderColor={colour.control.border.default}
|
|
158
|
-
controlBgChecked={colour.control.background.selected}
|
|
159
|
-
controlBgDefault={colour.control.background.default}
|
|
153
|
+
asChild
|
|
160
154
|
>
|
|
161
|
-
<
|
|
155
|
+
<StyledControlTrack
|
|
156
|
+
switchChecked={isChecked}
|
|
157
|
+
controlWidth={controlWidth}
|
|
158
|
+
controlHeight={controlHeight}
|
|
159
|
+
controlBorderWidth={borderWidthValue}
|
|
160
|
+
controlBorderColor={colour.control.border.default}
|
|
161
|
+
controlBgChecked={colour.control.background.selected}
|
|
162
|
+
controlBgDefault={colour.control.background.default}
|
|
163
|
+
pointerEvents={disabled ? "none" : "auto"}
|
|
164
|
+
>
|
|
162
165
|
<StyledThumb
|
|
163
166
|
thumbSize={thumbSize}
|
|
164
167
|
thumbBgColor={
|
|
@@ -168,8 +171,8 @@ export const Switch = React.forwardRef<View, SwitchProps>(
|
|
|
168
171
|
}
|
|
169
172
|
style={{ transform: [{ translateX: thumbTranslateX }] }}
|
|
170
173
|
/>
|
|
171
|
-
</
|
|
172
|
-
</
|
|
174
|
+
</StyledControlTrack>
|
|
175
|
+
</SwitchPrimitive.Root>
|
|
173
176
|
|
|
174
177
|
{(label || subText) && (
|
|
175
178
|
<StyledContent contentGap={parseTokenValue(spacing.content.gap)}>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { screen } from "@testing-library/react"
|
|
3
|
+
import { describe, it, expect } from "vitest"
|
|
4
|
+
import { renderWithTheme } from "../../../test-utils"
|
|
5
|
+
import { Tag } from "./Tag"
|
|
6
|
+
|
|
7
|
+
const MockIcon = ({ width, height, color }: any) => (
|
|
8
|
+
<svg data-testid="mock-icon" width={width} height={height} fill={color} />
|
|
9
|
+
)
|
|
10
|
+
MockIcon.category = "core" as const
|
|
11
|
+
|
|
12
|
+
describe("Tag", () => {
|
|
13
|
+
describe("when component is rendering", () => {
|
|
14
|
+
it("renders children text", () => {
|
|
15
|
+
renderWithTheme(<Tag>Company news</Tag>)
|
|
16
|
+
expect(screen.getByText("Company news")).toBeInTheDocument()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("renders with default variant and size", () => {
|
|
20
|
+
renderWithTheme(<Tag>Default</Tag>)
|
|
21
|
+
expect(screen.getByText("Default")).toBeInTheDocument()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe("when rendering all variants", () => {
|
|
26
|
+
it.each([
|
|
27
|
+
"primary",
|
|
28
|
+
"secondary",
|
|
29
|
+
"tertiary",
|
|
30
|
+
"promo",
|
|
31
|
+
"success",
|
|
32
|
+
"warning",
|
|
33
|
+
"error"
|
|
34
|
+
] as const)("renders %s variant", (variant) => {
|
|
35
|
+
renderWithTheme(<Tag variant={variant}>{variant}</Tag>)
|
|
36
|
+
expect(screen.getByText(variant)).toBeInTheDocument()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe("when rendering all sizes", () => {
|
|
41
|
+
it.each(["small", "medium", "large"] as const)(
|
|
42
|
+
"renders %s size",
|
|
43
|
+
(size) => {
|
|
44
|
+
renderWithTheme(<Tag size={size}>{size}</Tag>)
|
|
45
|
+
expect(screen.getByText(size)).toBeInTheDocument()
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe("when rendering with icon", () => {
|
|
51
|
+
it("renders with icon", () => {
|
|
52
|
+
renderWithTheme(<Tag icon={MockIcon}>With Icon</Tag>)
|
|
53
|
+
expect(screen.getByTestId("mock-icon")).toBeInTheDocument()
|
|
54
|
+
expect(screen.getByText("With Icon")).toBeInTheDocument()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("renders without icon when not provided", () => {
|
|
58
|
+
renderWithTheme(<Tag>No Icon</Tag>)
|
|
59
|
+
expect(screen.queryByTestId("mock-icon")).not.toBeInTheDocument()
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe("when using with ref", () => {
|
|
64
|
+
it("forwards ref", () => {
|
|
65
|
+
const ref = React.createRef<any>()
|
|
66
|
+
renderWithTheme(<Tag ref={ref}>Ref test</Tag>)
|
|
67
|
+
expect(ref.current).toBeTruthy()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import { View, StyleSheet } from "react-native"
|
|
3
|
+
import { TextArea } from "./TextArea"
|
|
4
|
+
import type { TextAreaProps } from "./TextArea"
|
|
5
|
+
import type { InputState } from "../Input/InputField"
|
|
6
|
+
import { Icon } from "../Icon"
|
|
7
|
+
import { Search, Info } from "@butternutbox/pawprint-icons/core"
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
title: "Atoms/TextArea",
|
|
11
|
+
component: TextArea,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: "centered",
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component:
|
|
17
|
+
"Multi-line text input component for longer text entry. Supports both simple props API and compound component API."
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
argTypes: {
|
|
22
|
+
label: {
|
|
23
|
+
control: "text",
|
|
24
|
+
description: "Label text"
|
|
25
|
+
},
|
|
26
|
+
placeholder: {
|
|
27
|
+
control: "text",
|
|
28
|
+
description: "Placeholder text"
|
|
29
|
+
},
|
|
30
|
+
description: {
|
|
31
|
+
control: "text",
|
|
32
|
+
description: "Help text below textarea"
|
|
33
|
+
},
|
|
34
|
+
error: {
|
|
35
|
+
control: "text",
|
|
36
|
+
description: "Error message"
|
|
37
|
+
},
|
|
38
|
+
state: {
|
|
39
|
+
control: "select",
|
|
40
|
+
options: ["default", "error", "success"],
|
|
41
|
+
description: "Visual state of the textarea"
|
|
42
|
+
},
|
|
43
|
+
optionalText: {
|
|
44
|
+
control: "text",
|
|
45
|
+
description: "Optional indicator next to label"
|
|
46
|
+
},
|
|
47
|
+
maxLength: {
|
|
48
|
+
control: "number",
|
|
49
|
+
description:
|
|
50
|
+
"Maximum character length (shows character counter automatically)"
|
|
51
|
+
},
|
|
52
|
+
rows: {
|
|
53
|
+
control: "number",
|
|
54
|
+
description: "Number of visible text rows"
|
|
55
|
+
},
|
|
56
|
+
editable: {
|
|
57
|
+
control: "boolean",
|
|
58
|
+
description: "Controls whether the textarea is editable"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const Default = (args: TextAreaProps) => (
|
|
64
|
+
<View style={{ width: 320 }}>
|
|
65
|
+
<TextArea {...args} />
|
|
66
|
+
</View>
|
|
67
|
+
)
|
|
68
|
+
Default.args = {
|
|
69
|
+
label: "Label",
|
|
70
|
+
placeholder: "Placeholder",
|
|
71
|
+
description: "Help text"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const WithCharacterCount = () => (
|
|
75
|
+
<View style={styles.column}>
|
|
76
|
+
<TextArea
|
|
77
|
+
label="Description"
|
|
78
|
+
placeholder="Enter your description..."
|
|
79
|
+
description="Maximum 100 characters"
|
|
80
|
+
maxLength={100}
|
|
81
|
+
rows={4}
|
|
82
|
+
/>
|
|
83
|
+
<TextArea
|
|
84
|
+
label="With Default Value"
|
|
85
|
+
placeholder="Enter your description..."
|
|
86
|
+
description="Maximum 100 characters"
|
|
87
|
+
defaultValue="Some text here"
|
|
88
|
+
maxLength={100}
|
|
89
|
+
rows={4}
|
|
90
|
+
/>
|
|
91
|
+
</View>
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
export const WithIcons = () => (
|
|
95
|
+
<View style={styles.column}>
|
|
96
|
+
<TextArea
|
|
97
|
+
label="Leading Icon"
|
|
98
|
+
placeholder="Placeholder"
|
|
99
|
+
leadingIcon={<Icon icon={Search} size="md" />}
|
|
100
|
+
description="Help text"
|
|
101
|
+
/>
|
|
102
|
+
<TextArea
|
|
103
|
+
label="Trailing Icon"
|
|
104
|
+
placeholder="Placeholder"
|
|
105
|
+
trailingIcon={<Icon icon={Search} size="md" />}
|
|
106
|
+
description="Help text"
|
|
107
|
+
/>
|
|
108
|
+
<TextArea
|
|
109
|
+
label="Both Icons"
|
|
110
|
+
placeholder="Placeholder"
|
|
111
|
+
leadingIcon={<Icon icon={Search} size="md" />}
|
|
112
|
+
trailingIcon={<Icon icon={Info} size="md" />}
|
|
113
|
+
description="Help text"
|
|
114
|
+
/>
|
|
115
|
+
</View>
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
export const States = () => (
|
|
119
|
+
<View style={styles.column}>
|
|
120
|
+
<TextArea
|
|
121
|
+
label="Default State"
|
|
122
|
+
placeholder="Enter text"
|
|
123
|
+
description="Normal textarea state"
|
|
124
|
+
/>
|
|
125
|
+
<TextArea
|
|
126
|
+
label="Error State"
|
|
127
|
+
placeholder="Enter text"
|
|
128
|
+
state="error"
|
|
129
|
+
description="Manually set to error state"
|
|
130
|
+
/>
|
|
131
|
+
<TextArea
|
|
132
|
+
label="Error with Custom Message"
|
|
133
|
+
placeholder="Enter text"
|
|
134
|
+
state="error"
|
|
135
|
+
description="Manually set to error state"
|
|
136
|
+
error="Custom error message"
|
|
137
|
+
/>
|
|
138
|
+
<TextArea
|
|
139
|
+
label="Success State"
|
|
140
|
+
placeholder="Enter text"
|
|
141
|
+
state="success"
|
|
142
|
+
description="Manually set to success state"
|
|
143
|
+
/>
|
|
144
|
+
<TextArea
|
|
145
|
+
label="Disabled"
|
|
146
|
+
placeholder="Enter text"
|
|
147
|
+
description="Help text"
|
|
148
|
+
editable={false}
|
|
149
|
+
/>
|
|
150
|
+
</View>
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
export const Controlled = () => {
|
|
154
|
+
const [value, setValue] = useState("")
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<View style={styles.column}>
|
|
158
|
+
<TextArea
|
|
159
|
+
label="Controlled TextArea"
|
|
160
|
+
value={value}
|
|
161
|
+
onValueChange={setValue}
|
|
162
|
+
placeholder="Type something..."
|
|
163
|
+
description={`You typed: ${value.length} characters`}
|
|
164
|
+
/>
|
|
165
|
+
</View>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const CustomStateValidationWithSuccess = () => {
|
|
170
|
+
const [description, setDescription] = useState("")
|
|
171
|
+
const [descriptionState, setDescriptionState] =
|
|
172
|
+
useState<InputState>("default")
|
|
173
|
+
const [errorMessage, setErrorMessage] = useState("")
|
|
174
|
+
|
|
175
|
+
const validateDescription = (value: string) => {
|
|
176
|
+
if (!value) {
|
|
177
|
+
setDescriptionState("default")
|
|
178
|
+
setErrorMessage("")
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const hasMinLength = value.length >= 10
|
|
183
|
+
const hasMaxLength = value.length <= 200
|
|
184
|
+
|
|
185
|
+
if (!hasMinLength) {
|
|
186
|
+
setDescriptionState("error")
|
|
187
|
+
setErrorMessage("Description must be at least 10 characters")
|
|
188
|
+
} else if (!hasMaxLength) {
|
|
189
|
+
setDescriptionState("error")
|
|
190
|
+
setErrorMessage("Description is too long (max 200 characters)")
|
|
191
|
+
} else {
|
|
192
|
+
setDescriptionState("success")
|
|
193
|
+
setErrorMessage("")
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const handleChange = (newValue: string) => {
|
|
198
|
+
setDescription(newValue)
|
|
199
|
+
validateDescription(newValue)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<View style={styles.column}>
|
|
204
|
+
<TextArea
|
|
205
|
+
label="Description"
|
|
206
|
+
placeholder="Enter description..."
|
|
207
|
+
value={description}
|
|
208
|
+
onValueChange={handleChange}
|
|
209
|
+
state={descriptionState}
|
|
210
|
+
description="10-200 characters"
|
|
211
|
+
error={errorMessage}
|
|
212
|
+
/>
|
|
213
|
+
</View>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export const Validation = () => {
|
|
218
|
+
const [requiredValue, setRequiredValue] = useState("")
|
|
219
|
+
const [requiredTouched, setRequiredTouched] = useState(false)
|
|
220
|
+
const requiredError =
|
|
221
|
+
requiredTouched && requiredValue.length === 0
|
|
222
|
+
? "This field is required"
|
|
223
|
+
: ""
|
|
224
|
+
|
|
225
|
+
const [minLengthValue, setMinLengthValue] = useState("")
|
|
226
|
+
const [minLengthBlurred, setMinLengthBlurred] = useState(false)
|
|
227
|
+
const minLengthError =
|
|
228
|
+
minLengthBlurred && minLengthValue.length > 0 && minLengthValue.length < 20
|
|
229
|
+
? "Must be at least 20 characters"
|
|
230
|
+
: ""
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<View style={styles.column}>
|
|
234
|
+
<TextArea
|
|
235
|
+
label="valueMissing - Required Field"
|
|
236
|
+
placeholder="Enter text"
|
|
237
|
+
description="Validates on change • Leave empty to see error"
|
|
238
|
+
value={requiredValue}
|
|
239
|
+
onValueChange={(v) => {
|
|
240
|
+
setRequiredValue(v)
|
|
241
|
+
setRequiredTouched(true)
|
|
242
|
+
}}
|
|
243
|
+
state={requiredError ? "error" : "default"}
|
|
244
|
+
error={requiredError}
|
|
245
|
+
/>
|
|
246
|
+
|
|
247
|
+
<TextArea
|
|
248
|
+
label="tooShort - Min Length"
|
|
249
|
+
placeholder="Min 20 chars"
|
|
250
|
+
description="Validates on blur • Type less than 20 characters"
|
|
251
|
+
value={minLengthValue}
|
|
252
|
+
onValueChange={setMinLengthValue}
|
|
253
|
+
onBlur={() => setMinLengthBlurred(true)}
|
|
254
|
+
state={minLengthError ? "error" : "default"}
|
|
255
|
+
error={minLengthError}
|
|
256
|
+
/>
|
|
257
|
+
</View>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export const CompoundComponentAPI = () => (
|
|
262
|
+
<View style={styles.column}>
|
|
263
|
+
<TextArea.Root>
|
|
264
|
+
<TextArea.Label>Description</TextArea.Label>
|
|
265
|
+
<TextArea.Field placeholder="Enter text..." />
|
|
266
|
+
<TextArea.Description>Provide details</TextArea.Description>
|
|
267
|
+
</TextArea.Root>
|
|
268
|
+
<TextArea.Root>
|
|
269
|
+
<TextArea.Label state="error">Description</TextArea.Label>
|
|
270
|
+
<TextArea.Field placeholder="Enter text..." state="error" />
|
|
271
|
+
<TextArea.Error>Invalid description</TextArea.Error>
|
|
272
|
+
</TextArea.Root>
|
|
273
|
+
<TextArea.Root>
|
|
274
|
+
<TextArea.Label state="success">Description</TextArea.Label>
|
|
275
|
+
<TextArea.Field placeholder="Enter text..." state="success" />
|
|
276
|
+
<TextArea.Description state="success">
|
|
277
|
+
Description verified
|
|
278
|
+
</TextArea.Description>
|
|
279
|
+
</TextArea.Root>
|
|
280
|
+
<TextArea.Root>
|
|
281
|
+
<TextArea.Label
|
|
282
|
+
optionalText="(optional)"
|
|
283
|
+
maxLength={50}
|
|
284
|
+
currentLength={15}
|
|
285
|
+
>
|
|
286
|
+
Comments
|
|
287
|
+
</TextArea.Label>
|
|
288
|
+
<TextArea.Field
|
|
289
|
+
placeholder="Add comments..."
|
|
290
|
+
defaultValue="Some comments"
|
|
291
|
+
maxLength={50}
|
|
292
|
+
/>
|
|
293
|
+
</TextArea.Root>
|
|
294
|
+
</View>
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
const styles = StyleSheet.create({
|
|
298
|
+
column: {
|
|
299
|
+
flexDirection: "column",
|
|
300
|
+
gap: 24,
|
|
301
|
+
width: 320
|
|
302
|
+
}
|
|
303
|
+
})
|