@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
|
@@ -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
|
|
@@ -52,9 +52,13 @@ const StyledControl = styled(SwitchPrimitive.Root)<{
|
|
|
52
52
|
minWidth: controlWidth,
|
|
53
53
|
minHeight: controlHeight,
|
|
54
54
|
borderRadius: controlHeight,
|
|
55
|
-
|
|
55
|
+
// Keep the border width constant and only toggle the colour. Animating
|
|
56
|
+
// the width between 0 and 2 was causing a momentary background bleed on
|
|
57
|
+
// Android when transitioning back to the off state.
|
|
58
|
+
borderWidth: controlBorderWidth,
|
|
56
59
|
borderColor: switchChecked ? "transparent" : controlBorderColor,
|
|
57
60
|
backgroundColor: switchChecked ? controlBgChecked : controlBgDefault,
|
|
61
|
+
overflow: "hidden",
|
|
58
62
|
justifyContent: "center"
|
|
59
63
|
})
|
|
60
64
|
)
|
|
@@ -113,9 +117,13 @@ export const Switch = React.forwardRef<View, SwitchProps>(
|
|
|
113
117
|
const controlHeight = parseTokenValue(size.control.height)
|
|
114
118
|
const thumbSize = parseTokenValue(size.thumb.default)
|
|
115
119
|
const borderWidthValue = parseTokenValue(borderWidth.control.default)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
// Thumb sits 2px inside the inner border edge in both states. With the
|
|
121
|
+
// border width constant, the absolute-position reference frame is the
|
|
122
|
+
// padding box in both states, so the on-state inset is
|
|
123
|
+
// controlWidth - 2*border - thumbSize - inset.
|
|
124
|
+
const inset = 2
|
|
125
|
+
const offTranslateX = inset
|
|
126
|
+
const onTranslateX = controlWidth - borderWidthValue * 2 - thumbSize - inset
|
|
119
127
|
|
|
120
128
|
const animValue = useRef(new Animated.Value(isChecked ? 1 : 0)).current
|
|
121
129
|
|
|
@@ -129,7 +137,7 @@ export const Switch = React.forwardRef<View, SwitchProps>(
|
|
|
129
137
|
|
|
130
138
|
const thumbTranslateX = animValue.interpolate({
|
|
131
139
|
inputRange: [0, 1],
|
|
132
|
-
outputRange: [
|
|
140
|
+
outputRange: [offTranslateX, onTranslateX]
|
|
133
141
|
})
|
|
134
142
|
|
|
135
143
|
const handleCheckedChange = (checked: boolean) => {
|
|
@@ -146,19 +154,22 @@ export const Switch = React.forwardRef<View, SwitchProps>(
|
|
|
146
154
|
switchOpacity={disabled ? parseFloat(opacity.disabled) : 1}
|
|
147
155
|
{...rest}
|
|
148
156
|
>
|
|
149
|
-
<
|
|
157
|
+
<SwitchPrimitive.Root
|
|
150
158
|
checked={isChecked}
|
|
151
159
|
onCheckedChange={handleCheckedChange}
|
|
152
160
|
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}
|
|
161
|
+
asChild
|
|
160
162
|
>
|
|
161
|
-
<
|
|
163
|
+
<StyledControlTrack
|
|
164
|
+
switchChecked={isChecked}
|
|
165
|
+
controlWidth={controlWidth}
|
|
166
|
+
controlHeight={controlHeight}
|
|
167
|
+
controlBorderWidth={borderWidthValue}
|
|
168
|
+
controlBorderColor={colour.control.border.default}
|
|
169
|
+
controlBgChecked={colour.control.background.selected}
|
|
170
|
+
controlBgDefault={colour.control.background.default}
|
|
171
|
+
pointerEvents={disabled ? "none" : "auto"}
|
|
172
|
+
>
|
|
162
173
|
<StyledThumb
|
|
163
174
|
thumbSize={thumbSize}
|
|
164
175
|
thumbBgColor={
|
|
@@ -168,8 +179,8 @@ export const Switch = React.forwardRef<View, SwitchProps>(
|
|
|
168
179
|
}
|
|
169
180
|
style={{ transform: [{ translateX: thumbTranslateX }] }}
|
|
170
181
|
/>
|
|
171
|
-
</
|
|
172
|
-
</
|
|
182
|
+
</StyledControlTrack>
|
|
183
|
+
</SwitchPrimitive.Root>
|
|
173
184
|
|
|
174
185
|
{(label || subText) && (
|
|
175
186
|
<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
|
+
})
|