@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
@@ -0,0 +1,83 @@
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 { ButtonDock } from "./ButtonDock"
6
+
7
+ describe("ButtonDock", () => {
8
+ describe("when component is rendering", () => {
9
+ it("renders children", () => {
10
+ renderWithTheme(
11
+ <ButtonDock>
12
+ <button>Continue</button>
13
+ </ButtonDock>
14
+ )
15
+ expect(screen.getByText("Continue")).toBeInTheDocument()
16
+ })
17
+
18
+ it("renders with default stacked variant", () => {
19
+ renderWithTheme(
20
+ <ButtonDock>
21
+ <button>Primary</button>
22
+ <button>Secondary</button>
23
+ </ButtonDock>
24
+ )
25
+ expect(screen.getByText("Primary")).toBeInTheDocument()
26
+ expect(screen.getByText("Secondary")).toBeInTheDocument()
27
+ })
28
+ })
29
+
30
+ describe("when rendering variants", () => {
31
+ it("renders stacked variant", () => {
32
+ renderWithTheme(
33
+ <ButtonDock variant="stacked">
34
+ <button>Action</button>
35
+ </ButtonDock>
36
+ )
37
+ expect(screen.getByText("Action")).toBeInTheDocument()
38
+ })
39
+
40
+ it("renders inline variant", () => {
41
+ renderWithTheme(
42
+ <ButtonDock variant="inline">
43
+ <button>Action</button>
44
+ </ButtonDock>
45
+ )
46
+ expect(screen.getByText("Action")).toBeInTheDocument()
47
+ })
48
+ })
49
+
50
+ describe("when rendering with description", () => {
51
+ it("renders description in stacked variant", () => {
52
+ renderWithTheme(
53
+ <ButtonDock description="Helper text">
54
+ <button>Action</button>
55
+ </ButtonDock>
56
+ )
57
+ expect(screen.getByText("Helper text")).toBeInTheDocument()
58
+ })
59
+ })
60
+
61
+ describe("when rendering with leading content", () => {
62
+ it("renders leading content in stacked variant", () => {
63
+ renderWithTheme(
64
+ <ButtonDock leadingContent={<span>Leading</span>}>
65
+ <button>Action</button>
66
+ </ButtonDock>
67
+ )
68
+ expect(screen.getByText("Leading")).toBeInTheDocument()
69
+ })
70
+ })
71
+
72
+ describe("when using with ref", () => {
73
+ it("forwards ref", () => {
74
+ const ref = React.createRef<any>()
75
+ renderWithTheme(
76
+ <ButtonDock ref={ref}>
77
+ <button>Action</button>
78
+ </ButtonDock>
79
+ )
80
+ expect(ref.current).toBeTruthy()
81
+ })
82
+ })
83
+ })
@@ -39,12 +39,14 @@ const StyledDockRoot = styled(View)<{
39
39
  })
40
40
  )
41
41
 
42
- const StyledStackedInner = styled(View)({
43
- alignItems: "center",
44
- gap: 16,
45
- width: "100%",
46
- maxWidth: 520
47
- })
42
+ const StyledStackedInner = styled(View)<{ innerGap: number }>(
43
+ ({ innerGap }) => ({
44
+ alignItems: "center",
45
+ gap: innerGap,
46
+ width: "100%",
47
+ maxWidth: 520
48
+ })
49
+ )
48
50
 
49
51
  const StyledButtonGroup = styled(View)<{
50
52
  groupDirection: "column" | "row"
@@ -85,7 +87,7 @@ const ButtonDock = React.forwardRef<View, ButtonDockProps>(
85
87
  ) => {
86
88
  const theme = useTheme()
87
89
  const { buttonDock, buttonGroup } = theme.tokens.components
88
- const { dimensions } = theme.tokens.semantics
90
+ const { dimensions, typography } = theme.tokens.semantics
89
91
  const isStacked = variant === "stacked"
90
92
 
91
93
  const groupGap = parseTokenValue(
@@ -101,12 +103,14 @@ const ButtonDock = React.forwardRef<View, ButtonDockProps>(
101
103
  dockBorderTopWidth={parseTokenValue(dimensions.borderWidth.sm)}
102
104
  dockBorderTopColor={buttonDock.colour.border}
103
105
  dockPaddingVertical={parseTokenValue(
104
- buttonDock.spacing[variant].desktop.verticalPadding
106
+ buttonDock.spacing[variant].mobile.topPadding
105
107
  )}
106
108
  {...rest}
107
109
  >
108
110
  {isStacked ? (
109
- <StyledStackedInner>
111
+ <StyledStackedInner
112
+ innerGap={parseTokenValue(buttonGroup.spacing.gap)}
113
+ >
110
114
  {leadingContent}
111
115
  <StyledButtonGroup
112
116
  groupDirection="column"
@@ -118,12 +122,11 @@ const ButtonDock = React.forwardRef<View, ButtonDockProps>(
118
122
  </StyledButtonGroup>
119
123
  {description && (
120
124
  <Typography
121
- variant="body"
122
- size="md"
123
- weight="medium"
125
+ token={typography.body.medium.md}
126
+ color={buttonGroup.colour.text}
124
127
  align="center"
125
128
  >
126
- {typeof description === "string" ? description : description}
129
+ {description}
127
130
  </Typography>
128
131
  )}
129
132
  </StyledStackedInner>
@@ -11,65 +11,84 @@ export default {
11
11
  layout: {
12
12
  control: { type: "select" },
13
13
  options: ["stacked", "inline"],
14
- description: "Layout direction (stacked or inline)"
14
+ description: "Layout direction for the buttons"
15
15
  },
16
16
  description: {
17
17
  control: { type: "text" },
18
- description: "Optional text below buttons"
18
+ description: "Optional text displayed below the buttons"
19
19
  }
20
20
  }
21
21
  }
22
22
 
23
- export const Playground = {
24
- args: {
25
- layout: "stacked",
26
- description: "Choose an option below"
23
+ type PlaygroundArgs = ButtonGroupProps & {
24
+ primaryLabel: string
25
+ secondaryLabel: string
26
+ }
27
+
28
+ export const Playground = ({
29
+ primaryLabel,
30
+ secondaryLabel,
31
+ ...args
32
+ }: PlaygroundArgs) => (
33
+ <ButtonGroup {...args}>
34
+ <Button colour="primary">{primaryLabel}</Button>
35
+ {secondaryLabel?.trim() && (
36
+ <Button colour="secondary">{secondaryLabel}</Button>
37
+ )}
38
+ </ButtonGroup>
39
+ )
40
+ Playground.args = {
41
+ layout: "stacked",
42
+ description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
43
+ primaryLabel: "Confirm",
44
+ secondaryLabel: "Cancel"
45
+ }
46
+ Playground.argTypes = {
47
+ primaryLabel: {
48
+ control: { type: "text" },
49
+ description: "Label for the primary button"
27
50
  },
28
- render: (args: ButtonGroupProps) => (
29
- <ButtonGroup {...args}>
30
- <Button fullWidth>Confirm</Button>
31
- <Button fullWidth variant="outlined">
32
- Cancel
33
- </Button>
34
- </ButtonGroup>
35
- )
51
+ secondaryLabel: {
52
+ control: { type: "text" },
53
+ description:
54
+ "Label for the secondary button (leave empty for single button)"
55
+ }
36
56
  }
37
57
 
38
58
  export const Stacked = () => (
39
59
  <View style={styles.column}>
40
- <ButtonGroup layout="stacked" description="Two buttons stacked vertically">
41
- <Button fullWidth>Primary action</Button>
42
- <Button fullWidth variant="outlined">
43
- Secondary action
44
- </Button>
60
+ <ButtonGroup description="Lorem ipsum dolor sit amet, consectetur adipiscing elit.">
61
+ <Button colour="primary">Confirm</Button>
62
+ <Button colour="secondary">Cancel</Button>
45
63
  </ButtonGroup>
46
64
  </View>
47
65
  )
48
66
 
49
67
  export const Inline = () => (
50
68
  <View style={styles.column}>
51
- <ButtonGroup layout="inline" description="Two buttons side by side">
52
- <Button>Confirm</Button>
53
- <Button variant="outlined">Cancel</Button>
69
+ <ButtonGroup
70
+ layout="inline"
71
+ description="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
72
+ >
73
+ <Button colour="secondary">Cancel</Button>
74
+ <Button colour="primary">Confirm</Button>
54
75
  </ButtonGroup>
55
76
  </View>
56
77
  )
57
78
 
58
79
  export const SingleButton = () => (
59
80
  <View style={styles.column}>
60
- <ButtonGroup layout="stacked" description="Just one button">
61
- <Button fullWidth>Continue</Button>
81
+ <ButtonGroup description="Lorem ipsum dolor sit amet, consectetur adipiscing elit.">
82
+ <Button colour="primary">Continue</Button>
62
83
  </ButtonGroup>
63
84
  </View>
64
85
  )
65
86
 
66
87
  export const WithoutDescription = () => (
67
88
  <View style={styles.column}>
68
- <ButtonGroup layout="stacked">
69
- <Button fullWidth>Confirm</Button>
70
- <Button fullWidth variant="outlined">
71
- Cancel
72
- </Button>
89
+ <ButtonGroup>
90
+ <Button colour="primary">Confirm</Button>
91
+ <Button colour="secondary">Cancel</Button>
73
92
  </ButtonGroup>
74
93
  </View>
75
94
  )
@@ -0,0 +1,73 @@
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 { ButtonGroup } from "./ButtonGroup"
6
+
7
+ describe("ButtonGroup", () => {
8
+ describe("when component is rendering", () => {
9
+ it("renders children buttons", () => {
10
+ renderWithTheme(
11
+ <ButtonGroup>
12
+ <button>Confirm</button>
13
+ <button>Cancel</button>
14
+ </ButtonGroup>
15
+ )
16
+ expect(screen.getByText("Confirm")).toBeInTheDocument()
17
+ expect(screen.getByText("Cancel")).toBeInTheDocument()
18
+ })
19
+ })
20
+
21
+ describe("when rendering layouts", () => {
22
+ it("renders stacked layout by default", () => {
23
+ renderWithTheme(
24
+ <ButtonGroup>
25
+ <button>Action</button>
26
+ </ButtonGroup>
27
+ )
28
+ expect(screen.getByText("Action")).toBeInTheDocument()
29
+ })
30
+
31
+ it("renders inline layout", () => {
32
+ renderWithTheme(
33
+ <ButtonGroup layout="inline">
34
+ <button>Action</button>
35
+ </ButtonGroup>
36
+ )
37
+ expect(screen.getByText("Action")).toBeInTheDocument()
38
+ })
39
+ })
40
+
41
+ describe("when rendering with description", () => {
42
+ it("renders description text", () => {
43
+ renderWithTheme(
44
+ <ButtonGroup description="Choose an option">
45
+ <button>Yes</button>
46
+ <button>No</button>
47
+ </ButtonGroup>
48
+ )
49
+ expect(screen.getByText("Choose an option")).toBeInTheDocument()
50
+ })
51
+
52
+ it("does not render description when not provided", () => {
53
+ renderWithTheme(
54
+ <ButtonGroup>
55
+ <button>Yes</button>
56
+ </ButtonGroup>
57
+ )
58
+ expect(screen.queryByText("Choose an option")).not.toBeInTheDocument()
59
+ })
60
+ })
61
+
62
+ describe("when using with ref", () => {
63
+ it("forwards ref", () => {
64
+ const ref = React.createRef<any>()
65
+ renderWithTheme(
66
+ <ButtonGroup ref={ref}>
67
+ <button>Action</button>
68
+ </ButtonGroup>
69
+ )
70
+ expect(ref.current).toBeTruthy()
71
+ })
72
+ })
73
+ })
@@ -26,7 +26,7 @@ const StyledGroupRoot = styled(View)<{
26
26
  }))
27
27
 
28
28
  const StyledButtonRow = styled(View)<{
29
- rowDirection: "row-reverse" | "column"
29
+ rowDirection: "row" | "column"
30
30
  rowAlign?: "center"
31
31
  rowGap: number
32
32
  }>(({ rowDirection, rowAlign, rowGap }) => ({
@@ -36,6 +36,14 @@ const StyledButtonRow = styled(View)<{
36
36
  ...(rowAlign ? { alignItems: rowAlign } : {})
37
37
  }))
38
38
 
39
+ const StyledChildSlot = styled(View)<{ slotInline: boolean }>(
40
+ ({ slotInline }) => ({
41
+ ...(slotInline
42
+ ? { flex: 1, minWidth: 0 }
43
+ : { width: "100%", alignSelf: "stretch" })
44
+ })
45
+ )
46
+
39
47
  /**
40
48
  * ButtonGroup arranges 1 or 2 buttons in a stacked or inline layout
41
49
  * with an optional description below.
@@ -67,7 +75,7 @@ export const ButtonGroup = React.forwardRef<View, ButtonGroupProps>(
67
75
  {...rest}
68
76
  >
69
77
  <StyledButtonRow
70
- rowDirection={isInline ? "row-reverse" : "column"}
78
+ rowDirection={isInline ? "row" : "column"}
71
79
  rowAlign={isInline ? "center" : undefined}
72
80
  rowGap={parseTokenValue(
73
81
  isInline
@@ -75,7 +83,21 @@ export const ButtonGroup = React.forwardRef<View, ButtonGroupProps>(
75
83
  : buttonGroup.spacing.stacked.gap
76
84
  )}
77
85
  >
78
- {children}
86
+ {React.Children.map(children, (child, index) => {
87
+ if (!React.isValidElement(child)) return child
88
+ return (
89
+ <StyledChildSlot key={index} slotInline={isInline}>
90
+ {React.cloneElement(
91
+ child as React.ReactElement<{
92
+ fullWidth?: boolean
93
+ }>,
94
+ {
95
+ fullWidth: true
96
+ }
97
+ )}
98
+ </StyledChildSlot>
99
+ )
100
+ })}
79
101
  </StyledButtonRow>
80
102
  {description && (
81
103
  <Typography
@@ -1,10 +1,17 @@
1
1
  import React from "react"
2
2
  import { View, StyleSheet } from "react-native"
3
+ import {
4
+ LabradorChefsHat,
5
+ CockerSpanielChefsHat,
6
+ CockapooBowl
7
+ } from "@butternutbox/pawprint-illustrations/breeds"
3
8
  import { Checkbox } from "./Checkbox"
4
9
  import type { CheckboxProps } from "./Checkbox"
5
10
  import { CheckboxGroup } from "./CheckboxGroup"
6
11
  import { Typography } from "../../atoms/Typography"
7
12
 
13
+ const ILLUSTRATION_SIZE = 72
14
+
8
15
  export default {
9
16
  title: "Molecules/Checkbox",
10
17
  component: Checkbox,
@@ -109,6 +116,71 @@ export const Tile = () => (
109
116
  </View>
110
117
  )
111
118
 
119
+ export const WithIllustration = () => (
120
+ <View style={styles.column}>
121
+ <View style={styles.section}>
122
+ <Typography size="sm" weight="semiBold" color="tertiary">
123
+ Vertical
124
+ </Typography>
125
+ <CheckboxGroup orientation="vertical">
126
+ <Checkbox
127
+ variant="tile"
128
+ label="Chicken"
129
+ subText="Tender & tasty"
130
+ illustration={
131
+ <LabradorChefsHat
132
+ width={ILLUSTRATION_SIZE}
133
+ height={ILLUSTRATION_SIZE}
134
+ />
135
+ }
136
+ />
137
+ <Checkbox
138
+ variant="tile"
139
+ label="Treats"
140
+ subText="Tasty rewards"
141
+ illustration={
142
+ <CockerSpanielChefsHat
143
+ width={ILLUSTRATION_SIZE}
144
+ height={ILLUSTRATION_SIZE}
145
+ />
146
+ }
147
+ />
148
+ <Checkbox
149
+ variant="tile"
150
+ label="Dry Food"
151
+ subText="Crunchy kibble"
152
+ illustration={
153
+ <CockapooBowl
154
+ width={ILLUSTRATION_SIZE}
155
+ height={ILLUSTRATION_SIZE}
156
+ />
157
+ }
158
+ />
159
+ </CheckboxGroup>
160
+ </View>
161
+
162
+ <View style={styles.section}>
163
+ <Typography size="sm" weight="semiBold" color="tertiary">
164
+ Selected
165
+ </Typography>
166
+ <CheckboxGroup orientation="vertical">
167
+ <Checkbox
168
+ variant="tile"
169
+ label="Chicken"
170
+ subText="Tender & tasty"
171
+ defaultChecked
172
+ illustration={
173
+ <LabradorChefsHat
174
+ width={ILLUSTRATION_SIZE}
175
+ height={ILLUSTRATION_SIZE}
176
+ />
177
+ }
178
+ />
179
+ </CheckboxGroup>
180
+ </View>
181
+ </View>
182
+ )
183
+
112
184
  export const AllStates = () => (
113
185
  <View style={styles.column}>
114
186
  {(["standalone", "tile"] as const).map((variant) => (
@@ -0,0 +1,117 @@
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 { Checkbox } from "./Checkbox"
7
+
8
+ describe("Checkbox", () => {
9
+ describe("when component is rendering", () => {
10
+ it("renders label text", () => {
11
+ renderWithTheme(<Checkbox label="Accept terms" />)
12
+ expect(screen.getByText("Accept terms")).toBeInTheDocument()
13
+ })
14
+
15
+ it("renders label and subText", () => {
16
+ renderWithTheme(
17
+ <Checkbox label="Accept terms" subText="Required to proceed" />
18
+ )
19
+ expect(screen.getByText("Accept terms")).toBeInTheDocument()
20
+ expect(screen.getByText("Required to proceed")).toBeInTheDocument()
21
+ })
22
+ })
23
+
24
+ describe("when rendering variants", () => {
25
+ it("renders standalone variant", () => {
26
+ renderWithTheme(<Checkbox variant="standalone" label="Standalone" />)
27
+ expect(screen.getByText("Standalone")).toBeInTheDocument()
28
+ })
29
+
30
+ it("renders tile variant", () => {
31
+ renderWithTheme(<Checkbox variant="tile" label="Tile" />)
32
+ expect(screen.getByText("Tile")).toBeInTheDocument()
33
+ })
34
+
35
+ it("renders tile variant with illustration", () => {
36
+ renderWithTheme(
37
+ <Checkbox
38
+ variant="tile"
39
+ label="With illustration"
40
+ illustration={<span data-testid="illustration">🐶</span>}
41
+ />
42
+ )
43
+ expect(screen.getByTestId("illustration")).toBeInTheDocument()
44
+ })
45
+ })
46
+
47
+ describe("when component is uncontrolled", () => {
48
+ it("toggles checked state when clicked", async () => {
49
+ const user = userEvent.setup()
50
+ renderWithTheme(<Checkbox label="Toggle" />)
51
+
52
+ const checkbox = screen.getByRole("checkbox")
53
+ expect(checkbox).toHaveAttribute("aria-checked", "false")
54
+
55
+ await user.click(checkbox)
56
+ expect(checkbox).toHaveAttribute("aria-checked", "true")
57
+
58
+ await user.click(checkbox)
59
+ expect(checkbox).toHaveAttribute("aria-checked", "false")
60
+ })
61
+
62
+ it("can be initially checked via defaultChecked", () => {
63
+ renderWithTheme(<Checkbox label="Checked" defaultChecked />)
64
+
65
+ const checkbox = screen.getByRole("checkbox")
66
+ expect(checkbox).toHaveAttribute("aria-checked", "true")
67
+ })
68
+ })
69
+
70
+ describe("when component is controlled", () => {
71
+ it("calls onCheckedChange when clicked", async () => {
72
+ const user = userEvent.setup()
73
+ const onCheckedChange = vi.fn()
74
+ renderWithTheme(
75
+ <Checkbox
76
+ label="Controlled"
77
+ checked={false}
78
+ onCheckedChange={onCheckedChange}
79
+ />
80
+ )
81
+
82
+ await user.click(screen.getByRole("checkbox"))
83
+ expect(onCheckedChange).toHaveBeenCalledWith(true)
84
+ })
85
+
86
+ it("reflects controlled checked state", () => {
87
+ renderWithTheme(
88
+ <Checkbox label="Checked" checked={true} onCheckedChange={() => {}} />
89
+ )
90
+ expect(screen.getByRole("checkbox")).toHaveAttribute(
91
+ "aria-checked",
92
+ "true"
93
+ )
94
+ })
95
+ })
96
+
97
+ describe("when component is disabled", () => {
98
+ it("prevents interaction when disabled", async () => {
99
+ const user = userEvent.setup()
100
+ const onCheckedChange = vi.fn()
101
+ renderWithTheme(
102
+ <Checkbox label="Disabled" disabled onCheckedChange={onCheckedChange} />
103
+ )
104
+
105
+ await user.click(screen.getByRole("checkbox"))
106
+ expect(onCheckedChange).not.toHaveBeenCalled()
107
+ })
108
+ })
109
+
110
+ describe("when using with ref", () => {
111
+ it("forwards ref", () => {
112
+ const ref = React.createRef<any>()
113
+ renderWithTheme(<Checkbox ref={ref} label="Ref test" />)
114
+ expect(ref.current).toBeTruthy()
115
+ })
116
+ })
117
+ })