@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,170 @@
1
+ import React, { useEffect, useRef } from "react"
2
+ import { Pressable, View, PressableProps, Animated, Easing } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import { useTheme } from "@emotion/react"
5
+ import * as SelectPrimitive from "@rn-primitives/select"
6
+ import { Slot } from "@rn-primitives/slot"
7
+ import { Icon } from "../../atoms/Icon"
8
+ import {
9
+ KeyboardArrowDown,
10
+ Error as ErrorIcon,
11
+ CheckCircle as SuccessIcon
12
+ } from "@butternutbox/pawprint-icons/core"
13
+ import type { InputState } from "../../atoms/Input/InputField"
14
+
15
+ type SelectFieldTriggerOwnProps = {
16
+ state?: InputState
17
+ leadingIcon?: React.ReactNode
18
+ children?: React.ReactNode
19
+ isOpen?: boolean
20
+ }
21
+
22
+ export type SelectFieldTriggerProps = SelectFieldTriggerOwnProps &
23
+ Omit<PressableProps, keyof SelectFieldTriggerOwnProps>
24
+
25
+ const parseTokenValue = (value: string): number => parseFloat(value)
26
+
27
+ const StyledTriggerWrapper = styled(Animated.View)<{ state: InputState }>(({
28
+ theme,
29
+ state: _state
30
+ }) => {
31
+ const { spacing, colour, borderRadius, size } = theme.tokens.components.inputs
32
+
33
+ return {
34
+ flexDirection: "row",
35
+ alignItems: "center",
36
+ gap: parseTokenValue(spacing.field.gap),
37
+ minWidth: parseTokenValue(size.field.minWidth),
38
+ width: "100%",
39
+ height: parseTokenValue(size.field.height),
40
+ backgroundColor: colour.field.background.default,
41
+ borderRadius: parseTokenValue(borderRadius.field.default)
42
+ }
43
+ })
44
+
45
+ const StyledIconWrapper = styled(View)({
46
+ flexShrink: 0,
47
+ alignItems: "center",
48
+ justifyContent: "center"
49
+ })
50
+
51
+ const StyledArrowIcon = styled(Animated.View)({
52
+ flexShrink: 0,
53
+ alignItems: "center",
54
+ justifyContent: "center"
55
+ })
56
+
57
+ export const SelectFieldTrigger = React.forwardRef<
58
+ View,
59
+ SelectFieldTriggerProps
60
+ >(
61
+ (
62
+ { state = "default", leadingIcon, children, isOpen = false, ...rest },
63
+ ref
64
+ ) => {
65
+ const theme = useTheme()
66
+ const { colour, borderWidth, spacing } = theme.tokens.components.inputs
67
+
68
+ const hasState = state === "error" || state === "success"
69
+
70
+ const baseBorderWidth = parseTokenValue(borderWidth.field.default)
71
+ const selectedBorderWidth = parseTokenValue(borderWidth.field.selected)
72
+ const baseVerticalPadding = parseTokenValue(spacing.field.verticalPadding)
73
+ const baseHorizontalPadding = parseTokenValue(
74
+ spacing.field.horizontalPadding
75
+ )
76
+ const borderDelta = selectedBorderWidth - baseBorderWidth
77
+
78
+ const borderColor =
79
+ state === "error"
80
+ ? colour.field.border.error
81
+ : state === "success"
82
+ ? colour.field.border.success
83
+ : isOpen
84
+ ? colour.field.border.selected
85
+ : colour.field.border.default
86
+
87
+ const isActiveBorder = isOpen || hasState
88
+ const borderAnim = useRef(
89
+ new Animated.Value(isActiveBorder ? 1 : 0)
90
+ ).current
91
+ const rotationAnim = useRef(new Animated.Value(isOpen ? 1 : 0)).current
92
+
93
+ useEffect(() => {
94
+ Animated.parallel([
95
+ Animated.timing(borderAnim, {
96
+ toValue: isActiveBorder ? 1 : 0,
97
+ duration: 50,
98
+ easing: Easing.ease,
99
+ useNativeDriver: false
100
+ }),
101
+ Animated.timing(rotationAnim, {
102
+ toValue: isOpen ? 1 : 0,
103
+ duration: 250,
104
+ easing: Easing.bezier(0.4, 0.0, 0.2, 1),
105
+ useNativeDriver: true
106
+ })
107
+ ]).start()
108
+ }, [isActiveBorder, isOpen, borderAnim, rotationAnim])
109
+
110
+ const animatedBorderWidth = borderAnim.interpolate({
111
+ inputRange: [0, 1],
112
+ outputRange: [baseBorderWidth, selectedBorderWidth]
113
+ })
114
+
115
+ const animatedPaddingVertical = borderAnim.interpolate({
116
+ inputRange: [0, 1],
117
+ outputRange: [baseVerticalPadding, baseVerticalPadding - borderDelta]
118
+ })
119
+
120
+ const animatedPaddingHorizontal = borderAnim.interpolate({
121
+ inputRange: [0, 1],
122
+ outputRange: [baseHorizontalPadding, baseHorizontalPadding - borderDelta]
123
+ })
124
+
125
+ const animatedRotation = rotationAnim.interpolate({
126
+ inputRange: [0, 1],
127
+ outputRange: ["0deg", "180deg"]
128
+ })
129
+
130
+ const stateIcon =
131
+ state === "error" ? (
132
+ <Icon icon={ErrorIcon} size="md" colour="error" />
133
+ ) : state === "success" ? (
134
+ <Icon icon={SuccessIcon} size="md" colour="success" />
135
+ ) : null
136
+
137
+ return (
138
+ <SelectPrimitive.Trigger asChild>
139
+ <Slot ref={ref}>
140
+ <Pressable {...rest}>
141
+ <StyledTriggerWrapper
142
+ state={state}
143
+ style={[
144
+ {
145
+ borderColor,
146
+ borderWidth: animatedBorderWidth,
147
+ paddingVertical: animatedPaddingVertical,
148
+ paddingHorizontal: animatedPaddingHorizontal
149
+ }
150
+ ]}
151
+ >
152
+ {leadingIcon && (
153
+ <StyledIconWrapper>{leadingIcon}</StyledIconWrapper>
154
+ )}
155
+ {children}
156
+ {stateIcon && <StyledIconWrapper>{stateIcon}</StyledIconWrapper>}
157
+ <StyledArrowIcon
158
+ style={{ transform: [{ rotate: animatedRotation }] }}
159
+ >
160
+ <Icon icon={KeyboardArrowDown} size="md" colour="primary" />
161
+ </StyledArrowIcon>
162
+ </StyledTriggerWrapper>
163
+ </Pressable>
164
+ </Slot>
165
+ </SelectPrimitive.Trigger>
166
+ )
167
+ }
168
+ )
169
+
170
+ SelectFieldTrigger.displayName = "SelectField.Trigger"
@@ -0,0 +1,31 @@
1
+ import React from "react"
2
+ import { Text, TextProps } from "react-native"
3
+ import styled from "@emotion/native"
4
+ import * as SelectPrimitive from "@rn-primitives/select"
5
+
6
+ type SelectFieldValueProps = {
7
+ placeholder?: string
8
+ } & Omit<TextProps, "children">
9
+
10
+ const StyledValue = styled(Text)(({ theme }) => {
11
+ const { colour } = theme.tokens.components.inputs
12
+
13
+ return {
14
+ flex: 1,
15
+ minWidth: 0,
16
+ textAlign: "left",
17
+ color: colour.field.text.default
18
+ }
19
+ })
20
+
21
+ export const SelectFieldValue = React.forwardRef<Text, SelectFieldValueProps>(
22
+ ({ placeholder, ...rest }, ref) => {
23
+ return (
24
+ <StyledValue ref={ref} {...rest}>
25
+ <SelectPrimitive.Value placeholder={placeholder ?? ""} />
26
+ </StyledValue>
27
+ )
28
+ }
29
+ )
30
+
31
+ SelectFieldValue.displayName = "SelectField.Value"
@@ -0,0 +1,2 @@
1
+ export { useSelectField } from "./useSelectField"
2
+ export type { SelectValidationRule } from "./useSelectField"
@@ -0,0 +1,84 @@
1
+ import { useState, useMemo } from "react"
2
+ import type { SelectFieldProps } from "../SelectField"
3
+ import type { InputState } from "../../../atoms/Input/InputField"
4
+ import type { Option } from "@rn-primitives/select"
5
+
6
+ export type SelectValidationRule<T = string | Option> = {
7
+ test: (value: T | null) => boolean
8
+ message: string
9
+ }
10
+
11
+ type SelectOptions<T = string | Option> = {
12
+ initialValue?: T | null
13
+ onValueChange?: (value: T | null) => void
14
+ validationRule?: SelectValidationRule<T>
15
+ }
16
+
17
+ type SelectReturn<T = string | Option> = {
18
+ value: T | null
19
+ onValueChange: (value: T | null) => void
20
+ state?: InputState
21
+ error?: string
22
+ }
23
+
24
+ /**
25
+ * Hook for managing SelectField state with built-in validation.
26
+ * Handles value, validation state, and error message automatically.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * // Simple usage without validation
31
+ * const selectProps = useSelectField()
32
+ * <SelectField {...selectProps} label="Country" />
33
+ *
34
+ * // With validation rule
35
+ * const selectProps = useSelectField({
36
+ * validationRule: {
37
+ * test: (v) => v !== null,
38
+ * message: "Please select an option"
39
+ * }
40
+ * })
41
+ * <SelectField {...selectProps} label="Category" />
42
+ * ```
43
+ */
44
+ export function useSelectField<T = string | Option>(
45
+ options: SelectOptions<T> = {}
46
+ ): SelectReturn<T> {
47
+ const {
48
+ initialValue,
49
+ onValueChange: onValueChangeCallback,
50
+ validationRule
51
+ } = options
52
+
53
+ const [value, setValue] = useState<T | null>(initialValue ?? null)
54
+
55
+ const handleValueChange: SelectFieldProps<T>["onValueChange"] = (
56
+ newValue
57
+ ) => {
58
+ const typedValue = Array.isArray(newValue) ? newValue[0] : newValue
59
+ setValue(typedValue ?? null)
60
+ onValueChangeCallback?.(typedValue as any)
61
+ }
62
+
63
+ const { state, error } = useMemo(() => {
64
+ if (!validationRule) {
65
+ return { state: undefined, error: undefined }
66
+ }
67
+
68
+ if (value === null || value === initialValue) {
69
+ return { state: "default" as InputState, error: undefined }
70
+ }
71
+
72
+ const isValid = validationRule.test(value)
73
+ return {
74
+ state: (isValid ? "success" : "error") as InputState,
75
+ error: isValid ? undefined : validationRule.message
76
+ }
77
+ }, [value, validationRule, initialValue])
78
+
79
+ return {
80
+ value,
81
+ onValueChange: handleValueChange,
82
+ ...(validationRule && { state, error })
83
+ }
84
+ }
@@ -0,0 +1,10 @@
1
+ export { SelectField } from "./SelectField"
2
+ export type { SelectFieldProps } from "./SelectField"
3
+ export { useSelectField } from "./hooks"
4
+ export type { SelectValidationRule } from "./hooks"
5
+ export { SelectFieldTrigger } from "./SelectFieldTrigger"
6
+ export type { SelectFieldTriggerProps } from "./SelectFieldTrigger"
7
+ export { SelectFieldValue } from "./SelectFieldValue"
8
+ export { SelectFieldContent } from "./SelectFieldContent"
9
+ export { SelectFieldItem } from "./SelectFieldItem"
10
+ export type { SelectFieldItemProps } from "./SelectFieldItem"
@@ -0,0 +1,102 @@
1
+ import React from "react"
2
+ import { screen } from "@testing-library/react"
3
+ import { describe, it, expect } from "vitest"
4
+ import { renderWithTheme } from "../../../test-utils"
5
+ import { Slider } from "./Slider"
6
+
7
+ describe("Slider", () => {
8
+ describe("when component is rendering", () => {
9
+ it("renders without crashing", () => {
10
+ const { container } = renderWithTheme(<Slider />)
11
+ expect(container.firstChild).toBeTruthy()
12
+ })
13
+
14
+ it("renders with slider role", () => {
15
+ renderWithTheme(<Slider />)
16
+ expect(screen.getByRole("slider")).toBeInTheDocument()
17
+ })
18
+ })
19
+
20
+ describe("when rendering labels", () => {
21
+ it("renders leading label", () => {
22
+ renderWithTheme(<Slider leadingLabel="0kg" />)
23
+ expect(screen.getByText("0kg")).toBeInTheDocument()
24
+ })
25
+
26
+ it("renders trailing label", () => {
27
+ renderWithTheme(<Slider trailingLabel="50kg+" />)
28
+ expect(screen.getByText("50kg+")).toBeInTheDocument()
29
+ })
30
+
31
+ it("renders both labels", () => {
32
+ renderWithTheme(<Slider leadingLabel="0kg" trailingLabel="50kg+" />)
33
+ expect(screen.getByText("0kg")).toBeInTheDocument()
34
+ expect(screen.getByText("50kg+")).toBeInTheDocument()
35
+ })
36
+ })
37
+
38
+ describe("when rendering description", () => {
39
+ it("renders description text", () => {
40
+ renderWithTheme(<Slider description="Drag to select weight" />)
41
+ expect(screen.getByText("Drag to select weight")).toBeInTheDocument()
42
+ })
43
+ })
44
+
45
+ describe("when rendering icons", () => {
46
+ it("renders leading icon", () => {
47
+ renderWithTheme(
48
+ <Slider
49
+ leadingIcon={<span data-testid="lead-icon">-</span>}
50
+ leadingLabel="Min"
51
+ />
52
+ )
53
+ expect(screen.getByTestId("lead-icon")).toBeInTheDocument()
54
+ })
55
+
56
+ it("renders trailing icon", () => {
57
+ renderWithTheme(
58
+ <Slider
59
+ trailingIcon={<span data-testid="trail-icon">+</span>}
60
+ trailingLabel="Max"
61
+ />
62
+ )
63
+ expect(screen.getByTestId("trail-icon")).toBeInTheDocument()
64
+ })
65
+ })
66
+
67
+ describe("when rendering with value props", () => {
68
+ it("renders with defaultValue", () => {
69
+ renderWithTheme(<Slider defaultValue={25} />)
70
+ expect(screen.getByRole("slider")).toHaveAttribute("aria-valuenow", "25")
71
+ })
72
+
73
+ it("renders with controlled value", () => {
74
+ renderWithTheme(<Slider value={50} />)
75
+ expect(screen.getByRole("slider")).toHaveAttribute("aria-valuenow", "50")
76
+ })
77
+
78
+ it("renders with min and max", () => {
79
+ renderWithTheme(<Slider min={10} max={200} />)
80
+ expect(screen.getByRole("slider")).toHaveAttribute("aria-valuemin", "10")
81
+ expect(screen.getByRole("slider")).toHaveAttribute("aria-valuemax", "200")
82
+ })
83
+ })
84
+
85
+ describe("when disabled", () => {
86
+ it("sets disabled aria attribute", () => {
87
+ renderWithTheme(<Slider disabled />)
88
+ expect(screen.getByRole("slider")).toHaveAttribute(
89
+ "aria-disabled",
90
+ "true"
91
+ )
92
+ })
93
+ })
94
+
95
+ describe("when using with ref", () => {
96
+ it("forwards ref", () => {
97
+ const ref = React.createRef<any>()
98
+ renderWithTheme(<Slider ref={ref} />)
99
+ expect(ref.current).toBeTruthy()
100
+ })
101
+ })
102
+ })