@butternutbox/pawprint-native 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/CHANGELOG.md +30 -0
  3. package/COMPONENT_GUIDELINES.md +111 -4
  4. package/dist/index.cjs +12413 -1459
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +1111 -13
  7. package/dist/index.d.ts +1111 -13
  8. package/dist/index.js +12365 -1457
  9. package/dist/index.js.map +1 -1
  10. package/package.json +29 -11
  11. package/src/__mocks__/asset-stub.ts +1 -0
  12. package/src/__mocks__/emotion-native.tsx +18 -0
  13. package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
  14. package/src/__mocks__/react-native-reanimated.tsx +79 -0
  15. package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
  16. package/src/__mocks__/react-native-svg.tsx +27 -0
  17. package/src/__mocks__/react-native-worklets.tsx +11 -0
  18. package/src/__mocks__/react-native.tsx +338 -0
  19. package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
  20. package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
  21. package/src/__mocks__/rn-primitives/select.tsx +116 -0
  22. package/src/__mocks__/rn-primitives/slider.tsx +40 -0
  23. package/src/__mocks__/rn-primitives/slot.tsx +30 -0
  24. package/src/__mocks__/rn-primitives/switch.tsx +24 -0
  25. package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
  26. package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
  27. package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
  28. package/src/components/atoms/Avatar/Avatar.tsx +68 -22
  29. package/src/components/atoms/Avatar/index.ts +1 -6
  30. package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
  31. package/src/components/atoms/Badge/Badge.test.tsx +90 -0
  32. package/src/components/atoms/Button/Button.test.tsx +123 -0
  33. package/src/components/atoms/Button/Button.tsx +1 -1
  34. package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
  35. package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
  36. package/src/components/atoms/CarouselControls/index.ts +2 -0
  37. package/src/components/atoms/Hint/Hint.test.tsx +36 -0
  38. package/src/components/atoms/Icon/Icon.test.tsx +98 -0
  39. package/src/components/atoms/Icon/Icon.tsx +5 -1
  40. package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
  41. package/src/components/atoms/Illustration/Illustration.stories.tsx +2 -2
  42. package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
  43. package/src/components/atoms/Illustration/Illustration.tsx +3 -3
  44. package/src/components/atoms/Input/Input.stories.tsx +129 -86
  45. package/src/components/atoms/Input/Input.test.tsx +306 -0
  46. package/src/components/atoms/Input/Input.tsx +9 -1
  47. package/src/components/atoms/Input/InputField.tsx +226 -74
  48. package/src/components/atoms/Link/Link.test.tsx +89 -0
  49. package/src/components/atoms/Link/Link.tsx +7 -6
  50. package/src/components/atoms/Logo/Logo.registry.ts +30 -5
  51. package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
  52. package/src/components/atoms/Logo/Logo.test.tsx +56 -0
  53. package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
  54. package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
  55. package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
  56. package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
  57. package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
  58. package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
  59. package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
  60. package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
  61. package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
  62. package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
  63. package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
  64. package/src/components/atoms/Logo/assets/index.ts +11 -0
  65. package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
  66. package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
  67. package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
  68. package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
  69. package/src/components/atoms/NumberInput/index.ts +4 -0
  70. package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
  71. package/src/components/atoms/Spinner/Spinner.tsx +14 -5
  72. package/src/components/atoms/Switch/Switch.test.tsx +92 -0
  73. package/src/components/atoms/Switch/Switch.tsx +28 -17
  74. package/src/components/atoms/Tag/Tag.test.tsx +70 -0
  75. package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
  76. package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
  77. package/src/components/atoms/TextArea/TextArea.tsx +171 -0
  78. package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
  79. package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
  80. package/src/components/atoms/TextArea/index.ts +6 -0
  81. package/src/components/atoms/Typography/Typography.test.tsx +94 -0
  82. package/src/components/atoms/index.ts +3 -0
  83. package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
  84. package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
  85. package/src/components/molecules/Accordion/Accordion.tsx +284 -0
  86. package/src/components/molecules/Accordion/index.ts +6 -0
  87. package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
  88. package/src/components/molecules/Animated/Animated.tsx +283 -0
  89. package/src/components/molecules/Animated/index.ts +10 -0
  90. package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +44 -25
  91. package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
  92. package/src/components/molecules/ButtonDock/ButtonDock.tsx +16 -13
  93. package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +48 -29
  94. package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
  95. package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
  96. package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
  97. package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
  98. package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
  99. package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
  100. package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
  101. package/src/components/molecules/CopyField/CopyField.tsx +156 -0
  102. package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
  103. package/src/components/molecules/CopyField/hooks/index.ts +1 -0
  104. package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
  105. package/src/components/molecules/CopyField/index.ts +4 -0
  106. package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
  107. package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
  108. package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
  109. package/src/components/molecules/DatePicker/index.ts +2 -0
  110. package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
  111. package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
  112. package/src/components/molecules/Drawer/Drawer.tsx +187 -0
  113. package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
  114. package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
  115. package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
  116. package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
  117. package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
  118. package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
  119. package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
  120. package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
  121. package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
  122. package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
  123. package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
  124. package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
  125. package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
  126. package/src/components/molecules/Drawer/index.ts +12 -0
  127. package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
  128. package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
  129. package/src/components/molecules/FilterTab/index.ts +2 -0
  130. package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
  131. package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
  132. package/src/components/molecules/MessageCard/index.ts +10 -0
  133. package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
  134. package/src/components/molecules/Notification/Notification.tsx +426 -0
  135. package/src/components/molecules/Notification/index.ts +2 -0
  136. package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
  137. package/src/components/molecules/NumberField/NumberField.tsx +186 -0
  138. package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
  139. package/src/components/molecules/NumberField/index.ts +2 -0
  140. package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
  141. package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
  142. package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
  143. package/src/components/molecules/PasswordField/PasswordFieldError.tsx +53 -0
  144. package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
  145. package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +95 -0
  146. package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
  147. package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
  148. package/src/components/molecules/PasswordField/index.ts +10 -0
  149. package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +204 -0
  150. package/src/components/molecules/PictureSelector/PictureSelector.tsx +335 -0
  151. package/src/components/molecules/PictureSelector/index.ts +5 -0
  152. package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
  153. package/src/components/molecules/Progress/Progress.tsx +184 -0
  154. package/src/components/molecules/Progress/index.ts +2 -0
  155. package/src/components/molecules/Radio/Radio.test.tsx +104 -0
  156. package/src/components/molecules/Radio/Radio.tsx +1 -2
  157. package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
  158. package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
  159. package/src/components/molecules/SearchField/SearchField.tsx +143 -0
  160. package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
  161. package/src/components/molecules/SearchField/hooks/index.ts +1 -0
  162. package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
  163. package/src/components/molecules/SearchField/index.ts +4 -0
  164. package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
  165. package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
  166. package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
  167. package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
  168. package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
  169. package/src/components/molecules/SelectField/SelectField.tsx +236 -0
  170. package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
  171. package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
  172. package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
  173. package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
  174. package/src/components/molecules/SelectField/hooks/index.ts +2 -0
  175. package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
  176. package/src/components/molecules/SelectField/index.ts +10 -0
  177. package/src/components/molecules/Slider/Slider.test.tsx +102 -0
  178. package/src/components/molecules/Slider/Slider.tsx +293 -180
  179. package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
  180. package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
  181. package/src/components/molecules/Tooltip/index.ts +2 -0
  182. package/src/components/molecules/index.ts +15 -0
  183. package/src/test-utils.tsx +20 -0
  184. package/tsconfig.json +1 -1
  185. package/tsup.config.ts +16 -2
  186. package/vitest.config.ts +114 -0
  187. package/vitest.setup.ts +16 -0
@@ -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.md
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
- const animation = Animated.loop(
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
- animation.start()
57
- return () => animation.stop()
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 StyledControl = styled(SwitchPrimitive.Root)<{
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
- borderWidth: switchChecked ? 0 : controlBorderWidth,
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
- const outerInset = 5
117
- const inset = outerInset - borderWidthValue
118
- const translateX = controlWidth - thumbSize - outerInset * 2
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: [inset, inset + translateX]
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
- <StyledControl
157
+ <SwitchPrimitive.Root
150
158
  checked={isChecked}
151
159
  onCheckedChange={handleCheckedChange}
152
160
  disabled={disabled}
153
- switchChecked={isChecked}
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
- <SwitchPrimitive.Thumb asChild>
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
- </SwitchPrimitive.Thumb>
172
- </StyledControl>
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
+ })