@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,143 @@
1
+ import React from "react"
2
+ import { View, ViewProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import type { InputState, InputFieldProps } from "../../atoms/Input/InputField"
5
+ import { InputError } from "../../atoms/Input/InputError"
6
+ import { InputLabel } from "../../atoms/Input/InputLabel"
7
+ import { InputDescription } from "../../atoms/Input/InputDescription"
8
+ import { SearchFieldInput } from "./SearchFieldInput"
9
+
10
+ const parseTokenValue = (value: string): number => parseFloat(value)
11
+
12
+ const StyledRoot = styled(View)(({ theme }) => {
13
+ const { spacing } = theme.tokens.components.inputs
14
+
15
+ return {
16
+ gap: parseTokenValue(spacing.gap)
17
+ }
18
+ })
19
+
20
+ type SearchFieldOwnProps = {
21
+ label?: string
22
+ description?: string
23
+ error?: string
24
+ state?: InputState
25
+ optionalText?: string
26
+ onClear?: () => void
27
+ onValueChange?: (value: string) => void
28
+ }
29
+
30
+ export type SearchFieldProps = SearchFieldOwnProps &
31
+ Omit<InputFieldProps, keyof SearchFieldOwnProps> &
32
+ Omit<ViewProps, keyof SearchFieldOwnProps>
33
+
34
+ /**
35
+ * Search field component for text search with clear functionality.
36
+ * Supports both a simple props API and a flexible compound component API.
37
+ *
38
+ * **Simple Props API:**
39
+ * @example
40
+ * ```tsx
41
+ * // Recommended: Use the useSearchField hook
42
+ * const searchProps = useSearchField()
43
+ * <SearchField {...searchProps} label="Search" placeholder="Search..." />
44
+ *
45
+ * // Or manually manage state:
46
+ * const [searchValue, setSearchValue] = useState("")
47
+ * <SearchField
48
+ * label="Search"
49
+ * placeholder="Search..."
50
+ * value={searchValue}
51
+ * onValueChange={(value) => setSearchValue(value)}
52
+ * onClear={() => setSearchValue("")}
53
+ * />
54
+ * ```
55
+ *
56
+ * **Compound Component API:**
57
+ * @example
58
+ * <SearchField.Root>
59
+ * <SearchField.Label optionalText="(optional)">Search</SearchField.Label>
60
+ * <SearchField.Field
61
+ * placeholder="Search..."
62
+ * onClear={() => setValue("")}
63
+ * />
64
+ * <SearchField.Description>Search for items</SearchField.Description>
65
+ * <SearchField.Error>Search is required</SearchField.Error>
66
+ * </SearchField.Root>
67
+ *
68
+ * @param {string} [label] - Label text (props API)
69
+ * @param {string} [description] - Description/help text (props API)
70
+ * @param {string} [error] - Error message to display (props API, does not affect visual state - use state prop for that)
71
+ * @param {InputState} [state] - Visual state of the input: 'default', 'error', or 'success' (props API)
72
+ * @param {string} [optionalText] - Optional text to display next to label (props API)
73
+ * @param {function} [onClear] - Clear button press handler
74
+ * @param {function} [onValueChange] - Value change handler (receives string value)
75
+ * @param {string} [value] - Controlled value
76
+ * @param {string} [defaultValue] - Default value for uncontrolled mode
77
+ * @param {function} [onChangeText] - Change event handler
78
+ * @param {boolean} [editable] - Controls whether the input is editable
79
+ *
80
+ * Also supports all native TextInput props.
81
+ */
82
+ const SearchFieldRoot = React.forwardRef<View, SearchFieldProps>(
83
+ (
84
+ {
85
+ label,
86
+ description,
87
+ error,
88
+ state = "default",
89
+ optionalText,
90
+ onClear,
91
+ onValueChange,
92
+ value,
93
+ children,
94
+ ...inputFieldProps
95
+ },
96
+ ref
97
+ ) => {
98
+ if (children) {
99
+ return <StyledRoot ref={ref}>{children}</StyledRoot>
100
+ }
101
+
102
+ return (
103
+ <StyledRoot ref={ref}>
104
+ {label && (
105
+ <InputLabel optionalText={optionalText} state={state}>
106
+ {label}
107
+ </InputLabel>
108
+ )}
109
+ <SearchFieldInput
110
+ state={state}
111
+ value={value}
112
+ onClear={onClear}
113
+ onValueChange={onValueChange}
114
+ {...inputFieldProps}
115
+ />
116
+ {description && (
117
+ <InputDescription state={state}>{description}</InputDescription>
118
+ )}
119
+ {error && state === "error" && <InputError>{error}</InputError>}
120
+ </StyledRoot>
121
+ )
122
+ }
123
+ )
124
+
125
+ SearchFieldRoot.displayName = "SearchField"
126
+
127
+ type SearchFieldComponent = React.ForwardRefExoticComponent<
128
+ SearchFieldProps & React.RefAttributes<View>
129
+ > & {
130
+ Root: typeof StyledRoot
131
+ Label: typeof InputLabel
132
+ Field: typeof SearchFieldInput
133
+ Description: typeof InputDescription
134
+ Error: typeof InputError
135
+ }
136
+
137
+ export const SearchField = Object.assign(SearchFieldRoot, {
138
+ Root: StyledRoot,
139
+ Label: InputLabel,
140
+ Field: SearchFieldInput,
141
+ Description: InputDescription,
142
+ Error: InputError
143
+ }) as SearchFieldComponent
@@ -0,0 +1,63 @@
1
+ import React from "react"
2
+ import { Pressable, TextInput } from "react-native"
3
+ import { InputField } from "../../atoms/Input/InputField"
4
+ import { Icon } from "../../atoms/Icon"
5
+ import { Search, Cancel } from "@butternutbox/pawprint-icons/core"
6
+ import type { SearchFieldProps } from "./SearchField"
7
+
8
+ export const SearchFieldInput = React.forwardRef<
9
+ TextInput,
10
+ Omit<SearchFieldProps, "label" | "description" | "error" | "optionalText">
11
+ >(({ onClear, onValueChange, value, state, ...inputFieldProps }, ref) => {
12
+ const [internalValue, setInternalValue] = React.useState("")
13
+ const inputRef = React.useRef<TextInput>(null)
14
+
15
+ React.useImperativeHandle(ref, () => inputRef.current!)
16
+
17
+ const handleValueChange = (newValue: string) => {
18
+ setInternalValue(newValue)
19
+ onValueChange?.(newValue)
20
+ }
21
+
22
+ const currentValue = value !== undefined ? value : internalValue
23
+
24
+ const hasText = Boolean(currentValue && currentValue.length > 0)
25
+ const showClearButton = hasText
26
+
27
+ const handleClear = () => {
28
+ setInternalValue("")
29
+
30
+ if (inputRef.current && value === undefined) {
31
+ inputRef.current.clear()
32
+ }
33
+
34
+ onClear?.()
35
+ }
36
+
37
+ const searchIcon = <Icon icon={Search} size="md" aria-label="Search" />
38
+
39
+ const clearButton = showClearButton ? (
40
+ <Pressable
41
+ onPress={handleClear}
42
+ accessibilityLabel="Clear search"
43
+ accessibilityRole="button"
44
+ hitSlop={8}
45
+ >
46
+ <Icon icon={Cancel} size="md" />
47
+ </Pressable>
48
+ ) : null
49
+
50
+ return (
51
+ <InputField
52
+ ref={inputRef}
53
+ {...inputFieldProps}
54
+ {...(state !== undefined && { state })}
55
+ value={currentValue}
56
+ onChangeText={handleValueChange}
57
+ leadingIcon={searchIcon}
58
+ actionIcon={clearButton}
59
+ />
60
+ )
61
+ })
62
+
63
+ SearchFieldInput.displayName = "SearchField.Field"
@@ -0,0 +1 @@
1
+ export { useSearchField } from "./useSearchField"
@@ -0,0 +1,56 @@
1
+ import { useState } from "react"
2
+ import type { SearchFieldProps } from "../SearchField"
3
+
4
+ type UseSearchFieldOptions = {
5
+ initialValue?: string
6
+ onValueChange?: (value: string) => void
7
+ onClear?: () => void
8
+ }
9
+
10
+ type UseSearchFieldReturn = Pick<
11
+ SearchFieldProps,
12
+ "value" | "onValueChange" | "onClear"
13
+ >
14
+
15
+ /**
16
+ * Hook for managing SearchField state with optional additional callbacks.
17
+ * Handles the common pattern of controlled SearchField with value/onValueChange/onClear.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * const searchProps = useSearchField({
22
+ * initialValue: "",
23
+ * onValueChange: (value) => triggerSearch(value),
24
+ * onClear: () => resetResults()
25
+ * })
26
+ *
27
+ * <SearchField {...searchProps} placeholder="Search..." />
28
+ * ```
29
+ *
30
+ * @param {string} [initialValue=""] - Initial search value
31
+ * @param {(value: string) => void} [onValueChange] - Optional callback called after value changes
32
+ * @param {() => void} [onClear] - Optional callback called after clearing (in addition to clearing the value)
33
+ */
34
+ export function useSearchField({
35
+ initialValue = "",
36
+ onValueChange: onValueChangeCallback,
37
+ onClear: onClearCallback
38
+ }: UseSearchFieldOptions = {}): UseSearchFieldReturn {
39
+ const [value, setValue] = useState(initialValue)
40
+
41
+ const handleValueChange: SearchFieldProps["onValueChange"] = (newValue) => {
42
+ setValue(newValue)
43
+ onValueChangeCallback?.(newValue)
44
+ }
45
+
46
+ const handleClear = () => {
47
+ setValue("")
48
+ onClearCallback?.()
49
+ }
50
+
51
+ return {
52
+ value,
53
+ onValueChange: handleValueChange,
54
+ onClear: handleClear
55
+ }
56
+ }
@@ -0,0 +1,4 @@
1
+ export { SearchField } from "./SearchField"
2
+ export type { SearchFieldProps } from "./SearchField"
3
+ export { SearchFieldInput } from "./SearchFieldInput"
4
+ export { useSearchField } from "./hooks"
@@ -37,6 +37,16 @@ const sevenOptions = [
37
37
  { value: "sun", label: "Sun" }
38
38
  ]
39
39
 
40
+ const scrollingOptions = [
41
+ { value: "all", label: "All recipes" },
42
+ { value: "chicken", label: "Chicken" },
43
+ { value: "beef", label: "Beef" },
44
+ { value: "lamb", label: "Lamb" },
45
+ { value: "turkey", label: "Turkey" },
46
+ { value: "duck", label: "Duck" },
47
+ { value: "fish", label: "White fish" }
48
+ ]
49
+
40
50
  export const FixedWidth = () => (
41
51
  <View style={styles.column}>
42
52
  <Typography size="sm" weight="semiBold" color="tertiary">
@@ -54,14 +64,27 @@ export const FixedWidth = () => (
54
64
 
55
65
  export const IntrinsicWidth = () => (
56
66
  <View style={styles.column}>
57
- <Typography size="sm" weight="semiBold" color="tertiary">
58
- Intrinsic layout (scrollable)
59
- </Typography>
60
- <SegmentedControl
61
- layout="intrinsic"
62
- options={sevenOptions}
63
- defaultValue="mon"
64
- />
67
+ <View style={styles.section}>
68
+ <Typography size="sm" weight="semiBold" color="tertiary">
69
+ Intrinsic layout (short labels)
70
+ </Typography>
71
+ <SegmentedControl
72
+ layout="intrinsic"
73
+ options={sevenOptions}
74
+ defaultValue="mon"
75
+ />
76
+ </View>
77
+
78
+ <View style={styles.section}>
79
+ <Typography size="sm" weight="semiBold" color="tertiary">
80
+ Intrinsic layout (overflows — tap chevrons to scroll)
81
+ </Typography>
82
+ <SegmentedControl
83
+ layout="intrinsic"
84
+ options={scrollingOptions}
85
+ defaultValue="all"
86
+ />
87
+ </View>
65
88
  </View>
66
89
  )
67
90
 
@@ -0,0 +1,141 @@
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 { SegmentedControl } from "./SegmentedControl"
7
+
8
+ const defaultOptions = [
9
+ { value: "left", label: "Left" },
10
+ { value: "center", label: "Center" },
11
+ { value: "right", label: "Right" }
12
+ ]
13
+
14
+ describe("SegmentedControl", () => {
15
+ describe("when component is rendering", () => {
16
+ it("renders all options", () => {
17
+ renderWithTheme(<SegmentedControl options={defaultOptions} />)
18
+ expect(screen.getByText("Left")).toBeInTheDocument()
19
+ expect(screen.getByText("Center")).toBeInTheDocument()
20
+ expect(screen.getByText("Right")).toBeInTheDocument()
21
+ })
22
+ })
23
+
24
+ describe("when using options API", () => {
25
+ it("selects first option by default", () => {
26
+ renderWithTheme(<SegmentedControl options={defaultOptions} />)
27
+ const buttons = screen.getAllByRole("button")
28
+ expect(buttons[0]).toHaveAttribute("aria-selected", "true")
29
+ })
30
+
31
+ it("selects the defaultValue option", () => {
32
+ renderWithTheme(
33
+ <SegmentedControl options={defaultOptions} defaultValue="center" />
34
+ )
35
+ const buttons = screen.getAllByRole("button")
36
+ expect(buttons[1]).toHaveAttribute("aria-selected", "true")
37
+ })
38
+ })
39
+
40
+ describe("when component is interactive", () => {
41
+ it("calls onValueChange when option is clicked", async () => {
42
+ const user = userEvent.setup()
43
+ const onValueChange = vi.fn()
44
+ renderWithTheme(
45
+ <SegmentedControl
46
+ options={defaultOptions}
47
+ onValueChange={onValueChange}
48
+ />
49
+ )
50
+
51
+ await user.click(screen.getByText("Right"))
52
+ expect(onValueChange).toHaveBeenCalledWith("right")
53
+ })
54
+
55
+ it("selects clicked option in uncontrolled mode", async () => {
56
+ const user = userEvent.setup()
57
+ renderWithTheme(<SegmentedControl options={defaultOptions} />)
58
+
59
+ await user.click(screen.getByText("Right"))
60
+ const buttons = screen.getAllByRole("button")
61
+ expect(buttons[2]).toHaveAttribute("aria-selected", "true")
62
+ })
63
+ })
64
+
65
+ describe("when component is controlled", () => {
66
+ it("reflects controlled value", () => {
67
+ renderWithTheme(
68
+ <SegmentedControl options={defaultOptions} value="center" />
69
+ )
70
+ const buttons = screen.getAllByRole("button")
71
+ expect(buttons[1]).toHaveAttribute("aria-selected", "true")
72
+ })
73
+ })
74
+
75
+ describe("when component is disabled", () => {
76
+ it("disables all options", async () => {
77
+ const user = userEvent.setup()
78
+ const onValueChange = vi.fn()
79
+ renderWithTheme(
80
+ <SegmentedControl
81
+ options={defaultOptions}
82
+ disabled
83
+ onValueChange={onValueChange}
84
+ />
85
+ )
86
+
87
+ await user.click(screen.getByText("Right"))
88
+ expect(onValueChange).not.toHaveBeenCalled()
89
+ })
90
+
91
+ it("disables individual options", async () => {
92
+ const user = userEvent.setup()
93
+ const onValueChange = vi.fn()
94
+ renderWithTheme(
95
+ <SegmentedControl
96
+ options={[
97
+ { value: "a", label: "A" },
98
+ { value: "b", label: "B", disabled: true },
99
+ { value: "c", label: "C" }
100
+ ]}
101
+ onValueChange={onValueChange}
102
+ />
103
+ )
104
+
105
+ await user.click(screen.getByText("B"))
106
+ expect(onValueChange).not.toHaveBeenCalled()
107
+
108
+ await user.click(screen.getByText("C"))
109
+ expect(onValueChange).toHaveBeenCalledWith("c")
110
+ })
111
+ })
112
+
113
+ describe("when rendering layouts", () => {
114
+ it("renders fixed layout", () => {
115
+ renderWithTheme(
116
+ <SegmentedControl options={defaultOptions} layout="fixed" />
117
+ )
118
+ expect(screen.getByText("Left")).toBeInTheDocument()
119
+ })
120
+
121
+ it("renders intrinsic layout", () => {
122
+ renderWithTheme(
123
+ <SegmentedControl options={defaultOptions} layout="intrinsic" />
124
+ )
125
+ expect(screen.getByText("Left")).toBeInTheDocument()
126
+ })
127
+ })
128
+
129
+ describe("when using compound component API", () => {
130
+ it("renders children items", () => {
131
+ renderWithTheme(
132
+ <SegmentedControl>
133
+ <SegmentedControl.Item value="a">Alpha</SegmentedControl.Item>
134
+ <SegmentedControl.Item value="b">Beta</SegmentedControl.Item>
135
+ </SegmentedControl>
136
+ )
137
+ expect(screen.getByText("Alpha")).toBeInTheDocument()
138
+ expect(screen.getByText("Beta")).toBeInTheDocument()
139
+ })
140
+ })
141
+ })