@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,285 @@
1
+ import React, { useState } from "react"
2
+ import { StyleSheet, View } from "react-native"
3
+ import { Button } from "../../atoms/Button"
4
+ import { Typography } from "../../atoms/Typography"
5
+ import { Drawer } from "./Drawer"
6
+ import type { DrawerProps } from "./Drawer"
7
+
8
+ export default {
9
+ title: "Molecules/Drawer",
10
+ component: Drawer,
11
+ parameters: {
12
+ docs: {
13
+ description: {
14
+ component:
15
+ "A bottom-sheet drawer with drag-to-dismiss and three header variants."
16
+ }
17
+ }
18
+ },
19
+ argTypes: {
20
+ defaultOpen: {
21
+ control: { type: "boolean" },
22
+ description: "Open on mount (uncontrolled)"
23
+ }
24
+ }
25
+ }
26
+
27
+ const bodyText =
28
+ "Fresh, nutritious meals tailored to your dog's unique needs — delivered straight to your door."
29
+
30
+ export const Default = () => (
31
+ <View style={styles.container}>
32
+ <Drawer.Root>
33
+ <Drawer.Trigger>
34
+ <Button variant="filled" colour="primary">
35
+ Open drawer
36
+ </Button>
37
+ </Drawer.Trigger>
38
+ <Drawer.Portal>
39
+ <Drawer.Overlay />
40
+ <Drawer.Content>
41
+ <Drawer.Header variant="titleAndText">
42
+ <Drawer.Title>Your plan</Drawer.Title>
43
+ <Drawer.Description>Tailored just for your dog</Drawer.Description>
44
+ <Drawer.Close />
45
+ </Drawer.Header>
46
+ <Drawer.Body>
47
+ <Typography>{bodyText}</Typography>
48
+ </Drawer.Body>
49
+ <Drawer.Footer>
50
+ <Button variant="filled" colour="primary">
51
+ Get started
52
+ </Button>
53
+ </Drawer.Footer>
54
+ </Drawer.Content>
55
+ </Drawer.Portal>
56
+ </Drawer.Root>
57
+ </View>
58
+ )
59
+
60
+ export const ImageHeader = () => (
61
+ <View style={styles.container}>
62
+ <Drawer.Root>
63
+ <Drawer.Trigger>
64
+ <Button variant="filled" colour="primary">
65
+ Open image drawer
66
+ </Button>
67
+ </Drawer.Trigger>
68
+ <Drawer.Portal>
69
+ <Drawer.Overlay />
70
+ <Drawer.Content>
71
+ <Drawer.Header
72
+ variant="image"
73
+ imageSource={{
74
+ uri: "https://images.unsplash.com/photo-1587300003388-59208cc962cb?w=600"
75
+ }}
76
+ >
77
+ <Drawer.Close />
78
+ </Drawer.Header>
79
+ <Drawer.Body>
80
+ <Drawer.Title>Meet our recipes</Drawer.Title>
81
+ <Typography>{bodyText}</Typography>
82
+ </Drawer.Body>
83
+ <Drawer.Footer>
84
+ <Button variant="filled" colour="primary">
85
+ Explore recipes
86
+ </Button>
87
+ </Drawer.Footer>
88
+ </Drawer.Content>
89
+ </Drawer.Portal>
90
+ </Drawer.Root>
91
+ </View>
92
+ )
93
+
94
+ export const FullBleedHeader = () => (
95
+ <View style={styles.container}>
96
+ <Drawer.Root>
97
+ <Drawer.Trigger>
98
+ <Button variant="filled" colour="primary">
99
+ Open full bleed drawer
100
+ </Button>
101
+ </Drawer.Trigger>
102
+ <Drawer.Portal>
103
+ <Drawer.Overlay />
104
+ <Drawer.Content>
105
+ <Drawer.Header variant="fullBleed">
106
+ <Drawer.Title>Special offer</Drawer.Title>
107
+ <Drawer.Description>For new customers only</Drawer.Description>
108
+ <Drawer.Close />
109
+ </Drawer.Header>
110
+ <Drawer.Body>
111
+ <Typography>{bodyText}</Typography>
112
+ </Drawer.Body>
113
+ <Drawer.Footer>
114
+ <Button variant="filled" colour="primary">
115
+ Claim offer
116
+ </Button>
117
+ </Drawer.Footer>
118
+ </Drawer.Content>
119
+ </Drawer.Portal>
120
+ </Drawer.Root>
121
+ </View>
122
+ )
123
+
124
+ export const WithoutFooter = () => (
125
+ <View style={styles.container}>
126
+ <Drawer.Root>
127
+ <Drawer.Trigger>
128
+ <Button variant="filled" colour="primary">
129
+ Open drawer
130
+ </Button>
131
+ </Drawer.Trigger>
132
+ <Drawer.Portal>
133
+ <Drawer.Overlay />
134
+ <Drawer.Content>
135
+ <Drawer.Header variant="titleAndText">
136
+ <Drawer.Title>Information</Drawer.Title>
137
+ <Drawer.Close />
138
+ </Drawer.Header>
139
+ <Drawer.Body>
140
+ <Typography>{bodyText}</Typography>
141
+ </Drawer.Body>
142
+ </Drawer.Content>
143
+ </Drawer.Portal>
144
+ </Drawer.Root>
145
+ </View>
146
+ )
147
+
148
+ export const WithoutHeader = () => (
149
+ <View style={styles.container}>
150
+ <Drawer.Root>
151
+ <Drawer.Trigger>
152
+ <Button variant="filled" colour="primary">
153
+ Open drawer
154
+ </Button>
155
+ </Drawer.Trigger>
156
+ <Drawer.Portal>
157
+ <Drawer.Overlay />
158
+ <Drawer.Content>
159
+ <Drawer.Grabber />
160
+ <Drawer.Close />
161
+ <Drawer.Body>
162
+ <Typography>{bodyText}</Typography>
163
+ </Drawer.Body>
164
+ <Drawer.Footer>
165
+ <Button variant="filled" colour="primary">
166
+ Continue
167
+ </Button>
168
+ </Drawer.Footer>
169
+ </Drawer.Content>
170
+ </Drawer.Portal>
171
+ </Drawer.Root>
172
+ </View>
173
+ )
174
+
175
+ export const LongContent = () => (
176
+ <View style={styles.container}>
177
+ <Drawer.Root>
178
+ <Drawer.Trigger>
179
+ <Button variant="filled" colour="primary">
180
+ Open long drawer
181
+ </Button>
182
+ </Drawer.Trigger>
183
+ <Drawer.Portal>
184
+ <Drawer.Overlay />
185
+ <Drawer.Content>
186
+ <Drawer.Header variant="titleAndText">
187
+ <Drawer.Title>Frequently asked questions</Drawer.Title>
188
+ <Drawer.Close />
189
+ </Drawer.Header>
190
+ <Drawer.Body>
191
+ {Array.from({ length: 14 }, (_, i) => (
192
+ <Typography
193
+ key={i}
194
+ >{`Question ${i + 1}: ${bodyText}`}</Typography>
195
+ ))}
196
+ </Drawer.Body>
197
+ <Drawer.Footer>
198
+ <Button variant="filled" colour="primary">
199
+ Got it
200
+ </Button>
201
+ </Drawer.Footer>
202
+ </Drawer.Content>
203
+ </Drawer.Portal>
204
+ </Drawer.Root>
205
+ </View>
206
+ )
207
+
208
+ export const Controlled = () => {
209
+ const [open, setOpen] = useState(false)
210
+ return (
211
+ <View style={styles.container}>
212
+ <View style={styles.row}>
213
+ <Button variant="filled" colour="primary" onPress={() => setOpen(true)}>
214
+ Open
215
+ </Button>
216
+ <Button
217
+ variant="outlined"
218
+ colour="secondary"
219
+ onPress={() => setOpen(false)}
220
+ >
221
+ Close externally
222
+ </Button>
223
+ </View>
224
+ <Drawer.Root open={open} onOpenChange={setOpen}>
225
+ <Drawer.Portal>
226
+ <Drawer.Overlay />
227
+ <Drawer.Content>
228
+ <Drawer.Header variant="titleAndText">
229
+ <Drawer.Title>Controlled</Drawer.Title>
230
+ <Drawer.Close />
231
+ </Drawer.Header>
232
+ <Drawer.Body>
233
+ <Typography>Open state is managed externally.</Typography>
234
+ </Drawer.Body>
235
+ </Drawer.Content>
236
+ </Drawer.Portal>
237
+ </Drawer.Root>
238
+ </View>
239
+ )
240
+ }
241
+
242
+ export const Playground = (args: DrawerProps) => (
243
+ <View style={styles.container}>
244
+ <Drawer.Root {...args}>
245
+ <Drawer.Trigger>
246
+ <Button variant="filled" colour="primary">
247
+ Open drawer
248
+ </Button>
249
+ </Drawer.Trigger>
250
+ <Drawer.Portal>
251
+ <Drawer.Overlay />
252
+ <Drawer.Content>
253
+ <Drawer.Header variant="titleAndText">
254
+ <Drawer.Title>Drawer title</Drawer.Title>
255
+ <Drawer.Description>Drawer description</Drawer.Description>
256
+ <Drawer.Close />
257
+ </Drawer.Header>
258
+ <Drawer.Body>
259
+ <Typography>{bodyText}</Typography>
260
+ </Drawer.Body>
261
+ <Drawer.Footer>
262
+ <Button variant="filled" colour="primary">
263
+ Action
264
+ </Button>
265
+ </Drawer.Footer>
266
+ </Drawer.Content>
267
+ </Drawer.Portal>
268
+ </Drawer.Root>
269
+ </View>
270
+ )
271
+
272
+ Playground.args = {
273
+ defaultOpen: false
274
+ }
275
+
276
+ const styles = StyleSheet.create({
277
+ container: {
278
+ padding: 16
279
+ },
280
+ row: {
281
+ flexDirection: "row",
282
+ gap: 12,
283
+ marginBottom: 16
284
+ }
285
+ })
@@ -0,0 +1,180 @@
1
+ import React from "react"
2
+ import { screen, fireEvent } from "@testing-library/react"
3
+ import { describe, it, expect, vi } from "vitest"
4
+ import { renderWithTheme } from "../../../test-utils"
5
+ import { Drawer } from "./Drawer"
6
+
7
+ const BasicDrawer = ({
8
+ open,
9
+ onOpenChange
10
+ }: {
11
+ open?: boolean
12
+ onOpenChange?: (v: boolean) => void
13
+ }) => (
14
+ <Drawer.Root open={open} onOpenChange={onOpenChange}>
15
+ <Drawer.Trigger>
16
+ <button>Open</button>
17
+ </Drawer.Trigger>
18
+ <Drawer.Portal>
19
+ <Drawer.Overlay />
20
+ <Drawer.Content>
21
+ <Drawer.Header variant="titleAndText">
22
+ <Drawer.Title>Drawer title</Drawer.Title>
23
+ <Drawer.Description>Drawer description</Drawer.Description>
24
+ <Drawer.Close />
25
+ </Drawer.Header>
26
+ <Drawer.Body>
27
+ <p>Body content</p>
28
+ </Drawer.Body>
29
+ <Drawer.Footer>
30
+ <p>Footer content</p>
31
+ </Drawer.Footer>
32
+ </Drawer.Content>
33
+ </Drawer.Portal>
34
+ </Drawer.Root>
35
+ )
36
+
37
+ describe("Drawer", () => {
38
+ describe("rendering", () => {
39
+ it("renders the trigger", () => {
40
+ renderWithTheme(<BasicDrawer />)
41
+ expect(screen.getByText("Open")).toBeInTheDocument()
42
+ })
43
+
44
+ it("does not render drawer content when closed", () => {
45
+ renderWithTheme(<BasicDrawer />)
46
+ expect(screen.queryByText("Drawer title")).not.toBeInTheDocument()
47
+ })
48
+
49
+ it("renders all subcomponents when open", () => {
50
+ renderWithTheme(<BasicDrawer open />)
51
+ expect(screen.getByText("Drawer title")).toBeInTheDocument()
52
+ expect(screen.getByText("Drawer description")).toBeInTheDocument()
53
+ expect(screen.getByText("Body content")).toBeInTheDocument()
54
+ expect(screen.getByText("Footer content")).toBeInTheDocument()
55
+ })
56
+ })
57
+
58
+ describe("open / close", () => {
59
+ it("opens when trigger is pressed", () => {
60
+ renderWithTheme(<BasicDrawer />)
61
+ fireEvent.click(screen.getByText("Open"))
62
+ expect(screen.getByText("Drawer title")).toBeInTheDocument()
63
+ })
64
+
65
+ it("calls onOpenChange(true) when trigger is pressed (controlled)", () => {
66
+ const onOpenChange = vi.fn()
67
+ renderWithTheme(<BasicDrawer open={false} onOpenChange={onOpenChange} />)
68
+ fireEvent.click(screen.getByText("Open"))
69
+ expect(onOpenChange).toHaveBeenCalledWith(true)
70
+ })
71
+
72
+ it("calls onOpenChange(false) when close button is pressed", () => {
73
+ const onOpenChange = vi.fn()
74
+ renderWithTheme(<BasicDrawer open onOpenChange={onOpenChange} />)
75
+ fireEvent.click(screen.getByLabelText("Close"))
76
+ expect(onOpenChange).toHaveBeenCalledWith(false)
77
+ })
78
+ })
79
+
80
+ describe("controlled mode", () => {
81
+ it("respects controlled open=true", () => {
82
+ renderWithTheme(<BasicDrawer open />)
83
+ expect(screen.getByText("Drawer title")).toBeInTheDocument()
84
+ })
85
+
86
+ it("respects controlled open=false", () => {
87
+ renderWithTheme(<BasicDrawer open={false} />)
88
+ expect(screen.queryByText("Drawer title")).not.toBeInTheDocument()
89
+ })
90
+ })
91
+
92
+ describe("header variants", () => {
93
+ it("renders titleAndText variant", () => {
94
+ renderWithTheme(
95
+ <Drawer.Root open>
96
+ <Drawer.Portal>
97
+ <Drawer.Content>
98
+ <Drawer.Header variant="titleAndText">
99
+ <Drawer.Title>Title</Drawer.Title>
100
+ <Drawer.Close />
101
+ </Drawer.Header>
102
+ </Drawer.Content>
103
+ </Drawer.Portal>
104
+ </Drawer.Root>
105
+ )
106
+ expect(screen.getByText("Title")).toBeInTheDocument()
107
+ })
108
+
109
+ it("renders fullBleed variant", () => {
110
+ renderWithTheme(
111
+ <Drawer.Root open>
112
+ <Drawer.Portal>
113
+ <Drawer.Content>
114
+ <Drawer.Header variant="fullBleed">
115
+ <Drawer.Title>Full bleed</Drawer.Title>
116
+ <Drawer.Close />
117
+ </Drawer.Header>
118
+ </Drawer.Content>
119
+ </Drawer.Portal>
120
+ </Drawer.Root>
121
+ )
122
+ expect(screen.getByText("Full bleed")).toBeInTheDocument()
123
+ })
124
+
125
+ it("renders image variant", () => {
126
+ renderWithTheme(
127
+ <Drawer.Root open>
128
+ <Drawer.Portal>
129
+ <Drawer.Content>
130
+ <Drawer.Header variant="image">
131
+ <Drawer.Close />
132
+ </Drawer.Header>
133
+ </Drawer.Content>
134
+ </Drawer.Portal>
135
+ </Drawer.Root>
136
+ )
137
+ expect(screen.getByLabelText("Close")).toBeInTheDocument()
138
+ })
139
+ })
140
+
141
+ describe("without header", () => {
142
+ it("renders grabber and floating close when no header is used", () => {
143
+ renderWithTheme(
144
+ <Drawer.Root open>
145
+ <Drawer.Portal>
146
+ <Drawer.Content>
147
+ <Drawer.Grabber />
148
+ <Drawer.Close />
149
+ <Drawer.Body>
150
+ <p>No header</p>
151
+ </Drawer.Body>
152
+ </Drawer.Content>
153
+ </Drawer.Portal>
154
+ </Drawer.Root>
155
+ )
156
+ expect(screen.getByText("No header")).toBeInTheDocument()
157
+ expect(screen.getByLabelText("Close")).toBeInTheDocument()
158
+ })
159
+ })
160
+
161
+ describe("accessibility", () => {
162
+ it("close button has accessible label", () => {
163
+ renderWithTheme(<BasicDrawer open />)
164
+ expect(screen.getByLabelText("Close")).toBeInTheDocument()
165
+ })
166
+
167
+ it("supports custom close aria-label", () => {
168
+ renderWithTheme(
169
+ <Drawer.Root open>
170
+ <Drawer.Portal>
171
+ <Drawer.Content>
172
+ <Drawer.Close aria-label="Dismiss drawer" />
173
+ </Drawer.Content>
174
+ </Drawer.Portal>
175
+ </Drawer.Root>
176
+ )
177
+ expect(screen.getByLabelText("Dismiss drawer")).toBeInTheDocument()
178
+ })
179
+ })
180
+ })
@@ -0,0 +1,187 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
+ import { Modal, StyleSheet, View, type ViewProps } from "react-native"
3
+ import { Slot } from "@rn-primitives/slot"
4
+ import { DrawerContext } from "./DrawerContext"
5
+ import { DrawerOverlay } from "./DrawerOverlay"
6
+ import { DrawerContent } from "./DrawerContent"
7
+ import { DrawerGrabber } from "./DrawerGrabber"
8
+ import { DrawerHeader } from "./DrawerHeader"
9
+ import { DrawerTitle } from "./DrawerTitle"
10
+ import { DrawerDescription } from "./DrawerDescription"
11
+ import { DrawerClose } from "./DrawerClose"
12
+ import { DrawerBody } from "./DrawerBody"
13
+ import { DrawerFooter } from "./DrawerFooter"
14
+
15
+ // ─── Root ─────────────────────────────────────────────────────────────────────
16
+
17
+ type DrawerRootOwnProps = {
18
+ open?: boolean
19
+ defaultOpen?: boolean
20
+ onOpenChange?: (open: boolean) => void
21
+ children: React.ReactNode
22
+ }
23
+
24
+ export type DrawerProps = DrawerRootOwnProps &
25
+ Omit<ViewProps, keyof DrawerRootOwnProps>
26
+
27
+ /**
28
+ * Root component for the native Drawer. Manages open/close state and provides
29
+ * context to all child components.
30
+ *
31
+ * Supports both controlled (`open` + `onOpenChange`) and uncontrolled
32
+ * (`defaultOpen`) usage.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * <Drawer.Root>
37
+ * <Drawer.Trigger>
38
+ * <Button>Open</Button>
39
+ * </Drawer.Trigger>
40
+ * <Drawer.Portal>
41
+ * <Drawer.Overlay />
42
+ * <Drawer.Content>
43
+ * <Drawer.Header variant="titleAndText">
44
+ * <Drawer.Title>Title</Drawer.Title>
45
+ * <Drawer.Close />
46
+ * </Drawer.Header>
47
+ * <Drawer.Body>Content</Drawer.Body>
48
+ * <Drawer.Footer>
49
+ * <Button>Action</Button>
50
+ * </Drawer.Footer>
51
+ * </Drawer.Content>
52
+ * </Drawer.Portal>
53
+ * </Drawer.Root>
54
+ * ```
55
+ */
56
+ const DrawerRoot = React.forwardRef<View, DrawerProps>(
57
+ (
58
+ {
59
+ open: controlledOpen,
60
+ defaultOpen = false,
61
+ onOpenChange,
62
+ children,
63
+ ...rest
64
+ },
65
+ ref
66
+ ) => {
67
+ const isControlled = useRef(controlledOpen !== undefined)
68
+ const [internalOpen, setInternalOpen] = useState(defaultOpen)
69
+ const [modalVisible, setModalVisible] = useState(
70
+ isControlled.current ? (controlledOpen ?? false) : defaultOpen
71
+ )
72
+
73
+ const isOpen = isControlled.current
74
+ ? (controlledOpen ?? false)
75
+ : internalOpen
76
+
77
+ useEffect(() => {
78
+ if (isOpen) setModalVisible(true)
79
+ }, [isOpen])
80
+
81
+ const openDrawer = useCallback(() => {
82
+ if (!isControlled.current) setInternalOpen(true)
83
+ onOpenChange?.(true)
84
+ }, [onOpenChange])
85
+
86
+ const closeDrawer = useCallback(() => {
87
+ if (!isControlled.current) setInternalOpen(false)
88
+ onOpenChange?.(false)
89
+ }, [onOpenChange])
90
+
91
+ const onExitComplete = useCallback(() => {
92
+ setModalVisible(false)
93
+ }, [])
94
+
95
+ const ctx = useMemo(
96
+ () => ({ isOpen, modalVisible, openDrawer, closeDrawer, onExitComplete }),
97
+ [isOpen, modalVisible, openDrawer, closeDrawer, onExitComplete]
98
+ )
99
+
100
+ return (
101
+ <DrawerContext.Provider value={ctx}>
102
+ <View ref={ref} {...rest}>
103
+ {children}
104
+ </View>
105
+ </DrawerContext.Provider>
106
+ )
107
+ }
108
+ )
109
+
110
+ DrawerRoot.displayName = "Drawer"
111
+
112
+ // ─── Trigger ──────────────────────────────────────────────────────────────────
113
+
114
+ export type DrawerTriggerProps = {
115
+ children: React.ReactElement
116
+ }
117
+
118
+ /**
119
+ * Merges `onPress` onto its single child element to open the drawer.
120
+ * Works with any pressable child (Button, Pressable, TouchableOpacity, etc.)
121
+ * without wrapping it in an extra Pressable that would swallow touch events.
122
+ */
123
+ const DrawerTrigger = React.forwardRef<View, DrawerTriggerProps>(
124
+ ({ children }, ref) => {
125
+ const { openDrawer } = React.useContext(DrawerContext)
126
+ return (
127
+ <Slot ref={ref} onPress={openDrawer}>
128
+ {children}
129
+ </Slot>
130
+ )
131
+ }
132
+ )
133
+
134
+ DrawerTrigger.displayName = "Drawer.Trigger"
135
+
136
+ // ─── Portal ───────────────────────────────────────────────────────────────────
137
+
138
+ type DrawerPortalProps = {
139
+ children: React.ReactNode
140
+ }
141
+
142
+ /**
143
+ * Renders its children above the rest of the app using React Native `Modal`.
144
+ * The modal stays mounted during exit animations and unmounts once complete.
145
+ */
146
+ const DrawerPortal = ({ children }: DrawerPortalProps) => {
147
+ const { modalVisible, closeDrawer } = React.useContext(DrawerContext)
148
+
149
+ if (!modalVisible) return null
150
+
151
+ return (
152
+ <Modal
153
+ visible
154
+ transparent
155
+ animationType="none"
156
+ statusBarTranslucent
157
+ onRequestClose={closeDrawer}
158
+ >
159
+ <View style={styles.modalContainer}>{children}</View>
160
+ </Modal>
161
+ )
162
+ }
163
+
164
+ DrawerPortal.displayName = "Drawer.Portal"
165
+
166
+ // ─── Compound export ──────────────────────────────────────────────────────────
167
+
168
+ export const Drawer = Object.assign(DrawerRoot, {
169
+ Root: DrawerRoot,
170
+ Trigger: DrawerTrigger,
171
+ Portal: DrawerPortal,
172
+ Overlay: DrawerOverlay,
173
+ Content: DrawerContent,
174
+ Grabber: DrawerGrabber,
175
+ Header: DrawerHeader,
176
+ Title: DrawerTitle,
177
+ Description: DrawerDescription,
178
+ Close: DrawerClose,
179
+ Body: DrawerBody,
180
+ Footer: DrawerFooter
181
+ })
182
+
183
+ const styles = StyleSheet.create({
184
+ modalContainer: {
185
+ flex: 1
186
+ }
187
+ })