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

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 (51) hide show
  1. package/components/Card/Card.tsx +1 -1
  2. package/components/Checkbox/CheckboxBase.ios.tsx +1 -4
  3. package/components/Checkbox/CheckboxBase.tsx +2 -7
  4. package/components/DatePicker/DateCalendar.tsx +4 -4
  5. package/components/DatePicker/DatePickerModal.tsx +2 -1
  6. package/components/DatePickerInline/DatePickerDockedHeader.tsx +3 -3
  7. package/components/DatePickerInline/DatePickerInline.tsx +1 -1
  8. package/components/DatePickerInline/DatePickerInlineBase.tsx +2 -2
  9. package/components/DatePickerInline/DatePickerInlineHeader.tsx +43 -17
  10. package/components/DatePickerInline/HeaderItem.tsx +2 -2
  11. package/components/DatePickerInline/MonthPicker.tsx +64 -54
  12. package/components/DatePickerInline/Swiper.native.tsx +2 -2
  13. package/components/DatePickerInline/Swiper.tsx +3 -3
  14. package/components/DatePickerInline/YearPicker.tsx +136 -112
  15. package/components/DatePickerInline/{DatePickerContext.tsx → store.tsx} +7 -3
  16. package/components/DatePickerInline/types.ts +1 -1
  17. package/components/Divider/Divider.tsx +192 -0
  18. package/components/Divider/index.tsx +11 -0
  19. package/components/Drawer/DrawerItemGroup.tsx +3 -7
  20. package/components/IconButton/IconButton.tsx +2 -12
  21. package/components/List/List.tsx +507 -0
  22. package/components/List/context.tsx +28 -0
  23. package/components/List/index.ts +9 -0
  24. package/components/List/types.ts +149 -0
  25. package/components/{ListItem → List}/utils.ts +47 -50
  26. package/components/Menu/Menu.tsx +156 -12
  27. package/components/Menu/index.tsx +11 -7
  28. package/components/Menu/utils.ts +21 -70
  29. package/components/RadioButton/RadioButtonAndroid.tsx +38 -54
  30. package/components/RadioButton/RadioButtonIOS.tsx +2 -16
  31. package/components/Select/Select.tsx +139 -497
  32. package/components/Select/context.tsx +14 -32
  33. package/components/Select/types.ts +44 -53
  34. package/components/Select/utils.ts +15 -47
  35. package/components/Text/textFactory.tsx +17 -5
  36. package/components/TimePicker/TimeInput.tsx +2 -7
  37. package/components/TimePicker/utils.ts +0 -4
  38. package/components/TouchableRipple/TouchableRipple.native.tsx +36 -5
  39. package/components/TouchableRipple/TouchableRipple.tsx +53 -19
  40. package/components/TouchableRipple/rippleFromForegroundColor.ts +21 -0
  41. package/package.json +4 -2
  42. package/components/HorizontalDivider/HorizontalDivider.tsx +0 -103
  43. package/components/HorizontalDivider/index.tsx +0 -9
  44. package/components/ListItem/ListItem.tsx +0 -138
  45. package/components/ListItem/ListItemDescription.tsx +0 -25
  46. package/components/ListItem/ListItemTitle.tsx +0 -25
  47. package/components/ListItem/index.tsx +0 -14
  48. package/components/Menu/MenuDivider.tsx +0 -13
  49. package/components/Menu/MenuItem.tsx +0 -128
  50. package/components/VerticalDivider/VerticalDivider.tsx +0 -100
  51. package/components/VerticalDivider/index.tsx +0 -9
@@ -1,44 +1,27 @@
1
1
  import type { View } from 'react-native';
2
2
 
3
3
  import { createFastContext } from '../../fast-context';
4
+ import {
5
+ ListContext,
6
+ ListContextProvider,
7
+ useListContext,
8
+ useListContextValue,
9
+ useListStoreRef,
10
+ } from '../List';
4
11
  import { registerPortalContext } from '../Portal';
5
- import type { DefaultItemT, SelectContextValue, SelectDropdownContextValue } from './types';
6
-
7
- // SelectContext - holds value, onAdd, onRemove with fast-context for optimized rendering
8
- const selectContextDefaultValue: SelectContextValue<DefaultItemT> = {
9
- value: null,
10
- multiple: false,
11
- onAdd: () => {},
12
- onRemove: () => {},
13
- disabled: false,
14
- error: false,
15
- labelKey: 'label',
16
- options: [],
17
- searchQuery: '',
18
- setSearchQuery: () => {},
19
- filteredOptions: [],
20
- };
21
-
22
- const {
23
- useStoreRef: useSelectStoreRef,
24
- Provider: SelectContextProvider,
25
- useContext: useSelectContext,
26
- useContextValue: useSelectContextValue,
27
- Context: SelectContext,
28
- } = createFastContext<SelectContextValue<DefaultItemT>>(selectContextDefaultValue, true);
12
+ import type { SelectDropdownContextValue } from './types';
29
13
 
30
14
  export {
31
- SelectContext,
32
- SelectContextProvider,
33
- useSelectContext,
34
- useSelectContextValue,
35
- useSelectStoreRef,
15
+ ListContext as SelectContext,
16
+ ListContextProvider as SelectContextProvider,
17
+ useListContext as useSelectContext,
18
+ useListContextValue as useSelectContextValue,
19
+ useListStoreRef as useSelectStoreRef,
36
20
  };
37
21
 
38
22
  // SelectDropdownContext - holds isOpen, onClose, triggerRef with fast-context
39
23
  export type SelectDropdownContextType = SelectDropdownContextValue & {
40
24
  triggerRef: React.RefObject<View> | null;
41
- contentRef: React.RefObject<any> | null;
42
25
  triggerLayout: { width: number; height: number } | null;
43
26
  setTriggerLayout: (layout: { width: number; height: number }) => void;
44
27
  };
@@ -48,7 +31,6 @@ const selectDropdownContextDefaultValue: SelectDropdownContextType = {
48
31
  onClose: () => {},
49
32
  onOpen: () => {},
50
33
  triggerRef: null,
51
- contentRef: null,
52
34
  triggerLayout: null,
53
35
  setTriggerLayout: () => {},
54
36
  };
@@ -69,4 +51,4 @@ export {
69
51
  useSelectDropdownStoreRef,
70
52
  };
71
53
 
72
- registerPortalContext([SelectContext, SelectDropdownContext]);
54
+ registerPortalContext([SelectDropdownContext]);
@@ -1,13 +1,16 @@
1
1
  import type { ComponentType, ReactNode } from 'react';
2
- import type {
3
- GestureResponderEvent,
4
- ScrollViewProps,
5
- TextInputProps,
6
- ViewProps,
7
- } from 'react-native';
2
+ import type { GestureResponderEvent, ViewProps } from 'react-native';
8
3
 
4
+ import type { ListValue } from '../List';
9
5
  import type { PopoverProps } from '../Popover';
10
6
 
7
+ export type {
8
+ ListContentProps as SelectContentProps,
9
+ ListContextValue as SelectContextValue,
10
+ ListGroupProps as SelectGroupProps,
11
+ ListSearchInputProps as SelectSearchInputProps,
12
+ } from '../List';
13
+
11
14
  export type DefaultItemT = {
12
15
  id: string | number;
13
16
  label?: string;
@@ -15,21 +18,6 @@ export type DefaultItemT = {
15
18
  [key: string]: any;
16
19
  };
17
20
 
18
- // SelectContext types
19
- export type SelectContextValue<Option extends DefaultItemT = DefaultItemT> = {
20
- value: Option | Option[] | null;
21
- multiple: boolean;
22
- onAdd: (item: Option) => void;
23
- onRemove: (item: Option) => void;
24
- disabled?: boolean;
25
- error?: boolean;
26
- labelKey?: string;
27
- options: Option[];
28
- searchQuery: string;
29
- setSearchQuery: (query: string) => void;
30
- filteredOptions: Option[];
31
- };
32
-
33
21
  // SelectDropdownContext types
34
22
  export type SelectDropdownContextValue = {
35
23
  isOpen: boolean;
@@ -38,25 +26,41 @@ export type SelectDropdownContextValue = {
38
26
  };
39
27
 
40
28
  // SelectProvider props
41
- export type SelectProps<Option extends DefaultItemT = DefaultItemT> = {
29
+ type SelectPropsBase<Option extends DefaultItemT = DefaultItemT> = {
42
30
  children: ReactNode;
43
- value?: Option['id'] | Option['id'][] | null;
44
- defaultValue?: Option['id'] | Option['id'][] | null;
45
- onChange?: (
46
- value: Option['id'] | Option['id'][] | null,
47
- item: Option,
48
- event?: GestureResponderEvent,
49
- ) => void;
50
- multiple?: boolean;
51
31
  disabled?: boolean;
52
32
  error?: boolean;
53
- labelKey?: string;
54
33
  options: Option[];
55
34
  searchKey?: string;
56
35
  onSearchChange?: (query: string) => void;
57
36
  hideSelected?: boolean;
58
37
  };
59
38
 
39
+ type SingleSelectProps<Option extends DefaultItemT = DefaultItemT> = {
40
+ 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;
48
+ };
49
+
50
+ type MultipleSelectProps<Option extends DefaultItemT = DefaultItemT> = {
51
+ 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;
59
+ };
60
+
61
+ export type SelectProps<Option extends DefaultItemT = DefaultItemT> = SelectPropsBase<Option> &
62
+ (SingleSelectProps<Option> | MultipleSelectProps<Option>);
63
+
60
64
  // Select.Trigger props
61
65
  export type SelectTriggerProps = ViewProps & {
62
66
  children?: ReactNode;
@@ -65,6 +69,7 @@ export type SelectTriggerProps = ViewProps & {
65
69
  // Select.Value props
66
70
  export type SelectValueProps = ViewProps & {
67
71
  placeholder?: string;
72
+ labelKey?: string;
68
73
  renderValue?: (value: DefaultItemT | DefaultItemT[] | null) => ReactNode;
69
74
  };
70
75
 
@@ -78,36 +83,22 @@ export type SelectDropdownProps = Omit<
78
83
  wrapperComponentProps?: Record<string, any>;
79
84
  };
80
85
 
81
- // Select.Content props
82
- export type SelectContentProps<Option extends DefaultItemT = DefaultItemT> = Omit<
83
- ScrollViewProps,
84
- 'children'
85
- > & {
86
- children: (item: Option, isSelected: boolean) => ReactNode;
87
- ContainerComponent?: ComponentType<any>;
88
- emptyState?: ReactNode;
89
- };
90
-
91
- // Select.Group props
92
- export type SelectGroupProps = ViewProps & {
93
- children: ReactNode;
94
- label?: string;
95
- };
96
-
97
86
  // Select.Option props
98
- export type SelectOptionProps<Option extends DefaultItemT = DefaultItemT> = ViewProps & {
87
+ // `accessibilityRole` and `role` are intentionally omitted: Select.Option forces them to
88
+ // "option" on web so the dropdown's keyboard navigator (which queries [role="option"]) can
89
+ // always find the rows. Allowing callers to override would silently break keyboard nav.
90
+ export type SelectOptionProps<Option extends DefaultItemT = DefaultItemT> = Omit<
91
+ ViewProps,
92
+ 'accessibilityRole' | 'role'
93
+ > & {
99
94
  /**
100
95
  * Unique id for the option
101
96
  */
102
97
  value: Option['id'];
103
- children?: ReactNode;
104
- renderItem?: (item: Option, isSelected: boolean) => ReactNode;
98
+ children: ReactNode;
105
99
  onPress?: (item: Option, event: GestureResponderEvent) => void;
106
100
  /**
107
101
  * When true, the option can't be selected (similar to HTML option disabled)
108
102
  */
109
103
  disabled?: boolean;
110
104
  };
111
-
112
- // Select.SearchInput props
113
- export type SelectSearchInputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & {};
@@ -2,6 +2,21 @@ import { StyleSheet } from 'react-native-unistyles';
2
2
 
3
3
  import { getRegisteredComponentStylesWithFallback } from '../../core';
4
4
 
5
+ /** Web-only marker on Select.Option roots so keyboard nav does not depend on role/accessibilityRole overrides. */
6
+ export const SELECT_OPTION_DATA_ATTR = 'data-molecules-select-option';
7
+
8
+ const SELECT_OPTION_SELECTOR = `[${SELECT_OPTION_DATA_ATTR}], [data-option-id], [role="option"]`;
9
+
10
+ export function collectWebSelectKeyboardOptionElements(container: ParentNode): HTMLElement[] {
11
+ return Array.from(container.querySelectorAll(SELECT_OPTION_SELECTOR)).filter(
12
+ (el): el is HTMLElement => {
13
+ if (!(el instanceof HTMLElement)) return false;
14
+ if (el.getAttribute('aria-disabled') === 'true') return false;
15
+ return true;
16
+ },
17
+ );
18
+ }
19
+
5
20
  const triggerDefaultStyles = StyleSheet.create(theme => ({
6
21
  trigger: {
7
22
  borderRadius: theme.shapes.corner.extraSmall,
@@ -83,57 +98,10 @@ export const defaultStyles = StyleSheet.create(theme => ({
83
98
  gap: 6,
84
99
  maxWidth: '90%',
85
100
  },
86
- groupLabel: {
87
- paddingHorizontal: theme.spacings['4'],
88
- paddingVertical: theme.spacings['2'],
89
- fontWeight: '600',
90
- color: theme.colors.onSurface,
91
- },
92
- item: {
93
- paddingHorizontal: theme.spacings['4'],
94
- paddingVertical: theme.spacings['3'],
95
- backgroundColor: 'transparent',
96
-
97
- _web: {
98
- cursor: 'pointer',
99
- outlineStyle: 'none',
100
- _hover: {
101
- backgroundColor: theme.colors.stateLayer.hover.primary,
102
- },
103
- _focus: {
104
- backgroundColor: theme.colors.stateLayer.focussed.primary,
105
- },
106
- },
107
- },
108
- itemSelected: {
109
- backgroundColor: theme.colors.stateLayer.hover.primary,
110
- },
111
- itemDisabled: {
112
- opacity: 0.38,
113
- _web: {
114
- cursor: 'not-allowed',
115
- },
116
- },
117
- itemDisabledText: {
118
- color: theme.colors.onSurfaceVariant,
119
- },
120
101
  searchInput: {
121
102
  marginHorizontal: theme.spacings['2'],
122
103
  marginVertical: theme.spacings['3'],
123
104
  },
124
- searchInputInput: {
125
- height: 42,
126
- },
127
- emptyState: {
128
- paddingHorizontal: theme.spacings['4'],
129
- paddingVertical: theme.spacings['6'],
130
- alignItems: 'center',
131
- justifyContent: 'center',
132
- },
133
- emptyStateText: {
134
- color: theme.colors.onSurfaceVariant,
135
- fontSize: 14,
136
- },
137
105
  }));
138
106
 
139
107
  export const triggerStyles = getRegisteredComponentStylesWithFallback(
@@ -1,6 +1,8 @@
1
1
  import { type ComponentType, createContext, forwardRef, memo, useContext } from 'react';
2
2
  import { Text, type TextProps } from 'react-native';
3
- import { StyleSheet } from 'react-native-unistyles';
3
+ import { StyleSheet, useUnistyles } from 'react-native-unistyles';
4
+
5
+ import { MD3TypescaleKey } from '../../types/theme';
4
6
 
5
7
  const HasAncestorContext = createContext(false);
6
8
 
@@ -8,18 +10,28 @@ const defaultStyles = StyleSheet.create(theme => ({
8
10
  root: { color: theme.colors.onSurface, ...theme.typescale.bodyMedium },
9
11
  }));
10
12
 
13
+ export type TypescaleKey = `${MD3TypescaleKey}`;
14
+
15
+ export type TextFactoryProps = TextProps & {
16
+ typescale?: TypescaleKey;
17
+ };
18
+
11
19
  export const textFactory = (
12
20
  componentStyles: typeof defaultStyles = defaultStyles,
13
21
  isBlockLevelElement = false,
14
22
  DefaultComponent: ComponentType<any> = Text,
15
23
  ) => {
16
24
  return memo(
17
- forwardRef((props: TextProps, ref: any) => {
18
- const { style, ...rest } = props;
25
+ forwardRef((props: TextFactoryProps, ref: any) => {
26
+ const { style, typescale, ...rest } = props;
19
27
  const hasAncestorText = useContext(HasAncestorContext);
28
+ const { theme } = useUnistyles();
29
+
30
+ const typescaleStyle = typescale ? theme.typescale[typescale] : undefined;
20
31
 
21
- const styles =
22
- hasAncestorText && !isBlockLevelElement ? style : [componentStyles?.root, style];
32
+ const baseStyle =
33
+ hasAncestorText && !isBlockLevelElement ? null : componentStyles?.root;
34
+ const styles = [baseStyle, typescaleStyle, style];
23
35
 
24
36
  return hasAncestorText ? (
25
37
  <DefaultComponent ref={ref} style={styles} {...rest} />
@@ -66,7 +66,7 @@ function TimeInput(
66
66
  return str.length === 1 ? `0${str}` : str;
67
67
  }, [value, inputFocused, rawText, error]);
68
68
 
69
- const { rippleColor, containerStyle, textInputStyle, buttonStyle } = useMemo(() => {
69
+ const { containerStyle, textInputStyle, buttonStyle } = useMemo(() => {
70
70
  const {
71
71
  container,
72
72
  input,
@@ -79,7 +79,6 @@ function TimeInput(
79
79
  const isKeyboardInput = inputType === inputTypes.keyboard;
80
80
 
81
81
  return {
82
- rippleColor: timePickerInputStyles.root?._rippleColor,
83
82
  containerStyle: container,
84
83
  textInputStyle: [
85
84
  input,
@@ -146,11 +145,7 @@ function TimeInput(
146
145
  />
147
146
  <>
148
147
  {onPress && inputType === inputTypes.picker ? (
149
- <TouchableRipple
150
- style={buttonStyle}
151
- rippleColor={rippleColor}
152
- onPress={onPressInput}
153
- borderless={true}>
148
+ <TouchableRipple style={buttonStyle} onPress={onPressInput} borderless={true}>
154
149
  <View />
155
150
  </TouchableRipple>
156
151
  ) : null}
@@ -97,10 +97,6 @@ const timePickerInputsStylesDefault = StyleSheet.create(theme => ({
97
97
  }));
98
98
 
99
99
  const timePickerInputStylesDefault = StyleSheet.create(theme => ({
100
- root: {
101
- rippleColor: theme.colors.onPrimaryContainer,
102
- } as any,
103
-
104
100
  container: {
105
101
  position: 'relative',
106
102
  },
@@ -10,8 +10,10 @@ import {
10
10
  type ViewStyle,
11
11
  } from 'react-native';
12
12
 
13
+ import { useTheme } from '../../hooks/useTheme';
13
14
  import { extractPropertiesFromStyles } from '../../utils/extractPropertiesFromStyles';
14
15
  import { Slot } from '../Slot';
16
+ import { rippleColorFromBackground } from './rippleFromForegroundColor';
15
17
  import { touchableRippleStyles } from './utils';
16
18
 
17
19
  const ANDROID_VERSION_LOLLIPOP = 21;
@@ -24,6 +26,7 @@ type Props = ComponentProps<typeof TouchableWithoutFeedback> & {
24
26
  onPress?: () => void | null;
25
27
  rippleColor?: string;
26
28
  underlayColor?: string;
29
+ rippleAlpha?: number;
27
30
  children: ReactNode;
28
31
  style?: StyleProp<ViewStyle>;
29
32
  /**
@@ -55,6 +58,7 @@ const TouchableRipple = (
55
58
  disabled: disabledProp,
56
59
  rippleColor: rippleColorProp,
57
60
  underlayColor: underlayColorProp,
61
+ rippleAlpha = 0.24,
58
62
  children,
59
63
  asChild = false,
60
64
  ...rest
@@ -62,20 +66,47 @@ const TouchableRipple = (
62
66
  ref: any,
63
67
  ) => {
64
68
  const disabled = disabledProp;
69
+ const theme = useTheme();
65
70
 
66
71
  const componentStyles = touchableRippleStyles;
67
72
 
68
73
  const { rippleColor, underlayColor, containerStyle } = useMemo(() => {
69
- const { rippleColor: _rippleColor } = extractPropertiesFromStyles(
74
+ const { rippleColor: themeRippleColor, backgroundColor } = extractPropertiesFromStyles(
70
75
  [componentStyles.root, style],
71
- ['rippleColor'],
76
+ ['rippleColor', 'backgroundColor'],
72
77
  );
78
+ const tokenResolvedColor =
79
+ typeof rippleColorProp === 'string'
80
+ ? theme.colors[rippleColorProp as keyof typeof theme.colors]
81
+ : undefined;
82
+ const rippleColorResolvedProp =
83
+ typeof tokenResolvedColor === 'string' ? tokenResolvedColor : rippleColorProp;
84
+
85
+ const fallback =
86
+ typeof themeRippleColor === 'string' && themeRippleColor.length > 0
87
+ ? themeRippleColor
88
+ : 'rgba(0, 0, 0, 0.32)';
89
+
90
+ const resolvedRipple =
91
+ rippleColorResolvedProp ??
92
+ (backgroundColor != null && backgroundColor !== ''
93
+ ? rippleColorFromBackground(String(backgroundColor), fallback, rippleAlpha)
94
+ : fallback);
95
+
73
96
  return {
74
- rippleColor: rippleColorProp || _rippleColor,
75
- underlayColor: underlayColorProp || rippleColorProp,
97
+ rippleColor: resolvedRipple,
98
+ underlayColor: underlayColorProp ?? resolvedRipple,
76
99
  containerStyle: [borderless && styles.borderless, componentStyles.root, style],
77
100
  };
78
- }, [borderless, componentStyles.root, rippleColorProp, style, underlayColorProp]);
101
+ }, [
102
+ borderless,
103
+ componentStyles.root,
104
+ rippleColorProp,
105
+ style,
106
+ underlayColorProp,
107
+ rippleAlpha,
108
+ theme,
109
+ ]);
79
110
 
80
111
  // A workaround for ripple on Android P is to use useForeground + overflow: 'hidden'
81
112
  // https://github.com/facebook/react-native/issues/6480
@@ -1,6 +1,7 @@
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,
@@ -9,7 +10,9 @@ import {
9
10
  } from 'react-native';
10
11
  import { StyleSheet } from 'react-native-unistyles';
11
12
 
13
+ import { useTheme } from '../../hooks/useTheme';
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,6 +122,7 @@ const TouchableRipple = (
114
122
  disabled: disabledProp,
115
123
  rippleColor: rippleColorProp,
116
124
  underlayColor: _underlayColor,
125
+ rippleAlpha = 0.24,
117
126
  onPress,
118
127
  children,
119
128
  onPressIn: onPressInProp,
@@ -126,24 +135,27 @@ 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]);
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
+ ];
147
159
 
148
160
  // Track whether pointer is currently down for handling pointer leave
149
161
  const isPointerDownRef = useRef(false);
@@ -165,6 +177,16 @@ const TouchableRipple = (
165
177
  const computedStyle = window.getComputedStyle(button);
166
178
  const dimensions = button.getBoundingClientRect();
167
179
 
180
+ const resolvedRippleColor =
181
+ rippleColorResolvedProp ??
182
+ (Platform.OS === 'web'
183
+ ? rippleColorFromBackground(
184
+ computedStyle.backgroundColor,
185
+ String(themeRippleFallback),
186
+ rippleAlpha,
187
+ )
188
+ : String(themeRippleFallback));
189
+
168
190
  let touchX: number;
169
191
  let touchY: number;
170
192
 
@@ -225,7 +247,7 @@ const TouchableRipple = (
225
247
  Object.assign(ripple.style, {
226
248
  position: 'absolute',
227
249
  pointerEvents: 'none',
228
- backgroundColor: rippleColor,
250
+ backgroundColor: resolvedRippleColor,
229
251
  borderRadius: '50%',
230
252
 
231
253
  /* Transition configuration */
@@ -261,7 +283,14 @@ const TouchableRipple = (
261
283
  });
262
284
  });
263
285
  },
264
- [onPressInProp, disabled, centered, rippleColor],
286
+ [
287
+ onPressInProp,
288
+ disabled,
289
+ centered,
290
+ rippleColorResolvedProp,
291
+ themeRippleFallback,
292
+ rippleAlpha,
293
+ ],
265
294
  );
266
295
 
267
296
  const fadeOutRipples = useCallback((target: HTMLElement) => {
@@ -345,9 +374,14 @@ const TouchableRipple = (
345
374
  onPointerCancel: handlePointerCancel,
346
375
  };
347
376
 
377
+ const accessibilityRoleProp = (rest as { accessibilityRole?: unknown }).accessibilityRole;
378
+ const roleProp = (rest as { role?: unknown }).role;
379
+ const applyDefaultWebButtonRole =
380
+ !!onPress && accessibilityRoleProp === undefined && roleProp === undefined;
381
+
348
382
  return (
349
383
  <Component
350
- {...(onPress ? { role: 'button' } : {})}
384
+ {...(applyDefaultWebButtonRole ? { role: 'button' } : {})}
351
385
  {...rest}
352
386
  style={containerStyle}
353
387
  ref={ref}
@@ -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,11 +1,13 @@
1
1
  {
2
2
  "name": "react-native-molecules",
3
- "version": "0.5.0-beta.21",
3
+ "version": "0.5.0-beta.22",
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
12
  "components/TimePicker/context.tsx"
11
13
  ],
@@ -78,7 +80,7 @@
78
80
  "react-native": "0.81.4",
79
81
  "react-native-builder-bob": "^0.17.1",
80
82
  "react-native-reanimated": "~4.1.1",
81
- "react-native-unistyles": "^3.0.22",
83
+ "react-native-unistyles": "^3.2.4",
82
84
  "react-native-web": "~0.21.1"
83
85
  },
84
86
  "eslintIgnore": [