react-native-molecules 0.5.0-beta.22 → 0.5.0-beta.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.
@@ -5,12 +5,12 @@ import {
5
5
  Pressable,
6
6
  type PressableProps,
7
7
  type StyleProp,
8
- View,
9
8
  type ViewStyle,
10
9
  } from 'react-native';
11
10
  import { StyleSheet } from 'react-native-unistyles';
12
11
 
13
12
  import { useTheme } from '../../hooks/useTheme';
13
+ import { noop } from '../../utils/lodash';
14
14
  import { Slot } from '../Slot';
15
15
  import { rippleColorFromBackground } from './rippleFromForegroundColor';
16
16
  import { touchableRippleStyles } from './utils';
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-molecules",
3
- "version": "0.5.0-beta.22",
3
+ "version": "0.5.0-beta.23",
4
4
  "author": "Thet Aung <thetaung.dev@gmail.com>",
5
5
  "license": "MIT",
6
6
  "main": "index.ts",
@@ -9,7 +9,8 @@
9
9
  "components/DatePickerInline/store.tsx",
10
10
  "components/List/context.tsx",
11
11
  "components/Select/context.tsx",
12
- "components/TimePicker/context.tsx"
12
+ "components/TimePicker/context.tsx",
13
+ "components/Popover/common.ts"
13
14
  ],
14
15
  "files": [
15
16
  "components",