@butternutbox/pawprint-native 1.3.0 → 1.4.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.
@@ -0,0 +1,87 @@
1
+ import React from "react"
2
+ import { View } from "react-native"
3
+ import { Button, type ButtonProps } from "../../atoms/Button"
4
+ import { IconButton, type IconButtonProps } from "../../atoms/IconButton"
5
+ import {
6
+ getButtonDefaults,
7
+ getIconButtonColour,
8
+ useNavigationHeaderContext
9
+ } from "./NavigationHeaderContext"
10
+
11
+ export type NavigationHeaderButtonProps = Omit<
12
+ ButtonProps,
13
+ "size" | "fullWidth"
14
+ >
15
+
16
+ export type NavigationHeaderIconButtonProps = Omit<IconButtonProps, "size">
17
+
18
+ /**
19
+ * Text button for the navigation header action group. A medium `Button` with
20
+ * the header defaults: white `filled` / `tertiary` on the `brand` surface and
21
+ * `outlined` on other surfaces.
22
+ *
23
+ * Passing `variant` or `colour` overrides the default.
24
+ *
25
+ * @param {"filled" | "outlined" | "text"} [variant] - Button style.
26
+ * @param {"primary" | "secondary" | "tertiary"} [colour] - Filled colour.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * <NavigationHeader.Button onPress={editQuantity}>
31
+ * Pouch quantity
32
+ * </NavigationHeader.Button>
33
+ * ```
34
+ */
35
+ export const NavigationHeaderButton = React.forwardRef<
36
+ View,
37
+ NavigationHeaderButtonProps
38
+ >(({ variant, colour, ...props }, ref) => {
39
+ const defaults = getButtonDefaults(useNavigationHeaderContext())
40
+
41
+ return (
42
+ <Button
43
+ ref={ref}
44
+ size="md"
45
+ variant={variant ?? defaults.variant}
46
+ colour={colour ?? defaults.colour}
47
+ {...props}
48
+ />
49
+ )
50
+ })
51
+
52
+ NavigationHeaderButton.displayName = "NavigationHeader.Button"
53
+
54
+ /**
55
+ * Icon button for the navigation header action group. A medium filled
56
+ * `IconButton`, `tertiary` on the `brand` surface and `secondary` on other
57
+ * surfaces.
58
+ *
59
+ * Passing `variant` or `colour` overrides the default.
60
+ *
61
+ * @param {PawprintIcon} icon - **(required)** Icon component.
62
+ * @param {string} aria-label - **(required)** Accessible label.
63
+ * @param {"filled" | "outlined" | "text"} [variant="filled"] - Button style.
64
+ * @param {"primary" | "secondary" | "tertiary"} [colour] - Filled colour.
65
+ *
66
+ * @example
67
+ * ```tsx
68
+ * <NavigationHeader.IconButton icon={Add} aria-label="Add dog" />
69
+ * ```
70
+ */
71
+ export const NavigationHeaderIconButton = React.forwardRef<
72
+ View,
73
+ NavigationHeaderIconButtonProps
74
+ >(({ colour, ...props }, ref) => {
75
+ const context = useNavigationHeaderContext()
76
+
77
+ return (
78
+ <IconButton
79
+ ref={ref}
80
+ size="md"
81
+ colour={colour ?? getIconButtonColour(context)}
82
+ {...props}
83
+ />
84
+ )
85
+ })
86
+
87
+ NavigationHeaderIconButton.displayName = "NavigationHeader.IconButton"
@@ -0,0 +1,38 @@
1
+ import React from "react"
2
+ import type { ButtonColour, ButtonVariant } from "../../atoms/Button"
3
+ import type { IconButtonColour } from "../../atoms/IconButton"
4
+
5
+ type NavigationHeaderSurface = "brand" | "subtle" | "plain"
6
+
7
+ type NavigationHeaderContextValue = {
8
+ surface: NavigationHeaderSurface
9
+ }
10
+
11
+ const NavigationHeaderContext =
12
+ React.createContext<NavigationHeaderContextValue>({ surface: "brand" })
13
+
14
+ const useNavigationHeaderContext = () =>
15
+ React.useContext(NavigationHeaderContext)
16
+
17
+ const getButtonDefaults = ({
18
+ surface
19
+ }: NavigationHeaderContextValue): {
20
+ variant: ButtonVariant
21
+ colour: ButtonColour
22
+ } =>
23
+ surface === "brand"
24
+ ? { variant: "filled", colour: "tertiary" }
25
+ : { variant: "outlined", colour: "primary" }
26
+
27
+ const getIconButtonColour = ({
28
+ surface
29
+ }: NavigationHeaderContextValue): IconButtonColour =>
30
+ surface === "brand" ? "tertiary" : "secondary"
31
+
32
+ export {
33
+ NavigationHeaderContext,
34
+ useNavigationHeaderContext,
35
+ getButtonDefaults,
36
+ getIconButtonColour
37
+ }
38
+ export type { NavigationHeaderSurface, NavigationHeaderContextValue }
@@ -0,0 +1,14 @@
1
+ export { NavigationHeader } from "./NavigationHeader"
2
+ export type {
3
+ NavigationHeaderProps,
4
+ NavigationHeaderVariant,
5
+ NavigationHeaderSurface
6
+ } from "./NavigationHeader"
7
+ export {
8
+ NavigationHeaderButton,
9
+ NavigationHeaderIconButton
10
+ } from "./NavigationHeaderButton"
11
+ export type {
12
+ NavigationHeaderButtonProps,
13
+ NavigationHeaderIconButtonProps
14
+ } from "./NavigationHeaderButton"
@@ -375,6 +375,24 @@ describe("SelectField", () => {
375
375
  fireEvent.click(screen.getByTestId("select-overlay"))
376
376
  expect(screen.queryByPlaceholderText("Search...")).not.toBeInTheDocument()
377
377
  })
378
+
379
+ it("re-focuses the search input when it blurs while the dropdown is open", () => {
380
+ renderWithTheme(
381
+ <SelectField label="Country" placeholder="Select a country" searchable>
382
+ <SelectField.Item value="uk">United Kingdom</SelectField.Item>
383
+ </SelectField>
384
+ )
385
+
386
+ openDropdownWithContent()
387
+ const input = screen.getByPlaceholderText("Search...")
388
+ vi.useFakeTimers()
389
+ input.blur()
390
+ expect(input).not.toHaveFocus()
391
+
392
+ act(() => vi.advanceTimersByTime(50))
393
+ vi.useRealTimers()
394
+ expect(input).toHaveFocus()
395
+ })
378
396
  })
379
397
 
380
398
  describe("when searchable with an async query", () => {
@@ -315,6 +315,17 @@ const SelectFieldRoot = React.forwardRef<View, SelectFieldProps>(
315
315
  // Re-running here re-focuses the input so typing is not interrupted.
316
316
  }, [isOpen, searchable, isSearching, searchQuery])
317
317
 
318
+ const isOpenRef = React.useRef(isOpen)
319
+ React.useEffect(() => {
320
+ isOpenRef.current = isOpen
321
+ })
322
+
323
+ const handleSearchBlur = () => {
324
+ setTimeout(() => {
325
+ if (isOpenRef.current) searchInputRef.current?.focus()
326
+ }, 50)
327
+ }
328
+
318
329
  const isCompound = React.Children.toArray(children).some(
319
330
  (child) =>
320
331
  React.isValidElement(child) && child.type === SelectFieldContent
@@ -447,6 +458,7 @@ const SelectFieldRoot = React.forwardRef<View, SelectFieldProps>(
447
458
  ref={searchInputRef}
448
459
  value={searchQuery}
449
460
  onChangeText={setSearchQuery}
461
+ onBlur={handleSearchBlur}
450
462
  placeholder={searchPlaceholder ?? "Search..."}
451
463
  returnKeyType="search"
452
464
  clearButtonMode="never"
@@ -27,3 +27,4 @@ export * from "./ProductListingCard"
27
27
  export * from "./ProductDisplayCard"
28
28
  export * from "./TabNavigation"
29
29
  export * from "./FloatingActionButton"
30
+ export * from "./NavigationHeader"