panelui-native 0.6.0 → 0.7.0
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/README.md +1 -1
- package/lib/module/components/accordion/index.js +7 -5
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/alert/index.js +3 -6
- package/lib/module/components/alert/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +67 -3
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +36 -1
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/empty-state/index.js +4 -5
- package/lib/module/components/empty-state/index.js.map +1 -1
- package/lib/module/components/inline-select/index.js +171 -46
- package/lib/module/components/inline-select/index.js.map +1 -1
- package/lib/module/components/input/index.js +28 -5
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/input-group/index.js +0 -1
- package/lib/module/components/input-group/index.js.map +1 -1
- package/lib/module/components/item/index.js +359 -0
- package/lib/module/components/item/index.js.map +1 -0
- package/lib/module/components/label/index.js +5 -3
- package/lib/module/components/label/index.js.map +1 -1
- package/lib/module/components/message/index.js +318 -0
- package/lib/module/components/message/index.js.map +1 -0
- package/lib/module/components/scroll-fade/index.js +119 -71
- package/lib/module/components/scroll-fade/index.js.map +1 -1
- package/lib/module/components/select/index.js +35 -6
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/shimmer/index.js +111 -64
- package/lib/module/components/shimmer/index.js.map +1 -1
- package/lib/module/components/surface/index.js +3 -2
- package/lib/module/components/surface/index.js.map +1 -1
- package/lib/module/components/switch/index.js +20 -1
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/toast/index.js +7 -8
- package/lib/module/components/toast/index.js.map +1 -1
- package/lib/module/components/typography/index.js +3 -3
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-breakpoint.js +2 -2
- package/lib/module/hooks/use-copy-to-clipboard.js +0 -2
- package/lib/module/hooks/use-copy-to-clipboard.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +120 -0
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -0
- package/lib/module/icons/index.js +4 -4
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js +53 -0
- package/lib/module/native/index.js.map +1 -0
- package/lib/module/primitives/keyboard-avoider.js +40 -0
- package/lib/module/primitives/keyboard-avoider.js.map +1 -0
- package/lib/module/providers/panel-ui-provider.js +24 -4
- package/lib/module/providers/panel-ui-provider.js.map +1 -1
- package/lib/module/theme/use-theme.js +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +7 -5
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/alert/index.d.ts +3 -6
- package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +21 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +11 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/empty-state/index.d.ts +4 -5
- package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
- package/lib/typescript/src/components/inline-select/index.d.ts +29 -6
- package/lib/typescript/src/components/inline-select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input/index.d.ts +13 -0
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input-group/index.d.ts +0 -1
- package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/item/index.d.ts +286 -0
- package/lib/typescript/src/components/item/index.d.ts.map +1 -0
- package/lib/typescript/src/components/label/index.d.ts +5 -3
- package/lib/typescript/src/components/label/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message/index.d.ts +170 -0
- package/lib/typescript/src/components/message/index.d.ts.map +1 -0
- package/lib/typescript/src/components/scroll-fade/index.d.ts +18 -9
- package/lib/typescript/src/components/scroll-fade/index.d.ts.map +1 -1
- package/lib/typescript/src/components/select/index.d.ts +16 -1
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/shimmer/index.d.ts +38 -20
- package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -1
- package/lib/typescript/src/components/surface/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts +10 -0
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/toast/index.d.ts +4 -4
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +1 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-breakpoint.d.ts +2 -2
- package/lib/typescript/src/hooks/use-copy-to-clipboard.d.ts +0 -2
- package/lib/typescript/src/hooks/use-copy-to-clipboard.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +20 -0
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +4 -4
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +35 -0
- package/lib/typescript/src/native/index.d.ts.map +1 -0
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts +22 -0
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -0
- package/lib/typescript/src/providers/panel-ui-provider.d.ts +1 -1
- package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
- package/lib/typescript/src/theme/use-theme.d.ts +1 -1
- package/package.json +12 -3
- package/src/components/accordion/index.tsx +7 -5
- package/src/components/alert/index.tsx +3 -6
- package/src/components/bottom-sheet/index.tsx +82 -2
- package/src/components/button/index.tsx +52 -0
- package/src/components/empty-state/index.tsx +4 -5
- package/src/components/inline-select/index.tsx +191 -44
- package/src/components/input/index.tsx +39 -3
- package/src/components/input-group/index.tsx +0 -1
- package/src/components/item/index.tsx +387 -0
- package/src/components/label/index.tsx +5 -3
- package/src/components/message/index.tsx +312 -0
- package/src/components/scroll-fade/index.tsx +163 -51
- package/src/components/select/index.tsx +54 -9
- package/src/components/shimmer/index.tsx +151 -58
- package/src/components/surface/index.tsx +3 -2
- package/src/components/switch/index.tsx +27 -1
- package/src/components/toast/index.tsx +7 -8
- package/src/components/typography/index.tsx +3 -3
- package/src/hooks/index.ts +5 -0
- package/src/hooks/use-breakpoint.ts +2 -2
- package/src/hooks/use-copy-to-clipboard.ts +0 -2
- package/src/hooks/use-keyboard-avoidance.ts +140 -0
- package/src/icons/index.tsx +4 -4
- package/src/index.ts +32 -0
- package/src/native/index.ts +79 -0
- package/src/primitives/keyboard-avoider.tsx +49 -0
- package/src/providers/panel-ui-provider.tsx +30 -9
- package/src/theme/use-theme.ts +1 -1
- package/theme.css +5 -3
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ScrollFade — fades the edges of a scroll container.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* timelines, so the fades are SVG gradients overlaid on each edge, with their
|
|
8
|
-
* opacity following the scroll position.
|
|
4
|
+
* Content that runs past a boundary reads better fading out than being cut
|
|
5
|
+
* off, and the fade doubles as an affordance: an edge that is fading is an
|
|
6
|
+
* edge with more content behind it.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Scroll position, content size and viewport size are all held in shared
|
|
9
|
+
* values and consumed by `useAnimatedStyle`, so the fades track the scroll on
|
|
10
|
+
* the UI thread without re-rendering React.
|
|
12
11
|
*
|
|
13
12
|
* ```tsx
|
|
14
13
|
* <ScrollFade>
|
|
@@ -20,39 +19,63 @@ import {
|
|
|
20
19
|
Children,
|
|
21
20
|
cloneElement,
|
|
22
21
|
isValidElement,
|
|
23
|
-
|
|
22
|
+
useMemo,
|
|
23
|
+
type ComponentType,
|
|
24
24
|
type ReactElement,
|
|
25
25
|
type ReactNode,
|
|
26
26
|
} from 'react';
|
|
27
27
|
import {
|
|
28
|
+
StyleSheet,
|
|
28
29
|
View,
|
|
30
|
+
type LayoutChangeEvent,
|
|
29
31
|
type NativeScrollEvent,
|
|
30
32
|
type NativeSyntheticEvent,
|
|
31
33
|
type ViewProps,
|
|
32
34
|
} from 'react-native';
|
|
33
|
-
import
|
|
35
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
36
|
+
import Animated, {
|
|
37
|
+
useAnimatedScrollHandler,
|
|
38
|
+
useAnimatedStyle,
|
|
39
|
+
useComposedEventHandler,
|
|
40
|
+
useDerivedValue,
|
|
41
|
+
useSharedValue,
|
|
42
|
+
type AnimatedScrollViewProps,
|
|
43
|
+
type DerivedValue,
|
|
44
|
+
} from 'react-native-reanimated';
|
|
34
45
|
import { useCSSVariable } from 'uniwind';
|
|
35
46
|
|
|
36
47
|
/** Past this many pixels from an edge, that edge's fade is fully shown. */
|
|
37
|
-
const
|
|
48
|
+
const DEFAULT_FADE_IN_DISTANCE = 24;
|
|
38
49
|
|
|
39
50
|
export interface ScrollFadeProps extends ViewProps {
|
|
40
51
|
className?: string;
|
|
41
52
|
/** Depth of the fade in pixels. */
|
|
42
53
|
size?: number;
|
|
43
54
|
/** Which edges fade. */
|
|
44
|
-
edges?: 'both' | 'start' | 'end';
|
|
55
|
+
edges?: 'both' | 'start' | 'end' | 'none';
|
|
56
|
+
/**
|
|
57
|
+
* Scroll axis. Inferred from the child's `horizontal` prop when omitted —
|
|
58
|
+
* pass it explicitly for children that scroll horizontally without that prop
|
|
59
|
+
* (a `FlatList` with `horizontal` set through `contentContainerStyle`, say).
|
|
60
|
+
*/
|
|
61
|
+
orientation?: 'horizontal' | 'vertical';
|
|
45
62
|
/**
|
|
46
63
|
* Colour the fade resolves to — normally whatever sits behind the
|
|
47
64
|
* scrollable. Defaults to the theme's background.
|
|
48
65
|
*/
|
|
49
66
|
color?: string;
|
|
67
|
+
/** Distance in pixels over which an edge fades from clear to full. */
|
|
68
|
+
fadeInDistance?: number;
|
|
69
|
+
/** Set false to render the child with no fades at all. */
|
|
70
|
+
enabled?: boolean;
|
|
50
71
|
children?: ReactNode;
|
|
51
72
|
}
|
|
52
73
|
|
|
53
74
|
interface ScrollableProps {
|
|
54
75
|
horizontal?: boolean;
|
|
55
|
-
onScroll?:
|
|
76
|
+
onScroll?: AnimatedScrollViewProps['onScroll'];
|
|
77
|
+
onLayout?: (event: LayoutChangeEvent) => void;
|
|
78
|
+
onContentSizeChange?: (width: number, height: number) => void;
|
|
56
79
|
scrollEventThrottle?: number;
|
|
57
80
|
}
|
|
58
81
|
|
|
@@ -60,50 +83,106 @@ export function ScrollFade({
|
|
|
60
83
|
className,
|
|
61
84
|
size = 48,
|
|
62
85
|
edges = 'both',
|
|
86
|
+
orientation,
|
|
63
87
|
color,
|
|
88
|
+
fadeInDistance = DEFAULT_FADE_IN_DISTANCE,
|
|
89
|
+
enabled = true,
|
|
64
90
|
children,
|
|
65
91
|
...props
|
|
66
92
|
}: ScrollFadeProps) {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
93
|
+
const offset = useSharedValue(0);
|
|
94
|
+
const contentLength = useSharedValue(0);
|
|
95
|
+
const viewportLength = useSharedValue(0);
|
|
69
96
|
|
|
70
97
|
const themeBackground = useCSSVariable('--color-background');
|
|
71
98
|
const fadeColor =
|
|
72
99
|
color ?? (typeof themeBackground === 'string' ? themeBackground : '#000000');
|
|
73
100
|
|
|
74
101
|
const child = Children.only(children) as ReactElement<ScrollableProps>;
|
|
75
|
-
const horizontal =
|
|
102
|
+
const horizontal =
|
|
103
|
+
orientation !== undefined
|
|
104
|
+
? orientation === 'horizontal'
|
|
105
|
+
: isValidElement(child) && !!child.props.horizontal;
|
|
76
106
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
107
|
+
// Reanimated can only drive a scroll handler on an animated component, and
|
|
108
|
+
// `Animated.ScrollView` is not necessarily what was passed in — the child may
|
|
109
|
+
// be a FlatList, a SectionList or a custom scrollable. Keyed on the element
|
|
110
|
+
// *type*, not the element: rebuilding the wrapper would remount the list.
|
|
111
|
+
const childType = isValidElement(child)
|
|
112
|
+
? (child.type as ComponentType<ScrollableProps>)
|
|
113
|
+
: null;
|
|
114
|
+
const AnimatedScrollable = useMemo(
|
|
115
|
+
() => (childType ? Animated.createAnimatedComponent(childType) : null),
|
|
116
|
+
[childType]
|
|
117
|
+
);
|
|
83
118
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
119
|
+
const scrollHandler = useAnimatedScrollHandler({
|
|
120
|
+
onScroll: (event) => {
|
|
121
|
+
const { contentOffset, contentSize, layoutMeasurement } = event;
|
|
122
|
+
offset.value = horizontal ? contentOffset.x : contentOffset.y;
|
|
123
|
+
contentLength.value = horizontal ? contentSize.width : contentSize.height;
|
|
124
|
+
viewportLength.value = horizontal
|
|
125
|
+
? layoutMeasurement.width
|
|
126
|
+
: layoutMeasurement.height;
|
|
127
|
+
},
|
|
128
|
+
});
|
|
88
129
|
|
|
89
|
-
|
|
130
|
+
// A consumer `onScroll` is composed rather than dropped — but because the
|
|
131
|
+
// child is now an animated component it has to be an animated handler too.
|
|
132
|
+
const onScroll = useComposedEventHandler([
|
|
133
|
+
scrollHandler,
|
|
134
|
+
(child.props.onScroll as typeof scrollHandler | undefined) ?? null,
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
// Measured up front as well as on scroll, so an end edge with content behind
|
|
138
|
+
// it fades from the first frame rather than waiting for a scroll event.
|
|
139
|
+
const onLayout = (event: LayoutChangeEvent) => {
|
|
140
|
+
const { width, height } = event.nativeEvent.layout;
|
|
141
|
+
viewportLength.value = horizontal ? width : height;
|
|
142
|
+
child.props.onLayout?.(event);
|
|
90
143
|
};
|
|
91
144
|
|
|
92
|
-
const
|
|
93
|
-
?
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
145
|
+
const onContentSizeChange = (width: number, height: number) => {
|
|
146
|
+
contentLength.value = horizontal ? width : height;
|
|
147
|
+
child.props.onContentSizeChange?.(width, height);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const startOpacity = useDerivedValue(() =>
|
|
151
|
+
Math.min(offset.value / fadeInDistance, 1)
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const endOpacity = useDerivedValue(() => {
|
|
155
|
+
// Nothing to fade towards when the content fits inside the viewport.
|
|
156
|
+
const remaining = contentLength.value - viewportLength.value - offset.value;
|
|
157
|
+
return Math.min(Math.max(remaining, 0) / fadeInDistance, 1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const scrollable =
|
|
161
|
+
AnimatedScrollable && isValidElement(child) ? (
|
|
162
|
+
<AnimatedScrollable
|
|
163
|
+
{...child.props}
|
|
164
|
+
onScroll={onScroll}
|
|
165
|
+
onLayout={onLayout}
|
|
166
|
+
onContentSizeChange={onContentSizeChange}
|
|
167
|
+
scrollEventThrottle={child.props.scrollEventThrottle ?? 16}
|
|
168
|
+
/>
|
|
169
|
+
) : (
|
|
170
|
+
child
|
|
171
|
+
);
|
|
98
172
|
|
|
99
|
-
|
|
100
|
-
|
|
173
|
+
if (!enabled || edges === 'none') {
|
|
174
|
+
return (
|
|
175
|
+
<View {...props} className={className}>
|
|
176
|
+
{scrollable}
|
|
177
|
+
</View>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
101
180
|
|
|
102
181
|
return (
|
|
103
182
|
<View {...props} className={className}>
|
|
104
183
|
{scrollable}
|
|
105
184
|
|
|
106
|
-
{
|
|
185
|
+
{edges !== 'end' ? (
|
|
107
186
|
<Fade
|
|
108
187
|
color={fadeColor}
|
|
109
188
|
size={size}
|
|
@@ -112,7 +191,7 @@ export function ScrollFade({
|
|
|
112
191
|
edge="start"
|
|
113
192
|
/>
|
|
114
193
|
) : null}
|
|
115
|
-
{
|
|
194
|
+
{edges !== 'start' ? (
|
|
116
195
|
<Fade
|
|
117
196
|
color={fadeColor}
|
|
118
197
|
size={size}
|
|
@@ -138,31 +217,64 @@ function Fade({
|
|
|
138
217
|
color: string;
|
|
139
218
|
size: number;
|
|
140
219
|
horizontal: boolean;
|
|
141
|
-
opacity: number
|
|
220
|
+
opacity: DerivedValue<number>;
|
|
142
221
|
edge: 'start' | 'end';
|
|
143
222
|
}) {
|
|
144
223
|
const isStart = edge === 'start';
|
|
145
|
-
const id = `scroll-fade-${edge}-${horizontal ? 'x' : 'y'}`;
|
|
146
224
|
|
|
147
225
|
const position = horizontal
|
|
148
226
|
? { top: 0, bottom: 0, width: size, ...(isStart ? { left: 0 } : { right: 0 }) }
|
|
149
227
|
: { left: 0, right: 0, height: size, ...(isStart ? { top: 0 } : { bottom: 0 }) };
|
|
150
228
|
|
|
151
|
-
|
|
229
|
+
const style = useAnimatedStyle(() => ({ opacity: opacity.value }));
|
|
230
|
+
|
|
231
|
+
// The gradient runs from the edge inwards, so its stops are reversed on the
|
|
152
232
|
// end edge.
|
|
153
|
-
const [
|
|
233
|
+
const colors: [string, string] = isStart
|
|
234
|
+
? [withAlpha(color, 1), withAlpha(color, 0)]
|
|
235
|
+
: [withAlpha(color, 0), withAlpha(color, 1)];
|
|
154
236
|
|
|
155
237
|
return (
|
|
156
|
-
<View
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
</View>
|
|
238
|
+
<Animated.View
|
|
239
|
+
pointerEvents="none"
|
|
240
|
+
style={[{ position: 'absolute' }, position, style]}
|
|
241
|
+
>
|
|
242
|
+
<LinearGradient
|
|
243
|
+
colors={colors}
|
|
244
|
+
start={{ x: 0, y: 0 }}
|
|
245
|
+
end={horizontal ? { x: 1, y: 0 } : { x: 0, y: 1 }}
|
|
246
|
+
style={StyleSheet.absoluteFill}
|
|
247
|
+
/>
|
|
248
|
+
</Animated.View>
|
|
167
249
|
);
|
|
168
250
|
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Gradients need a transparent stop of the *same* colour — `transparent` is
|
|
254
|
+
* black at zero alpha on Android, which shows as a grey smear.
|
|
255
|
+
*/
|
|
256
|
+
function withAlpha(color: string, alpha: number): string {
|
|
257
|
+
if (color.startsWith('#')) {
|
|
258
|
+
const hex = color.slice(1);
|
|
259
|
+
const full =
|
|
260
|
+
hex.length === 3
|
|
261
|
+
? hex
|
|
262
|
+
.split('')
|
|
263
|
+
.map((c) => c + c)
|
|
264
|
+
.join('')
|
|
265
|
+
: hex.slice(0, 6);
|
|
266
|
+
const r = parseInt(full.slice(0, 2), 16);
|
|
267
|
+
const g = parseInt(full.slice(2, 4), 16);
|
|
268
|
+
const b = parseInt(full.slice(4, 6), 16);
|
|
269
|
+
if (Number.isNaN(r + g + b)) return color;
|
|
270
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const channels = color.match(/rgba?\(([^)]+)\)/)?.[1];
|
|
274
|
+
if (channels) {
|
|
275
|
+
const [r, g, b] = channels.split(',').map((part) => part.trim());
|
|
276
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return color;
|
|
280
|
+
}
|
|
@@ -12,6 +12,7 @@ import { Pressable, ScrollView, View } from 'react-native';
|
|
|
12
12
|
import { tv } from 'tailwind-variants';
|
|
13
13
|
import { useCSSVariable } from 'uniwind';
|
|
14
14
|
import { CheckIcon, ChevronDownIcon } from '../../icons';
|
|
15
|
+
import { getNativeUI } from '../../native';
|
|
15
16
|
import { Text } from '../../primitives/text';
|
|
16
17
|
import { BottomSheet } from '../bottom-sheet';
|
|
17
18
|
|
|
@@ -86,6 +87,21 @@ export interface SelectProps {
|
|
|
86
87
|
/** Sheet title shown above the options. */
|
|
87
88
|
title?: string;
|
|
88
89
|
disabled?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Render the platform's own picker instead of the trigger-and-sheet pair.
|
|
92
|
+
* Requires the optional `@expo/ui` package; without it this prop does
|
|
93
|
+
* nothing.
|
|
94
|
+
*
|
|
95
|
+
* **Theme tokens do not apply** — the platform draws the picker, so
|
|
96
|
+
* `className` and `title` are ignored. `Select.Item` children still declare
|
|
97
|
+
* the options.
|
|
98
|
+
*/
|
|
99
|
+
native?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Native picker style. `menu` is a compact button opening a dropdown;
|
|
102
|
+
* `wheel` is an always-visible rotor (iOS; falls back to `menu` elsewhere).
|
|
103
|
+
*/
|
|
104
|
+
nativeAppearance?: 'menu' | 'wheel';
|
|
89
105
|
children: ReactNode;
|
|
90
106
|
}
|
|
91
107
|
|
|
@@ -96,22 +112,27 @@ function SelectRoot({
|
|
|
96
112
|
placeholder = 'Select an option',
|
|
97
113
|
title,
|
|
98
114
|
disabled,
|
|
115
|
+
native,
|
|
116
|
+
nativeAppearance = 'menu',
|
|
99
117
|
children,
|
|
100
118
|
}: SelectProps) {
|
|
101
119
|
const [open, setOpen] = useState(false);
|
|
120
|
+
const nativeUI = native ? getNativeUI() : null;
|
|
102
121
|
|
|
103
|
-
const
|
|
104
|
-
|
|
122
|
+
const options = useMemo(() => {
|
|
123
|
+
const collected: SelectItemProps[] = [];
|
|
105
124
|
Children.forEach(children, (child) => {
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
child.props.value === value
|
|
109
|
-
) {
|
|
110
|
-
label = child.props.label;
|
|
125
|
+
if (isValidElement<SelectItemProps>(child)) {
|
|
126
|
+
collected.push({ value: child.props.value, label: child.props.label });
|
|
111
127
|
}
|
|
112
128
|
});
|
|
113
|
-
return
|
|
114
|
-
}, [children
|
|
129
|
+
return collected;
|
|
130
|
+
}, [children]);
|
|
131
|
+
|
|
132
|
+
const selectedLabel = useMemo(
|
|
133
|
+
() => options.find((option) => option.value === value)?.label,
|
|
134
|
+
[options, value]
|
|
135
|
+
);
|
|
115
136
|
|
|
116
137
|
const context = useMemo<SelectContextValue>(
|
|
117
138
|
() => ({
|
|
@@ -129,6 +150,30 @@ function SelectRoot({
|
|
|
129
150
|
});
|
|
130
151
|
const chevronColor = useCSSVariable('--color-muted-foreground');
|
|
131
152
|
|
|
153
|
+
if (nativeUI) {
|
|
154
|
+
const { Host, Picker } = nativeUI;
|
|
155
|
+
// The native picker has no empty state, so an unset value shows the first
|
|
156
|
+
// option rather than the placeholder.
|
|
157
|
+
return (
|
|
158
|
+
<Host matchContents>
|
|
159
|
+
<Picker
|
|
160
|
+
selectedValue={value ?? options[0]?.value ?? ''}
|
|
161
|
+
onValueChange={(next: string) => onValueChange(next)}
|
|
162
|
+
appearance={nativeAppearance}
|
|
163
|
+
enabled={!disabled}
|
|
164
|
+
>
|
|
165
|
+
{options.map((option) => (
|
|
166
|
+
<Picker.Item
|
|
167
|
+
key={option.value}
|
|
168
|
+
label={option.label}
|
|
169
|
+
value={option.value}
|
|
170
|
+
/>
|
|
171
|
+
))}
|
|
172
|
+
</Picker>
|
|
173
|
+
</Host>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
132
177
|
return (
|
|
133
178
|
<SelectContext.Provider value={context}>
|
|
134
179
|
<Pressable
|