@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.
- package/.turbo/turbo-build.log +15 -15
- package/CHANGELOG.md +30 -0
- package/COMPONENT_GUIDELINES.md +111 -4
- package/dist/index.cjs +12413 -1459
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1111 -13
- package/dist/index.d.ts +1111 -13
- package/dist/index.js +12365 -1457
- package/dist/index.js.map +1 -1
- package/package.json +29 -11
- package/src/__mocks__/asset-stub.ts +1 -0
- package/src/__mocks__/emotion-native.tsx +18 -0
- package/src/__mocks__/react-native-gesture-handler.tsx +41 -0
- package/src/__mocks__/react-native-reanimated.tsx +79 -0
- package/src/__mocks__/react-native-safe-area-context.tsx +6 -0
- package/src/__mocks__/react-native-svg.tsx +27 -0
- package/src/__mocks__/react-native-worklets.tsx +11 -0
- package/src/__mocks__/react-native.tsx +338 -0
- package/src/__mocks__/rn-primitives/avatar.tsx +24 -0
- package/src/__mocks__/rn-primitives/checkbox.tsx +19 -0
- package/src/__mocks__/rn-primitives/select.tsx +116 -0
- package/src/__mocks__/rn-primitives/slider.tsx +40 -0
- package/src/__mocks__/rn-primitives/slot.tsx +30 -0
- package/src/__mocks__/rn-primitives/switch.tsx +24 -0
- package/src/__mocks__/rn-primitives/toggle.tsx +16 -0
- package/src/components/atoms/Avatar/Avatar.stories.tsx +57 -49
- package/src/components/atoms/Avatar/Avatar.test.tsx +269 -0
- package/src/components/atoms/Avatar/Avatar.tsx +68 -22
- package/src/components/atoms/Avatar/index.ts +1 -6
- package/src/components/atoms/Badge/Badge.stories.tsx +5 -29
- package/src/components/atoms/Badge/Badge.test.tsx +90 -0
- package/src/components/atoms/Button/Button.test.tsx +123 -0
- package/src/components/atoms/Button/Button.tsx +1 -1
- package/src/components/atoms/CarouselControls/CarouselControls.stories.tsx +217 -0
- package/src/components/atoms/CarouselControls/CarouselControls.tsx +127 -0
- package/src/components/atoms/CarouselControls/index.ts +2 -0
- package/src/components/atoms/Hint/Hint.test.tsx +36 -0
- package/src/components/atoms/Icon/Icon.test.tsx +98 -0
- package/src/components/atoms/Icon/Icon.tsx +5 -1
- package/src/components/atoms/IconButton/IconButton.test.tsx +101 -0
- package/src/components/atoms/Illustration/Illustration.stories.tsx +2 -2
- package/src/components/atoms/Illustration/Illustration.test.tsx +55 -0
- package/src/components/atoms/Illustration/Illustration.tsx +3 -3
- package/src/components/atoms/Input/Input.stories.tsx +129 -86
- package/src/components/atoms/Input/Input.test.tsx +306 -0
- package/src/components/atoms/Input/Input.tsx +9 -1
- package/src/components/atoms/Input/InputField.tsx +226 -74
- package/src/components/atoms/Link/Link.test.tsx +89 -0
- package/src/components/atoms/Link/Link.tsx +7 -6
- package/src/components/atoms/Logo/Logo.registry.ts +30 -5
- package/src/components/atoms/Logo/Logo.stories.tsx +108 -0
- package/src/components/atoms/Logo/Logo.test.tsx +56 -0
- package/src/components/atoms/Logo/assets/BCorp.tsx +113 -0
- package/src/components/atoms/Logo/assets/ButternutFavicon.tsx +33 -0
- package/src/components/atoms/Logo/assets/ButternutPrimary.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedBottom.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutTabbedTop.tsx +294 -0
- package/src/components/atoms/Logo/assets/ButternutWordmark.tsx +274 -0
- package/src/components/atoms/Logo/assets/PsiBufetFavicon.tsx +45 -0
- package/src/components/atoms/Logo/assets/PsiBufetPrimary.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedBottom.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetTabbedTop.tsx +218 -0
- package/src/components/atoms/Logo/assets/PsiBufetWordmark.tsx +195 -0
- package/src/components/atoms/Logo/assets/index.ts +11 -0
- package/src/components/atoms/NumberInput/NumberInput.stories.tsx +183 -0
- package/src/components/atoms/NumberInput/NumberInput.test.tsx +261 -0
- package/src/components/atoms/NumberInput/NumberInput.tsx +129 -0
- package/src/components/atoms/NumberInput/NumberInputField.tsx +77 -0
- package/src/components/atoms/NumberInput/index.ts +4 -0
- package/src/components/atoms/Spinner/Spinner.test.tsx +46 -0
- package/src/components/atoms/Spinner/Spinner.tsx +14 -5
- package/src/components/atoms/Switch/Switch.test.tsx +92 -0
- package/src/components/atoms/Switch/Switch.tsx +28 -17
- package/src/components/atoms/Tag/Tag.test.tsx +70 -0
- package/src/components/atoms/TextArea/TextArea.stories.tsx +303 -0
- package/src/components/atoms/TextArea/TextArea.test.tsx +416 -0
- package/src/components/atoms/TextArea/TextArea.tsx +171 -0
- package/src/components/atoms/TextArea/TextAreaField.tsx +304 -0
- package/src/components/atoms/TextArea/TextAreaLabel.tsx +103 -0
- package/src/components/atoms/TextArea/index.ts +6 -0
- package/src/components/atoms/Typography/Typography.test.tsx +94 -0
- package/src/components/atoms/index.ts +3 -0
- package/src/components/molecules/Accordion/Accordion.stories.tsx +177 -0
- package/src/components/molecules/Accordion/Accordion.test.tsx +185 -0
- package/src/components/molecules/Accordion/Accordion.tsx +284 -0
- package/src/components/molecules/Accordion/index.ts +6 -0
- package/src/components/molecules/Animated/Animated.stories.tsx +254 -0
- package/src/components/molecules/Animated/Animated.tsx +283 -0
- package/src/components/molecules/Animated/index.ts +10 -0
- package/src/components/molecules/ButtonDock/ButtonDock.stories.tsx +44 -25
- package/src/components/molecules/ButtonDock/ButtonDock.test.tsx +83 -0
- package/src/components/molecules/ButtonDock/ButtonDock.tsx +16 -13
- package/src/components/molecules/ButtonGroup/ButtonGroup.stories.tsx +48 -29
- package/src/components/molecules/ButtonGroup/ButtonGroup.test.tsx +73 -0
- package/src/components/molecules/ButtonGroup/ButtonGroup.tsx +25 -3
- package/src/components/molecules/Checkbox/Checkbox.stories.tsx +72 -0
- package/src/components/molecules/Checkbox/Checkbox.test.tsx +117 -0
- package/src/components/molecules/Checkbox/Checkbox.tsx +101 -95
- package/src/components/molecules/CopyField/CopyField.stories.tsx +313 -0
- package/src/components/molecules/CopyField/CopyField.test.tsx +431 -0
- package/src/components/molecules/CopyField/CopyField.tsx +156 -0
- package/src/components/molecules/CopyField/CopyFieldInput.tsx +127 -0
- package/src/components/molecules/CopyField/hooks/index.ts +1 -0
- package/src/components/molecules/CopyField/hooks/useCopyField.ts +25 -0
- package/src/components/molecules/CopyField/index.ts +4 -0
- package/src/components/molecules/DatePicker/DatePicker.stories.tsx +298 -0
- package/src/components/molecules/DatePicker/DatePicker.test.tsx +201 -0
- package/src/components/molecules/DatePicker/DatePicker.tsx +590 -0
- package/src/components/molecules/DatePicker/index.ts +2 -0
- package/src/components/molecules/Drawer/Drawer.stories.tsx +285 -0
- package/src/components/molecules/Drawer/Drawer.test.tsx +180 -0
- package/src/components/molecules/Drawer/Drawer.tsx +187 -0
- package/src/components/molecules/Drawer/DrawerBody.tsx +80 -0
- package/src/components/molecules/Drawer/DrawerClose.tsx +76 -0
- package/src/components/molecules/Drawer/DrawerContent.tsx +339 -0
- package/src/components/molecules/Drawer/DrawerContext.ts +19 -0
- package/src/components/molecules/Drawer/DrawerDescription.tsx +31 -0
- package/src/components/molecules/Drawer/DrawerDragContext.ts +11 -0
- package/src/components/molecules/Drawer/DrawerFooter.tsx +49 -0
- package/src/components/molecules/Drawer/DrawerFooterContext.ts +6 -0
- package/src/components/molecules/Drawer/DrawerGrabber.tsx +62 -0
- package/src/components/molecules/Drawer/DrawerHeader.tsx +244 -0
- package/src/components/molecules/Drawer/DrawerHeaderContext.ts +13 -0
- package/src/components/molecules/Drawer/DrawerOverlay.tsx +53 -0
- package/src/components/molecules/Drawer/DrawerTitle.tsx +32 -0
- package/src/components/molecules/Drawer/index.ts +12 -0
- package/src/components/molecules/FilterTab/FilterTab.stories.tsx +210 -0
- package/src/components/molecules/FilterTab/FilterTab.tsx +310 -0
- package/src/components/molecules/FilterTab/index.ts +2 -0
- package/src/components/molecules/MessageCard/MessageCard.stories.tsx +169 -0
- package/src/components/molecules/MessageCard/MessageCard.tsx +362 -0
- package/src/components/molecules/MessageCard/index.ts +10 -0
- package/src/components/molecules/Notification/Notification.stories.tsx +219 -0
- package/src/components/molecules/Notification/Notification.tsx +426 -0
- package/src/components/molecules/Notification/index.ts +2 -0
- package/src/components/molecules/NumberField/NumberField.stories.tsx +231 -0
- package/src/components/molecules/NumberField/NumberField.tsx +186 -0
- package/src/components/molecules/NumberField/NumberFieldInput.tsx +287 -0
- package/src/components/molecules/NumberField/index.ts +2 -0
- package/src/components/molecules/PasswordField/PasswordField.stories.tsx +362 -0
- package/src/components/molecules/PasswordField/PasswordField.test.tsx +369 -0
- package/src/components/molecules/PasswordField/PasswordField.tsx +194 -0
- package/src/components/molecules/PasswordField/PasswordFieldError.tsx +53 -0
- package/src/components/molecules/PasswordField/PasswordFieldInput.tsx +73 -0
- package/src/components/molecules/PasswordField/PasswordFieldRequirements.tsx +95 -0
- package/src/components/molecules/PasswordField/hooks/index.ts +2 -0
- package/src/components/molecules/PasswordField/hooks/usePasswordField.ts +113 -0
- package/src/components/molecules/PasswordField/index.ts +10 -0
- package/src/components/molecules/PictureSelector/PictureSelector.stories.tsx +204 -0
- package/src/components/molecules/PictureSelector/PictureSelector.tsx +335 -0
- package/src/components/molecules/PictureSelector/index.ts +5 -0
- package/src/components/molecules/Progress/Progress.stories.tsx +145 -0
- package/src/components/molecules/Progress/Progress.tsx +184 -0
- package/src/components/molecules/Progress/index.ts +2 -0
- package/src/components/molecules/Radio/Radio.test.tsx +104 -0
- package/src/components/molecules/Radio/Radio.tsx +1 -2
- package/src/components/molecules/SearchField/SearchField.stories.tsx +242 -0
- package/src/components/molecules/SearchField/SearchField.test.tsx +318 -0
- package/src/components/molecules/SearchField/SearchField.tsx +143 -0
- package/src/components/molecules/SearchField/SearchFieldInput.tsx +63 -0
- package/src/components/molecules/SearchField/hooks/index.ts +1 -0
- package/src/components/molecules/SearchField/hooks/useSearchField.ts +56 -0
- package/src/components/molecules/SearchField/index.ts +4 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.stories.tsx +31 -8
- package/src/components/molecules/SegmentedControl/SegmentedControl.test.tsx +141 -0
- package/src/components/molecules/SegmentedControl/SegmentedControl.tsx +237 -23
- package/src/components/molecules/SelectField/SelectField.stories.tsx +320 -0
- package/src/components/molecules/SelectField/SelectField.test.tsx +254 -0
- package/src/components/molecules/SelectField/SelectField.tsx +236 -0
- package/src/components/molecules/SelectField/SelectFieldContent.tsx +85 -0
- package/src/components/molecules/SelectField/SelectFieldItem.tsx +133 -0
- package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +170 -0
- package/src/components/molecules/SelectField/SelectFieldValue.tsx +31 -0
- package/src/components/molecules/SelectField/hooks/index.ts +2 -0
- package/src/components/molecules/SelectField/hooks/useSelectField.ts +84 -0
- package/src/components/molecules/SelectField/index.ts +10 -0
- package/src/components/molecules/Slider/Slider.test.tsx +102 -0
- package/src/components/molecules/Slider/Slider.tsx +293 -180
- package/src/components/molecules/Tooltip/Tooltip.stories.tsx +168 -0
- package/src/components/molecules/Tooltip/Tooltip.tsx +326 -0
- package/src/components/molecules/Tooltip/index.ts +2 -0
- package/src/components/molecules/index.ts +15 -0
- package/src/test-utils.tsx +20 -0
- package/tsconfig.json +1 -1
- package/tsup.config.ts +16 -2
- package/vitest.config.ts +114 -0
- 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
|
+
}
|
|
@@ -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
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
})
|