@butternutbox/pawprint-native 2.0.1 → 2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butternutbox/pawprint-native",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -54,10 +54,10 @@
54
54
  }
55
55
  },
56
56
  "devDependencies": {
57
- "@butternutbox/pawprint-icons": "workspace:*",
58
- "@butternutbox/pawprint-illustrations": "workspace:*",
59
- "@repo/eslint-config": "workspace:*",
60
- "@repo/typescript-config": "workspace:*",
57
+ "@butternutbox/pawprint-icons": "1.8.1",
58
+ "@butternutbox/pawprint-illustrations": "3.1.1",
59
+ "@repo/eslint-config": "0.0.0",
60
+ "@repo/typescript-config": "0.0.0",
61
61
  "@testing-library/dom": "^10.4.1",
62
62
  "@testing-library/jest-dom": "^6.6.3",
63
63
  "@testing-library/react": "^16.3.0",
@@ -80,4 +80,4 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  }
83
- }
83
+ }
@@ -121,6 +121,11 @@ type DatePickerOwnProps = {
121
121
  getMonthLabel?: (year: number, month: number, locale?: Locale) => string
122
122
  weekDayLabels?: [string, string, string, string, string, string, string]
123
123
  getDateState?: (date: Date) => DateItemState
124
+ getDateAccessibilityLabel?: (
125
+ date: Date,
126
+ state: DateItemState,
127
+ monthLabel: string
128
+ ) => string
124
129
  isDateDisabled?: (date: Date) => boolean
125
130
  minDate?: Date
126
131
  maxDate?: Date
@@ -229,6 +234,8 @@ const StyledIndicator = styled(View)<{
229
234
  * @param {[string,string,string,string,string,string,string]} [weekDayLabels] - Day-column labels Mon→Sun.
230
235
  * @param {(date: Date) => DateItemState} [getDateState] - Returns the visual state for a date.
231
236
  * Defaults to: past="previous", today="current", future="default".
237
+ * @param {(date: Date, state: DateItemState, monthLabel: string) => string} [getDateAccessibilityLabel] - Returns the screen-reader label for a date,
238
+ * e.g. to announce delivery states. Defaults to "{day} {monthLabel}".
232
239
  * @param {(date: Date) => boolean} [isDateDisabled] - Returns true to disable a date.
233
240
  * @param {Date} [minDate] - Dates before this (exclusive of time) will be disabled.
234
241
  * @param {Date} [maxDate] - Dates after this (exclusive of time) will be disabled.
@@ -270,6 +277,7 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
270
277
  getMonthLabel,
271
278
  weekDayLabels = DEFAULT_WEEK_LABELS,
272
279
  getDateState,
280
+ getDateAccessibilityLabel,
273
281
  isDateDisabled,
274
282
  minDate,
275
283
  maxDate,
@@ -470,6 +478,7 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
470
478
  token={datePicker.month.typography}
471
479
  align="center"
472
480
  color={datePicker.month.colour.text.default}
481
+ accessibilityRole="header"
473
482
  >
474
483
  {resolvedMonthLabel}
475
484
  </Typography>
@@ -493,11 +502,7 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
493
502
  entering={MONTH_SLIDE_ENTERING[slideDir]}
494
503
  exiting={MONTH_SLIDE_EXITING[slideDir]}
495
504
  >
496
- <StyledDatesGrid
497
- rowGap={rowGap}
498
- accessible
499
- accessibilityLabel={resolvedMonthLabel}
500
- >
505
+ <StyledDatesGrid rowGap={rowGap}>
501
506
  {calendarRows.map((row, rowIdx) => (
502
507
  <StyledDateRow key={rowIdx}>
503
508
  {row.map((date, colIdx) =>
@@ -547,7 +552,13 @@ export const DatePicker = React.forwardRef<View, DatePickerProps>(
547
552
  disabled={isDisabled}
548
553
  accessible
549
554
  accessibilityRole="button"
550
- accessibilityLabel={`${date.getDate()} ${resolvedMonthLabel}`}
555
+ accessibilityLabel={
556
+ getDateAccessibilityLabel?.(
557
+ date,
558
+ state,
559
+ resolvedMonthLabel
560
+ ) ?? `${date.getDate()} ${resolvedMonthLabel}`
561
+ }
551
562
  accessibilityState={{
552
563
  selected: isSelected,
553
564
  disabled: isDisabled
@@ -433,9 +433,8 @@ export const DrawerContent = React.forwardRef<View, DrawerContentProps>(
433
433
  panelShadowColor={modal.shadow.color}
434
434
  panelShadowOffsetY={parseTokenValue(modal.shadow.offsetY)}
435
435
  panelShadowBlur={parseTokenValue(modal.shadow.blur)}
436
- accessible
437
- accessibilityRole="none"
438
436
  accessibilityViewIsModal
437
+ onAccessibilityEscape={closeDrawer}
439
438
  {...props}
440
439
  >
441
440
  <DrawerBodyMaxContext.Provider value={bodyMax}>
@@ -151,6 +151,19 @@ export const WithOptionalText = () => (
151
151
  </StoryWrapper>
152
152
  )
153
153
 
154
+ export const NotSearchable = () => (
155
+ <StoryWrapper>
156
+ <Section title="Pick from the list only">
157
+ <SelectField
158
+ label="Country"
159
+ placeholder="Select a country"
160
+ searchable={false}
161
+ options={COUNTRIES}
162
+ />
163
+ </Section>
164
+ </StoryWrapper>
165
+ )
166
+
154
167
  export const Controlled = () => {
155
168
  const [country, setCountry] = useState<string | null>("ca")
156
169
 
@@ -293,6 +293,47 @@ describe("SelectField", () => {
293
293
  })
294
294
  })
295
295
 
296
+ describe("when not searchable", () => {
297
+ it("can't be typed into", () => {
298
+ const input = renderField({ searchable: false })
299
+
300
+ expect(input).toBeDisabled()
301
+ })
302
+
303
+ it("stays closed until the field is pressed", () => {
304
+ renderField({ searchable: false })
305
+
306
+ expect(screen.queryByText("United Kingdom")).not.toBeInTheDocument()
307
+ })
308
+
309
+ it("shows every option when the field is pressed", () => {
310
+ const input = renderField({ searchable: false })
311
+ fireEvent.click(input)
312
+
313
+ COUNTRIES.forEach((option) => {
314
+ expect(screen.getByText(option.label)).toBeInTheDocument()
315
+ })
316
+ })
317
+
318
+ it("selects the pressed option and closes", () => {
319
+ const onValueChange = vi.fn()
320
+ const input = renderField({ searchable: false, onValueChange })
321
+ fireEvent.click(input)
322
+ fireEvent.click(screen.getByText("Canada"))
323
+
324
+ expect(onValueChange).toHaveBeenCalledWith("ca")
325
+ expect(input).toHaveValue("Canada")
326
+ expect(screen.queryByText("Australia")).not.toBeInTheDocument()
327
+ })
328
+
329
+ it("does not open when disabled", () => {
330
+ const input = renderField({ searchable: false, disabled: true })
331
+ fireEvent.click(input)
332
+
333
+ expect(screen.queryByText("United Kingdom")).not.toBeInTheDocument()
334
+ })
335
+ })
336
+
296
337
  describe("controlled mode", () => {
297
338
  it("shows the label for the controlled value", () => {
298
339
  const input = renderField({ value: "au" })
@@ -2,6 +2,7 @@ import React from "react"
2
2
  import {
3
3
  Keyboard,
4
4
  Pressable,
5
+ StyleSheet,
5
6
  TextInput,
6
7
  useWindowDimensions,
7
8
  View,
@@ -40,6 +41,7 @@ type SelectFieldOwnProps = {
40
41
  optionalText?: string
41
42
  placeholder?: string
42
43
  leadingIcon?: React.ReactNode
44
+ searchable?: boolean
43
45
  noResultsText?: string
44
46
  errorText?: string
45
47
  onSearchQuery?: SelectOptionsResolver
@@ -73,6 +75,7 @@ export const SelectField = React.forwardRef<View, SelectFieldProps>(
73
75
  optionalText,
74
76
  placeholder,
75
77
  leadingIcon,
78
+ searchable = true,
76
79
  noResultsText = "No results found",
77
80
  errorText = "Something went wrong",
78
81
  onSearchQuery,
@@ -244,7 +247,7 @@ export const SelectField = React.forwardRef<View, SelectFieldProps>(
244
247
  setInputText("")
245
248
  setFilterQuery("")
246
249
  commitValue(null)
247
- inputRef.current?.focus()
250
+ if (searchable) inputRef.current?.focus()
248
251
  }
249
252
 
250
253
  const fieldState: InputState = error ? "error" : state
@@ -272,24 +275,34 @@ export const SelectField = React.forwardRef<View, SelectFieldProps>(
272
275
  if (!isOpen || !anchorRect) return null
273
276
 
274
277
  return (
275
- <SelectFieldDropdown
276
- anchor={anchorRect}
277
- placement={placement.direction}
278
- maxHeight={placement.maxHeight}
279
- screenHeight={windowHeight}
280
- accessibilityLiveRegion="polite"
281
- >
282
- <SelectFieldList
283
- state={optionsState}
284
- selectedValue={selectedValue}
285
- onSelect={onOptionPress}
286
- noResultsText={noResultsText}
287
- errorText={errorText}
288
- searchHintText={searchHintText}
289
- />
290
- </SelectFieldDropdown>
278
+ <>
279
+ {!searchable && (
280
+ <Pressable
281
+ style={StyleSheet.absoluteFill}
282
+ onPress={() => setIsFocused(false)}
283
+ accessible={false}
284
+ />
285
+ )}
286
+ <SelectFieldDropdown
287
+ anchor={anchorRect}
288
+ placement={placement.direction}
289
+ maxHeight={placement.maxHeight}
290
+ screenHeight={windowHeight}
291
+ accessibilityLiveRegion="polite"
292
+ >
293
+ <SelectFieldList
294
+ state={optionsState}
295
+ selectedValue={selectedValue}
296
+ onSelect={onOptionPress}
297
+ noResultsText={noResultsText}
298
+ errorText={errorText}
299
+ searchHintText={searchHintText}
300
+ />
301
+ </SelectFieldDropdown>
302
+ </>
291
303
  )
292
304
  }, [
305
+ searchable,
293
306
  isOpen,
294
307
  anchorRect,
295
308
  placement,
@@ -313,11 +326,14 @@ export const SelectField = React.forwardRef<View, SelectFieldProps>(
313
326
  <InputField
314
327
  ref={inputRef}
315
328
  value={inputText}
316
- onChangeText={handleChangeText}
317
- onFocus={handleFocus}
318
- onBlur={handleBlur}
329
+ onChangeText={searchable ? handleChangeText : undefined}
330
+ onFocus={searchable ? handleFocus : undefined}
331
+ onBlur={searchable ? handleBlur : undefined}
332
+ onPress={
333
+ !searchable && !disabled ? () => setIsFocused(true) : undefined
334
+ }
319
335
  placeholder={placeholder}
320
- editable={!disabled}
336
+ editable={searchable && !disabled}
321
337
  state={fieldState}
322
338
  hideStateIcons={Boolean(clearAction)}
323
339
  leadingIcon={leadingIcon}