react-native-molecules 0.5.0-beta.21 → 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.
Files changed (59) hide show
  1. package/components/Button/Button.tsx +3 -1
  2. package/components/Card/Card.tsx +1 -1
  3. package/components/Checkbox/CheckboxBase.ios.tsx +1 -4
  4. package/components/Checkbox/CheckboxBase.tsx +2 -7
  5. package/components/DatePicker/DateCalendar.tsx +4 -4
  6. package/components/DatePicker/DatePickerModal.tsx +2 -1
  7. package/components/DatePicker/utils.ts +2 -0
  8. package/components/DatePickerInline/DatePickerDockedHeader.tsx +3 -3
  9. package/components/DatePickerInline/DatePickerInline.tsx +1 -1
  10. package/components/DatePickerInline/DatePickerInlineBase.tsx +2 -2
  11. package/components/DatePickerInline/DatePickerInlineHeader.tsx +43 -17
  12. package/components/DatePickerInline/HeaderItem.tsx +2 -2
  13. package/components/DatePickerInline/MonthPicker.tsx +58 -64
  14. package/components/DatePickerInline/Swiper.native.tsx +2 -2
  15. package/components/DatePickerInline/Swiper.tsx +3 -3
  16. package/components/DatePickerInline/YearPicker.tsx +108 -119
  17. package/components/DatePickerInline/{DatePickerContext.tsx → store.tsx} +7 -3
  18. package/components/DatePickerInline/types.ts +1 -1
  19. package/components/Divider/Divider.tsx +192 -0
  20. package/components/Divider/index.tsx +11 -0
  21. package/components/Drawer/DrawerItemGroup.tsx +3 -7
  22. package/components/IconButton/IconButton.tsx +2 -12
  23. package/components/List/List.tsx +275 -0
  24. package/components/List/context.tsx +26 -0
  25. package/components/List/index.ts +8 -0
  26. package/components/List/types.ts +117 -0
  27. package/components/List/utils.ts +79 -0
  28. package/components/Menu/Menu.tsx +146 -19
  29. package/components/Menu/index.tsx +9 -7
  30. package/components/Menu/utils.ts +21 -70
  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/RadioButton/RadioButtonAndroid.tsx +38 -54
  37. package/components/RadioButton/RadioButtonIOS.tsx +2 -16
  38. package/components/Select/Select.tsx +307 -501
  39. package/components/Select/context.tsx +39 -32
  40. package/components/Select/types.ts +63 -56
  41. package/components/Select/utils.ts +19 -44
  42. package/components/Text/textFactory.tsx +17 -5
  43. package/components/TimePicker/TimeInput.tsx +2 -7
  44. package/components/TimePicker/utils.ts +0 -4
  45. package/components/TouchableRipple/TouchableRipple.native.tsx +36 -5
  46. package/components/TouchableRipple/TouchableRipple.tsx +121 -163
  47. package/components/TouchableRipple/rippleFromForegroundColor.ts +21 -0
  48. package/package.json +6 -3
  49. package/components/HorizontalDivider/HorizontalDivider.tsx +0 -103
  50. package/components/HorizontalDivider/index.tsx +0 -9
  51. package/components/ListItem/ListItem.tsx +0 -138
  52. package/components/ListItem/ListItemDescription.tsx +0 -25
  53. package/components/ListItem/ListItemTitle.tsx +0 -25
  54. package/components/ListItem/index.tsx +0 -14
  55. package/components/ListItem/utils.ts +0 -115
  56. package/components/Menu/MenuDivider.tsx +0 -13
  57. package/components/Menu/MenuItem.tsx +0 -128
  58. package/components/VerticalDivider/VerticalDivider.tsx +0 -100
  59. package/components/VerticalDivider/index.tsx +0 -9
@@ -1,15 +1,18 @@
1
- import { forwardRef, memo, type ReactNode, useCallback, useMemo, useRef } from 'react';
1
+ import { forwardRef, memo, type ReactNode, useCallback, useRef } from 'react';
2
2
  import {
3
3
  type GestureResponderEvent,
4
+ Platform,
4
5
  Pressable,
5
6
  type PressableProps,
6
7
  type StyleProp,
7
- View,
8
8
  type ViewStyle,
9
9
  } from 'react-native';
10
10
  import { StyleSheet } from 'react-native-unistyles';
11
11
 
12
+ import { useTheme } from '../../hooks/useTheme';
13
+ import { noop } from '../../utils/lodash';
12
14
  import { Slot } from '../Slot';
15
+ import { rippleColorFromBackground } from './rippleFromForegroundColor';
13
16
  import { touchableRippleStyles } from './utils';
14
17
 
15
18
  export type Props = PressableProps & {
@@ -46,6 +49,11 @@ export type Props = PressableProps & {
46
49
  * Color of the underlay for the highlight effect (Android < 5.0 and iOS).
47
50
  */
48
51
  underlayColor?: string;
52
+ /**
53
+ * Alpha used for auto-derived ripple color when `rippleColor` is not provided.
54
+ * @default 0.24
55
+ */
56
+ rippleAlpha?: number;
49
57
  /**
50
58
  * Content of the `TouchableRipple`.
51
59
  */
@@ -114,7 +122,8 @@ const TouchableRipple = (
114
122
  disabled: disabledProp,
115
123
  rippleColor: rippleColorProp,
116
124
  underlayColor: _underlayColor,
117
- onPress,
125
+ rippleAlpha = 0.24,
126
+ onPress = noop,
118
127
  children,
119
128
  onPressIn: onPressInProp,
120
129
  onPressOut: onPressOutProp,
@@ -126,85 +135,57 @@ const TouchableRipple = (
126
135
  ) => {
127
136
  // TODO - enable ripple onLongPress, need to check for mobile as well
128
137
  const disabled = disabledProp;
138
+ const theme = useTheme();
129
139
 
130
140
  const componentStyles = touchableRippleStyles;
131
141
 
132
- const { rippleColor, containerStyle } = useMemo(() => {
133
- const { rippleColor: defaultRippleColor } = componentStyles.root;
134
-
135
- return {
136
- rippleColor: rippleColorProp || defaultRippleColor,
137
- containerStyle: [
138
- styles.touchable,
139
- { borderRadius: 'inherit' },
140
- borderless && styles.borderless,
141
- // ...(Platform.OS === 'web' && !disabled ? ({ cursor: 'pointer' } as any) : {}),
142
- componentStyles.root,
143
- style,
144
- ],
145
- };
146
- }, [borderless, componentStyles.root, rippleColorProp, style]);
147
-
148
- // Track whether pointer is currently down for handling pointer leave
149
- const isPointerDownRef = useRef(false);
150
- // Store current target element to clean up ripples on pointer up/leave
151
- const currentTargetRef = useRef<HTMLElement | null>(null);
152
-
153
- // Using 'any' for event types to support both React DOM PointerEvent and React Native events
154
- // This is a web-only file, so we primarily handle DOM pointer events
155
- const handlePointerDown = useCallback(
156
- (e: any) => {
157
- onPressInProp?.(e as GestureResponderEvent);
158
-
159
- if (disabled) return;
160
-
161
- isPointerDownRef.current = true;
162
-
163
- const button = e.currentTarget as HTMLElement;
164
- currentTargetRef.current = button;
165
- const computedStyle = window.getComputedStyle(button);
166
- const dimensions = button.getBoundingClientRect();
167
-
168
- let touchX: number;
169
- let touchY: number;
170
-
171
- if (centered) {
172
- // If centered, always position ripple at center
173
- touchX = dimensions.width / 2;
174
- touchY = dimensions.height / 2;
175
- } else if ('clientX' in e && 'clientY' in e) {
176
- // Web pointer event - calculate position relative to element
177
- touchX = e.clientX - dimensions.left;
178
- touchY = e.clientY - dimensions.top;
179
- } else if (e.nativeEvent) {
180
- // React Native gesture event
181
- const { changedTouches, touches } = e.nativeEvent;
182
- const touch = touches?.[0] ?? changedTouches?.[0];
183
- if (touch) {
184
- touchX = touch.locationX ?? dimensions.width / 2;
185
- touchY = touch.locationY ?? dimensions.height / 2;
186
- } else {
187
- touchX = dimensions.width / 2;
188
- touchY = dimensions.height / 2;
189
- }
190
- } else {
191
- // Fallback to center (keyboard activation)
192
- touchX = dimensions.width / 2;
193
- touchY = dimensions.height / 2;
194
- }
142
+ const { rippleColor: themeRippleFallback } = componentStyles.root;
143
+
144
+ const tokenResolvedColor =
145
+ typeof rippleColorProp === 'string'
146
+ ? theme.colors[rippleColorProp as keyof typeof theme.colors]
147
+ : undefined;
148
+
149
+ const rippleColorResolvedProp =
150
+ typeof tokenResolvedColor === 'string' ? tokenResolvedColor : rippleColorProp;
151
+ const containerStyle = [
152
+ styles.touchable,
153
+ { borderRadius: 'inherit' },
154
+ borderless && styles.borderless,
155
+ // ...(Platform.OS === 'web' && !disabled ? ({ cursor: 'pointer' } as any) : {}),
156
+ componentStyles.root,
157
+ style,
158
+ ];
159
+
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);
165
+
166
+ const startRipple = useCallback(
167
+ (host: HTMLElement, x: number, y: number) => {
168
+ const computedStyle = window.getComputedStyle(host);
169
+ const dimensions = host.getBoundingClientRect();
170
+
171
+ const resolvedRippleColor =
172
+ rippleColorResolvedProp ??
173
+ (Platform.OS === 'web'
174
+ ? rippleColorFromBackground(
175
+ computedStyle.backgroundColor,
176
+ String(themeRippleFallback),
177
+ rippleAlpha,
178
+ )
179
+ : String(themeRippleFallback));
195
180
 
196
- // Get the size of the button to determine how big the ripple should be
197
181
  const size = centered
198
- ? // If ripple is always centered, we don't need to make it too big
199
- Math.min(dimensions.width, dimensions.height) * 1.25
200
- : // Otherwise make it twice as big so clicking on one end spreads ripple to other
201
- Math.max(dimensions.width, dimensions.height) * 2;
182
+ ? Math.min(dimensions.width, dimensions.height) * 1.25
183
+ : Math.max(dimensions.width, dimensions.height) * 2;
202
184
 
203
- // Create a container for our ripple effect so we don't need to change the parent's style
204
- const container = document.createElement('span');
185
+ const expandDuration = Math.min(size * 1.5, 350);
205
186
 
187
+ const container = document.createElement('span');
206
188
  container.setAttribute('data-molecules-ripple', '');
207
-
208
189
  Object.assign(container.style, {
209
190
  position: 'absolute',
210
191
  pointerEvents: 'none',
@@ -219,39 +200,28 @@ const TouchableRipple = (
219
200
  overflow: centered ? 'visible' : 'hidden',
220
201
  });
221
202
 
222
- // Create span to show the ripple effect
223
203
  const ripple = document.createElement('span');
224
-
225
204
  Object.assign(ripple.style, {
226
205
  position: 'absolute',
227
206
  pointerEvents: 'none',
228
- backgroundColor: rippleColor,
207
+ backgroundColor: resolvedRippleColor,
229
208
  borderRadius: '50%',
230
-
231
- /* Transition configuration */
232
- transitionProperty: 'transform opacity',
233
- transitionDuration: `${Math.min(size * 1.5, 350)}ms`,
209
+ transitionProperty: 'transform, opacity',
210
+ transitionDuration: `${expandDuration}ms`,
234
211
  transitionTimingFunction: 'linear',
235
212
  transformOrigin: 'center',
236
-
237
- /* We'll animate these properties */
238
213
  transform: 'translate3d(-50%, -50%, 0) scale3d(0.1, 0.1, 0.1)',
239
214
  opacity: '0.5',
240
-
241
- // Position the ripple where cursor was
242
- left: `${touchX}px`,
243
- top: `${touchY}px`,
215
+ left: `${x}px`,
216
+ top: `${y}px`,
244
217
  width: `${size}px`,
245
218
  height: `${size}px`,
246
219
  });
247
220
 
248
- // Finally, append it to DOM
249
221
  container.appendChild(ripple);
250
- button.appendChild(container);
222
+ host.appendChild(container);
223
+ activeRippleRef.current = container;
251
224
 
252
- // rAF runs in the same frame as the event handler
253
- // Use double rAF to ensure the transition class is added in next frame
254
- // This will make sure that the transition animation is triggered
255
225
  requestAnimationFrame(() => {
256
226
  requestAnimationFrame(() => {
257
227
  Object.assign(ripple.style, {
@@ -261,99 +231,87 @@ const TouchableRipple = (
261
231
  });
262
232
  });
263
233
  },
264
- [onPressInProp, disabled, centered, rippleColor],
234
+ [centered, rippleColorResolvedProp, themeRippleFallback, rippleAlpha],
265
235
  );
266
236
 
267
- const fadeOutRipples = useCallback((target: HTMLElement) => {
268
- const containers = target.querySelectorAll(
269
- '[data-molecules-ripple]',
270
- ) as NodeListOf<HTMLElement>;
271
-
272
- requestAnimationFrame(() => {
273
- requestAnimationFrame(() => {
274
- containers.forEach(container => {
275
- const ripple = container.firstChild as HTMLSpanElement;
276
-
277
- Object.assign(ripple.style, {
278
- transitionDuration: '250ms',
279
- opacity: 0,
280
- });
281
-
282
- // Finally remove the span after the transition
283
- setTimeout(() => {
284
- const { parentNode } = container;
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);
285
251
 
286
- if (parentNode) {
287
- parentNode.removeChild(container);
288
- }
289
- }, 500);
290
- });
291
- });
252
+ Object.assign(ripple.style, {
253
+ transitionDuration: '250ms',
254
+ opacity: '0',
292
255
  });
293
256
  }, []);
294
257
 
295
- const handlePointerUp = useCallback(
296
- (e: any) => {
297
- onPressOutProp?.(e as GestureResponderEvent);
298
-
299
- if (disabled || !isPointerDownRef.current) return;
300
-
301
- isPointerDownRef.current = false;
302
- currentTargetRef.current = null;
303
-
304
- const target = e.currentTarget as HTMLElement;
305
- fadeOutRipples(target);
306
- },
307
- [onPressOutProp, disabled, fadeOutRipples],
308
- );
309
-
310
- const handlePointerLeave = useCallback(
311
- (e: any) => {
312
- // Only fade out if pointer was down (dragging out of element)
313
- if (disabled || !isPointerDownRef.current) return;
258
+ const handlePressIn = useCallback(
259
+ (e: GestureResponderEvent) => {
260
+ onPressInProp?.(e);
261
+ if (disabled) return;
314
262
 
315
- isPointerDownRef.current = false;
316
- 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
+ }
317
282
 
318
- const target = e.currentTarget as HTMLElement;
319
- fadeOutRipples(target);
283
+ startRipple(host, x, y);
320
284
  },
321
- [disabled, fadeOutRipples],
285
+ [onPressInProp, disabled, centered, startRipple],
322
286
  );
323
287
 
324
- const handlePointerCancel = useCallback(
325
- (e: any) => {
326
- if (disabled || !isPointerDownRef.current) return;
327
-
328
- isPointerDownRef.current = false;
329
- currentTargetRef.current = null;
330
-
331
- const target = e.currentTarget as HTMLElement;
332
- fadeOutRipples(target);
288
+ const handlePressOut = useCallback(
289
+ (e: GestureResponderEvent) => {
290
+ onPressOutProp?.(e);
291
+ const container = activeRippleRef.current;
292
+ activeRippleRef.current = null;
293
+ fadeRipple(container);
333
294
  },
334
- [disabled, fadeOutRipples],
295
+ [onPressOutProp, fadeRipple],
335
296
  );
336
297
 
337
- const Component = asChild ? Slot : onPress ? Pressable : View;
298
+ const Component = asChild ? Slot : Pressable;
338
299
 
339
- // Use pointer events for universal compatibility (works on any HTML element)
340
- // These events work with mouse, touch, and stylus inputs
341
- const pointerEventProps = {
342
- onPointerDown: handlePointerDown,
343
- onPointerUp: handlePointerUp,
344
- onPointerLeave: handlePointerLeave,
345
- onPointerCancel: handlePointerCancel,
346
- };
300
+ const accessibilityRoleProp = (rest as { accessibilityRole?: unknown }).accessibilityRole;
301
+ const roleProp = (rest as { role?: unknown }).role;
302
+ const applyDefaultWebButtonRole =
303
+ !!onPress && accessibilityRoleProp === undefined && roleProp === undefined;
347
304
 
348
305
  return (
349
306
  <Component
350
- {...(onPress ? { role: 'button' } : {})}
307
+ {...(applyDefaultWebButtonRole ? { role: 'button' } : {})}
351
308
  {...rest}
352
309
  style={containerStyle}
353
310
  ref={ref}
354
311
  onPress={onPress}
355
- disabled={disabled}
356
- {...pointerEventProps}>
312
+ onPressIn={handlePressIn}
313
+ onPressOut={handlePressOut}
314
+ disabled={disabled}>
357
315
  {children}
358
316
  </Component>
359
317
  );
@@ -0,0 +1,21 @@
1
+ import setColor from 'color';
2
+
3
+ /** Ripple ink derived from background color for better contrast. */
4
+ export function rippleColorFromBackground(
5
+ backgroundColor: string | undefined,
6
+ fallback: string,
7
+ alpha: number = 0.24,
8
+ ): string {
9
+ if (!backgroundColor || backgroundColor === '') {
10
+ return fallback;
11
+ }
12
+ try {
13
+ const base = setColor(backgroundColor);
14
+ if (base.alpha() < 0.05) {
15
+ return fallback;
16
+ }
17
+ return base.isLight() ? `rgba(0, 0, 0, ${alpha})` : `rgba(255, 255, 255, ${alpha})`;
18
+ } catch {
19
+ return fallback;
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "react-native-molecules",
3
- "version": "0.5.0-beta.21",
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",
7
7
  "sideEffects": [
8
8
  "components/DatePicker/context.tsx",
9
+ "components/DatePickerInline/store.tsx",
10
+ "components/List/context.tsx",
9
11
  "components/Select/context.tsx",
10
- "components/TimePicker/context.tsx"
12
+ "components/TimePicker/context.tsx",
13
+ "components/Popover/common.ts"
11
14
  ],
12
15
  "files": [
13
16
  "components",
@@ -78,7 +81,7 @@
78
81
  "react-native": "0.81.4",
79
82
  "react-native-builder-bob": "^0.17.1",
80
83
  "react-native-reanimated": "~4.1.1",
81
- "react-native-unistyles": "^3.0.22",
84
+ "react-native-unistyles": "^3.2.4",
82
85
  "react-native-web": "~0.21.1"
83
86
  },
84
87
  "eslintIgnore": [
@@ -1,103 +0,0 @@
1
- import { memo } from 'react';
2
- import { type StyleProp, View, type ViewProps, type ViewStyle } from 'react-native';
3
- import { StyleSheet } from 'react-native-unistyles';
4
-
5
- import { getRegisteredComponentStylesWithFallback } from '../../core';
6
-
7
- export type Props = Omit<ViewProps, 'children'> & {
8
- /**
9
- * left inset of the divider.
10
- */
11
- leftInset?: number;
12
- /**
13
- * right inset of the divider.
14
- */
15
- rightInset?: number;
16
- /**
17
- * Whether divider should be bolded.
18
- */
19
- bold?: boolean;
20
- /**
21
- * Vertical spacing of the Divider
22
- */
23
- spacing?: number;
24
- style?: StyleProp<ViewStyle>;
25
- };
26
-
27
- /**
28
- * A divider is a thin, lightweight separator that groups content in lists and page layouts.
29
- *
30
- * <div class="screenshots">
31
- * <figure>
32
- * <img class="medium" src="screenshots/divider.png" />
33
- * </figure>
34
- * </div>
35
- *
36
- * ## Usage
37
- * ```js
38
- * import * as React from 'react';
39
- * import { View } from 'react-native';
40
- * import { Divider, Text } from 'react-native-paper';
41
- *
42
- * const MyComponent = () => (
43
- * <View>
44
- * <Text>Lemon</Text>
45
- * <Divider />
46
- * <Text>Mango</Text>
47
- * <Divider />
48
- * </View>
49
- * );
50
- *
51
- * export default MyComponent;
52
- * ```
53
- */
54
-
55
- const HorizontalDivider = ({
56
- leftInset = 0,
57
- rightInset = 0,
58
- style,
59
- bold = false,
60
- spacing = 0,
61
- ...rest
62
- }: Props) => {
63
- horizontalDividerStyles.useVariants({
64
- isBold: bold,
65
- });
66
-
67
- return (
68
- <View
69
- {...rest}
70
- style={
71
- [
72
- horizontalDividerStyles.root,
73
- leftInset && { marginLeft: leftInset },
74
- rightInset && { marginRight: rightInset },
75
- spacing && { marginVertical: spacing },
76
- style,
77
- ] as StyleProp<ViewStyle>
78
- }
79
- />
80
- );
81
- };
82
-
83
- export const horizontalDividerStylesDefault = StyleSheet.create(theme => ({
84
- root: {
85
- height: StyleSheet.hairlineWidth,
86
- background: theme.colors.outlineVariant,
87
-
88
- variants: {
89
- isBold: {
90
- true: {
91
- height: 1,
92
- },
93
- },
94
- },
95
- },
96
- }));
97
-
98
- export const horizontalDividerStyles = getRegisteredComponentStylesWithFallback(
99
- 'HorizontalDivider',
100
- horizontalDividerStylesDefault,
101
- );
102
-
103
- export default memo(HorizontalDivider);
@@ -1,9 +0,0 @@
1
- import { getRegisteredComponentWithFallback } from '../../core';
2
- import HorizontalDividerDefault from './HorizontalDivider';
3
-
4
- export const HorizontalDivider = getRegisteredComponentWithFallback(
5
- 'HorizontalDivider',
6
- HorizontalDividerDefault,
7
- );
8
-
9
- export { type Props as HorizontalDividerProps, horizontalDividerStyles } from './HorizontalDivider';