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.
- package/components/Button/Button.tsx +5 -20
- package/components/Button/utils.ts +0 -1
- package/components/Chip/Chip.tsx +39 -51
- package/components/Chip/utils.ts +3 -7
- package/components/IconButton/IconButton.tsx +42 -57
- package/components/IconButton/utils.ts +4 -5
- package/components/Select/Select.tsx +360 -501
- package/components/Select/index.ts +7 -14
- package/components/Select/types.ts +2 -4
- package/components/Select/utils.ts +215 -0
- package/components/Slot/Slot.tsx +244 -0
- package/components/Slot/compose-refs.tsx +60 -0
- package/components/Slot/index.tsx +8 -0
- package/components/Surface/Surface.android.tsx +20 -7
- package/components/Surface/Surface.ios.tsx +22 -29
- package/components/Surface/Surface.tsx +14 -4
- package/components/Surface/utils.ts +44 -6
- package/components/Switch/Switch.tsx +8 -2
- package/components/TextInput/TextInput.tsx +2 -2
- package/components/TouchableRipple/TouchableRipple.native.tsx +35 -13
- package/components/TouchableRipple/TouchableRipple.tsx +119 -46
- package/hooks/useControlledValue.tsx +20 -4
- package/hooks/useWhatHasUpdated.tsx +48 -0
- package/package.json +1 -1
- package/styles/shadow.ts +2 -1
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
SelectGroupProps,
|
|
9
|
-
SelectOptionProps,
|
|
10
|
-
SelectProviderProps,
|
|
11
|
-
SelectSearchInputProps,
|
|
12
|
-
SelectTriggerProps,
|
|
13
|
-
SelectValueProps,
|
|
14
|
-
} from './types';
|
|
1
|
+
import { getRegisteredComponentWithFallback } from '../../core';
|
|
2
|
+
import SelectDefault from './Select';
|
|
3
|
+
|
|
4
|
+
export const Select = getRegisteredComponentWithFallback('Select', SelectDefault);
|
|
5
|
+
|
|
6
|
+
export type * from './types';
|
|
7
|
+
export * from './utils';
|
|
@@ -38,7 +38,7 @@ export type SelectDropdownContextValue = {
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
// SelectProvider props
|
|
41
|
-
export type
|
|
41
|
+
export type SelectProps<Option extends DefaultItemT = DefaultItemT> = {
|
|
42
42
|
children: ReactNode;
|
|
43
43
|
value?: Option['id'] | Option['id'][] | null;
|
|
44
44
|
defaultValue?: Option['id'] | Option['id'][] | null;
|
|
@@ -110,6 +110,4 @@ export type SelectOptionProps<Option extends DefaultItemT = DefaultItemT> = View
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// Select.SearchInput props
|
|
113
|
-
export type SelectSearchInputProps = TextInputProps & {
|
|
114
|
-
onQueryChange?: (query: string) => void;
|
|
115
|
-
};
|
|
113
|
+
export type SelectSearchInputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & {};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { View } from 'react-native';
|
|
2
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
3
|
+
|
|
4
|
+
import { getRegisteredComponentStylesWithFallback } from '../../core';
|
|
5
|
+
import { createFastContext } from '../../fast-context';
|
|
6
|
+
import { registerPortalContext } from '../Portal';
|
|
7
|
+
import type { DefaultItemT, SelectContextValue, SelectDropdownContextValue } from './types';
|
|
8
|
+
|
|
9
|
+
// SelectContext - holds value, onAdd, onRemove with fast-context for optimized rendering
|
|
10
|
+
const selectContextDefaultValue: SelectContextValue<DefaultItemT> = {
|
|
11
|
+
value: null,
|
|
12
|
+
multiple: false,
|
|
13
|
+
onAdd: () => {},
|
|
14
|
+
onRemove: () => {},
|
|
15
|
+
disabled: false,
|
|
16
|
+
error: false,
|
|
17
|
+
labelKey: 'label',
|
|
18
|
+
options: [],
|
|
19
|
+
searchQuery: '',
|
|
20
|
+
setSearchQuery: () => {},
|
|
21
|
+
filteredOptions: [],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
useStoreRef: useSelectStoreRef,
|
|
26
|
+
Provider: SelectContextProvider,
|
|
27
|
+
useContext: useSelectContext,
|
|
28
|
+
useContextValue: useSelectContextValue,
|
|
29
|
+
Context: SelectContext,
|
|
30
|
+
} = createFastContext<SelectContextValue<DefaultItemT>>(selectContextDefaultValue, true);
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
SelectContext,
|
|
34
|
+
SelectContextProvider,
|
|
35
|
+
useSelectContext,
|
|
36
|
+
useSelectContextValue,
|
|
37
|
+
useSelectStoreRef,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// SelectDropdownContext - holds isOpen, onClose, triggerRef with fast-context
|
|
41
|
+
export type SelectDropdownContextType = SelectDropdownContextValue & {
|
|
42
|
+
triggerRef: React.RefObject<View> | null;
|
|
43
|
+
contentRef: React.RefObject<any> | null;
|
|
44
|
+
triggerLayout: { width: number; height: number } | null;
|
|
45
|
+
setTriggerLayout: (layout: { width: number; height: number }) => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const selectDropdownContextDefaultValue: SelectDropdownContextType = {
|
|
49
|
+
isOpen: false,
|
|
50
|
+
onClose: () => {},
|
|
51
|
+
onOpen: () => {},
|
|
52
|
+
triggerRef: null,
|
|
53
|
+
contentRef: null,
|
|
54
|
+
triggerLayout: null,
|
|
55
|
+
setTriggerLayout: () => {},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const {
|
|
59
|
+
useStoreRef: useSelectDropdownStoreRef,
|
|
60
|
+
Provider: SelectDropdownContextProvider,
|
|
61
|
+
useContext: useSelectDropdownContext,
|
|
62
|
+
useContextValue: useSelectDropdownContextValue,
|
|
63
|
+
Context: SelectDropdownContext,
|
|
64
|
+
} = createFastContext<SelectDropdownContextType>(selectDropdownContextDefaultValue, true);
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
SelectDropdownContext,
|
|
68
|
+
SelectDropdownContextProvider,
|
|
69
|
+
useSelectDropdownContext,
|
|
70
|
+
useSelectDropdownContextValue,
|
|
71
|
+
useSelectDropdownStoreRef,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
registerPortalContext([SelectContext, SelectDropdownContext]);
|
|
75
|
+
|
|
76
|
+
const triggerDefaultStyles = StyleSheet.create(theme => ({
|
|
77
|
+
trigger: {
|
|
78
|
+
borderRadius: theme.shapes.corner.extraSmall,
|
|
79
|
+
paddingHorizontal: theme.spacings['3'],
|
|
80
|
+
paddingVertical: theme.spacings['2'],
|
|
81
|
+
minHeight: 48,
|
|
82
|
+
flexDirection: 'row',
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
justifyContent: 'space-between',
|
|
85
|
+
width: '100%',
|
|
86
|
+
variants: {
|
|
87
|
+
state: {
|
|
88
|
+
disabled: {
|
|
89
|
+
opacity: 0.38,
|
|
90
|
+
backgroundColor: theme.colors.surfaceVariant,
|
|
91
|
+
},
|
|
92
|
+
errorDisabled: {
|
|
93
|
+
opacity: 0.38,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
outline: {
|
|
99
|
+
position: 'absolute',
|
|
100
|
+
top: 0,
|
|
101
|
+
left: 0,
|
|
102
|
+
right: 0,
|
|
103
|
+
bottom: 0,
|
|
104
|
+
borderRadius: theme.shapes.corner.extraSmall,
|
|
105
|
+
borderWidth: 1,
|
|
106
|
+
borderColor: theme.colors.outline,
|
|
107
|
+
pointerEvents: 'none',
|
|
108
|
+
variants: {
|
|
109
|
+
state: {
|
|
110
|
+
focused: {
|
|
111
|
+
borderWidth: 2,
|
|
112
|
+
borderColor: theme.colors.primary,
|
|
113
|
+
},
|
|
114
|
+
hovered: {
|
|
115
|
+
borderColor: theme.colors.onSurface,
|
|
116
|
+
},
|
|
117
|
+
hoveredAndFocused: {
|
|
118
|
+
borderWidth: 2,
|
|
119
|
+
borderColor: theme.colors.primary,
|
|
120
|
+
},
|
|
121
|
+
disabled: {
|
|
122
|
+
borderColor: theme.colors.onSurface,
|
|
123
|
+
},
|
|
124
|
+
error: {
|
|
125
|
+
borderColor: theme.colors.error,
|
|
126
|
+
},
|
|
127
|
+
errorFocused: {
|
|
128
|
+
borderWidth: 2,
|
|
129
|
+
borderColor: theme.colors.error,
|
|
130
|
+
},
|
|
131
|
+
errorHovered: {
|
|
132
|
+
borderColor: theme.colors.onErrorContainer,
|
|
133
|
+
},
|
|
134
|
+
errorFocusedAndHovered: {
|
|
135
|
+
borderWidth: 2,
|
|
136
|
+
borderColor: theme.colors.error,
|
|
137
|
+
},
|
|
138
|
+
errorDisabled: {
|
|
139
|
+
borderColor: theme.colors.error,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
triggerIcon: {
|
|
145
|
+
marginLeft: theme.spacings['2'],
|
|
146
|
+
color: theme.colors.onSurfaceVariant,
|
|
147
|
+
},
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
export const defaultStyles = StyleSheet.create(theme => ({
|
|
151
|
+
chipContainer: {
|
|
152
|
+
flexDirection: 'row',
|
|
153
|
+
flexWrap: 'wrap',
|
|
154
|
+
gap: 6,
|
|
155
|
+
maxWidth: '90%',
|
|
156
|
+
},
|
|
157
|
+
groupLabel: {
|
|
158
|
+
paddingHorizontal: theme.spacings['4'],
|
|
159
|
+
paddingVertical: theme.spacings['2'],
|
|
160
|
+
fontWeight: '600',
|
|
161
|
+
color: theme.colors.onSurface,
|
|
162
|
+
},
|
|
163
|
+
item: {
|
|
164
|
+
paddingHorizontal: theme.spacings['4'],
|
|
165
|
+
paddingVertical: theme.spacings['3'],
|
|
166
|
+
backgroundColor: 'transparent',
|
|
167
|
+
|
|
168
|
+
_web: {
|
|
169
|
+
cursor: 'pointer',
|
|
170
|
+
outlineStyle: 'none',
|
|
171
|
+
_hover: {
|
|
172
|
+
backgroundColor: theme.colors.stateLayer.hover.primary,
|
|
173
|
+
},
|
|
174
|
+
_focus: {
|
|
175
|
+
backgroundColor: theme.colors.stateLayer.focussed.primary,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
itemSelected: {
|
|
180
|
+
backgroundColor: theme.colors.stateLayer.hover.primary,
|
|
181
|
+
},
|
|
182
|
+
itemDisabled: {
|
|
183
|
+
opacity: 0.38,
|
|
184
|
+
_web: {
|
|
185
|
+
cursor: 'not-allowed',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
itemDisabledText: {
|
|
189
|
+
color: theme.colors.onSurfaceVariant,
|
|
190
|
+
},
|
|
191
|
+
searchInput: {
|
|
192
|
+
marginHorizontal: theme.spacings['2'],
|
|
193
|
+
marginVertical: theme.spacings['3'],
|
|
194
|
+
},
|
|
195
|
+
searchInputInput: {
|
|
196
|
+
height: 42,
|
|
197
|
+
},
|
|
198
|
+
emptyState: {
|
|
199
|
+
paddingHorizontal: theme.spacings['4'],
|
|
200
|
+
paddingVertical: theme.spacings['6'],
|
|
201
|
+
alignItems: 'center',
|
|
202
|
+
justifyContent: 'center',
|
|
203
|
+
},
|
|
204
|
+
emptyStateText: {
|
|
205
|
+
color: theme.colors.onSurfaceVariant,
|
|
206
|
+
fontSize: 14,
|
|
207
|
+
},
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
export const triggerStyles = getRegisteredComponentStylesWithFallback(
|
|
211
|
+
'Select_Trigger',
|
|
212
|
+
triggerDefaultStyles,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
export const styles = getRegisteredComponentStylesWithFallback('Select', defaultStyles);
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { composeRefs } from '@radix-ui/react-compose-refs';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { type PressableProps, type StyleProp, type ViewProps, type ViewStyle } from 'react-native';
|
|
4
|
+
|
|
5
|
+
declare module 'react' {
|
|
6
|
+
interface ReactElement {
|
|
7
|
+
$$typeof?: symbol | string;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const REACT_LAZY_TYPE = Symbol.for('react.lazy');
|
|
12
|
+
|
|
13
|
+
interface LazyReactElement extends React.ReactElement {
|
|
14
|
+
$$typeof: typeof REACT_LAZY_TYPE;
|
|
15
|
+
_payload: PromiseLike<Exclude<React.ReactNode, PromiseLike<any>>>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* -------------------------------------------------------------------------------------------------
|
|
19
|
+
* Slot
|
|
20
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
21
|
+
|
|
22
|
+
export type Usable<T> = PromiseLike<T> | React.Context<T>;
|
|
23
|
+
const use: typeof React.use | undefined = (React as any)[' use '.trim().toString()];
|
|
24
|
+
|
|
25
|
+
interface SlotProps
|
|
26
|
+
extends Omit<ViewProps, 'children'>,
|
|
27
|
+
Partial<
|
|
28
|
+
Pick<
|
|
29
|
+
PressableProps,
|
|
30
|
+
'onPress' | 'onPressIn' | 'onPressOut' | 'onLongPress' | 'disabled'
|
|
31
|
+
>
|
|
32
|
+
> {
|
|
33
|
+
children?: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
37
|
+
return typeof value === 'object' && value !== null && 'then' in value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isLazyComponent(element: React.ReactNode): element is LazyReactElement {
|
|
41
|
+
return (
|
|
42
|
+
element != null &&
|
|
43
|
+
typeof element === 'object' &&
|
|
44
|
+
'$$typeof' in element &&
|
|
45
|
+
element.$$typeof === REACT_LAZY_TYPE &&
|
|
46
|
+
'_payload' in element &&
|
|
47
|
+
isPromiseLike(element._payload)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* @__NO_SIDE_EFFECTS__ */ export function createSlot(ownerName: string) {
|
|
52
|
+
const SlotClone = createSlotClone(ownerName);
|
|
53
|
+
const Slot = React.forwardRef<any, SlotProps>((props, forwardedRef) => {
|
|
54
|
+
const { children: childrenProp, ...slotProps } = props;
|
|
55
|
+
let children = childrenProp;
|
|
56
|
+
if (isLazyComponent(children) && typeof use === 'function') {
|
|
57
|
+
children = use(children._payload);
|
|
58
|
+
}
|
|
59
|
+
const childrenArray = React.Children.toArray(children);
|
|
60
|
+
const slottable = childrenArray.find(isSlottable);
|
|
61
|
+
|
|
62
|
+
if (slottable) {
|
|
63
|
+
// the new element to render is the one passed as a child of `Slottable`
|
|
64
|
+
const newElement = slottable.props.children;
|
|
65
|
+
|
|
66
|
+
const newChildren = childrenArray.map(child => {
|
|
67
|
+
if (child === slottable) {
|
|
68
|
+
// because the new element will be the one rendered, we are only interested
|
|
69
|
+
// in grabbing its children (`newElement.props.children`)
|
|
70
|
+
if (React.Children.count(newElement) > 1) return React.Children.only(null);
|
|
71
|
+
return React.isValidElement(newElement)
|
|
72
|
+
? (newElement.props as { children: React.ReactNode }).children
|
|
73
|
+
: null;
|
|
74
|
+
} else {
|
|
75
|
+
return child;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<SlotClone {...slotProps} ref={forwardedRef}>
|
|
81
|
+
{React.isValidElement(newElement)
|
|
82
|
+
? React.cloneElement(newElement, undefined, newChildren)
|
|
83
|
+
: null}
|
|
84
|
+
</SlotClone>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<SlotClone {...slotProps} ref={forwardedRef}>
|
|
90
|
+
{children}
|
|
91
|
+
</SlotClone>
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
Slot.displayName = `${ownerName}.Slot`;
|
|
96
|
+
return Slot;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const Slot = createSlot('Slot');
|
|
100
|
+
|
|
101
|
+
/* -------------------------------------------------------------------------------------------------
|
|
102
|
+
* SlotClone
|
|
103
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
104
|
+
|
|
105
|
+
interface SlotCloneProps {
|
|
106
|
+
children: React.ReactNode;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* @__NO_SIDE_EFFECTS__ */ function createSlotClone(ownerName: string) {
|
|
110
|
+
const SlotClone = React.forwardRef<any, SlotCloneProps>((props, forwardedRef) => {
|
|
111
|
+
const { children: childrenProp, ...slotProps } = props;
|
|
112
|
+
let children = childrenProp;
|
|
113
|
+
if (isLazyComponent(children) && typeof use === 'function') {
|
|
114
|
+
children = use(children._payload);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (React.isValidElement(children)) {
|
|
118
|
+
const childrenRef = getElementRef(children);
|
|
119
|
+
const mergedProps = mergeProps(slotProps, children.props as AnyProps);
|
|
120
|
+
// do not pass ref to React.Fragment for React 19 compatibility
|
|
121
|
+
if (children.type !== React.Fragment) {
|
|
122
|
+
mergedProps.ref = forwardedRef
|
|
123
|
+
? composeRefs(forwardedRef, childrenRef)
|
|
124
|
+
: childrenRef;
|
|
125
|
+
}
|
|
126
|
+
return React.cloneElement(children, mergedProps);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return React.Children.count(children) > 1 ? React.Children.only(null) : null;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
SlotClone.displayName = `${ownerName}.SlotClone`;
|
|
133
|
+
return SlotClone;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* -------------------------------------------------------------------------------------------------
|
|
137
|
+
* Slottable
|
|
138
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
139
|
+
|
|
140
|
+
const SLOTTABLE_IDENTIFIER = Symbol('radix.slottable');
|
|
141
|
+
|
|
142
|
+
interface SlottableProps {
|
|
143
|
+
children: React.ReactNode;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface SlottableComponent extends React.FC<SlottableProps> {
|
|
147
|
+
__radixId: symbol;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* @__NO_SIDE_EFFECTS__ */ export function createSlottable(ownerName: string) {
|
|
151
|
+
const Slottable: SlottableComponent = ({ children }) => {
|
|
152
|
+
return <>{children}</>;
|
|
153
|
+
};
|
|
154
|
+
Slottable.displayName = `${ownerName}.Slottable`;
|
|
155
|
+
Slottable.__radixId = SLOTTABLE_IDENTIFIER;
|
|
156
|
+
return Slottable;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const Slottable = createSlottable('Slottable');
|
|
160
|
+
|
|
161
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
162
|
+
|
|
163
|
+
type AnyProps = Record<string, any>;
|
|
164
|
+
|
|
165
|
+
function isSlottable(
|
|
166
|
+
child: React.ReactNode,
|
|
167
|
+
): child is React.ReactElement<SlottableProps, typeof Slottable> {
|
|
168
|
+
return (
|
|
169
|
+
React.isValidElement(child) &&
|
|
170
|
+
typeof child.type === 'function' &&
|
|
171
|
+
'__radixId' in child.type &&
|
|
172
|
+
child.type.__radixId === SLOTTABLE_IDENTIFIER
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function mergeProps(slotProps: AnyProps, childProps: AnyProps) {
|
|
177
|
+
// all child props should override
|
|
178
|
+
const overrideProps = { ...childProps };
|
|
179
|
+
|
|
180
|
+
for (const propName in childProps) {
|
|
181
|
+
const slotPropValue = slotProps[propName];
|
|
182
|
+
const childPropValue = childProps[propName];
|
|
183
|
+
|
|
184
|
+
const isHandler = /^on[A-Z]/.test(propName);
|
|
185
|
+
if (isHandler) {
|
|
186
|
+
// if the handler exists on both, we compose them
|
|
187
|
+
if (slotPropValue && childPropValue) {
|
|
188
|
+
overrideProps[propName] = (...args: unknown[]) => {
|
|
189
|
+
const result = childPropValue(...args);
|
|
190
|
+
slotPropValue(...args);
|
|
191
|
+
return result;
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// but if it exists only on the slot, we use only this one
|
|
195
|
+
else if (slotPropValue) {
|
|
196
|
+
overrideProps[propName] = slotPropValue;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// if it's `style`, we merge them (React Native styles can be arrays)
|
|
200
|
+
else if (propName === 'style') {
|
|
201
|
+
const slotStyle = slotPropValue as StyleProp<ViewStyle> | undefined;
|
|
202
|
+
const childStyle = childPropValue as StyleProp<ViewStyle> | undefined;
|
|
203
|
+
if (slotStyle || childStyle) {
|
|
204
|
+
overrideProps[propName] = [slotStyle, childStyle].filter(
|
|
205
|
+
Boolean,
|
|
206
|
+
) as StyleProp<ViewStyle>;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { ...slotProps, ...overrideProps };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Before React 19 accessing `element.props.ref` will throw a warning and suggest using `element.ref`
|
|
215
|
+
// After React 19 accessing `element.ref` does the opposite.
|
|
216
|
+
// https://github.com/facebook/react/pull/28348
|
|
217
|
+
//
|
|
218
|
+
// Access the ref using the method that doesn't yield a warning.
|
|
219
|
+
function getElementRef(element: React.ReactElement) {
|
|
220
|
+
// React <=18 in DEV
|
|
221
|
+
let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get;
|
|
222
|
+
let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;
|
|
223
|
+
if (mayWarn) {
|
|
224
|
+
return (element as any).ref;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// React 19 in DEV
|
|
228
|
+
getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get;
|
|
229
|
+
mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;
|
|
230
|
+
if (mayWarn) {
|
|
231
|
+
return (element.props as { ref?: React.Ref<unknown> }).ref;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Not DEV
|
|
235
|
+
return (element.props as { ref?: React.Ref<unknown> }).ref || (element as any).ref;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export {
|
|
239
|
+
//
|
|
240
|
+
Slot as Root,
|
|
241
|
+
Slot,
|
|
242
|
+
Slottable,
|
|
243
|
+
};
|
|
244
|
+
export type { SlotProps };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
type PossibleRef<T> = React.Ref<T> | undefined;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Set a given ref to a given value
|
|
7
|
+
* This utility takes care of different types of refs: callback refs and RefObject(s)
|
|
8
|
+
*/
|
|
9
|
+
function setRef<T>(ref: PossibleRef<T>, value: T) {
|
|
10
|
+
if (typeof ref === 'function') {
|
|
11
|
+
return ref(value);
|
|
12
|
+
} else if (ref !== null && ref !== undefined) {
|
|
13
|
+
ref.current = value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A utility to compose multiple refs together
|
|
19
|
+
* Accepts callback refs and RefObject(s)
|
|
20
|
+
*/
|
|
21
|
+
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
|
|
22
|
+
return node => {
|
|
23
|
+
let hasCleanup = false;
|
|
24
|
+
const cleanups = refs.map(ref => {
|
|
25
|
+
const cleanup = setRef(ref, node);
|
|
26
|
+
if (!hasCleanup && typeof cleanup === 'function') {
|
|
27
|
+
hasCleanup = true;
|
|
28
|
+
}
|
|
29
|
+
return cleanup;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// React <19 will log an error to the console if a callback ref returns a
|
|
33
|
+
// value. We don't use ref cleanups internally so this will only happen if a
|
|
34
|
+
// user's ref callback returns a value, which we only expect if they are
|
|
35
|
+
// using the cleanup functionality added in React 19.
|
|
36
|
+
if (hasCleanup) {
|
|
37
|
+
return () => {
|
|
38
|
+
for (let i = 0; i < cleanups.length; i++) {
|
|
39
|
+
const cleanup = cleanups[i];
|
|
40
|
+
if (typeof cleanup === 'function') {
|
|
41
|
+
cleanup();
|
|
42
|
+
} else {
|
|
43
|
+
setRef(refs[i], null);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A custom hook that composes multiple refs
|
|
53
|
+
* Accepts callback refs and RefObject(s)
|
|
54
|
+
*/
|
|
55
|
+
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
|
|
56
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
|
+
return React.useCallback(composeRefs(...refs), refs);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { composeRefs, useComposedRefs };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getRegisteredComponentWithFallback } from '../../core';
|
|
2
|
+
import { Slot as SlotComponent, Slottable } from './Slot';
|
|
3
|
+
|
|
4
|
+
const SlotDefault = Object.assign(SlotComponent, { Slottable, Root: SlotComponent });
|
|
5
|
+
|
|
6
|
+
export const Slot = getRegisteredComponentWithFallback('Slot', SlotDefault);
|
|
7
|
+
|
|
8
|
+
export { createSlot, createSlottable, type SlotProps } from './Slot';
|
|
@@ -2,11 +2,13 @@ import { forwardRef, memo, type ReactNode, useMemo } from 'react';
|
|
|
2
2
|
import { Animated, type StyleProp, View, type ViewProps, type ViewStyle } from 'react-native';
|
|
3
3
|
import { useUnistyles } from 'react-native-unistyles';
|
|
4
4
|
|
|
5
|
-
import { inputRange } from '../../styles/shadow';
|
|
6
5
|
import type { MD3Elevation } from '../../types/theme';
|
|
7
6
|
import { extractPropertiesFromStyles } from '../../utils/extractPropertiesFromStyles';
|
|
7
|
+
import { Slot } from '../Slot';
|
|
8
8
|
import { BackgroundContextWrapper } from './BackgroundContextWrapper';
|
|
9
|
-
import { defaultStyles
|
|
9
|
+
import { defaultStyles } from './utils';
|
|
10
|
+
|
|
11
|
+
const AnimatedView = Animated.createAnimatedComponent(View);
|
|
10
12
|
|
|
11
13
|
export type Props = ViewProps & {
|
|
12
14
|
/**
|
|
@@ -20,11 +22,19 @@ export type Props = ViewProps & {
|
|
|
20
22
|
* TestID used for testing purposes
|
|
21
23
|
*/
|
|
22
24
|
testID?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Change the component to the HTML tag or custom component use the passed child.
|
|
27
|
+
* This will merge the props of the Surface with the props of the child element.
|
|
28
|
+
*/
|
|
29
|
+
asChild?: boolean;
|
|
23
30
|
};
|
|
24
31
|
|
|
25
32
|
const elevationLevel = [0, 1, 2, 6, 8, 12];
|
|
26
33
|
|
|
27
|
-
const Surface = (
|
|
34
|
+
const Surface = (
|
|
35
|
+
{ elevation: _elevation = 1, style, children, testID, asChild = false, ...props }: Props,
|
|
36
|
+
ref: any,
|
|
37
|
+
) => {
|
|
28
38
|
const { theme } = useUnistyles();
|
|
29
39
|
|
|
30
40
|
const backgroundColor = (() => {
|
|
@@ -33,6 +43,7 @@ const Surface = ({ elevation = 1, style, children, testID, ...props }: Props, re
|
|
|
33
43
|
})();
|
|
34
44
|
|
|
35
45
|
const { memoizedStyles, surfaceBackground } = useMemo(() => {
|
|
46
|
+
const elevation = typeof _elevation === 'number' ? (_elevation > 5 ? 5 : _elevation) : 0;
|
|
36
47
|
return {
|
|
37
48
|
memoizedStyles: [
|
|
38
49
|
{
|
|
@@ -41,7 +52,7 @@ const Surface = ({ elevation = 1, style, children, testID, ...props }: Props, re
|
|
|
41
52
|
defaultStyles.root,
|
|
42
53
|
style,
|
|
43
54
|
{
|
|
44
|
-
elevation:
|
|
55
|
+
elevation: elevationLevel[elevation],
|
|
45
56
|
},
|
|
46
57
|
] as StyleProp<ViewStyle>,
|
|
47
58
|
surfaceBackground: extractPropertiesFromStyles(
|
|
@@ -49,13 +60,15 @@ const Surface = ({ elevation = 1, style, children, testID, ...props }: Props, re
|
|
|
49
60
|
['backgroundColor'],
|
|
50
61
|
).backgroundColor,
|
|
51
62
|
};
|
|
52
|
-
}, [backgroundColor,
|
|
63
|
+
}, [backgroundColor, _elevation, style]);
|
|
64
|
+
|
|
65
|
+
const Component = asChild ? Slot : AnimatedView;
|
|
53
66
|
|
|
54
67
|
return (
|
|
55
68
|
<BackgroundContextWrapper backgroundColor={surfaceBackground}>
|
|
56
|
-
<
|
|
69
|
+
<Component ref={ref} {...props} testID={testID} style={memoizedStyles}>
|
|
57
70
|
{children}
|
|
58
|
-
</
|
|
71
|
+
</Component>
|
|
59
72
|
</BackgroundContextWrapper>
|
|
60
73
|
);
|
|
61
74
|
};
|