@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
|
@@ -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,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 (
|
|
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
|
+
})
|
|
@@ -42,7 +42,7 @@ export default {
|
|
|
42
42
|
argTypes: {
|
|
43
43
|
size: {
|
|
44
44
|
control: { type: "select" },
|
|
45
|
-
options: ["sm", "lg"] as IllustrationSize[],
|
|
45
|
+
options: ["sm", "lg", "xl"] as IllustrationSize[],
|
|
46
46
|
description: "Size variant (controls height)"
|
|
47
47
|
},
|
|
48
48
|
illustration: {
|
|
@@ -71,7 +71,7 @@ Default.args = {
|
|
|
71
71
|
size: "sm"
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
const allSizes: IllustrationSize[] = ["sm", "lg"]
|
|
74
|
+
const allSizes: IllustrationSize[] = ["sm", "lg", "xl"]
|
|
75
75
|
|
|
76
76
|
export const AllSizes = () => {
|
|
77
77
|
const sample = illustrationMap[defaultIllustration] as PawprintIllustration
|
|
@@ -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
|
+
})
|
|
@@ -3,7 +3,7 @@ import { View, ViewProps } from "react-native"
|
|
|
3
3
|
import styled from "@emotion/native"
|
|
4
4
|
import { useTheme } from "@emotion/react"
|
|
5
5
|
|
|
6
|
-
type IllustrationSize = "sm" | "lg"
|
|
6
|
+
type IllustrationSize = "sm" | "lg" | "xl"
|
|
7
7
|
|
|
8
8
|
type PawprintIllustration = React.ComponentType<{
|
|
9
9
|
width?: number
|
|
@@ -42,7 +42,7 @@ const StyledRoot = styled(View)<{
|
|
|
42
42
|
* ```
|
|
43
43
|
*
|
|
44
44
|
* @param illustration - **(required)** Illustration component
|
|
45
|
-
* @param size - *(optional)* Size variant: sm (default), lg
|
|
45
|
+
* @param size - *(optional)* Size variant: sm (default), lg, xl
|
|
46
46
|
* @param aria-label - *(optional)* Accessible label
|
|
47
47
|
*/
|
|
48
48
|
const Illustration = React.forwardRef<View, IllustrationProps>(
|
|
@@ -69,7 +69,7 @@ const Illustration = React.forwardRef<View, IllustrationProps>(
|
|
|
69
69
|
accessible={!!ariaLabel}
|
|
70
70
|
{...rest}
|
|
71
71
|
>
|
|
72
|
-
<IllustrationComponent height={dimension} />
|
|
72
|
+
<IllustrationComponent width={dimension} height={dimension} />
|
|
73
73
|
</StyledRoot>
|
|
74
74
|
)
|
|
75
75
|
}
|