react-native-molecules 0.5.0-beta.22 → 0.5.0-beta.24

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 (82) hide show
  1. package/components/Accordion/Accordion.tsx +1 -1
  2. package/components/Accordion/AccordionItem.tsx +1 -1
  3. package/components/Button/Button.tsx +3 -1
  4. package/components/Checkbox/Checkbox.tsx +2 -1
  5. package/components/DateField/useDateFieldState.ts +2 -2
  6. package/components/DatePicker/DatePickerProvider.tsx +1 -1
  7. package/components/DatePicker/utils.ts +2 -0
  8. package/components/DatePickerInline/DatePickerInline.tsx +1 -1
  9. package/components/DatePickerInline/DatePickerInlineBase.tsx +1 -1
  10. package/components/DatePickerInline/Day.tsx +1 -1
  11. package/components/DatePickerInline/MonthPicker.tsx +24 -40
  12. package/components/DatePickerInline/Swiper.tsx +1 -1
  13. package/components/DatePickerInline/SwiperUtils.ts +1 -1
  14. package/components/DatePickerInline/YearPicker.tsx +44 -79
  15. package/components/DatePickerInline/dateUtils.tsx +1 -1
  16. package/components/DatePickerInline/store.tsx +2 -1
  17. package/components/Divider/index.tsx +2 -3
  18. package/components/ElementGroup/ElementGroup.tsx +1 -1
  19. package/components/FilePicker/FilePicker.tsx +1 -1
  20. package/components/Icon/iconFactory.tsx +2 -1
  21. package/components/IconButton/IconButton.tsx +39 -13
  22. package/components/IconButton/index.tsx +1 -0
  23. package/components/IconButton/types.ts +2 -0
  24. package/components/List/List.tsx +156 -387
  25. package/components/List/context.tsx +4 -5
  26. package/components/List/index.ts +0 -1
  27. package/components/List/types.ts +77 -109
  28. package/components/List/utils.ts +4 -37
  29. package/components/Menu/Menu.tsx +13 -30
  30. package/components/Menu/index.tsx +0 -2
  31. package/components/Popover/Popover.tsx +7 -10
  32. package/components/Popover/PopoverRoot.tsx +6 -20
  33. package/components/Popover/common.ts +4 -0
  34. package/components/Popover/index.ts +2 -8
  35. package/components/Popover/usePlatformMeasure.ts +4 -2
  36. package/components/Portal/Portal.tsx +1 -2
  37. package/components/RadioButton/RadioButtonGroup.tsx +1 -2
  38. package/components/Rating/Rating.tsx +1 -1
  39. package/components/Select/Select.tsx +304 -71
  40. package/components/Select/context.tsx +30 -3
  41. package/components/Select/index.ts +20 -2
  42. package/components/Select/types.ts +43 -25
  43. package/components/Select/utils.ts +18 -4
  44. package/components/Switch/Switch.ios.tsx +1 -1
  45. package/components/Switch/Switch.tsx +2 -1
  46. package/components/Tabs/Tabs.tsx +2 -2
  47. package/components/TextInput/TextInput.tsx +4 -3
  48. package/components/TimePicker/AnalogClock.tsx +1 -1
  49. package/components/TimePicker/TimeInputs.tsx +1 -1
  50. package/components/TimePicker/TimePicker.tsx +1 -1
  51. package/components/TimePicker/TimePickerModal.tsx +1 -1
  52. package/components/Tooltip/Tooltip.tsx +1 -1
  53. package/components/TouchableRipple/TouchableRipple.tsx +76 -152
  54. package/hocs/index.tsx +1 -1
  55. package/hocs/withKeyboardAccessibility.tsx +2 -3
  56. package/hooks/index.tsx +2 -6
  57. package/hooks/useContrastColor.ts +1 -2
  58. package/hooks/useFilePicker.tsx +1 -1
  59. package/hooks/useHandleNumberFormat.tsx +2 -2
  60. package/hooks/useMediaQuery.tsx +1 -2
  61. package/package.json +5 -28
  62. package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +1 -1
  63. package/shortcuts-manager/ShortcutsManager/utils.tsx +1 -1
  64. package/shortcuts-manager/useSetScopes/useSetScopes.tsx +1 -1
  65. package/shortcuts-manager/useShortcut/useShortcut.tsx +1 -1
  66. package/utils/extractTextStyles.ts +1 -2
  67. package/utils/formatNumberWithMask/formatNumberWithMask.ts +2 -1
  68. package/utils/index.ts +0 -3
  69. package/utils/normalizeToNumberString/normalizeToNumberString.ts +1 -1
  70. package/context-bridge/index.tsx +0 -87
  71. package/fast-context/index.tsx +0 -190
  72. package/hocs/typedMemo.tsx +0 -5
  73. package/hooks/useControlledValue.tsx +0 -84
  74. package/hooks/useLatest.tsx +0 -9
  75. package/hooks/useMergedRefs.ts +0 -14
  76. package/hooks/usePrevious.ts +0 -13
  77. package/hooks/useToggle.tsx +0 -24
  78. package/hooks/useWhatHasUpdated.tsx +0 -48
  79. package/utils/color.ts +0 -22
  80. package/utils/compare/index.ts +0 -54
  81. package/utils/lodash.ts +0 -121
  82. package/utils/repository.ts +0 -53
@@ -1,21 +1,16 @@
1
1
  import type { ComponentType, ReactNode } from 'react';
2
- import type { GestureResponderEvent, ViewProps } from 'react-native';
2
+ import type { GestureResponderEvent, TextInputProps, ViewProps } from 'react-native';
3
3
 
4
- import type { ListValue } from '../List';
4
+ import type { ListContentProps, ListItemId, ListValue } from '../List';
5
5
  import type { PopoverProps } from '../Popover';
6
6
 
7
- export type {
8
- ListContentProps as SelectContentProps,
9
- ListContextValue as SelectContextValue,
10
- ListGroupProps as SelectGroupProps,
11
- ListSearchInputProps as SelectSearchInputProps,
12
- } from '../List';
7
+ export type { ListContextValue as SelectContextValue } from '../List';
13
8
 
14
9
  export type DefaultItemT = {
15
10
  id: string | number;
16
11
  label?: string;
17
12
  selectable?: boolean;
18
- [key: string]: any;
13
+ [key: string]: unknown;
19
14
  };
20
15
 
21
16
  // SelectDropdownContext types
@@ -25,47 +20,70 @@ export type SelectDropdownContextValue = {
25
20
  onOpen: () => void;
26
21
  };
27
22
 
23
+ export type SelectSearchMode = 'client' | 'external';
24
+
25
+ export type SelectSearchKey<Option extends object = DefaultItemT> =
26
+ | string
27
+ | string[]
28
+ | ((item: Option, query: string) => boolean);
29
+
30
+ export type SelectSearchContextValue<Option extends DefaultItemT = DefaultItemT> = {
31
+ searchQuery: string;
32
+ setSearchQuery: (query: string) => void;
33
+ allOptions: Option[];
34
+ options: Option[];
35
+ optionById: Map<ListItemId, Option>;
36
+ getOptionId: (item: Option) => ListItemId;
37
+ };
38
+
28
39
  // SelectProvider props
29
40
  type SelectPropsBase<Option extends DefaultItemT = DefaultItemT> = {
30
41
  children: ReactNode;
31
42
  disabled?: boolean;
32
43
  error?: boolean;
33
44
  options: Option[];
34
- searchKey?: string;
45
+ searchKey?: SelectSearchKey<Option>;
46
+ searchQuery?: string;
47
+ defaultSearchQuery?: string;
35
48
  onSearchChange?: (query: string) => void;
36
- hideSelected?: boolean;
49
+ searchMode?: SelectSearchMode;
50
+ allowDeselect?: boolean;
51
+ getItemId?: (item: Option) => ListItemId;
37
52
  };
38
53
 
54
+ export type SelectSearchInputProps = Omit<TextInputProps, 'value' | 'onChangeText'>;
55
+
39
56
  type SingleSelectProps<Option extends DefaultItemT = DefaultItemT> = {
40
57
  multiple?: false | undefined;
41
- value?: ListValue<Option, false>;
42
- defaultValue?: ListValue<Option, false>;
43
- onChange?: (
44
- value: ListValue<Option, false>,
45
- item: Option,
46
- event?: GestureResponderEvent,
47
- ) => void;
58
+ value?: ListValue<false>;
59
+ defaultValue?: ListValue<false>;
60
+ onChange?: (value: ListValue<false>, item: Option, event?: GestureResponderEvent) => void;
48
61
  };
49
62
 
50
63
  type MultipleSelectProps<Option extends DefaultItemT = DefaultItemT> = {
51
64
  multiple: true;
52
- value?: ListValue<Option, true>;
53
- defaultValue?: ListValue<Option, true>;
54
- onChange?: (
55
- value: ListValue<Option, true>,
56
- item: Option,
57
- event?: GestureResponderEvent,
58
- ) => void;
65
+ value?: ListValue<true>;
66
+ defaultValue?: ListValue<true>;
67
+ onChange?: (value: ListValue<true>, item: Option, event?: GestureResponderEvent) => void;
59
68
  };
60
69
 
61
70
  export type SelectProps<Option extends DefaultItemT = DefaultItemT> = SelectPropsBase<Option> &
62
71
  (SingleSelectProps<Option> | MultipleSelectProps<Option>);
63
72
 
73
+ export type SelectContentProps<Option extends DefaultItemT = DefaultItemT> = Omit<
74
+ ListContentProps,
75
+ 'children'
76
+ > & {
77
+ children?: ReactNode | ((item: Option, isSelected: boolean) => ReactNode);
78
+ };
79
+
64
80
  // Select.Trigger props
65
81
  export type SelectTriggerProps = ViewProps & {
66
82
  children?: ReactNode;
67
83
  };
68
84
 
85
+ export type SelectTriggerOutlineProps = ViewProps;
86
+
69
87
  // Select.Value props
70
88
  export type SelectValueProps = ViewProps & {
71
89
  placeholder?: string;
@@ -39,6 +39,13 @@ const triggerDefaultStyles = StyleSheet.create(theme => ({
39
39
  },
40
40
  },
41
41
  },
42
+ triggerIcon: {
43
+ marginLeft: theme.spacings['2'],
44
+ color: theme.colors.onSurfaceVariant,
45
+ },
46
+ }));
47
+
48
+ const outlineDefaultStyles = StyleSheet.create(theme => ({
42
49
  outline: {
43
50
  position: 'absolute',
44
51
  top: 0,
@@ -85,28 +92,35 @@ const triggerDefaultStyles = StyleSheet.create(theme => ({
85
92
  },
86
93
  },
87
94
  },
88
- triggerIcon: {
89
- marginLeft: theme.spacings['2'],
90
- color: theme.colors.onSurfaceVariant,
91
- },
92
95
  }));
93
96
 
94
97
  export const defaultStyles = StyleSheet.create(theme => ({
98
+ valueText: {
99
+ flex: 1,
100
+ },
95
101
  chipContainer: {
96
102
  flexDirection: 'row',
97
103
  flexWrap: 'wrap',
98
104
  gap: 6,
99
105
  maxWidth: '90%',
106
+ flex: 1,
100
107
  },
101
108
  searchInput: {
102
109
  marginHorizontal: theme.spacings['2'],
103
110
  marginVertical: theme.spacings['3'],
104
111
  },
112
+ searchInputInput: {
113
+ height: 42,
114
+ },
105
115
  }));
106
116
 
107
117
  export const triggerStyles = getRegisteredComponentStylesWithFallback(
108
118
  'Select_Trigger',
109
119
  triggerDefaultStyles,
110
120
  );
121
+ export const selectOutlineStyles = getRegisteredComponentStylesWithFallback(
122
+ 'SelectOutline',
123
+ outlineDefaultStyles,
124
+ );
111
125
 
112
126
  export const styles = getRegisteredComponentStylesWithFallback('Select', defaultStyles);
@@ -1,3 +1,4 @@
1
+ import { useControlledValue } from '@react-native-molecules/utils/hooks';
1
2
  import { forwardRef, memo } from 'react';
2
3
  import {
3
4
  Switch as NativeSwitch,
@@ -6,7 +7,6 @@ import {
6
7
  type ViewStyle,
7
8
  } from 'react-native';
8
9
 
9
- import { useControlledValue } from '../../hooks';
10
10
  import type { IconType } from '../Icon';
11
11
 
12
12
  export type Props = SwitchProps & {
@@ -1,3 +1,4 @@
1
+ import { useControlledValue, useLatest } from '@react-native-molecules/utils/hooks';
1
2
  import { forwardRef, memo, useCallback, useEffect, useMemo, useRef } from 'react';
2
3
  import {
3
4
  Animated,
@@ -9,7 +10,7 @@ import {
9
10
  type ViewStyle,
10
11
  } from 'react-native';
11
12
 
12
- import { useActionState, useControlledValue, useLatest } from '../../hooks';
13
+ import { useActionState } from '../../hooks';
13
14
  import { resolveStateVariant } from '../../utils';
14
15
  import { Icon, type IconType } from '../Icon';
15
16
  import { switchStyles, useSwitchColors } from './utils';
@@ -1,3 +1,5 @@
1
+ import { noop } from '@react-native-molecules/utils/helpers/lodash';
2
+ import { useControlledValue } from '@react-native-molecules/utils/hooks';
1
3
  import {
2
4
  cloneElement,
3
5
  type ComponentType,
@@ -22,9 +24,7 @@ import {
22
24
  } from 'react-native';
23
25
 
24
26
  import { typedMemo } from '../../hocs';
25
- import { useControlledValue } from '../../hooks';
26
27
  import { extractSubcomponents } from '../../utils/extractSubcomponents';
27
- import { noop } from '../../utils/lodash';
28
28
  import type { TabItemProps } from './TabItem';
29
29
  import { tabsStyles } from './utils';
30
30
 
@@ -1,3 +1,4 @@
1
+ import { useControlledValue, useLatest } from '@react-native-molecules/utils/hooks';
1
2
  import React, {
2
3
  memo,
3
4
  type PropsWithoutRef,
@@ -31,9 +32,7 @@ import {
31
32
  View,
32
33
  } from 'react-native';
33
34
 
34
- import { useActionState } from '../../hooks/useActionState';
35
- import useControlledValue from '../../hooks/useControlledValue';
36
- import useLatest from '../../hooks/useLatest';
35
+ import { useActionState } from '../../hooks';
37
36
  import { createSyntheticEvent, resolveStateVariant } from '../../utils';
38
37
  import { extractSubcomponents } from '../../utils/extractSubcomponents';
39
38
  import { HelperText } from '../HelperText';
@@ -645,6 +644,7 @@ export const TextInputLeft = memo(
645
644
  style={[textInputLeftStyles.leftElement, style]}
646
645
  onLayout={handleLayout}
647
646
  accessibilityRole="none"
647
+ tabIndex={-1}
648
648
  {...rest}>
649
649
  {children}
650
650
  </Pressable>
@@ -683,6 +683,7 @@ export const TextInputRight = memo(
683
683
  onPress={onPress}
684
684
  style={[textInputRightStyles.rightElement, style]}
685
685
  accessibilityRole="none"
686
+ tabIndex={-1}
686
687
  {...rest}>
687
688
  {children}
688
689
  </Pressable>
@@ -1,3 +1,4 @@
1
+ import { useLatest } from '@react-native-molecules/utils/hooks';
1
2
  import { memo, useCallback, useMemo, useRef } from 'react';
2
3
  import {
3
4
  type GestureResponderEvent,
@@ -7,7 +8,6 @@ import {
7
8
  type ViewProps,
8
9
  } from 'react-native';
9
10
 
10
- import { useLatest } from '../../hooks';
11
11
  import AnalogClockHours from './AnalogClockHours';
12
12
  import AnalogClockMinutes from './AnalogClockMinutes';
13
13
  import AnimatedClockSwitcher from './AnimatedClockSwitcher';
@@ -1,10 +1,10 @@
1
1
  // @typescript-eslint/no-unused-vars
2
2
  // WORK IN PROGRESS
3
3
 
4
+ import { useLatest } from '@react-native-molecules/utils/hooks';
4
5
  import { memo, useCallback, useEffect, useRef, useState } from 'react';
5
6
  import { TextInput as TextInputNative, useWindowDimensions, View } from 'react-native';
6
7
 
7
- import { useLatest } from '../../hooks';
8
8
  import { resolveStateVariant } from '../../utils';
9
9
  import { Text } from '../Text';
10
10
  import AmPmSwitcher from './AmPmSwitcher';
@@ -1,8 +1,8 @@
1
+ import { useControlledValue } from '@react-native-molecules/utils/hooks';
1
2
  import { memo, useCallback, useMemo, useState } from 'react';
2
3
  import { type StyleProp, View, type ViewStyle } from 'react-native';
3
4
 
4
5
  import { getRegisteredComponentWithFallback } from '../../core';
5
- import { useControlledValue } from '../../hooks';
6
6
  import { format, parse } from '../../utils/date-fns';
7
7
  import { useOptionalDatePickerContext } from '../DatePicker/context';
8
8
  import AnalogClock from './AnalogClock';
@@ -1,9 +1,9 @@
1
+ import { useControlledValue } from '@react-native-molecules/utils/hooks';
1
2
  import type { ReactNode } from 'react';
2
3
  import { memo, useMemo } from 'react';
3
4
  import { KeyboardAvoidingView, Platform, View } from 'react-native';
4
5
 
5
6
  import { getRegisteredComponentWithFallback } from '../../core';
6
- import { useControlledValue } from '../../hooks';
7
7
  import { DatePickerActions, DatePickerProvider } from '../DatePicker';
8
8
  import type {
9
9
  DatePickerContextType,
@@ -1,3 +1,4 @@
1
+ import { useToggle } from '@react-native-molecules/utils/hooks';
1
2
  import {
2
3
  createContext,
3
4
  memo,
@@ -9,7 +10,6 @@ import {
9
10
  useRef,
10
11
  } from 'react';
11
12
 
12
- import { useToggle } from '../../hooks';
13
13
  import { extractSubcomponents } from '../../utils/extractSubcomponents';
14
14
 
15
15
  export type Props = {
@@ -1,3 +1,4 @@
1
+ import { noop } from '@react-native-molecules/utils/helpers/lodash';
1
2
  import { forwardRef, memo, type ReactNode, useCallback, useRef } from 'react';
2
3
  import {
3
4
  type GestureResponderEvent,
@@ -5,7 +6,6 @@ import {
5
6
  Pressable,
6
7
  type PressableProps,
7
8
  type StyleProp,
8
- View,
9
9
  type ViewStyle,
10
10
  } from 'react-native';
11
11
  import { StyleSheet } from 'react-native-unistyles';
@@ -123,7 +123,7 @@ const TouchableRipple = (
123
123
  rippleColor: rippleColorProp,
124
124
  underlayColor: _underlayColor,
125
125
  rippleAlpha = 0.24,
126
- onPress,
126
+ onPress = noop,
127
127
  children,
128
128
  onPressIn: onPressInProp,
129
129
  onPressOut: onPressOutProp,
@@ -157,25 +157,16 @@ const TouchableRipple = (
157
157
  style,
158
158
  ];
159
159
 
160
- // Track whether pointer is currently down for handling pointer leave
161
- const isPointerDownRef = useRef(false);
162
- // Store current target element to clean up ripples on pointer up/leave
163
- const currentTargetRef = useRef<HTMLElement | null>(null);
160
+ // The active ripple is tracked so onPressOut can fade it. Driving the lifecycle
161
+ // off Pressable's press events (instead of raw pointer events) means a nested
162
+ // element that captures the gesture won't trigger an orphan ripple — Pressable
163
+ // only fires onPressIn when its own press is being handled.
164
+ const activeRippleRef = useRef<HTMLElement | null>(null);
164
165
 
165
- // Using 'any' for event types to support both React DOM PointerEvent and React Native events
166
- // This is a web-only file, so we primarily handle DOM pointer events
167
- const handlePointerDown = useCallback(
168
- (e: any) => {
169
- onPressInProp?.(e as GestureResponderEvent);
170
-
171
- if (disabled) return;
172
-
173
- isPointerDownRef.current = true;
174
-
175
- const button = e.currentTarget as HTMLElement;
176
- currentTargetRef.current = button;
177
- const computedStyle = window.getComputedStyle(button);
178
- const dimensions = button.getBoundingClientRect();
166
+ const startRipple = useCallback(
167
+ (host: HTMLElement, x: number, y: number) => {
168
+ const computedStyle = window.getComputedStyle(host);
169
+ const dimensions = host.getBoundingClientRect();
179
170
 
180
171
  const resolvedRippleColor =
181
172
  rippleColorResolvedProp ??
@@ -187,46 +178,14 @@ const TouchableRipple = (
187
178
  )
188
179
  : String(themeRippleFallback));
189
180
 
190
- let touchX: number;
191
- let touchY: number;
192
-
193
- if (centered) {
194
- // If centered, always position ripple at center
195
- touchX = dimensions.width / 2;
196
- touchY = dimensions.height / 2;
197
- } else if ('clientX' in e && 'clientY' in e) {
198
- // Web pointer event - calculate position relative to element
199
- touchX = e.clientX - dimensions.left;
200
- touchY = e.clientY - dimensions.top;
201
- } else if (e.nativeEvent) {
202
- // React Native gesture event
203
- const { changedTouches, touches } = e.nativeEvent;
204
- const touch = touches?.[0] ?? changedTouches?.[0];
205
- if (touch) {
206
- touchX = touch.locationX ?? dimensions.width / 2;
207
- touchY = touch.locationY ?? dimensions.height / 2;
208
- } else {
209
- touchX = dimensions.width / 2;
210
- touchY = dimensions.height / 2;
211
- }
212
- } else {
213
- // Fallback to center (keyboard activation)
214
- touchX = dimensions.width / 2;
215
- touchY = dimensions.height / 2;
216
- }
217
-
218
- // Get the size of the button to determine how big the ripple should be
219
181
  const size = centered
220
- ? // If ripple is always centered, we don't need to make it too big
221
- Math.min(dimensions.width, dimensions.height) * 1.25
222
- : // Otherwise make it twice as big so clicking on one end spreads ripple to other
223
- Math.max(dimensions.width, dimensions.height) * 2;
182
+ ? Math.min(dimensions.width, dimensions.height) * 1.25
183
+ : Math.max(dimensions.width, dimensions.height) * 2;
224
184
 
225
- // Create a container for our ripple effect so we don't need to change the parent's style
226
- const container = document.createElement('span');
185
+ const expandDuration = Math.min(size * 1.5, 350);
227
186
 
187
+ const container = document.createElement('span');
228
188
  container.setAttribute('data-molecules-ripple', '');
229
-
230
189
  Object.assign(container.style, {
231
190
  position: 'absolute',
232
191
  pointerEvents: 'none',
@@ -241,39 +200,28 @@ const TouchableRipple = (
241
200
  overflow: centered ? 'visible' : 'hidden',
242
201
  });
243
202
 
244
- // Create span to show the ripple effect
245
203
  const ripple = document.createElement('span');
246
-
247
204
  Object.assign(ripple.style, {
248
205
  position: 'absolute',
249
206
  pointerEvents: 'none',
250
207
  backgroundColor: resolvedRippleColor,
251
208
  borderRadius: '50%',
252
-
253
- /* Transition configuration */
254
- transitionProperty: 'transform opacity',
255
- transitionDuration: `${Math.min(size * 1.5, 350)}ms`,
209
+ transitionProperty: 'transform, opacity',
210
+ transitionDuration: `${expandDuration}ms`,
256
211
  transitionTimingFunction: 'linear',
257
212
  transformOrigin: 'center',
258
-
259
- /* We'll animate these properties */
260
213
  transform: 'translate3d(-50%, -50%, 0) scale3d(0.1, 0.1, 0.1)',
261
214
  opacity: '0.5',
262
-
263
- // Position the ripple where cursor was
264
- left: `${touchX}px`,
265
- top: `${touchY}px`,
215
+ left: `${x}px`,
216
+ top: `${y}px`,
266
217
  width: `${size}px`,
267
218
  height: `${size}px`,
268
219
  });
269
220
 
270
- // Finally, append it to DOM
271
221
  container.appendChild(ripple);
272
- button.appendChild(container);
222
+ host.appendChild(container);
223
+ activeRippleRef.current = container;
273
224
 
274
- // rAF runs in the same frame as the event handler
275
- // Use double rAF to ensure the transition class is added in next frame
276
- // This will make sure that the transition animation is triggered
277
225
  requestAnimationFrame(() => {
278
226
  requestAnimationFrame(() => {
279
227
  Object.assign(ripple.style, {
@@ -283,96 +231,71 @@ const TouchableRipple = (
283
231
  });
284
232
  });
285
233
  },
286
- [
287
- onPressInProp,
288
- disabled,
289
- centered,
290
- rippleColorResolvedProp,
291
- themeRippleFallback,
292
- rippleAlpha,
293
- ],
234
+ [centered, rippleColorResolvedProp, themeRippleFallback, rippleAlpha],
294
235
  );
295
236
 
296
- const fadeOutRipples = useCallback((target: HTMLElement) => {
297
- const containers = target.querySelectorAll(
298
- '[data-molecules-ripple]',
299
- ) as NodeListOf<HTMLElement>;
300
-
301
- requestAnimationFrame(() => {
302
- requestAnimationFrame(() => {
303
- containers.forEach(container => {
304
- const ripple = container.firstChild as HTMLSpanElement;
305
-
306
- Object.assign(ripple.style, {
307
- transitionDuration: '250ms',
308
- opacity: 0,
309
- });
310
-
311
- // Finally remove the span after the transition
312
- setTimeout(() => {
313
- const { parentNode } = container;
314
-
315
- if (parentNode) {
316
- parentNode.removeChild(container);
317
- }
318
- }, 500);
319
- });
320
- });
237
+ const fadeRipple = useCallback((container: HTMLElement | null) => {
238
+ if (!container) return;
239
+ const ripple = container.firstChild as HTMLElement | null;
240
+ if (!ripple) {
241
+ container.parentNode?.removeChild(container);
242
+ return;
243
+ }
244
+
245
+ const onTransitionEnd = (ev: TransitionEvent) => {
246
+ if (ev.propertyName !== 'opacity') return;
247
+ ripple.removeEventListener('transitionend', onTransitionEnd);
248
+ container.parentNode?.removeChild(container);
249
+ };
250
+ ripple.addEventListener('transitionend', onTransitionEnd);
251
+
252
+ Object.assign(ripple.style, {
253
+ transitionDuration: '250ms',
254
+ opacity: '0',
321
255
  });
322
256
  }, []);
323
257
 
324
- const handlePointerUp = useCallback(
325
- (e: any) => {
326
- onPressOutProp?.(e as GestureResponderEvent);
327
-
328
- if (disabled || !isPointerDownRef.current) return;
329
-
330
- isPointerDownRef.current = false;
331
- currentTargetRef.current = null;
332
-
333
- const target = e.currentTarget as HTMLElement;
334
- fadeOutRipples(target);
335
- },
336
- [onPressOutProp, disabled, fadeOutRipples],
337
- );
338
-
339
- const handlePointerLeave = useCallback(
340
- (e: any) => {
341
- // Only fade out if pointer was down (dragging out of element)
342
- if (disabled || !isPointerDownRef.current) return;
258
+ const handlePressIn = useCallback(
259
+ (e: GestureResponderEvent) => {
260
+ onPressInProp?.(e);
261
+ if (disabled) return;
343
262
 
344
- isPointerDownRef.current = false;
345
- currentTargetRef.current = null;
263
+ const host = e.currentTarget as unknown as HTMLElement | null;
264
+ if (!host || typeof host.appendChild !== 'function') return;
265
+
266
+ const rect = host.getBoundingClientRect();
267
+ let x = rect.width / 2;
268
+ let y = rect.height / 2;
269
+
270
+ if (!centered) {
271
+ const ne: any = e.nativeEvent;
272
+ if (ne) {
273
+ if (typeof ne.locationX === 'number' && typeof ne.locationY === 'number') {
274
+ x = ne.locationX;
275
+ y = ne.locationY;
276
+ } else if (typeof ne.clientX === 'number' && typeof ne.clientY === 'number') {
277
+ x = ne.clientX - rect.left;
278
+ y = ne.clientY - rect.top;
279
+ }
280
+ }
281
+ }
346
282
 
347
- const target = e.currentTarget as HTMLElement;
348
- fadeOutRipples(target);
283
+ startRipple(host, x, y);
349
284
  },
350
- [disabled, fadeOutRipples],
285
+ [onPressInProp, disabled, centered, startRipple],
351
286
  );
352
287
 
353
- const handlePointerCancel = useCallback(
354
- (e: any) => {
355
- if (disabled || !isPointerDownRef.current) return;
356
-
357
- isPointerDownRef.current = false;
358
- currentTargetRef.current = null;
359
-
360
- const target = e.currentTarget as HTMLElement;
361
- fadeOutRipples(target);
288
+ const handlePressOut = useCallback(
289
+ (e: GestureResponderEvent) => {
290
+ onPressOutProp?.(e);
291
+ const container = activeRippleRef.current;
292
+ activeRippleRef.current = null;
293
+ fadeRipple(container);
362
294
  },
363
- [disabled, fadeOutRipples],
295
+ [onPressOutProp, fadeRipple],
364
296
  );
365
297
 
366
- const Component = asChild ? Slot : onPress ? Pressable : View;
367
-
368
- // Use pointer events for universal compatibility (works on any HTML element)
369
- // These events work with mouse, touch, and stylus inputs
370
- const pointerEventProps = {
371
- onPointerDown: handlePointerDown,
372
- onPointerUp: handlePointerUp,
373
- onPointerLeave: handlePointerLeave,
374
- onPointerCancel: handlePointerCancel,
375
- };
298
+ const Component = asChild ? Slot : Pressable;
376
299
 
377
300
  const accessibilityRoleProp = (rest as { accessibilityRole?: unknown }).accessibilityRole;
378
301
  const roleProp = (rest as { role?: unknown }).role;
@@ -386,8 +309,9 @@ const TouchableRipple = (
386
309
  style={containerStyle}
387
310
  ref={ref}
388
311
  onPress={onPress}
389
- disabled={disabled}
390
- {...pointerEventProps}>
312
+ onPressIn={handlePressIn}
313
+ onPressOut={handlePressOut}
314
+ disabled={disabled}>
391
315
  {children}
392
316
  </Component>
393
317
  );
package/hocs/index.tsx CHANGED
@@ -1,5 +1,5 @@
1
- export { default as typedMemo } from './typedMemo';
2
1
  export {
3
2
  useCurrentIndexStore,
4
3
  default as withKeyboardAccessibility,
5
4
  } from './withKeyboardAccessibility';
5
+ export { typedMemo } from '@react-native-molecules/utils/hocs';
@@ -1,3 +1,5 @@
1
+ import { createFastContext } from '@react-native-molecules/utils/fast-context';
2
+ import { useLatest } from '@react-native-molecules/utils/hooks';
1
3
  import {
2
4
  type ComponentType,
3
5
  forwardRef,
@@ -15,9 +17,6 @@ import type { FlatList } from 'react-native';
15
17
  import type { SectionList } from 'react-native';
16
18
  import { Platform } from 'react-native';
17
19
 
18
- import { createFastContext } from '../fast-context';
19
- import useLatest from '../hooks/useLatest';
20
-
21
20
  export type Store = {
22
21
  currentIndex: number | null;
23
22
  };
package/hooks/index.tsx CHANGED
@@ -4,7 +4,6 @@ export * from './useActionState';
4
4
  export * from './useActive';
5
5
  export { default as useColorMode } from './useColorMode';
6
6
  export { useContrastColor } from './useContrastColor';
7
- export { default as useControlledValue } from './useControlledValue';
8
7
  export * from './useFocus';
9
8
  export {
10
9
  type NumberMaskConfig,
@@ -13,10 +12,7 @@ export {
13
12
  } from './useHandleNumberFormat';
14
13
  export * from './useHover';
15
14
  export * from './useKeyboardDismissable';
16
- export { default as useLatest } from './useLatest';
17
- export { useMediaQuery } from './useMediaQuery';
18
- export { useMergedRefs } from './useMergedRefs';
19
- export { default as usePrevious } from './usePrevious';
15
+ export * from './useMediaQuery';
20
16
  export * from './useQueryFilter';
21
17
  export * from './useTheme';
22
- export { default as useToggle } from './useToggle';
18
+ export * from '@react-native-molecules/utils/hooks';