@butternutbox/pawprint-native 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/CHANGELOG.md +16 -0
  3. package/COMPONENT_GUIDELINES.md +111 -4
  4. package/dist/index.cjs +12370 -1455
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +1110 -11
  7. package/dist/index.d.ts +1110 -11
  8. package/dist/index.js +12324 -1455
  9. package/dist/index.js.map +1 -1
  10. package/package.json +28 -9
  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.test.tsx +55 -0
  42. package/src/components/atoms/Input/Input.stories.tsx +129 -86
  43. package/src/components/atoms/Input/Input.test.tsx +306 -0
  44. package/src/components/atoms/Input/Input.tsx +9 -1
  45. package/src/components/atoms/Input/InputField.tsx +226 -74
  46. package/src/components/atoms/Link/Link.test.tsx +89 -0
  47. package/src/components/atoms/Logo/Logo.registry.ts +30 -5
  48. package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
  49. package/src/components/atoms/Logo/Logo.test.tsx +56 -0
  50. package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
  51. package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
  52. package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
  53. package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
  54. package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
  55. package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
  56. package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
  57. package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
  58. package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
  59. package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
  60. package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
  61. package/src/components/atoms/Logo/assets/index.ts +11 -0
  62. package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
  63. package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
  64. package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
  65. package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
  66. package/src/components/atoms/NumberInput/index.ts +4 -0
  67. package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
  68. package/src/components/atoms/Spinner/Spinner.tsx +14 -5
  69. package/src/components/atoms/Switch/Switch.test.tsx +92 -0
  70. package/src/components/atoms/Switch/Switch.tsx +16 -13
  71. package/src/components/atoms/Tag/Tag.test.tsx +70 -0
  72. package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
  73. package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
  74. package/src/components/atoms/TextArea/TextArea.tsx +171 -0
  75. package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
  76. package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
  77. package/src/components/atoms/TextArea/index.ts +6 -0
  78. package/src/components/atoms/Typography/Typography.test.tsx +94 -0
  79. package/src/components/atoms/index.ts +3 -0
  80. package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
  81. package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
  82. package/src/components/molecules/Accordion/Accordion.tsx +284 -0
  83. package/src/components/molecules/Accordion/index.ts +6 -0
  84. package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
  85. package/src/components/molecules/Animated/Animated.tsx +283 -0
  86. package/src/components/molecules/Animated/index.ts +10 -0
  87. package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
  88. package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +8 -14
  89. package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
  90. package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
  91. package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
  92. package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
  93. package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
  94. package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
  95. package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
  96. package/src/components/molecules/CopyField/CopyField.tsx +156 -0
  97. package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
  98. package/src/components/molecules/CopyField/hooks/index.ts +1 -0
  99. package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
  100. package/src/components/molecules/CopyField/index.ts +4 -0
  101. package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
  102. package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
  103. package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
  104. package/src/components/molecules/DatePicker/index.ts +2 -0
  105. package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
  106. package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
  107. package/src/components/molecules/Drawer/Drawer.tsx +187 -0
  108. package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
  109. package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
  110. package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
  111. package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
  112. package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
  113. package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
  114. package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
  115. package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
  116. package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
  117. package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
  118. package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
  119. package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
  120. package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
  121. package/src/components/molecules/Drawer/index.ts +12 -0
  122. package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
  123. package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
  124. package/src/components/molecules/FilterTab/index.ts +2 -0
  125. package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
  126. package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
  127. package/src/components/molecules/MessageCard/index.ts +10 -0
  128. package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
  129. package/src/components/molecules/Notification/Notification.tsx +426 -0
  130. package/src/components/molecules/Notification/index.ts +2 -0
  131. package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
  132. package/src/components/molecules/NumberField/NumberField.tsx +186 -0
  133. package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
  134. package/src/components/molecules/NumberField/index.ts +2 -0
  135. package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
  136. package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
  137. package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
  138. package/src/components/molecules/PasswordField/PasswordFieldError.tsx +52 -0
  139. package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
  140. package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +92 -0
  141. package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
  142. package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
  143. package/src/components/molecules/PasswordField/index.ts +10 -0
  144. package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +243 -0
  145. package/src/components/molecules/PictureSelector/PictureSelector.tsx +313 -0
  146. package/src/components/molecules/PictureSelector/index.ts +5 -0
  147. package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
  148. package/src/components/molecules/Progress/Progress.tsx +184 -0
  149. package/src/components/molecules/Progress/index.ts +2 -0
  150. package/src/components/molecules/Radio/Radio.test.tsx +104 -0
  151. package/src/components/molecules/Radio/Radio.tsx +1 -2
  152. package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
  153. package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
  154. package/src/components/molecules/SearchField/SearchField.tsx +143 -0
  155. package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
  156. package/src/components/molecules/SearchField/hooks/index.ts +1 -0
  157. package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
  158. package/src/components/molecules/SearchField/index.ts +4 -0
  159. package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
  160. package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
  161. package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
  162. package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
  163. package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
  164. package/src/components/molecules/SelectField/SelectField.tsx +236 -0
  165. package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
  166. package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
  167. package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
  168. package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
  169. package/src/components/molecules/SelectField/hooks/index.ts +2 -0
  170. package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
  171. package/src/components/molecules/SelectField/index.ts +10 -0
  172. package/src/components/molecules/Slider/Slider.test.tsx +102 -0
  173. package/src/components/molecules/Slider/Slider.tsx +293 -180
  174. package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
  175. package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
  176. package/src/components/molecules/Tooltip/index.ts +2 -0
  177. package/src/components/molecules/index.ts +15 -0
  178. package/src/test-utils.tsx +20 -0
  179. package/tsconfig.json +1 -1
  180. package/tsup.config.ts +16 -2
  181. package/vitest.config.ts +114 -0
  182. package/vitest.setup.ts +16 -0
@@ -0,0 +1,127 @@
1
+ import React from "react"
2
+ import { View, type ViewProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { useTheme } from "@emotion/react"
5
+
6
+ const parseTokenValue = (value: string): number => parseFloat(value)
7
+
8
+ // ─── Styled primitives ────────────────────────────────────────────────────────
9
+
10
+ const Container = styled(View)<{ showBg: boolean }>(({ theme, showBg }) => {
11
+ const { colour, sizing, borderRadius } =
12
+ theme.tokens.components.carouselControls.carouselControl
13
+ const spacing = theme.tokens.semantics.dimensions.spacing
14
+
15
+ return {
16
+ flexDirection: "row",
17
+ alignItems: "center",
18
+ justifyContent: "center",
19
+ gap: parseTokenValue(spacing.xs),
20
+ minHeight: parseTokenValue(sizing.default),
21
+ paddingHorizontal: parseTokenValue(spacing.xs),
22
+ paddingVertical: parseTokenValue(spacing["2xs"]),
23
+ borderRadius: parseTokenValue(borderRadius.default),
24
+ backgroundColor: showBg ? colour.background.default : "transparent"
25
+ }
26
+ })
27
+
28
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
29
+
30
+ // For 6–7 item carousels a 5-dot sliding window is shown — identical logic
31
+ // to the web implementation.
32
+ function getVisibleDots(count: number, activeIndex: number): number[] {
33
+ if (count <= 5) {
34
+ return Array.from({ length: count }, (_, i) => i)
35
+ }
36
+ const windowStart = Math.min(Math.max(activeIndex - 2, 0), count - 5)
37
+ return Array.from({ length: 5 }, (_, i) => windowStart + i)
38
+ }
39
+
40
+ // ─── Props ────────────────────────────────────────────────────────────────────
41
+
42
+ export type CarouselControlsProps = {
43
+ /**
44
+ * Total number of carousel items. Renders one dot per item (max 5 visible
45
+ * for 6–7 items via a sliding window).
46
+ */
47
+ count: number
48
+ /**
49
+ * Zero-based index of the currently active item.
50
+ */
51
+ activeIndex: number
52
+ /**
53
+ * Show a white pill background behind the dots. Defaults to `true`.
54
+ *
55
+ * Set to `false` when placing controls on a light surface where the
56
+ * background pill would clash with the page background.
57
+ */
58
+ showBackground?: boolean
59
+ } & ViewProps
60
+
61
+ // ─── Component ────────────────────────────────────────────────────────────────
62
+
63
+ /**
64
+ * Carousel position indicator — a row of dots where the active dot is
65
+ * highlighted and inactive dots are shown at reduced opacity.
66
+ *
67
+ * For 6–7 item carousels a 5-dot sliding window is used with diminishing
68
+ * dot sizes to indicate proximity to the active item.
69
+ *
70
+ * @example
71
+ * ```tsx
72
+ * <CarouselControls count={4} activeIndex={currentIndex} />
73
+ * <CarouselControls count={4} activeIndex={currentIndex} showBackground={false} />
74
+ * ```
75
+ */
76
+ export const CarouselControls = React.forwardRef<View, CarouselControlsProps>(
77
+ ({ count, activeIndex, showBackground = true, ...props }, ref) => {
78
+ const theme = useTheme()
79
+ const { colour, sizing, slot } =
80
+ theme.tokens.components.carouselControls.carouselControl
81
+ const { opacity } = theme.tokens.primitives
82
+
83
+ const visibleDots = getVisibleDots(count, activeIndex)
84
+
85
+ function getDotSize(dotIndex: number): number {
86
+ if (dotIndex === activeIndex || count <= 5) {
87
+ return parseTokenValue(sizing.slot.default)
88
+ }
89
+ const distance = Math.abs(dotIndex - activeIndex)
90
+ if (distance === 1) return parseTokenValue(sizing.slot.inactiveMedium)
91
+ return parseTokenValue(sizing.slot.inactiveSmall)
92
+ }
93
+
94
+ return (
95
+ <Container
96
+ ref={ref}
97
+ showBg={showBackground}
98
+ accessibilityRole="image"
99
+ accessibilityLabel={`Slide ${activeIndex + 1} of ${count}`}
100
+ {...props}
101
+ >
102
+ {visibleDots.map((dotIndex) => {
103
+ const isActive = dotIndex === activeIndex
104
+ const size = getDotSize(dotIndex)
105
+
106
+ return (
107
+ <View
108
+ key={dotIndex}
109
+ accessible={false}
110
+ style={{
111
+ width: size,
112
+ height: size,
113
+ borderRadius: parseTokenValue(slot.borderRadius.default),
114
+ backgroundColor: isActive
115
+ ? colour.slot.active
116
+ : colour.slot.disabled,
117
+ opacity: isActive ? 1 : parseTokenValue(opacity["40"])
118
+ }}
119
+ />
120
+ )
121
+ })}
122
+ </Container>
123
+ )
124
+ }
125
+ )
126
+
127
+ CarouselControls.displayName = "CarouselControls"
@@ -0,0 +1,2 @@
1
+ export { CarouselControls } from "./CarouselControls"
2
+ export type { CarouselControlsProps } from "./CarouselControls"
@@ -0,0 +1,36 @@
1
+ import React from "react"
2
+ import { describe, it, expect } from "vitest"
3
+ import { renderWithTheme } from "../../../test-utils"
4
+ import { Hint } from "./Hint"
5
+
6
+ describe("Hint", () => {
7
+ describe("when component is rendering", () => {
8
+ it("renders without crashing", () => {
9
+ const { container } = renderWithTheme(<Hint />)
10
+ expect(container.firstChild).toBeTruthy()
11
+ })
12
+
13
+ it("renders with default variant", () => {
14
+ const { container } = renderWithTheme(<Hint />)
15
+ expect(container.firstChild).toBeTruthy()
16
+ })
17
+ })
18
+
19
+ describe("when rendering all variants", () => {
20
+ it.each(["default", "success", "warning", "error"] as const)(
21
+ "renders %s variant",
22
+ (variant) => {
23
+ const { container } = renderWithTheme(<Hint variant={variant} />)
24
+ expect(container.firstChild).toBeTruthy()
25
+ }
26
+ )
27
+ })
28
+
29
+ describe("when using with ref", () => {
30
+ it("forwards ref", () => {
31
+ const ref = React.createRef<any>()
32
+ renderWithTheme(<Hint ref={ref} />)
33
+ expect(ref.current).toBeTruthy()
34
+ })
35
+ })
36
+ })
@@ -0,0 +1,98 @@
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 { Icon } from "./Icon"
6
+
7
+ const MockIcon = ({ width, height, color }: any) => (
8
+ <svg data-testid="svg-icon" width={width} height={height} fill={color} />
9
+ )
10
+ MockIcon.category = "core" as const
11
+
12
+ const MockMarketingIcon = ({ width, height, color }: any) => (
13
+ <svg
14
+ data-testid="svg-icon-marketing"
15
+ width={width}
16
+ height={height}
17
+ fill={color}
18
+ />
19
+ )
20
+ MockMarketingIcon.category = "marketing" as const
21
+
22
+ describe("Icon", () => {
23
+ describe("when component is rendering", () => {
24
+ it("renders the icon component", () => {
25
+ renderWithTheme(<Icon icon={MockIcon} />)
26
+ expect(screen.getByTestId("svg-icon")).toBeInTheDocument()
27
+ })
28
+
29
+ it("renders with default size and colour", () => {
30
+ renderWithTheme(<Icon icon={MockIcon} />)
31
+ expect(screen.getByTestId("svg-icon")).toBeInTheDocument()
32
+ })
33
+ })
34
+
35
+ describe("when rendering all sizes", () => {
36
+ it.each(["xs", "sm", "md", "lg", "xl"] as const)(
37
+ "renders %s size (core)",
38
+ (size) => {
39
+ renderWithTheme(<Icon icon={MockIcon} size={size} />)
40
+ expect(screen.getByTestId("svg-icon")).toBeInTheDocument()
41
+ }
42
+ )
43
+
44
+ it("renders 2xl size (marketing)", () => {
45
+ renderWithTheme(<Icon icon={MockMarketingIcon} size="2xl" />)
46
+ expect(screen.getByTestId("svg-icon-marketing")).toBeInTheDocument()
47
+ })
48
+ })
49
+
50
+ describe("when rendering all colours", () => {
51
+ it.each([
52
+ "primary",
53
+ "secondary",
54
+ "disabled",
55
+ "success",
56
+ "warning",
57
+ "error",
58
+ "promo",
59
+ "info",
60
+ "alt",
61
+ "action-default",
62
+ "action-inverse"
63
+ ] as const)("renders %s colour", (colour) => {
64
+ renderWithTheme(<Icon icon={MockIcon} colour={colour} />)
65
+ expect(screen.getByTestId("svg-icon")).toBeInTheDocument()
66
+ })
67
+ })
68
+
69
+ describe("when customColour is provided", () => {
70
+ it("overrides the semantic colour variant", () => {
71
+ renderWithTheme(
72
+ <Icon icon={MockIcon} colour="primary" customColour="#123456" />
73
+ )
74
+ expect(screen.getByTestId("svg-icon")).toHaveAttribute("fill", "#123456")
75
+ })
76
+ })
77
+
78
+ describe("accessibility", () => {
79
+ it("sets image role and label when aria-label is provided", () => {
80
+ renderWithTheme(<Icon icon={MockIcon} aria-label="Go forward" />)
81
+ expect(screen.getByRole("image")).toBeInTheDocument()
82
+ expect(screen.getByLabelText("Go forward")).toBeInTheDocument()
83
+ })
84
+
85
+ it("does not set role when aria-label is not provided", () => {
86
+ renderWithTheme(<Icon icon={MockIcon} />)
87
+ expect(screen.queryByRole("image")).not.toBeInTheDocument()
88
+ })
89
+ })
90
+
91
+ describe("when using with ref", () => {
92
+ it("forwards ref", () => {
93
+ const ref = React.createRef<any>()
94
+ renderWithTheme(<Icon ref={ref} icon={MockIcon} />)
95
+ expect(ref.current).toBeTruthy()
96
+ })
97
+ })
98
+ })
@@ -30,6 +30,7 @@ type IconOwnProps = {
30
30
  icon: PawprintIcon
31
31
  size?: IconSize
32
32
  colour?: IconColour
33
+ customColour?: string
33
34
  "aria-label"?: string
34
35
  }
35
36
 
@@ -67,6 +68,7 @@ const Icon = React.forwardRef<View, IconProps>(
67
68
  (
68
69
  {
69
70
  icon: IconComponent,
71
+ customColour,
70
72
  size = "md",
71
73
  colour = "primary",
72
74
  "aria-label": ariaLabel,
@@ -79,7 +81,9 @@ const Icon = React.forwardRef<View, IconProps>(
79
81
 
80
82
  const iconTokens = theme.tokens.semantics.colour.icon
81
83
  let color: string
82
- if (colour === "action-default") {
84
+ if (customColour) {
85
+ color = customColour
86
+ } else if (colour === "action-default") {
83
87
  color = iconTokens.action.default
84
88
  } else if (colour === "action-inverse") {
85
89
  color = iconTokens.action.inverse
@@ -0,0 +1,101 @@
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 { IconButton } from "./IconButton"
7
+
8
+ const MockIcon = ({ width, height, color }: any) => (
9
+ <svg data-testid="svg-icon" width={width} height={height} fill={color} />
10
+ )
11
+ MockIcon.category = "core" as const
12
+
13
+ describe("IconButton", () => {
14
+ describe("when component is rendering", () => {
15
+ it("renders the icon", () => {
16
+ renderWithTheme(<IconButton icon={MockIcon} aria-label="Add item" />)
17
+ expect(screen.getByTestId("svg-icon")).toBeInTheDocument()
18
+ })
19
+ })
20
+
21
+ describe("when rendering variants", () => {
22
+ it.each(["filled", "outlined", "text"] as const)(
23
+ "renders %s variant",
24
+ (variant) => {
25
+ renderWithTheme(
26
+ <IconButton icon={MockIcon} variant={variant} aria-label="Action" />
27
+ )
28
+ expect(screen.getByTestId("svg-icon")).toBeInTheDocument()
29
+ }
30
+ )
31
+ })
32
+
33
+ describe("when rendering sizes", () => {
34
+ it.each(["sm", "md", "lg"] as const)("renders %s size", (size) => {
35
+ renderWithTheme(
36
+ <IconButton icon={MockIcon} size={size} aria-label="Action" />
37
+ )
38
+ expect(screen.getByTestId("svg-icon")).toBeInTheDocument()
39
+ })
40
+ })
41
+
42
+ describe("when component is interactive", () => {
43
+ it("calls onPress when pressed", async () => {
44
+ const user = userEvent.setup()
45
+ const onPress = vi.fn()
46
+ renderWithTheme(
47
+ <IconButton icon={MockIcon} aria-label="Add" onPress={onPress} />
48
+ )
49
+
50
+ await user.click(screen.getByLabelText("Add"))
51
+ expect(onPress).toHaveBeenCalledTimes(1)
52
+ })
53
+
54
+ it("does not call onPress when disabled", async () => {
55
+ const user = userEvent.setup()
56
+ const onPress = vi.fn()
57
+ renderWithTheme(
58
+ <IconButton
59
+ icon={MockIcon}
60
+ aria-label="Add"
61
+ onPress={onPress}
62
+ disabled
63
+ />
64
+ )
65
+
66
+ await user.click(screen.getByLabelText("Add"))
67
+ expect(onPress).not.toHaveBeenCalled()
68
+ })
69
+ })
70
+
71
+ describe("when loading", () => {
72
+ it("disables button when loading", async () => {
73
+ const user = userEvent.setup()
74
+ const onPress = vi.fn()
75
+ renderWithTheme(
76
+ <IconButton
77
+ icon={MockIcon}
78
+ aria-label="Add"
79
+ onPress={onPress}
80
+ loading
81
+ />
82
+ )
83
+
84
+ await user.click(screen.getByLabelText("Add"))
85
+ expect(onPress).not.toHaveBeenCalled()
86
+ })
87
+ })
88
+
89
+ describe("accessibility", () => {
90
+ it("has aria-label", () => {
91
+ renderWithTheme(<IconButton icon={MockIcon} aria-label="Delete item" />)
92
+ expect(screen.getByLabelText("Delete item")).toBeInTheDocument()
93
+ })
94
+
95
+ it("sets disabled accessibility state", () => {
96
+ renderWithTheme(<IconButton icon={MockIcon} aria-label="Add" disabled />)
97
+ const button = screen.getByLabelText("Add")
98
+ expect(button).toBeDisabled()
99
+ })
100
+ })
101
+ })
@@ -0,0 +1,55 @@
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 { Illustration } from "./Illustration"
6
+
7
+ const MockIllustration = ({ height }: { width?: number; height?: number }) => (
8
+ <svg data-testid="illustration-svg" height={height} />
9
+ )
10
+
11
+ describe("Illustration", () => {
12
+ describe("when component is rendering", () => {
13
+ it("renders the illustration", () => {
14
+ renderWithTheme(<Illustration illustration={MockIllustration} />)
15
+ expect(screen.getByTestId("illustration-svg")).toBeInTheDocument()
16
+ })
17
+ })
18
+
19
+ describe("when rendering sizes", () => {
20
+ it.each(["sm", "lg"] as const)("renders %s size", (size) => {
21
+ renderWithTheme(
22
+ <Illustration illustration={MockIllustration} size={size} />
23
+ )
24
+ expect(screen.getByTestId("illustration-svg")).toBeInTheDocument()
25
+ })
26
+ })
27
+
28
+ describe("accessibility", () => {
29
+ it("sets image role when aria-label is provided", () => {
30
+ renderWithTheme(
31
+ <Illustration
32
+ illustration={MockIllustration}
33
+ aria-label="Dog sitting"
34
+ />
35
+ )
36
+ expect(screen.getByRole("image")).toBeInTheDocument()
37
+ expect(screen.getByLabelText("Dog sitting")).toBeInTheDocument()
38
+ })
39
+
40
+ it("does not set role when aria-label is not provided", () => {
41
+ renderWithTheme(<Illustration illustration={MockIllustration} />)
42
+ expect(screen.queryByRole("image")).not.toBeInTheDocument()
43
+ })
44
+ })
45
+
46
+ describe("when using with ref", () => {
47
+ it("forwards ref", () => {
48
+ const ref = React.createRef<any>()
49
+ renderWithTheme(
50
+ <Illustration ref={ref} illustration={MockIllustration} />
51
+ )
52
+ expect(ref.current).toBeTruthy()
53
+ })
54
+ })
55
+ })