@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 @@
1
+ export { useCopyField } from "./useCopyField"
@@ -0,0 +1,25 @@
1
+ import { useState } from "react"
2
+
3
+ type UseCopyFieldOptions = {
4
+ initialValue?: string
5
+ onCopy?: (value: string) => void
6
+ }
7
+
8
+ export const useCopyField = (options: UseCopyFieldOptions = {}) => {
9
+ const { initialValue = "", onCopy } = options
10
+ const [value, setValue] = useState(initialValue)
11
+
12
+ const handleValueChange = (newValue: string) => {
13
+ setValue(newValue)
14
+ }
15
+
16
+ const handleCopy = (copiedValue: string) => {
17
+ onCopy?.(copiedValue)
18
+ }
19
+
20
+ return {
21
+ value,
22
+ onValueChange: handleValueChange,
23
+ onCopy: handleCopy
24
+ }
25
+ }
@@ -0,0 +1,4 @@
1
+ export { CopyField } from "./CopyField"
2
+ export type { CopyFieldProps } from "./CopyField"
3
+ export { CopyFieldInput } from "./CopyFieldInput"
4
+ export { useCopyField } from "./hooks"
@@ -0,0 +1,298 @@
1
+ import React, { useEffect, useState } from "react"
2
+ import { View, ScrollView, StyleSheet } from "react-native"
3
+ import { addDays, differenceInCalendarDays, isAfter, isSameDay } from "date-fns"
4
+ import { DatePicker } from "./DatePicker"
5
+ import type { DateItemState } from "./DatePicker"
6
+
7
+ const now = new Date()
8
+ const THIS_YEAR = now.getFullYear()
9
+ const THIS_MONTH = now.getMonth()
10
+
11
+ function monthOffset(year: number, month: number, offset: number) {
12
+ const d = new Date(year, month + offset, 1)
13
+ return { year: d.getFullYear(), month: d.getMonth() }
14
+ }
15
+
16
+ export default {
17
+ title: "Molecules/DatePicker",
18
+ component: DatePicker,
19
+ argTypes: {
20
+ year: {
21
+ control: { type: "number" },
22
+ description: "Full year (e.g. 2026)"
23
+ },
24
+ month: {
25
+ control: { type: "number", min: 0, max: 11 },
26
+ description: "Zero-based month index (0–11)"
27
+ },
28
+ showWeekHeader: {
29
+ control: { type: "boolean" },
30
+ description: "Show the Mon–Sun column headers"
31
+ },
32
+ showPrevControl: {
33
+ control: { type: "boolean" },
34
+ description: "Show the left chevron navigation control"
35
+ },
36
+ showNextControl: {
37
+ control: { type: "boolean" },
38
+ description: "Show the right chevron navigation control"
39
+ },
40
+ monthLabel: {
41
+ control: { type: "text" },
42
+ description:
43
+ "Override the month/year label. Defaults to Intl.DateTimeFormat output."
44
+ }
45
+ }
46
+ }
47
+
48
+ function useCalendarState(initialYear = THIS_YEAR, initialMonth = THIS_MONTH) {
49
+ const [year, setYear] = useState(initialYear)
50
+ const [month, setMonth] = useState(initialMonth)
51
+ const [selectedDate, setSelectedDate] = useState<Date | null>(null)
52
+
53
+ const prevMonth = () => {
54
+ if (month === 0) {
55
+ setYear((y) => y - 1)
56
+ setMonth(11)
57
+ } else {
58
+ setMonth((m) => m - 1)
59
+ }
60
+ }
61
+
62
+ const nextMonth = () => {
63
+ if (month === 11) {
64
+ setYear((y) => y + 1)
65
+ setMonth(0)
66
+ } else {
67
+ setMonth((m) => m + 1)
68
+ }
69
+ }
70
+
71
+ return { year, month, selectedDate, setSelectedDate, prevMonth, nextMonth }
72
+ }
73
+
74
+ export const Playground = {
75
+ args: {
76
+ year: THIS_YEAR,
77
+ month: THIS_MONTH,
78
+ showWeekHeader: true,
79
+ showPrevControl: true,
80
+ showNextControl: true
81
+ },
82
+ render: (args: {
83
+ year: number
84
+ month: number
85
+ showWeekHeader: boolean
86
+ showPrevControl: boolean
87
+ showNextControl: boolean
88
+ }) => {
89
+ const { year, month, selectedDate, setSelectedDate, prevMonth, nextMonth } =
90
+ useCalendarState(args.year, args.month)
91
+ return (
92
+ <View style={styles.container}>
93
+ <DatePicker
94
+ {...args}
95
+ year={year}
96
+ month={month}
97
+ selectedDate={selectedDate}
98
+ onDateSelect={setSelectedDate}
99
+ onPrevMonth={prevMonth}
100
+ onNextMonth={nextMonth}
101
+ />
102
+ </View>
103
+ )
104
+ }
105
+ }
106
+
107
+ const NEXT_BOX_DATE = addDays(new Date(), 4)
108
+ const SCHEDULED_BOX_DATES = [
109
+ addDays(NEXT_BOX_DATE, 21),
110
+ addDays(NEXT_BOX_DATE, 42),
111
+ addDays(NEXT_BOX_DATE, 63)
112
+ ]
113
+ // Simulates a bank holiday on the second scheduled slot; actual delivery bumps by 1 day
114
+ const UNDELIVERABLE_DATE = SCHEDULED_BOX_DATES[1]
115
+ const FUTURE_BOX_DATES = SCHEDULED_BOX_DATES.map((d) =>
116
+ isSameDay(d, UNDELIVERABLE_DATE) ? addDays(d, 1) : d
117
+ )
118
+
119
+ const deliveryDateState =
120
+ (currentDeliveryDate: Date, futureDates: Date[]) =>
121
+ (date: Date): DateItemState => {
122
+ const today = new Date()
123
+ const d = new Date(date.getFullYear(), date.getMonth(), date.getDate())
124
+ const t = new Date(today.getFullYear(), today.getMonth(), today.getDate())
125
+ if (d.getTime() < t.getTime()) return "previous"
126
+ if (d.getTime() === t.getTime()) return "current"
127
+ if (isSameDay(date, currentDeliveryDate)) return "nextBox"
128
+ if (futureDates.some((fd) => isSameDay(fd, date))) return "futureBox"
129
+ return "default"
130
+ }
131
+
132
+ function computeForecastedDates(
133
+ selectedDate: Date,
134
+ currentDeliveryDate: Date,
135
+ futureDates: Date[],
136
+ isUndeliverable: (date: Date) => boolean
137
+ ): Date[] {
138
+ const offset = differenceInCalendarDays(selectedDate, currentDeliveryDate)
139
+ return futureDates
140
+ .filter((d) => isAfter(d, currentDeliveryDate))
141
+ .map((d) => {
142
+ const projected = addDays(d, offset)
143
+ return isUndeliverable(projected) ? addDays(projected, 1) : projected
144
+ })
145
+ }
146
+
147
+ export const WithDeliveryDates = {
148
+ render: () => {
149
+ const { year, month, prevMonth, nextMonth } = useCalendarState()
150
+ const [selectedDate, setSelectedDate] = useState<Date | null>(null)
151
+ const [forecastedDates, setForecastedDates] = useState<Date[]>([])
152
+
153
+ useEffect(() => {
154
+ if (!selectedDate) {
155
+ setForecastedDates([])
156
+ } else {
157
+ setForecastedDates(
158
+ computeForecastedDates(
159
+ selectedDate,
160
+ NEXT_BOX_DATE,
161
+ FUTURE_BOX_DATES,
162
+ (date) => isSameDay(date, UNDELIVERABLE_DATE)
163
+ )
164
+ )
165
+ }
166
+ }, [selectedDate])
167
+
168
+ const handleSelect = (date: Date) => {
169
+ if (isSameDay(date, NEXT_BOX_DATE)) {
170
+ setSelectedDate(null)
171
+ } else {
172
+ setSelectedDate(date)
173
+ }
174
+ }
175
+
176
+ return (
177
+ <View style={styles.container}>
178
+ <DatePicker
179
+ year={year}
180
+ month={month}
181
+ selectedDate={selectedDate}
182
+ selectedDates={forecastedDates}
183
+ onDateSelect={handleSelect}
184
+ onPrevMonth={prevMonth}
185
+ onNextMonth={nextMonth}
186
+ getDateState={deliveryDateState(NEXT_BOX_DATE, FUTURE_BOX_DATES)}
187
+ isDateDisabled={(date) => isSameDay(date, UNDELIVERABLE_DATE)}
188
+ />
189
+ </View>
190
+ )
191
+ }
192
+ }
193
+
194
+ export const NoWeekHeader = {
195
+ render: () => {
196
+ const { year, month, selectedDate, setSelectedDate, prevMonth, nextMonth } =
197
+ useCalendarState()
198
+ return (
199
+ <View style={styles.container}>
200
+ <DatePicker
201
+ year={year}
202
+ month={month}
203
+ selectedDate={selectedDate}
204
+ onDateSelect={setSelectedDate}
205
+ onPrevMonth={prevMonth}
206
+ onNextMonth={nextMonth}
207
+ showWeekHeader={false}
208
+ getDateState={() => "default"}
209
+ />
210
+ </View>
211
+ )
212
+ }
213
+ }
214
+
215
+ export const NoControls = {
216
+ render: () => (
217
+ <View style={styles.container}>
218
+ <DatePicker
219
+ year={THIS_YEAR}
220
+ month={THIS_MONTH}
221
+ showPrevControl={false}
222
+ showNextControl={false}
223
+ getDateState={() => "default"}
224
+ />
225
+ </View>
226
+ )
227
+ }
228
+
229
+ export const ThreeMonthsStacked = {
230
+ render: () => {
231
+ const m1 = { year: THIS_YEAR, month: THIS_MONTH }
232
+ const m2 = monthOffset(THIS_YEAR, THIS_MONTH, 1)
233
+ const m3 = monthOffset(THIS_YEAR, THIS_MONTH, 2)
234
+ return (
235
+ <ScrollView contentContainerStyle={styles.container}>
236
+ <View style={styles.stack}>
237
+ <DatePicker
238
+ {...m1}
239
+ showPrevControl={false}
240
+ showNextControl={false}
241
+ getDateState={deliveryDateState(NEXT_BOX_DATE, FUTURE_BOX_DATES)}
242
+ isDateDisabled={(date) => isSameDay(date, UNDELIVERABLE_DATE)}
243
+ />
244
+ <DatePicker
245
+ {...m2}
246
+ showPrevControl={false}
247
+ showNextControl={false}
248
+ showWeekHeader={false}
249
+ getDateState={deliveryDateState(NEXT_BOX_DATE, FUTURE_BOX_DATES)}
250
+ isDateDisabled={(date) => isSameDay(date, UNDELIVERABLE_DATE)}
251
+ />
252
+ <DatePicker
253
+ {...m3}
254
+ showPrevControl={false}
255
+ showNextControl={false}
256
+ showWeekHeader={false}
257
+ getDateState={deliveryDateState(NEXT_BOX_DATE, FUTURE_BOX_DATES)}
258
+ isDateDisabled={(date) => isSameDay(date, UNDELIVERABLE_DATE)}
259
+ />
260
+ </View>
261
+ </ScrollView>
262
+ )
263
+ }
264
+ }
265
+
266
+ export const AllDateStates = {
267
+ render: () => (
268
+ <View style={styles.container}>
269
+ <DatePicker
270
+ year={THIS_YEAR}
271
+ month={THIS_MONTH}
272
+ selectedDate={new Date(THIS_YEAR, THIS_MONTH, 11)}
273
+ selectedDates={[new Date(THIS_YEAR, THIS_MONTH, 25)]}
274
+ showPrevControl={false}
275
+ showNextControl={false}
276
+ getDateState={(date) => {
277
+ const d = date.getDate()
278
+ if (d <= 5) return "previous"
279
+ if (d === 6) return "current"
280
+ if (d === 8 || d === 11) return "nextBox"
281
+ if (d === 25 || d === 26) return "futureBox"
282
+ return "default"
283
+ }}
284
+ isDateDisabled={(date) => date.getDate() === 18}
285
+ />
286
+ </View>
287
+ )
288
+ }
289
+
290
+ const styles = StyleSheet.create({
291
+ container: {
292
+ padding: 16
293
+ },
294
+ stack: {
295
+ flexDirection: "column",
296
+ gap: 24
297
+ }
298
+ })
@@ -0,0 +1,201 @@
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 { DatePicker } from "./DatePicker"
7
+
8
+ const MARCH_2026 = { year: 2026, month: 2 }
9
+
10
+ describe("DatePicker", () => {
11
+ describe("rendering", () => {
12
+ it("renders the month label", () => {
13
+ renderWithTheme(<DatePicker {...MARCH_2026} />)
14
+ expect(screen.getByText("March 2026")).toBeInTheDocument()
15
+ })
16
+
17
+ it("renders week headers by default", () => {
18
+ renderWithTheme(<DatePicker {...MARCH_2026} />)
19
+ expect(screen.getByText("Mon")).toBeInTheDocument()
20
+ expect(screen.getByText("Sun")).toBeInTheDocument()
21
+ })
22
+
23
+ it("hides week headers when showWeekHeader=false", () => {
24
+ renderWithTheme(<DatePicker {...MARCH_2026} showWeekHeader={false} />)
25
+ expect(screen.queryByText("Mon")).not.toBeInTheDocument()
26
+ })
27
+
28
+ it("renders all days of the month", () => {
29
+ renderWithTheme(<DatePicker {...MARCH_2026} />)
30
+ expect(screen.getByLabelText("1 March 2026")).toBeInTheDocument()
31
+ expect(screen.getByLabelText("31 March 2026")).toBeInTheDocument()
32
+ })
33
+
34
+ it("accepts a custom month label", () => {
35
+ renderWithTheme(<DatePicker {...MARCH_2026} monthLabel="März 2026" />)
36
+ expect(screen.getByText("März 2026")).toBeInTheDocument()
37
+ })
38
+
39
+ it("accepts custom week day labels", () => {
40
+ renderWithTheme(
41
+ <DatePicker
42
+ {...MARCH_2026}
43
+ weekDayLabels={["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]}
44
+ />
45
+ )
46
+ expect(screen.getByText("Mo")).toBeInTheDocument()
47
+ })
48
+ })
49
+
50
+ describe("navigation controls", () => {
51
+ it("renders prev and next buttons by default", () => {
52
+ renderWithTheme(<DatePicker {...MARCH_2026} />)
53
+ expect(screen.getByLabelText("Previous month")).toBeInTheDocument()
54
+ expect(screen.getByLabelText("Next month")).toBeInTheDocument()
55
+ })
56
+
57
+ it("calls onPrevMonth when prev button is pressed", async () => {
58
+ const user = userEvent.setup()
59
+ const onPrevMonth = vi.fn()
60
+ renderWithTheme(<DatePicker {...MARCH_2026} onPrevMonth={onPrevMonth} />)
61
+ await user.click(screen.getByLabelText("Previous month"))
62
+ expect(onPrevMonth).toHaveBeenCalledOnce()
63
+ })
64
+
65
+ it("calls onNextMonth when next button is pressed", async () => {
66
+ const user = userEvent.setup()
67
+ const onNextMonth = vi.fn()
68
+ renderWithTheme(<DatePicker {...MARCH_2026} onNextMonth={onNextMonth} />)
69
+ await user.click(screen.getByLabelText("Next month"))
70
+ expect(onNextMonth).toHaveBeenCalledOnce()
71
+ })
72
+
73
+ it("calls onMonthChange with correct year/month when prev is pressed", async () => {
74
+ const user = userEvent.setup()
75
+ const onMonthChange = vi.fn()
76
+ renderWithTheme(
77
+ <DatePicker {...MARCH_2026} onMonthChange={onMonthChange} />
78
+ )
79
+ await user.click(screen.getByLabelText("Previous month"))
80
+ expect(onMonthChange).toHaveBeenCalledWith(2026, 1)
81
+ })
82
+
83
+ it("calls onMonthChange with correct year/month when next is pressed", async () => {
84
+ const user = userEvent.setup()
85
+ const onMonthChange = vi.fn()
86
+ renderWithTheme(
87
+ <DatePicker {...MARCH_2026} onMonthChange={onMonthChange} />
88
+ )
89
+ await user.click(screen.getByLabelText("Next month"))
90
+ expect(onMonthChange).toHaveBeenCalledWith(2026, 3)
91
+ })
92
+ })
93
+
94
+ describe("date selection", () => {
95
+ it("calls onDateSelect with the correct date when a day is pressed", async () => {
96
+ const user = userEvent.setup()
97
+ const onDateSelect = vi.fn()
98
+ renderWithTheme(
99
+ <DatePicker
100
+ {...MARCH_2026}
101
+ onDateSelect={onDateSelect}
102
+ getDateState={() => "default"}
103
+ />
104
+ )
105
+ await user.click(screen.getByLabelText("15 March 2026"))
106
+ expect(onDateSelect).toHaveBeenCalledOnce()
107
+ const calledDate: Date = onDateSelect.mock.calls[0][0]
108
+ expect(calledDate.getDate()).toBe(15)
109
+ expect(calledDate.getMonth()).toBe(2)
110
+ expect(calledDate.getFullYear()).toBe(2026)
111
+ })
112
+
113
+ it("does not call onDateSelect for disabled dates", async () => {
114
+ const user = userEvent.setup()
115
+ const onDateSelect = vi.fn()
116
+ renderWithTheme(
117
+ <DatePicker
118
+ {...MARCH_2026}
119
+ onDateSelect={onDateSelect}
120
+ isDateDisabled={() => true}
121
+ />
122
+ )
123
+ await user.click(screen.getByLabelText("10 March 2026"))
124
+ expect(onDateSelect).not.toHaveBeenCalled()
125
+ })
126
+ })
127
+
128
+ describe("accessibility", () => {
129
+ it("marks a selected date with aria-selected", () => {
130
+ renderWithTheme(
131
+ <DatePicker
132
+ {...MARCH_2026}
133
+ selectedDate={new Date(2026, 2, 11)}
134
+ getDateState={() => "nextBox"}
135
+ />
136
+ )
137
+ expect(screen.getByLabelText("11 March 2026")).toHaveAttribute(
138
+ "aria-selected",
139
+ "true"
140
+ )
141
+ })
142
+
143
+ it("marks a disabled date with aria-disabled", () => {
144
+ renderWithTheme(
145
+ <DatePicker
146
+ {...MARCH_2026}
147
+ isDateDisabled={(date) => date.getDate() === 10}
148
+ />
149
+ )
150
+ expect(screen.getByLabelText("10 March 2026")).toHaveAttribute(
151
+ "aria-disabled",
152
+ "true"
153
+ )
154
+ })
155
+ })
156
+
157
+ describe("minDate / maxDate", () => {
158
+ it("disables dates before minDate", async () => {
159
+ const user = userEvent.setup()
160
+ const onDateSelect = vi.fn()
161
+ renderWithTheme(
162
+ <DatePicker
163
+ {...MARCH_2026}
164
+ minDate={new Date(2026, 2, 10)}
165
+ onDateSelect={onDateSelect}
166
+ />
167
+ )
168
+ await user.click(screen.getByLabelText("5 March 2026"))
169
+ expect(onDateSelect).not.toHaveBeenCalled()
170
+ })
171
+
172
+ it("allows the minDate itself", async () => {
173
+ const user = userEvent.setup()
174
+ const onDateSelect = vi.fn()
175
+ renderWithTheme(
176
+ <DatePicker
177
+ {...MARCH_2026}
178
+ minDate={new Date(2026, 2, 10)}
179
+ onDateSelect={onDateSelect}
180
+ getDateState={() => "default"}
181
+ />
182
+ )
183
+ await user.click(screen.getByLabelText("10 March 2026"))
184
+ expect(onDateSelect).toHaveBeenCalledOnce()
185
+ })
186
+
187
+ it("disables dates after maxDate", async () => {
188
+ const user = userEvent.setup()
189
+ const onDateSelect = vi.fn()
190
+ renderWithTheme(
191
+ <DatePicker
192
+ {...MARCH_2026}
193
+ maxDate={new Date(2026, 2, 20)}
194
+ onDateSelect={onDateSelect}
195
+ />
196
+ )
197
+ await user.click(screen.getByLabelText("25 March 2026"))
198
+ expect(onDateSelect).not.toHaveBeenCalled()
199
+ })
200
+ })
201
+ })