@webority-technologies/mobile 0.0.22 → 0.0.23

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 (181) hide show
  1. package/lib/commonjs/components/Accordion/Accordion.js +4 -2
  2. package/lib/commonjs/components/Avatar/Avatar.js +4 -2
  3. package/lib/commonjs/components/Badge/Badge.js +5 -5
  4. package/lib/commonjs/components/Banner/Banner.js +8 -4
  5. package/lib/commonjs/components/BottomNavigation/BottomNavigation.js +6 -4
  6. package/lib/commonjs/components/BottomSheet/BottomSheet.js +8 -9
  7. package/lib/commonjs/components/Box/Box.js +162 -0
  8. package/lib/commonjs/components/Box/index.js +37 -0
  9. package/lib/commonjs/components/Button/Button.js +7 -7
  10. package/lib/commonjs/components/Carousel/Carousel.js +4 -2
  11. package/lib/commonjs/components/Checkbox/Checkbox.js +14 -5
  12. package/lib/commonjs/components/DatePicker/DatePicker.js +9 -7
  13. package/lib/commonjs/components/DateRangePicker/DateRangePicker.js +5 -2
  14. package/lib/commonjs/components/Dialog/Dialog.js +2 -2
  15. package/lib/commonjs/components/FieldBase/FieldBase.js +8 -4
  16. package/lib/commonjs/components/FloatingActionButton/FloatingActionButton.js +13 -5
  17. package/lib/commonjs/components/FormField/FormField.js +61 -25
  18. package/lib/commonjs/components/Input/Input.js +41 -29
  19. package/lib/commonjs/components/KeyboardAwareScrollView/KeyboardAwareScrollView.js +102 -0
  20. package/lib/commonjs/components/KeyboardAwareScrollView/index.js +13 -0
  21. package/lib/commonjs/components/KeyboardToolbar/KeyboardToolbar.js +130 -0
  22. package/lib/commonjs/components/KeyboardToolbar/index.js +13 -0
  23. package/lib/commonjs/components/Modal/Modal.js +17 -6
  24. package/lib/commonjs/components/NumberInput/NumberInput.js +35 -28
  25. package/lib/commonjs/components/OTPInput/OTPInput.js +33 -18
  26. package/lib/commonjs/components/Radio/Radio.js +7 -5
  27. package/lib/commonjs/components/Radio/RadioGroup.js +10 -3
  28. package/lib/commonjs/components/SearchBar/SearchBar.js +4 -2
  29. package/lib/commonjs/components/SegmentedControl/SegmentedControl.js +20 -10
  30. package/lib/commonjs/components/Select/Select.js +33 -32
  31. package/lib/commonjs/components/Skeleton/SkeletonContent.js +5 -2
  32. package/lib/commonjs/components/Slider/Slider.js +42 -26
  33. package/lib/commonjs/components/Spinner/Spinner.js +5 -5
  34. package/lib/commonjs/components/Switch/Switch.js +29 -16
  35. package/lib/commonjs/components/Tabs/Tabs.js +4 -2
  36. package/lib/commonjs/components/Text/Text.js +142 -0
  37. package/lib/commonjs/components/Text/index.js +13 -0
  38. package/lib/commonjs/components/TimePicker/TimePicker.js +10 -7
  39. package/lib/commonjs/components/Toast/Toast.js +22 -10
  40. package/lib/commonjs/components/Tooltip/Tooltip.js +6 -2
  41. package/lib/commonjs/components/index.js +135 -89
  42. package/lib/commonjs/form/FormContext.js +40 -0
  43. package/lib/commonjs/form/index.js +68 -0
  44. package/lib/commonjs/form/path.js +79 -0
  45. package/lib/commonjs/form/rules.js +67 -0
  46. package/lib/commonjs/form/types.js +2 -0
  47. package/lib/commonjs/form/useField.js +54 -0
  48. package/lib/commonjs/form/useForm.js +316 -0
  49. package/lib/commonjs/hooks/index.js +14 -0
  50. package/lib/commonjs/hooks/useControllableState.js +30 -0
  51. package/lib/commonjs/hooks/useReducedMotion.js +31 -0
  52. package/lib/commonjs/index.js +96 -11
  53. package/lib/commonjs/theme/ThemeContext.js +30 -2
  54. package/lib/commonjs/theme/tokens.js +12 -0
  55. package/lib/module/components/Accordion/Accordion.js +4 -2
  56. package/lib/module/components/Avatar/Avatar.js +4 -2
  57. package/lib/module/components/Badge/Badge.js +5 -5
  58. package/lib/module/components/Banner/Banner.js +8 -4
  59. package/lib/module/components/BottomNavigation/BottomNavigation.js +6 -4
  60. package/lib/module/components/BottomSheet/BottomSheet.js +8 -9
  61. package/lib/module/components/Box/Box.js +156 -0
  62. package/lib/module/components/Box/index.js +4 -0
  63. package/lib/module/components/Button/Button.js +7 -7
  64. package/lib/module/components/Carousel/Carousel.js +4 -2
  65. package/lib/module/components/Checkbox/Checkbox.js +14 -5
  66. package/lib/module/components/DatePicker/DatePicker.js +9 -7
  67. package/lib/module/components/DateRangePicker/DateRangePicker.js +5 -2
  68. package/lib/module/components/Dialog/Dialog.js +2 -2
  69. package/lib/module/components/FieldBase/FieldBase.js +8 -4
  70. package/lib/module/components/FloatingActionButton/FloatingActionButton.js +13 -5
  71. package/lib/module/components/FormField/FormField.js +62 -26
  72. package/lib/module/components/Input/Input.js +41 -29
  73. package/lib/module/components/KeyboardAwareScrollView/KeyboardAwareScrollView.js +98 -0
  74. package/lib/module/components/KeyboardAwareScrollView/index.js +4 -0
  75. package/lib/module/components/KeyboardToolbar/KeyboardToolbar.js +125 -0
  76. package/lib/module/components/KeyboardToolbar/index.js +4 -0
  77. package/lib/module/components/Modal/Modal.js +17 -6
  78. package/lib/module/components/NumberInput/NumberInput.js +30 -23
  79. package/lib/module/components/OTPInput/OTPInput.js +30 -15
  80. package/lib/module/components/Radio/Radio.js +7 -5
  81. package/lib/module/components/Radio/RadioGroup.js +10 -3
  82. package/lib/module/components/SearchBar/SearchBar.js +4 -2
  83. package/lib/module/components/SegmentedControl/SegmentedControl.js +20 -10
  84. package/lib/module/components/Select/Select.js +33 -32
  85. package/lib/module/components/Skeleton/SkeletonContent.js +5 -2
  86. package/lib/module/components/Slider/Slider.js +42 -26
  87. package/lib/module/components/Spinner/Spinner.js +5 -5
  88. package/lib/module/components/Switch/Switch.js +29 -16
  89. package/lib/module/components/Tabs/Tabs.js +4 -2
  90. package/lib/module/components/Text/Text.js +138 -0
  91. package/lib/module/components/Text/index.js +4 -0
  92. package/lib/module/components/TimePicker/TimePicker.js +10 -7
  93. package/lib/module/components/Toast/Toast.js +22 -10
  94. package/lib/module/components/Tooltip/Tooltip.js +6 -2
  95. package/lib/module/components/index.js +4 -0
  96. package/lib/module/form/FormContext.js +32 -0
  97. package/lib/module/form/index.js +12 -0
  98. package/lib/module/form/path.js +72 -0
  99. package/lib/module/form/rules.js +52 -0
  100. package/lib/module/form/types.js +2 -0
  101. package/lib/module/form/useField.js +49 -0
  102. package/lib/module/form/useForm.js +312 -0
  103. package/lib/module/hooks/index.js +2 -0
  104. package/lib/module/hooks/useControllableState.js +26 -0
  105. package/lib/module/hooks/useReducedMotion.js +27 -0
  106. package/lib/module/index.js +3 -1
  107. package/lib/module/theme/ThemeContext.js +30 -2
  108. package/lib/module/theme/tokens.js +12 -0
  109. package/lib/typescript/commonjs/components/BottomNavigation/BottomNavigation.d.ts +1 -1
  110. package/lib/typescript/commonjs/components/Box/Box.d.ts +60 -0
  111. package/lib/typescript/commonjs/components/Box/index.d.ts +3 -0
  112. package/lib/typescript/commonjs/components/Button/Button.d.ts +1 -1
  113. package/lib/typescript/commonjs/components/Checkbox/Checkbox.d.ts +3 -2
  114. package/lib/typescript/commonjs/components/DatePicker/DatePicker.d.ts +3 -3
  115. package/lib/typescript/commonjs/components/Dialog/Dialog.d.ts +2 -2
  116. package/lib/typescript/commonjs/components/FormField/FormField.d.ts +13 -2
  117. package/lib/typescript/commonjs/components/KeyboardAwareScrollView/KeyboardAwareScrollView.d.ts +20 -0
  118. package/lib/typescript/commonjs/components/KeyboardAwareScrollView/index.d.ts +3 -0
  119. package/lib/typescript/commonjs/components/KeyboardToolbar/KeyboardToolbar.d.ts +29 -0
  120. package/lib/typescript/commonjs/components/KeyboardToolbar/index.d.ts +3 -0
  121. package/lib/typescript/commonjs/components/NumberInput/NumberInput.d.ts +3 -2
  122. package/lib/typescript/commonjs/components/OTPInput/OTPInput.d.ts +3 -2
  123. package/lib/typescript/commonjs/components/Radio/Radio.d.ts +2 -2
  124. package/lib/typescript/commonjs/components/Radio/RadioGroup.d.ts +3 -2
  125. package/lib/typescript/commonjs/components/SegmentedControl/SegmentedControl.d.ts +3 -2
  126. package/lib/typescript/commonjs/components/Slider/Slider.d.ts +6 -4
  127. package/lib/typescript/commonjs/components/Spinner/Spinner.d.ts +1 -1
  128. package/lib/typescript/commonjs/components/Switch/Switch.d.ts +3 -2
  129. package/lib/typescript/commonjs/components/Text/Text.d.ts +25 -0
  130. package/lib/typescript/commonjs/components/Text/index.d.ts +3 -0
  131. package/lib/typescript/commonjs/components/TimePicker/TimePicker.d.ts +3 -3
  132. package/lib/typescript/commonjs/components/index.d.ts +8 -0
  133. package/lib/typescript/commonjs/form/FormContext.d.ts +17 -0
  134. package/lib/typescript/commonjs/form/index.d.ts +9 -0
  135. package/lib/typescript/commonjs/form/path.d.ts +10 -0
  136. package/lib/typescript/commonjs/form/rules.d.ts +31 -0
  137. package/lib/typescript/commonjs/form/types.d.ts +94 -0
  138. package/lib/typescript/commonjs/form/useField.d.ts +27 -0
  139. package/lib/typescript/commonjs/form/useForm.d.ts +10 -0
  140. package/lib/typescript/commonjs/hooks/index.d.ts +3 -0
  141. package/lib/typescript/commonjs/hooks/useControllableState.d.ts +17 -0
  142. package/lib/typescript/commonjs/hooks/useReducedMotion.d.ts +8 -0
  143. package/lib/typescript/commonjs/index.d.ts +4 -2
  144. package/lib/typescript/commonjs/theme/types.d.ts +15 -0
  145. package/lib/typescript/module/components/BottomNavigation/BottomNavigation.d.ts +1 -1
  146. package/lib/typescript/module/components/Box/Box.d.ts +60 -0
  147. package/lib/typescript/module/components/Box/index.d.ts +3 -0
  148. package/lib/typescript/module/components/Button/Button.d.ts +1 -1
  149. package/lib/typescript/module/components/Checkbox/Checkbox.d.ts +3 -2
  150. package/lib/typescript/module/components/DatePicker/DatePicker.d.ts +3 -3
  151. package/lib/typescript/module/components/Dialog/Dialog.d.ts +2 -2
  152. package/lib/typescript/module/components/FormField/FormField.d.ts +13 -2
  153. package/lib/typescript/module/components/KeyboardAwareScrollView/KeyboardAwareScrollView.d.ts +20 -0
  154. package/lib/typescript/module/components/KeyboardAwareScrollView/index.d.ts +3 -0
  155. package/lib/typescript/module/components/KeyboardToolbar/KeyboardToolbar.d.ts +29 -0
  156. package/lib/typescript/module/components/KeyboardToolbar/index.d.ts +3 -0
  157. package/lib/typescript/module/components/NumberInput/NumberInput.d.ts +3 -2
  158. package/lib/typescript/module/components/OTPInput/OTPInput.d.ts +3 -2
  159. package/lib/typescript/module/components/Radio/Radio.d.ts +2 -2
  160. package/lib/typescript/module/components/Radio/RadioGroup.d.ts +3 -2
  161. package/lib/typescript/module/components/SegmentedControl/SegmentedControl.d.ts +3 -2
  162. package/lib/typescript/module/components/Slider/Slider.d.ts +6 -4
  163. package/lib/typescript/module/components/Spinner/Spinner.d.ts +1 -1
  164. package/lib/typescript/module/components/Switch/Switch.d.ts +3 -2
  165. package/lib/typescript/module/components/Text/Text.d.ts +25 -0
  166. package/lib/typescript/module/components/Text/index.d.ts +3 -0
  167. package/lib/typescript/module/components/TimePicker/TimePicker.d.ts +3 -3
  168. package/lib/typescript/module/components/index.d.ts +8 -0
  169. package/lib/typescript/module/form/FormContext.d.ts +17 -0
  170. package/lib/typescript/module/form/index.d.ts +9 -0
  171. package/lib/typescript/module/form/path.d.ts +10 -0
  172. package/lib/typescript/module/form/rules.d.ts +31 -0
  173. package/lib/typescript/module/form/types.d.ts +94 -0
  174. package/lib/typescript/module/form/useField.d.ts +27 -0
  175. package/lib/typescript/module/form/useForm.d.ts +10 -0
  176. package/lib/typescript/module/hooks/index.d.ts +3 -0
  177. package/lib/typescript/module/hooks/useControllableState.d.ts +17 -0
  178. package/lib/typescript/module/hooks/useReducedMotion.d.ts +8 -0
  179. package/lib/typescript/module/index.d.ts +4 -2
  180. package/lib/typescript/module/theme/types.d.ts +15 -0
  181. package/package.json +1 -1
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+
3
+ import React, { useEffect, useState } from 'react';
4
+ import { Keyboard, Platform, Pressable, StyleSheet, View } from 'react-native';
5
+ import { useTheme } from "../../theme/index.js";
6
+ import { useOptionalFormContext } from "../../form/FormContext.js";
7
+ import { Text } from "../Text/index.js";
8
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
+ const NavButton = ({
10
+ label,
11
+ accessibilityLabel,
12
+ onPress,
13
+ theme
14
+ }) => /*#__PURE__*/_jsx(Pressable, {
15
+ onPress: onPress,
16
+ disabled: !onPress,
17
+ accessibilityRole: "button",
18
+ accessibilityLabel: accessibilityLabel,
19
+ hitSlop: 8,
20
+ style: {
21
+ paddingHorizontal: theme.spacing.sm,
22
+ opacity: onPress ? 1 : 0.4
23
+ },
24
+ children: /*#__PURE__*/_jsx(Text, {
25
+ variant: "h2",
26
+ color: onPress ? 'link' : 'disabled',
27
+ children: label
28
+ })
29
+ });
30
+
31
+ /**
32
+ * An accessory bar pinned just above the keyboard — pure RN, no native
33
+ * dependency. Shows a Done (dismiss) button and optional prev/next chevrons
34
+ * that, inside a `<Form>`, walk the registered field order. Render it at the
35
+ * screen root (a `flex: 1` ancestor) so its `bottom: keyboardHeight` offset
36
+ * lands it on top of the keyboard. Renders nothing while the keyboard is hidden.
37
+ */
38
+ const KeyboardToolbar = props => {
39
+ const {
40
+ doneLabel = 'Done',
41
+ showNavigation,
42
+ onDone,
43
+ onNext,
44
+ onPrev,
45
+ leading,
46
+ style,
47
+ testID
48
+ } = props;
49
+ const theme = useTheme();
50
+ const form = useOptionalFormContext();
51
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
52
+ useEffect(() => {
53
+ const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
54
+ const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
55
+ const showSub = Keyboard.addListener(showEvent, e => setKeyboardHeight(e.endCoordinates?.height ?? 0));
56
+ const hideSub = Keyboard.addListener(hideEvent, () => setKeyboardHeight(0));
57
+ return () => {
58
+ showSub.remove();
59
+ hideSub.remove();
60
+ };
61
+ }, []);
62
+ if (keyboardHeight <= 0) return null;
63
+ const handleDone = onDone ?? (() => Keyboard.dismiss());
64
+ const handlePrev = onPrev ?? form?.focusPrev;
65
+ const handleNext = onNext ?? form?.focusNext;
66
+ const navEnabled = showNavigation ?? Boolean(handlePrev || handleNext);
67
+ return /*#__PURE__*/_jsxs(View, {
68
+ testID: testID,
69
+ accessibilityRole: "toolbar",
70
+ style: [styles.bar, {
71
+ bottom: keyboardHeight,
72
+ backgroundColor: theme.colors.background.elevated,
73
+ borderTopColor: theme.colors.border.primary,
74
+ paddingHorizontal: theme.spacing.md
75
+ }, style],
76
+ children: [navEnabled ? /*#__PURE__*/_jsxs(View, {
77
+ style: styles.nav,
78
+ children: [/*#__PURE__*/_jsx(NavButton, {
79
+ label: "\u2039",
80
+ accessibilityLabel: "Previous field",
81
+ onPress: handlePrev,
82
+ theme: theme
83
+ }), /*#__PURE__*/_jsx(NavButton, {
84
+ label: "\u203A",
85
+ accessibilityLabel: "Next field",
86
+ onPress: handleNext,
87
+ theme: theme
88
+ })]
89
+ }) : /*#__PURE__*/_jsx(View, {}), leading, /*#__PURE__*/_jsx(View, {
90
+ style: styles.spacer
91
+ }), /*#__PURE__*/_jsx(Pressable, {
92
+ onPress: handleDone,
93
+ accessibilityRole: "button",
94
+ accessibilityLabel: doneLabel,
95
+ hitSlop: 8,
96
+ children: /*#__PURE__*/_jsx(Text, {
97
+ variant: "body",
98
+ weight: "semibold",
99
+ color: "link",
100
+ children: doneLabel
101
+ })
102
+ })]
103
+ });
104
+ };
105
+ const styles = StyleSheet.create({
106
+ bar: {
107
+ position: 'absolute',
108
+ left: 0,
109
+ right: 0,
110
+ height: 44,
111
+ flexDirection: 'row',
112
+ alignItems: 'center',
113
+ borderTopWidth: StyleSheet.hairlineWidth
114
+ },
115
+ nav: {
116
+ flexDirection: 'row',
117
+ alignItems: 'center'
118
+ },
119
+ spacer: {
120
+ flex: 1
121
+ }
122
+ });
123
+ export { KeyboardToolbar };
124
+ export default KeyboardToolbar;
125
+ //# sourceMappingURL=KeyboardToolbar.js.map
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export { KeyboardToolbar } from "./KeyboardToolbar.js";
4
+ //# sourceMappingURL=index.js.map
@@ -39,6 +39,7 @@ const Modal = /*#__PURE__*/forwardRef((props, ref) => {
39
39
  const wasVisibleRef = useRef(false);
40
40
  const styles = useMemo(() => buildStyles(theme), [theme]);
41
41
  useEffect(() => {
42
+ const running = [];
42
43
  if (visible) {
43
44
  // Two Fabric quirks force this combination: backdrop View needs
44
45
  // `collapsable={false}` (see render) so flattening doesn't drop it,
@@ -46,21 +47,25 @@ const Modal = /*#__PURE__*/forwardRef((props, ref) => {
46
47
  // Animated.View that also sets backgroundColor inline renders as 0 on
47
48
  // RN 0.85 / Fabric / iOS, leaving the dim layer invisible. Single
48
49
  // 240ms tween — JS thread cost is negligible.
49
- Animated.timing(backdropAnim, {
50
+ const backdrop = Animated.timing(backdropAnim, {
50
51
  toValue: 1,
51
52
  duration,
52
53
  useNativeDriver: false
53
- }).start();
54
+ });
55
+ backdrop.start();
56
+ running.push(backdrop);
54
57
  if (presentation === 'bottom') {
55
- Animated.spring(translateYAnim, {
58
+ const slide = Animated.spring(translateYAnim, {
56
59
  toValue: 0,
57
60
  damping: theme.motion.spring.gentle.damping,
58
61
  stiffness: theme.motion.spring.gentle.stiffness,
59
62
  mass: theme.motion.spring.gentle.mass,
60
63
  useNativeDriver: true
61
- }).start();
64
+ });
65
+ slide.start();
66
+ running.push(slide);
62
67
  } else {
63
- Animated.parallel([Animated.spring(scaleAnim, {
68
+ const enter = Animated.parallel([Animated.spring(scaleAnim, {
64
69
  toValue: 1,
65
70
  damping: theme.motion.spring.gentle.damping,
66
71
  stiffness: theme.motion.spring.gentle.stiffness,
@@ -70,7 +75,9 @@ const Modal = /*#__PURE__*/forwardRef((props, ref) => {
70
75
  toValue: 1,
71
76
  duration,
72
77
  useNativeDriver: true
73
- })]).start();
78
+ })]);
79
+ enter.start();
80
+ running.push(enter);
74
81
  }
75
82
  } else {
76
83
  // backdropAnim is JS-driven (see note above) — reset directly so a
@@ -80,6 +87,10 @@ const Modal = /*#__PURE__*/forwardRef((props, ref) => {
80
87
  setNativeValue(opacityAnim, 0);
81
88
  setNativeValue(translateYAnim, screenHeight);
82
89
  }
90
+ // Stop in-flight tweens on unmount / visibility flip so an enter animation
91
+ // never resolves against a torn-down tree (fixes the jest async leak and
92
+ // the equivalent post-unmount work in the real app).
93
+ return () => running.forEach(animation => animation.stop());
83
94
  }, [visible, presentation, duration, backdropAnim, scaleAnim, opacityAnim, translateYAnim, screenHeight, theme.motion.spring.gentle.damping, theme.motion.spring.gentle.stiffness, theme.motion.spring.gentle.mass, scaleStartValue]);
84
95
 
85
96
  // Accessibility focus trap: when the modal opens, push screen-reader focus
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
5
+ import { useControllableState } from "../../hooks/index.js";
5
6
  import { useTheme } from "../../theme/index.js";
6
7
  import { triggerHaptic } from "../../utils/index.js";
7
8
  import { FieldBase, resolveFieldSize, resolveFieldTextStyle } from "../FieldBase/FieldBase.js";
@@ -43,6 +44,7 @@ const sanitizeTyped = (raw, allowDecimal) => {
43
44
  const NumberInput = /*#__PURE__*/forwardRef((props, ref) => {
44
45
  const {
45
46
  value,
47
+ defaultValue,
46
48
  onChange,
47
49
  min = Number.NEGATIVE_INFINITY,
48
50
  max = Number.POSITIVE_INFINITY,
@@ -66,6 +68,11 @@ const NumberInput = /*#__PURE__*/forwardRef((props, ref) => {
66
68
  textInputProps,
67
69
  testID
68
70
  } = props;
71
+ const [current, setCurrent] = useControllableState({
72
+ value,
73
+ defaultValue: defaultValue ?? props.min ?? 0,
74
+ onChange
75
+ });
69
76
  const theme = useTheme();
70
77
  const sizeTokens = resolveFieldSize(theme, size);
71
78
  const fieldText = resolveFieldTextStyle(theme, {
@@ -76,7 +83,7 @@ const NumberInput = /*#__PURE__*/forwardRef((props, ref) => {
76
83
  const longPressInterval = theme.components.numberInput?.longPressIntervalMs ?? DEFAULT_LONG_PRESS_INTERVAL;
77
84
  const pressHaptic = theme.components.numberInput?.pressHaptic ?? false;
78
85
  const inputRef = useRef(null);
79
- const [draft, setDraft] = useState(formatValue(value, allowDecimal, precision));
86
+ const [draft, setDraft] = useState(formatValue(current, allowDecimal, precision));
80
87
  const [editing, setEditing] = useState(false);
81
88
  const [focused, setFocused] = useState(false);
82
89
  const repeatTimeoutRef = useRef(null);
@@ -94,41 +101,41 @@ const NumberInput = /*#__PURE__*/forwardRef((props, ref) => {
94
101
  useEffect(() => clearRepeat, [clearRepeat]);
95
102
  useEffect(() => {
96
103
  if (!editing) {
97
- setDraft(formatValue(value, allowDecimal, precision));
104
+ setDraft(formatValue(current, allowDecimal, precision));
98
105
  }
99
- }, [value, allowDecimal, precision, editing]);
100
- const atMin = value <= min;
101
- const atMax = value >= max;
106
+ }, [current, allowDecimal, precision, editing]);
107
+ const atMin = current <= min;
108
+ const atMax = current >= max;
102
109
  const interactive = !disabled;
103
110
  const setExternal = useCallback(next => {
104
111
  const rounded = roundToPrecision(next, allowDecimal ? precision : 0);
105
112
  const clamped = clamp(rounded, min, max);
106
- if (clamped !== value) {
107
- onChange(clamped);
113
+ if (clamped !== current) {
114
+ setCurrent(clamped);
108
115
  }
109
- }, [allowDecimal, max, min, onChange, precision, value]);
116
+ }, [allowDecimal, max, min, setCurrent, precision, current]);
110
117
  const decrement = useCallback(() => {
111
118
  if (!interactive || atMin) return;
112
119
  if (pressHaptic) triggerHaptic('impactLight');
113
- setExternal(value - step);
114
- }, [atMin, interactive, pressHaptic, setExternal, step, value]);
120
+ setExternal(current - step);
121
+ }, [atMin, interactive, pressHaptic, setExternal, step, current]);
115
122
  const increment = useCallback(() => {
116
123
  if (!interactive || atMax) return;
117
124
  if (pressHaptic) triggerHaptic('impactLight');
118
- setExternal(value + step);
119
- }, [atMax, interactive, pressHaptic, setExternal, step, value]);
125
+ setExternal(current + step);
126
+ }, [atMax, interactive, pressHaptic, setExternal, step, current]);
120
127
  React.useImperativeHandle(ref, () => ({
121
128
  focus: () => inputRef.current?.focus(),
122
129
  blur: () => inputRef.current?.blur(),
123
130
  clear: () => {
124
131
  setDraft('');
125
132
  if (Number.isFinite(min) && min > 0) {
126
- onChange(min);
133
+ setCurrent(min);
127
134
  } else {
128
- onChange(0 < min ? min : 0 > max ? max : 0);
135
+ setCurrent(0 < min ? min : 0 > max ? max : 0);
129
136
  }
130
137
  }
131
- }), [max, min, onChange]);
138
+ }), [max, min, setCurrent]);
132
139
  const startRepeating = useCallback(direction => {
133
140
  if (!interactive) return;
134
141
  clearRepeat();
@@ -148,27 +155,27 @@ const NumberInput = /*#__PURE__*/forwardRef((props, ref) => {
148
155
  const parsed = Number(cleaned);
149
156
  if (cleaned !== '' && cleaned !== '-' && cleaned !== '.' && Number.isFinite(parsed)) {
150
157
  if (parsed >= min && parsed <= max) {
151
- if (parsed !== value) {
152
- onChange(roundToPrecision(parsed, allowDecimal ? precision : 0));
158
+ if (parsed !== current) {
159
+ setCurrent(roundToPrecision(parsed, allowDecimal ? precision : 0));
153
160
  }
154
161
  }
155
162
  }
156
- }, [allowDecimal, max, min, onChange, precision, value]);
163
+ }, [allowDecimal, max, min, setCurrent, precision, current]);
157
164
  const handleBlur = useCallback(() => {
158
165
  setEditing(false);
159
166
  setFocused(false);
160
167
  const parsed = Number(draft);
161
168
  if (draft === '' || !Number.isFinite(parsed)) {
162
- setDraft(formatValue(value, allowDecimal, precision));
169
+ setDraft(formatValue(current, allowDecimal, precision));
163
170
  return;
164
171
  }
165
172
  const rounded = roundToPrecision(parsed, allowDecimal ? precision : 0);
166
173
  const clamped = clamp(rounded, min, max);
167
- if (clamped !== value) {
168
- onChange(clamped);
174
+ if (clamped !== current) {
175
+ setCurrent(clamped);
169
176
  }
170
177
  setDraft(formatValue(clamped, allowDecimal, precision));
171
- }, [allowDecimal, draft, max, min, onChange, precision, value]);
178
+ }, [allowDecimal, draft, max, min, setCurrent, precision, current]);
172
179
  const handleFocus = useCallback(() => {
173
180
  setEditing(true);
174
181
  setFocused(true);
@@ -232,7 +239,7 @@ const NumberInput = /*#__PURE__*/forwardRef((props, ref) => {
232
239
  accessibilityRole: "adjustable",
233
240
  accessibilityLabel: accessibleInputLabel,
234
241
  accessibilityValue: {
235
- now: Number.isFinite(value) ? value : 0
242
+ now: Number.isFinite(current) ? current : 0
236
243
  },
237
244
  accessibilityState: {
238
245
  disabled
@@ -3,6 +3,7 @@
3
3
  import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
4
4
  import { Animated, Easing, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
5
5
  import { fontFor, useTheme, createAnimatedValue, setNativeValue } from "../../theme/index.js";
6
+ import { useReducedMotion, useControllableState } from "../../hooks/index.js";
6
7
  import { triggerHaptic } from "../../utils/index.js";
7
8
  import { FieldBase } from "../FieldBase/FieldBase.js";
8
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -36,6 +37,7 @@ const sanitizeChar = (input, kind) => {
36
37
  const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
37
38
  const {
38
39
  value,
40
+ defaultValue,
39
41
  onChange,
40
42
  onComplete,
41
43
  length = 6,
@@ -51,6 +53,11 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
51
53
  textStyle,
52
54
  testID
53
55
  } = props;
56
+ const [current, setCurrent] = useControllableState({
57
+ value,
58
+ defaultValue: defaultValue ?? '',
59
+ onChange
60
+ });
54
61
  const theme = useTheme();
55
62
  const styles = useMemo(() => buildStyles(theme), [theme]);
56
63
  const sizeStyles = {
@@ -81,6 +88,7 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
81
88
  }, [length]);
82
89
  const previousFocusedIndexRef = useRef(-1);
83
90
  const shakeOnError = theme.components.otpInput?.shakeOnError ?? false;
91
+ const reduceMotion = useReducedMotion();
84
92
  const errorHaptic = theme.components.otpInput?.errorHaptic ?? false;
85
93
  const selectionHaptic = theme.components.otpInput?.selectionHaptic ?? false;
86
94
  const hasError = Boolean(error);
@@ -90,11 +98,12 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
90
98
  // error is initially true — there's no transition to animate.
91
99
  useEffect(() => {
92
100
  const isFirstRun = previousErrorRef.current === null;
101
+ let anim;
93
102
  if (!isFirstRun && hasError && !previousErrorRef.current) {
94
103
  if (errorHaptic) triggerHaptic('notificationError');
95
- if (shakeOnError) {
104
+ if (shakeOnError && !reduceMotion) {
96
105
  setNativeValue(shake, 0);
97
- Animated.sequence([Animated.timing(shake, {
106
+ anim = Animated.sequence([Animated.timing(shake, {
98
107
  toValue: 1,
99
108
  duration: 75,
100
109
  easing: Easing.linear,
@@ -114,10 +123,12 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
114
123
  duration: 75,
115
124
  easing: Easing.linear,
116
125
  useNativeDriver: true
117
- })]).start();
126
+ })]);
127
+ anim.start();
118
128
  }
119
129
  }
120
130
  previousErrorRef.current = hasError;
131
+ return () => anim?.stop();
121
132
  }, [hasError, shake, shakeOnError, errorHaptic]);
122
133
 
123
134
  // Animate underline opacity for the focused cell. Skip on first mount
@@ -127,17 +138,21 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
127
138
  previousFocusedIndexRef.current = focusedIndex;
128
139
  if (previous === focusedIndex) return;
129
140
  if (previous === -1 && focusedIndex === -1) return;
141
+ const anims = [];
130
142
  underlines.forEach((anim, idx) => {
131
143
  const target = idx === focusedIndex ? 1 : 0;
132
144
  // Only animate the cells whose target changed.
133
145
  if (idx !== focusedIndex && idx !== previous) return;
134
- Animated.timing(anim, {
146
+ const animation = Animated.timing(anim, {
135
147
  toValue: target,
136
148
  duration: theme.motion.duration.fast,
137
149
  easing: Easing.bezier(...theme.motion.easing.standard),
138
150
  useNativeDriver: true
139
- }).start();
151
+ });
152
+ animation.start();
153
+ anims.push(animation);
140
154
  });
155
+ return () => anims.forEach(a => a.stop());
141
156
  }, [focusedIndex, underlines, theme]);
142
157
  const focusCell = useCallback(index => {
143
158
  const target = inputsRef.current[index];
@@ -152,10 +167,10 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
152
167
  current?.blur();
153
168
  },
154
169
  clear: () => {
155
- onChange('');
170
+ setCurrent('');
156
171
  focusCell(0);
157
172
  }
158
- }), [focusCell, focusedIndex, onChange]);
173
+ }), [focusCell, focusedIndex, setCurrent]);
159
174
  useEffect(() => {
160
175
  if (autoFocus && !disabled) {
161
176
  // Defer one frame so the inputs are mounted.
@@ -166,19 +181,19 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
166
181
  }, [autoFocus, disabled, focusCell]);
167
182
  const cells = useMemo(() => {
168
183
  const arr = new Array(length).fill('');
169
- for (let i = 0; i < Math.min(value.length, length); i += 1) {
170
- arr[i] = value[i] ?? '';
184
+ for (let i = 0; i < Math.min(current.length, length); i += 1) {
185
+ arr[i] = current[i] ?? '';
171
186
  }
172
187
  return arr;
173
- }, [value, length]);
188
+ }, [current, length]);
174
189
  const updateValue = useCallback(next => {
175
190
  const trimmed = next.slice(0, length);
176
- if (trimmed === value) return;
177
- onChange(trimmed);
191
+ if (trimmed === current) return;
192
+ setCurrent(trimmed);
178
193
  if (trimmed.length === length && onComplete) {
179
194
  onComplete(trimmed);
180
195
  }
181
- }, [length, onChange, onComplete, value]);
196
+ }, [length, setCurrent, onComplete, current]);
182
197
  const handleChangeText = useCallback((index, raw) => {
183
198
  // Strip the ZWSP placeholder (used so iOS fires onKeyPress/Backspace on otherwise-empty cells).
184
199
  const stripped = raw.replace(/\u200B/g, '');
@@ -208,7 +223,7 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
208
223
  writeIndex += 1;
209
224
  }
210
225
  const next = chars.join('').slice(0, length);
211
- const previousLength = value.length;
226
+ const previousLength = current.length;
212
227
  updateValue(next);
213
228
  if (next.length !== previousLength && selectionHaptic) {
214
229
  triggerHaptic('selection');
@@ -221,7 +236,7 @@ const OTPInput = /*#__PURE__*/forwardRef((props, ref) => {
221
236
  } else {
222
237
  focusCell(nextFocus);
223
238
  }
224
- }, [cells, focusCell, keyboardType, length, selectionHaptic, updateValue, value]);
239
+ }, [cells, focusCell, keyboardType, length, selectionHaptic, updateValue, current]);
225
240
  const handleKeyPress = useCallback((index, e) => {
226
241
  const key = e.nativeEvent.key;
227
242
  if (key !== 'Backspace') return;
@@ -38,7 +38,7 @@ const Radio = /*#__PURE__*/forwardRef((props, ref) => {
38
38
  const {
39
39
  value,
40
40
  selected: selectedProp,
41
- onSelect,
41
+ onChange,
42
42
  disabled: disabledProp = false,
43
43
  label,
44
44
  size = 'md',
@@ -68,13 +68,15 @@ const Radio = /*#__PURE__*/forwardRef((props, ref) => {
68
68
  const resolvedHaptic = haptic === undefined ? pressHapticEnabled ? 'selection' : false : haptic;
69
69
  const progress = useRef(createAnimatedValue(selected ? 1 : 0)).current;
70
70
  useEffect(() => {
71
- Animated.spring(progress, {
71
+ const anim = Animated.spring(progress, {
72
72
  toValue: selected ? 1 : 0,
73
73
  damping: theme.motion.spring.snappy.damping,
74
74
  stiffness: theme.motion.spring.snappy.stiffness,
75
75
  mass: theme.motion.spring.snappy.mass,
76
76
  useNativeDriver: true
77
- }).start();
77
+ });
78
+ anim.start();
79
+ return () => anim.stop();
78
80
  }, [selected, progress, theme.motion.spring.snappy]);
79
81
  const scale = progress.interpolate({
80
82
  inputRange: [0, 1],
@@ -84,9 +86,9 @@ const Radio = /*#__PURE__*/forwardRef((props, ref) => {
84
86
  if (disabled) return;
85
87
  if (resolvedHaptic !== false) triggerHaptic(resolvedHaptic);
86
88
  if (ctx) {
87
- ctx.onSelect(value);
89
+ ctx.onChange(value);
88
90
  } else {
89
- onSelect?.(value);
91
+ onChange?.(value);
90
92
  }
91
93
  rest.onPressOut?.(event);
92
94
  };
@@ -3,9 +3,11 @@
3
3
  import React, { useMemo } from 'react';
4
4
  import { View } from 'react-native';
5
5
  import { RadioGroupContext } from "./Radio.js";
6
+ import { useControllableState } from "../../hooks/index.js";
6
7
  import { jsx as _jsx } from "react/jsx-runtime";
7
8
  const RadioGroup = ({
8
9
  value,
10
+ defaultValue,
9
11
  onChange,
10
12
  children,
11
13
  disabled = false,
@@ -13,11 +15,16 @@ const RadioGroup = ({
13
15
  style,
14
16
  testID
15
17
  }) => {
18
+ const [current, setCurrent] = useControllableState({
19
+ value,
20
+ defaultValue: defaultValue ?? undefined,
21
+ onChange: onChange
22
+ });
16
23
  const ctx = useMemo(() => ({
17
- selectedValue: value,
18
- onSelect: onChange,
24
+ selectedValue: current,
25
+ onChange: setCurrent,
19
26
  disabled
20
- }), [value, onChange, disabled]);
27
+ }), [current, setCurrent, disabled]);
21
28
  return /*#__PURE__*/_jsx(RadioGroupContext.Provider, {
22
29
  value: ctx,
23
30
  children: /*#__PURE__*/_jsx(View, {
@@ -75,12 +75,14 @@ const SearchBar = /*#__PURE__*/forwardRef((props, ref) => {
75
75
  }, [debouncedValue, debounceMs, onChangeText]);
76
76
  const showCancelButton = showCancel && isFocused;
77
77
  useEffect(() => {
78
- Animated.timing(cancelAnim, {
78
+ const anim = Animated.timing(cancelAnim, {
79
79
  toValue: showCancelButton ? 1 : 0,
80
80
  duration: 200,
81
81
  easing: Easing.bezier(...theme.motion.easing.standard),
82
82
  useNativeDriver: false
83
- }).start();
83
+ });
84
+ anim.start();
85
+ return () => anim.stop();
84
86
  }, [showCancelButton, cancelAnim, theme.motion.easing.standard]);
85
87
  const handleChangeText = useCallback(text => {
86
88
  if (debounceMs !== undefined) {
@@ -12,6 +12,7 @@ import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState }
12
12
  import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
13
13
  import { useTheme, createAnimatedValue } from "../../theme/index.js";
14
14
  import { triggerHaptic } from "../../utils/index.js";
15
+ import { useControllableState } from "../../hooks/index.js";
15
16
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
17
  const sizeMap = {
17
18
  sm: {
@@ -29,7 +30,8 @@ const TRACK_PADDING = 2;
29
30
  const SegmentedControl = /*#__PURE__*/forwardRef((props, ref) => {
30
31
  const {
31
32
  segments,
32
- value,
33
+ value: valueProp,
34
+ defaultValue,
33
35
  onChange,
34
36
  size = 'md',
35
37
  fullWidth = true,
@@ -44,6 +46,11 @@ const SegmentedControl = /*#__PURE__*/forwardRef((props, ref) => {
44
46
  labelStyle,
45
47
  testID
46
48
  } = props;
49
+ const [current, setCurrent] = useControllableState({
50
+ value: valueProp,
51
+ defaultValue: defaultValue ?? segments[0]?.value,
52
+ onChange
53
+ });
47
54
  const theme = useTheme();
48
55
  const segTheme = theme.components.segmentedControl;
49
56
  const styles = useMemo(() => buildStyles(theme), [theme]);
@@ -64,23 +71,26 @@ const SegmentedControl = /*#__PURE__*/forwardRef((props, ref) => {
64
71
  // only animate translateX via the native driver.
65
72
  const [trackWidth, setTrackWidth] = useState(0);
66
73
  const thumbTranslateX = useRef(createAnimatedValue(0)).current;
67
- const activeIndex = Math.max(0, segments.findIndex(s => s.value === value));
74
+ const activeIndex = Math.max(0, segments.findIndex(s => s.value === current));
68
75
  const segmentWidth = trackWidth > 0 ? (trackWidth - trackPadding * 2) / Math.max(segments.length, 1) : 0;
69
76
  const animateThumb = useCallback((index, segWidth) => {
70
- if (segWidth <= 0) return;
77
+ if (segWidth <= 0) return undefined;
71
78
  const targetX = trackPadding + segWidth * index;
72
79
  const spring = theme.motion.spring.snappy;
73
- Animated.spring(thumbTranslateX, {
80
+ const anim = Animated.spring(thumbTranslateX, {
74
81
  toValue: targetX,
75
82
  damping: spring.damping,
76
83
  stiffness: spring.stiffness,
77
84
  mass: spring.mass,
78
85
  useNativeDriver: true
79
- }).start();
86
+ });
87
+ anim.start();
88
+ return anim;
80
89
  }, [theme.motion.spring.snappy, thumbTranslateX]);
81
90
  useEffect(() => {
82
91
  if (segmentWidth > 0) {
83
- animateThumb(activeIndex, segmentWidth);
92
+ const anim = animateThumb(activeIndex, segmentWidth);
93
+ return () => anim?.stop();
84
94
  }
85
95
  }, [activeIndex, segmentWidth, animateThumb]);
86
96
  const handleTrackLayout = useCallback(e => {
@@ -91,10 +101,10 @@ const SegmentedControl = /*#__PURE__*/forwardRef((props, ref) => {
91
101
  }, [trackWidth]);
92
102
  const handlePress = useCallback(segment => {
93
103
  if (disabled) return;
94
- if (segment.value === value) return;
104
+ if (segment.value === current) return;
95
105
  if (changeHapticEnabled) triggerHaptic('selection');
96
- onChange(segment.value);
97
- }, [disabled, onChange, value, changeHapticEnabled]);
106
+ setCurrent(segment.value);
107
+ }, [disabled, setCurrent, current, changeHapticEnabled]);
98
108
  const thumbBg = tone === 'primary' ? theme.colors.background.elevated : theme.colors.background.elevated;
99
109
  const activeTextColor = tone === 'primary' ? theme.colors.text.primary : theme.colors.text.primary;
100
110
  return /*#__PURE__*/_jsxs(View, {
@@ -125,7 +135,7 @@ const SegmentedControl = /*#__PURE__*/forwardRef((props, ref) => {
125
135
  ...theme.shadows.sm
126
136
  }, selectedIndicatorStyle]
127
137
  }) : null, segments.map(segment => {
128
- const isActive = segment.value === value;
138
+ const isActive = segment.value === current;
129
139
  return /*#__PURE__*/_jsxs(Pressable, {
130
140
  onPress: () => handlePress(segment),
131
141
  disabled: disabled,