react-native-molecules 0.5.0-beta.4 → 0.5.0-beta.5

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.
@@ -4,8 +4,11 @@ import { useUnistyles } from 'react-native-unistyles';
4
4
 
5
5
  import type { MD3Elevation } from '../../types/theme';
6
6
  import { extractPropertiesFromStyles } from '../../utils/extractPropertiesFromStyles';
7
+ import { Slot } from '../Slot';
7
8
  import { BackgroundContextWrapper } from './BackgroundContextWrapper';
8
- import { defaultStyles, getStyleForShadowLayer } from './utils';
9
+ import { defaultStyles, getCombinedShadowStyle } from './utils';
10
+
11
+ const AnimatedView = Animated.createAnimatedComponent(View);
9
12
 
10
13
  export type Props = ComponentPropsWithRef<typeof View> & {
11
14
  /**
@@ -27,6 +30,11 @@ export type Props = ComponentPropsWithRef<typeof View> & {
27
30
  * TestID used for testing purposes
28
31
  */
29
32
  testID?: string;
33
+ /**
34
+ * Change the component to the HTML tag or custom component use the passed child.
35
+ * This will merge the props of the Surface with the props of the child element.
36
+ */
37
+ asChild?: boolean;
30
38
  };
31
39
 
32
40
  /**
@@ -71,53 +79,38 @@ export type Props = ComponentPropsWithRef<typeof View> & {
71
79
  * });
72
80
  * ```
73
81
  */
74
- const Surface = ({ elevation = 1, style, children, testID, ...props }: Props, ref: any) => {
82
+ const Surface = (
83
+ { elevation = 1, style, children, testID, asChild = false, ...props }: Props,
84
+ ref: any,
85
+ ) => {
75
86
  const { theme } = useUnistyles();
76
87
  const backgroundColor = (() => {
77
88
  // @ts-ignore
78
89
  return theme.colors.elevation?.[`level${elevation}`];
79
90
  })();
80
91
 
81
- const { surfaceBackground, sharedStyle, layer0Style, layer1Style } = useMemo(() => {
82
- const { position, alignSelf, top, left, right, bottom, borderRadius } =
83
- extractPropertiesFromStyles(
84
- [defaultStyles.root as ViewStyle, style],
85
- ['position', 'alignSelf', 'top', 'left', 'right', 'bottom', 'borderRadius'],
86
- );
87
- const absoluteStyle = { position, alignSelf, top, right, bottom, left };
88
-
92
+ const { surfaceBackground, combinedStyle } = useMemo(() => {
89
93
  return {
90
94
  surfaceBackground: extractPropertiesFromStyles(
91
95
  [defaultStyles.root as ViewStyle, style],
92
96
  ['backgroundColor'],
93
97
  ).backgroundColor,
94
- sharedStyle: [
95
- { backgroundColor, borderRadius },
98
+ combinedStyle: [
99
+ { backgroundColor },
100
+ getCombinedShadowStyle(elevation),
96
101
  defaultStyles.root,
97
102
  style,
98
- {
99
- position: undefined,
100
- alignSelf: undefined,
101
- top: undefined,
102
- left: undefined,
103
- right: undefined,
104
- bottom: undefined,
105
- },
106
103
  ],
107
- layer0Style: [getStyleForShadowLayer(0, elevation), absoluteStyle, { borderRadius }],
108
- layer1Style: [getStyleForShadowLayer(1, elevation), { borderRadius }],
109
104
  };
110
105
  }, [backgroundColor, elevation, style]);
111
106
 
107
+ const Component = asChild ? Slot : AnimatedView;
108
+
112
109
  return (
113
110
  <BackgroundContextWrapper backgroundColor={surfaceBackground}>
114
- <View ref={ref} style={layer0Style}>
115
- <View style={layer1Style}>
116
- <View {...props} testID={testID} style={sharedStyle}>
117
- {children}
118
- </View>
119
- </View>
120
- </View>
111
+ <Component ref={ref} {...props} testID={testID} style={combinedStyle}>
112
+ {children}
113
+ </Component>
121
114
  </BackgroundContextWrapper>
122
115
  );
123
116
  };
@@ -1,11 +1,14 @@
1
1
  import { forwardRef, memo, type ReactNode, useMemo } from 'react';
2
- import { type StyleProp, View, type ViewProps, type ViewStyle } from 'react-native';
2
+ import { Animated, type StyleProp, View, type ViewProps, type ViewStyle } from 'react-native';
3
3
 
4
4
  import shadow from '../../styles/shadow';
5
5
  import type { MD3Elevation } from '../../types/theme';
6
+ import { Slot } from '../Slot';
6
7
  import { BackgroundContextWrapper } from './BackgroundContextWrapper';
7
8
  import { defaultStyles } from './utils';
8
9
 
10
+ const AnimatedView = Animated.createAnimatedComponent(View);
11
+
9
12
  export type Props = ViewProps & {
10
13
  /**
11
14
  * Content of the `Surface`.
@@ -18,11 +21,16 @@ export type Props = ViewProps & {
18
21
  * TestID used for testing purposes
19
22
  */
20
23
  testID?: string;
24
+ /**
25
+ * Change the component to the HTML tag or custom component use the passed child.
26
+ * This will merge the props of the Surface with the props of the child element.
27
+ */
28
+ asChild?: boolean;
21
29
  };
22
30
 
23
31
  // for Web
24
32
  const Surface = (
25
- { elevation = 1, style, children, testID, backgroundColor, ...props }: Props,
33
+ { elevation = 1, style, children, testID, backgroundColor, asChild = false, ...props }: Props,
26
34
  ref: any,
27
35
  ) => {
28
36
  const { surfaceStyle } = useMemo(() => {
@@ -36,11 +44,13 @@ const Surface = (
36
44
  };
37
45
  }, [backgroundColor, elevation, style]);
38
46
 
47
+ const Component = asChild ? Slot : AnimatedView;
48
+
39
49
  return (
40
50
  <BackgroundContextWrapper backgroundColor={backgroundColor!}>
41
- <View ref={ref} {...props} testID={testID} style={surfaceStyle}>
51
+ <Component ref={ref} {...props} testID={testID} style={surfaceStyle}>
42
52
  {children}
43
- </View>
53
+ </Component>
44
54
  </BackgroundContextWrapper>
45
55
  );
46
56
  };
@@ -72,10 +72,48 @@ export const getStyleForShadowLayer = (
72
72
  };
73
73
  };
74
74
 
75
- export const getElevationAndroid = (
76
- elevation: number,
77
- _inputRange: number[],
78
- elevationLevel: number[],
79
- ) => {
80
- return elevationLevel[elevation];
75
+ /**
76
+ * Combines the two shadow layers into a single shadow style.
77
+ * This approximates the two-layer shadow effect using a single shadow.
78
+ */
79
+ export const getCombinedShadowStyle = (elevation: number, shadowColor = _shadowColor) => {
80
+ if (elevation === 0) {
81
+ return {
82
+ shadowColor,
83
+ shadowOpacity: 0,
84
+ shadowOffset: { width: 0, height: 0 },
85
+ shadowRadius: 0,
86
+ };
87
+ }
88
+
89
+ const layer0 = iOSShadowOutputRanges[0];
90
+ const layer1 = iOSShadowOutputRanges[1];
91
+
92
+ // Use the larger shadow offset (from layer 0)
93
+ const shadowOffsetHeight = layer0.height[elevation];
94
+
95
+ // Use the larger shadow radius (from layer 0)
96
+ const shadowRadius = layer0.shadowRadius[elevation];
97
+
98
+ // Combine opacities (additive, capped at 1.0)
99
+ // This approximates the visual effect of two overlapping shadows
100
+ const shadowOpacity = Math.min(1.0, layer0.shadowOpacity + layer1.shadowOpacity);
101
+
102
+ return {
103
+ shadowColor,
104
+ shadowOpacity,
105
+ shadowOffset: {
106
+ width: 0,
107
+ height: shadowOffsetHeight,
108
+ },
109
+ shadowRadius,
110
+ };
81
111
  };
112
+
113
+ // export const getElevationAndroid = (
114
+ // elevation: number,
115
+ // _inputRange: number[],
116
+ // elevationLevel: number[],
117
+ // ) => {
118
+ // return elevationLevel[elevation];
119
+ // };
@@ -70,6 +70,7 @@ const Switch = (
70
70
  ref,
71
71
  actionsToListen: ['focus', 'hover', 'press'],
72
72
  });
73
+ const isFirstRender = useRef(true);
73
74
 
74
75
  const [value, onChange] = useControlledValue({
75
76
  value: valueProp,
@@ -98,8 +99,8 @@ const Switch = (
98
99
  state,
99
100
  });
100
101
 
101
- const toggleMarginAnimation = useRef(new Animated.Value(value ? 0 : 1)).current;
102
- const toggleSizeAnimation = useRef(new Animated.Value(value ? 0 : 1)).current;
102
+ const toggleMarginAnimation = useRef(new Animated.Value(value ? 1 : 0)).current;
103
+ const toggleSizeAnimation = useRef(new Animated.Value(value ? 1 : 0)).current;
103
104
 
104
105
  const thumbPosition = toggleMarginAnimation.interpolate({
105
106
  inputRange: [0, 1],
@@ -130,6 +131,11 @@ const Switch = (
130
131
  });
131
132
 
132
133
  useEffect(() => {
134
+ if (isFirstRender.current) {
135
+ isFirstRender.current = false;
136
+ return;
137
+ }
138
+
133
139
  Animated.timing(toggleMarginAnimation, {
134
140
  toValue: value ? 1 : 0,
135
141
  duration: 300,
@@ -194,8 +194,8 @@ const DefaultComponent = (props: RenderProps) => <NativeTextInput {...props} />;
194
194
  const TextInput = forwardRef<TextInputHandles, Props>(
195
195
  (
196
196
  {
197
- variant = 'flat',
198
- size = 'md',
197
+ variant = 'outlined',
198
+ size = 'sm',
199
199
  disabled = false,
200
200
  error: errorProp = false,
201
201
  multiline = false,
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, memo, useMemo } from 'react';
1
+ import { type ComponentProps, forwardRef, memo, type ReactNode, useMemo } from 'react';
2
2
  import {
3
3
  type BackgroundPropType,
4
4
  Platform,
@@ -7,25 +7,30 @@ import {
7
7
  StyleSheet,
8
8
  TouchableNativeFeedback,
9
9
  TouchableWithoutFeedback,
10
- View,
11
10
  type ViewStyle,
12
11
  } from 'react-native';
13
- import { withUnistyles } from 'react-native-unistyles';
14
12
 
13
+ import { extractPropertiesFromStyles } from '../../utils/extractPropertiesFromStyles';
14
+ import { Slot } from '../Slot';
15
15
  import { touchableRippleStyles } from './utils';
16
16
 
17
17
  const ANDROID_VERSION_LOLLIPOP = 21;
18
18
  const ANDROID_VERSION_PIE = 28;
19
19
 
20
- type Props = React.ComponentProps<typeof TouchableWithoutFeedback> & {
20
+ type Props = ComponentProps<typeof TouchableWithoutFeedback> & {
21
21
  borderless?: boolean;
22
22
  background?: BackgroundPropType;
23
23
  disabled?: boolean;
24
24
  onPress?: () => void | null;
25
25
  rippleColor?: string;
26
26
  underlayColor?: string;
27
- children: React.ReactNode;
27
+ children: ReactNode;
28
28
  style?: StyleProp<ViewStyle>;
29
+ /**
30
+ * Change the component to the HTML tag or custom component use the passed child.
31
+ * This will merge the props of the TouchableRipple with the props of the child element.
32
+ */
33
+ asChild?: boolean;
29
34
  };
30
35
 
31
36
  const TouchableRipple = (
@@ -37,6 +42,7 @@ const TouchableRipple = (
37
42
  rippleColor: rippleColorProp,
38
43
  underlayColor: underlayColorProp,
39
44
  children,
45
+ asChild = false,
40
46
  ...rest
41
47
  }: Props,
42
48
  ref: any,
@@ -46,8 +52,12 @@ const TouchableRipple = (
46
52
  const componentStyles = touchableRippleStyles;
47
53
 
48
54
  const { rippleColor, underlayColor, containerStyle } = useMemo(() => {
55
+ const { rippleColor: _rippleColor } = extractPropertiesFromStyles(
56
+ [componentStyles.root, style],
57
+ ['rippleColor'],
58
+ );
49
59
  return {
50
- rippleColor: rippleColorProp,
60
+ rippleColor: rippleColorProp || _rippleColor,
51
61
  underlayColor: underlayColorProp || rippleColorProp,
52
62
  containerStyle: [borderless && styles.borderless, componentStyles.root, style],
53
63
  };
@@ -58,6 +68,21 @@ const TouchableRipple = (
58
68
  const useForeground =
59
69
  Platform.OS === 'android' && Platform.Version >= ANDROID_VERSION_PIE && borderless;
60
70
 
71
+ if (asChild) {
72
+ // When asChild is true, use Slot to merge props with the child
73
+ // Note: TouchableNativeFeedback ripple won't work with asChild since it requires a View wrapper
74
+ return (
75
+ <Slot
76
+ {...rest}
77
+ style={containerStyle}
78
+ ref={ref}
79
+ onPress={rest.onPress}
80
+ disabled={disabled}>
81
+ {children}
82
+ </Slot>
83
+ );
84
+ }
85
+
61
86
  if (TouchableRipple.supported) {
62
87
  return (
63
88
  <TouchableNativeFeedback
@@ -65,12 +90,13 @@ const TouchableRipple = (
65
90
  ref={ref}
66
91
  disabled={disabled}
67
92
  useForeground={useForeground}
93
+ style={containerStyle}
68
94
  background={
69
95
  background != null
70
96
  ? background
71
97
  : TouchableNativeFeedback.Ripple(rippleColor!, borderless)
72
98
  }>
73
- <View style={containerStyle}>{React.Children.only(children)}</View>
99
+ <>{children}</>
74
100
  </TouchableNativeFeedback>
75
101
  );
76
102
  }
@@ -84,7 +110,7 @@ const TouchableRipple = (
84
110
  containerStyle,
85
111
  pressed && { backgroundColor: underlayColor },
86
112
  ]}>
87
- {React.Children.only(children)}
113
+ {children}
88
114
  </Pressable>
89
115
  );
90
116
  };
@@ -98,8 +124,4 @@ const styles = StyleSheet.create({
98
124
  TouchableRipple.supported =
99
125
  Platform.OS === 'android' && Platform.Version >= ANDROID_VERSION_LOLLIPOP;
100
126
 
101
- export default memo(
102
- withUnistyles(forwardRef(TouchableRipple), theme => ({
103
- rippleColor: theme.colors.onSurfaceRipple,
104
- })),
105
- );
127
+ export default memo(forwardRef(TouchableRipple));
@@ -1,4 +1,4 @@
1
- import { Children, forwardRef, memo, type ReactNode, useCallback, useMemo } from 'react';
1
+ import { forwardRef, memo, type ReactNode, useCallback, useMemo, useRef } from 'react';
2
2
  import {
3
3
  type GestureResponderEvent,
4
4
  Pressable,
@@ -9,6 +9,7 @@ import {
9
9
  } from 'react-native';
10
10
  import { StyleSheet } from 'react-native-unistyles';
11
11
 
12
+ import { Slot } from '../Slot';
12
13
  import { touchableRippleStyles } from './utils';
13
14
 
14
15
  export type Props = PressableProps & {
@@ -50,6 +51,11 @@ export type Props = PressableProps & {
50
51
  */
51
52
  children: ReactNode;
52
53
  style?: StyleProp<ViewStyle>;
54
+ /**
55
+ * Change the component to the HTML tag or custom component use the passed child.
56
+ * This will merge the props of the TouchableRipple with the props of the child element.
57
+ */
58
+ asChild?: boolean;
53
59
  };
54
60
 
55
61
  /**
@@ -96,6 +102,7 @@ const TouchableRipple = (
96
102
  onPressIn: onPressInProp,
97
103
  onPressOut: onPressOutProp,
98
104
  centered,
105
+ asChild = false,
99
106
  ...rest
100
107
  }: Props,
101
108
  ref: any,
@@ -121,29 +128,52 @@ const TouchableRipple = (
121
128
  };
122
129
  }, [borderless, componentStyles.root, rippleColorProp, style]);
123
130
 
124
- const handlePressIn = useCallback(
131
+ // Track whether pointer is currently down for handling pointer leave
132
+ const isPointerDownRef = useRef(false);
133
+ // Store current target element to clean up ripples on pointer up/leave
134
+ const currentTargetRef = useRef<HTMLElement | null>(null);
135
+
136
+ // Using 'any' for event types to support both React DOM PointerEvent and React Native events
137
+ // This is a web-only file, so we primarily handle DOM pointer events
138
+ const handlePointerDown = useCallback(
125
139
  (e: any) => {
126
- onPressInProp?.(e);
140
+ onPressInProp?.(e as GestureResponderEvent);
127
141
 
128
142
  if (disabled) return;
129
143
 
130
- const button = e.currentTarget;
144
+ isPointerDownRef.current = true;
145
+
146
+ const button = e.currentTarget as HTMLElement;
147
+ currentTargetRef.current = button;
131
148
  const computedStyle = window.getComputedStyle(button);
132
149
  const dimensions = button.getBoundingClientRect();
133
150
 
134
- let touchX;
135
- let touchY;
136
-
137
- const { changedTouches, touches } = e.nativeEvent;
138
- const touch = touches?.[0] ?? changedTouches?.[0];
151
+ let touchX: number;
152
+ let touchY: number;
139
153
 
140
- // If centered or it was pressed using keyboard - enter or space
141
- if (centered || !touch) {
154
+ if (centered) {
155
+ // If centered, always position ripple at center
142
156
  touchX = dimensions.width / 2;
143
157
  touchY = dimensions.height / 2;
158
+ } else if ('clientX' in e && 'clientY' in e) {
159
+ // Web pointer event - calculate position relative to element
160
+ touchX = e.clientX - dimensions.left;
161
+ touchY = e.clientY - dimensions.top;
162
+ } else if (e.nativeEvent) {
163
+ // React Native gesture event
164
+ const { changedTouches, touches } = e.nativeEvent;
165
+ const touch = touches?.[0] ?? changedTouches?.[0];
166
+ if (touch) {
167
+ touchX = touch.locationX ?? dimensions.width / 2;
168
+ touchY = touch.locationY ?? dimensions.height / 2;
169
+ } else {
170
+ touchX = dimensions.width / 2;
171
+ touchY = dimensions.height / 2;
172
+ }
144
173
  } else {
145
- touchX = touch.locationX ?? e.pageX;
146
- touchY = touch.locationY ?? e.pageY;
174
+ // Fallback to center (keyboard activation)
175
+ touchX = dimensions.width / 2;
176
+ touchY = dimensions.height / 2;
147
177
  }
148
178
 
149
179
  // Get the size of the button to determine how big the ripple should be
@@ -156,7 +186,7 @@ const TouchableRipple = (
156
186
  // Create a container for our ripple effect so we don't need to change the parent's style
157
187
  const container = document.createElement('span');
158
188
 
159
- container.setAttribute('data-paper-ripple', '');
189
+ container.setAttribute('data-molecules-ripple', '');
160
190
 
161
191
  Object.assign(container.style, {
162
192
  position: 'absolute',
@@ -217,42 +247,86 @@ const TouchableRipple = (
217
247
  [onPressInProp, disabled, centered, rippleColor],
218
248
  );
219
249
 
220
- const handlePressOut = useCallback(
221
- (e: any) => {
222
- onPressOutProp?.(e);
223
-
224
- if (disabled) return;
225
-
226
- const containers = e.currentTarget.querySelectorAll(
227
- '[data-paper-ripple]',
228
- ) as HTMLElement[];
250
+ const fadeOutRipples = useCallback((target: HTMLElement) => {
251
+ const containers = target.querySelectorAll(
252
+ '[data-molecules-ripple]',
253
+ ) as NodeListOf<HTMLElement>;
229
254
 
255
+ requestAnimationFrame(() => {
230
256
  requestAnimationFrame(() => {
231
- requestAnimationFrame(() => {
232
- containers.forEach(container => {
233
- const ripple = container.firstChild as HTMLSpanElement;
234
-
235
- Object.assign(ripple.style, {
236
- transitionDuration: '250ms',
237
- opacity: 0,
238
- });
239
-
240
- // Finally remove the span after the transition
241
- setTimeout(() => {
242
- const { parentNode } = container;
243
-
244
- if (parentNode) {
245
- parentNode.removeChild(container);
246
- }
247
- }, 500);
257
+ containers.forEach(container => {
258
+ const ripple = container.firstChild as HTMLSpanElement;
259
+
260
+ Object.assign(ripple.style, {
261
+ transitionDuration: '250ms',
262
+ opacity: 0,
248
263
  });
264
+
265
+ // Finally remove the span after the transition
266
+ setTimeout(() => {
267
+ const { parentNode } = container;
268
+
269
+ if (parentNode) {
270
+ parentNode.removeChild(container);
271
+ }
272
+ }, 500);
249
273
  });
250
274
  });
275
+ });
276
+ }, []);
277
+
278
+ const handlePointerUp = useCallback(
279
+ (e: any) => {
280
+ onPressOutProp?.(e as GestureResponderEvent);
281
+
282
+ if (disabled || !isPointerDownRef.current) return;
283
+
284
+ isPointerDownRef.current = false;
285
+ currentTargetRef.current = null;
286
+
287
+ const target = e.currentTarget as HTMLElement;
288
+ fadeOutRipples(target);
251
289
  },
252
- [onPressOutProp, disabled],
290
+ [onPressOutProp, disabled, fadeOutRipples],
253
291
  );
254
292
 
255
- const Component = onPress ? Pressable : View;
293
+ const handlePointerLeave = useCallback(
294
+ (e: any) => {
295
+ // Only fade out if pointer was down (dragging out of element)
296
+ if (disabled || !isPointerDownRef.current) return;
297
+
298
+ isPointerDownRef.current = false;
299
+ currentTargetRef.current = null;
300
+
301
+ const target = e.currentTarget as HTMLElement;
302
+ fadeOutRipples(target);
303
+ },
304
+ [disabled, fadeOutRipples],
305
+ );
306
+
307
+ const handlePointerCancel = useCallback(
308
+ (e: any) => {
309
+ if (disabled || !isPointerDownRef.current) return;
310
+
311
+ isPointerDownRef.current = false;
312
+ currentTargetRef.current = null;
313
+
314
+ const target = e.currentTarget as HTMLElement;
315
+ fadeOutRipples(target);
316
+ },
317
+ [disabled, fadeOutRipples],
318
+ );
319
+
320
+ const Component = asChild ? Slot : onPress ? Pressable : View;
321
+
322
+ // Use pointer events for universal compatibility (works on any HTML element)
323
+ // These events work with mouse, touch, and stylus inputs
324
+ const pointerEventProps = {
325
+ onPointerDown: handlePointerDown,
326
+ onPointerUp: handlePointerUp,
327
+ onPointerLeave: handlePointerLeave,
328
+ onPointerCancel: handlePointerCancel,
329
+ };
256
330
 
257
331
  return (
258
332
  <Component
@@ -261,10 +335,9 @@ const TouchableRipple = (
261
335
  style={containerStyle}
262
336
  ref={ref}
263
337
  onPress={onPress}
264
- onPressIn={handlePressIn}
265
- onPressOut={handlePressOut}
266
- disabled={disabled}>
267
- {Children.only(children)}
338
+ disabled={disabled}
339
+ {...pointerEventProps}>
340
+ {children}
268
341
  </Component>
269
342
  );
270
343
  };
@@ -1,5 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
 
3
+ import useLatest from './useLatest';
4
+
3
5
  type ReturnType<T> = [T, (value: T, ...args: any[]) => void];
4
6
 
5
7
  type Args<T> = {
@@ -31,21 +33,35 @@ const useControlledValue = <T,>({
31
33
 
32
34
  const isUncontrolled = useRef(valueProp).current === undefined;
33
35
  const [uncontrolledValue, setValue] = useState(value);
36
+ const valuePropRef = useLatest(valueProp);
37
+ const onChangeRef = useLatest(onChange);
38
+ const manipulateValueRef = useLatest(manipulateValue);
39
+ const uncontrolledValueRef = useLatest(uncontrolledValue);
34
40
 
35
41
  const updateValue = useCallback(
36
42
  (val: T, ...rest: any[]) => {
37
43
  if (disabled) return;
38
44
 
39
45
  if (isUncontrolled) {
40
- setValue(manipulateValue(val, uncontrolledValue));
46
+ setValue(manipulateValueRef.current(val, uncontrolledValueRef.current));
41
47
  }
42
48
 
43
- onChange?.(
44
- manipulateValue(val, isUncontrolled ? uncontrolledValue : valueProp),
49
+ onChangeRef.current?.(
50
+ manipulateValueRef.current(
51
+ val,
52
+ isUncontrolled ? uncontrolledValueRef.current : valuePropRef.current,
53
+ ),
45
54
  ...rest,
46
55
  );
47
56
  },
48
- [disabled, isUncontrolled, manipulateValue, onChange, uncontrolledValue, valueProp],
57
+ [
58
+ disabled,
59
+ isUncontrolled,
60
+ manipulateValueRef,
61
+ onChangeRef,
62
+ uncontrolledValueRef,
63
+ valuePropRef,
64
+ ],
49
65
  );
50
66
 
51
67
  useEffect(() => {