panelui-native 0.9.0 → 0.13.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 +5 -3
- package/lib/module/components/bottom-sheet/index.js +38 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +19 -1
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/frame/index.js +128 -15
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/input/index.js +107 -15
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/popover/index.js +22 -11
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/progress/index.js +52 -2
- package/lib/module/components/progress/index.js.map +1 -1
- package/lib/module/components/radio-group/index.js +34 -7
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/section-rail/index.js +355 -0
- package/lib/module/components/section-rail/index.js.map +1 -0
- package/lib/module/components/select/index.js +245 -56
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/slider/index.js +355 -0
- package/lib/module/components/slider/index.js.map +1 -0
- package/lib/module/components/switch/index.js +10 -1
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/tabs/index.js +186 -16
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/toggle-button/index.js +297 -0
- package/lib/module/components/toggle-button/index.js.map +1 -0
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +57 -8
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/hooks/use-scroll-sections.js +89 -0
- package/lib/module/hooks/use-scroll-sections.js.map +1 -0
- package/lib/module/index.js +4 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/primitives/keyboard-avoider.js +2 -0
- package/lib/module/primitives/keyboard-avoider.js.map +1 -1
- package/lib/module/utils/haptics.js +57 -0
- package/lib/module/utils/haptics.js.map +1 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +9 -4
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/frame/index.d.ts +47 -7
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input/index.d.ts +113 -2
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/popover/index.d.ts +7 -1
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress/index.d.ts +33 -0
- package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
- package/lib/typescript/src/components/radio-group/index.d.ts +7 -0
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/section-rail/index.d.ts +115 -0
- package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts +46 -7
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts +203 -0
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -0
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tabs/index.d.ts +46 -4
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/toggle-button/index.d.ts +244 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -0
- 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-keyboard-avoidance.d.ts +10 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts +31 -0
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +16 -1
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts +8 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
- package/lib/typescript/src/utils/haptics.d.ts +12 -0
- package/lib/typescript/src/utils/haptics.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/components/bottom-sheet/index.tsx +38 -2
- package/src/components/button/index.tsx +26 -5
- package/src/components/chip/index.tsx +333 -0
- package/src/components/frame/index.tsx +175 -29
- package/src/components/input/index.tsx +122 -16
- package/src/components/popover/index.tsx +49 -24
- package/src/components/progress/index.tsx +81 -1
- package/src/components/radio-group/index.tsx +55 -8
- package/src/components/section-rail/index.tsx +459 -0
- package/src/components/select/index.tsx +265 -52
- package/src/components/slider/index.tsx +371 -0
- package/src/components/switch/index.tsx +8 -1
- package/src/components/tabs/index.tsx +208 -28
- package/src/components/toggle-button/index.tsx +396 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/use-keyboard-avoidance.ts +73 -7
- package/src/hooks/use-scroll-sections.ts +127 -0
- package/src/index.ts +45 -6
- package/src/native/index.ts +13 -1
- package/src/primitives/keyboard-avoider.tsx +9 -1
- package/src/utils/haptics.ts +63 -0
- package/theme.css +6 -2
- package/lib/module/components/inline-select/index.js +0 -284
- package/lib/module/components/inline-select/index.js.map +0 -1
- package/lib/typescript/src/components/inline-select/index.d.ts +0 -48
- package/lib/typescript/src/components/inline-select/index.d.ts.map +0 -1
- package/src/components/inline-select/index.tsx +0 -332
|
@@ -1,28 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select — a picker with one trigger and three ways of showing its options.
|
|
3
|
+
*
|
|
4
|
+
* Which one is right depends on what surrounds the trigger, not on what the
|
|
5
|
+
* options are, which is why it is a prop rather than three components:
|
|
6
|
+
*
|
|
7
|
+
* - `sheet` (default) takes the bottom of the screen. Best for a long list, or
|
|
8
|
+
* on a small screen where an anchored panel would cover the thing you are
|
|
9
|
+
* choosing for.
|
|
10
|
+
* - `inline` expands the list in normal layout flow. Everything below moves
|
|
11
|
+
* down. Right inside a settings list, where that reads as the row growing;
|
|
12
|
+
* wrong anywhere the shift is jarring.
|
|
13
|
+
* - `overlay` floats the list above the page through a portal, anchored to the
|
|
14
|
+
* trigger and flipped above it when there is no room below. Nothing else on
|
|
15
|
+
* the screen moves.
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <Select value={region} onValueChange={setRegion} presentation="overlay">
|
|
19
|
+
* <Select.Item value="us" label="United States" />
|
|
20
|
+
* <Select.Item value="eu" label="Europe" />
|
|
21
|
+
* </Select>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
1
24
|
import {
|
|
2
25
|
Children,
|
|
3
26
|
createContext,
|
|
4
27
|
isValidElement,
|
|
28
|
+
useCallback,
|
|
5
29
|
useContext,
|
|
6
30
|
useMemo,
|
|
31
|
+
useRef,
|
|
7
32
|
useState,
|
|
8
|
-
type ReactElement,
|
|
9
33
|
type ReactNode,
|
|
10
34
|
} from 'react';
|
|
11
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
Pressable,
|
|
37
|
+
ScrollView,
|
|
38
|
+
useWindowDimensions,
|
|
39
|
+
View,
|
|
40
|
+
type LayoutChangeEvent,
|
|
41
|
+
} from 'react-native';
|
|
42
|
+
import Animated, {
|
|
43
|
+
FadeIn,
|
|
44
|
+
FadeOut,
|
|
45
|
+
useAnimatedStyle,
|
|
46
|
+
useSharedValue,
|
|
47
|
+
withTiming,
|
|
48
|
+
} from 'react-native-reanimated';
|
|
12
49
|
import { tv } from 'tailwind-variants';
|
|
13
50
|
import { useCSSVariable } from 'uniwind';
|
|
14
51
|
import { CheckIcon, ChevronDownIcon } from '../../icons';
|
|
15
52
|
import { getNativeUI } from '../../native';
|
|
53
|
+
import { Portal } from '../../primitives/portal';
|
|
16
54
|
import { Text } from '../../primitives/text';
|
|
17
55
|
import { BottomSheet } from '../bottom-sheet';
|
|
18
56
|
|
|
19
57
|
const selectVariants = tv({
|
|
20
58
|
slots: {
|
|
59
|
+
// `rounded-lg`, the same radius Button and Input use — a select sitting in
|
|
60
|
+
// a form beside either of them should read as the same family of control.
|
|
21
61
|
trigger:
|
|
22
|
-
'w-full flex-row items-center justify-between gap-3 rounded-
|
|
62
|
+
'w-full flex-row items-center justify-between gap-3 rounded-lg border border-input bg-background px-4 py-3.5',
|
|
23
63
|
triggerLabel: 'flex-1 text-base font-medium text-foreground',
|
|
24
64
|
placeholder: 'flex-1 text-base text-muted-foreground',
|
|
25
|
-
|
|
65
|
+
list: 'overflow-hidden rounded-xl border border-border bg-popover p-2 shadow-sm',
|
|
66
|
+
item: 'flex-row items-center gap-2 rounded-lg px-3 py-3',
|
|
26
67
|
itemLabel: 'flex-1 text-base font-medium text-foreground',
|
|
27
68
|
itemIndicator: 'h-5 w-5 items-center justify-center',
|
|
28
69
|
},
|
|
@@ -31,11 +72,29 @@ const selectVariants = tv({
|
|
|
31
72
|
true: { item: 'bg-accent' },
|
|
32
73
|
},
|
|
33
74
|
disabled: {
|
|
34
|
-
true: { trigger: 'opacity-
|
|
75
|
+
true: { trigger: 'opacity-[0.64]' },
|
|
76
|
+
},
|
|
77
|
+
presentation: {
|
|
78
|
+
sheet: {},
|
|
79
|
+
inline: { list: 'mt-2' },
|
|
80
|
+
overlay: { list: 'shadow-lg' },
|
|
35
81
|
},
|
|
36
82
|
},
|
|
83
|
+
defaultVariants: {
|
|
84
|
+
presentation: 'sheet',
|
|
85
|
+
},
|
|
37
86
|
});
|
|
38
87
|
|
|
88
|
+
export type SelectPresentation = 'sheet' | 'inline' | 'overlay';
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Height given to the native host per appearance — a menu is a compact button,
|
|
92
|
+
* a wheel is a full rotor. The host is told its size rather than asked to
|
|
93
|
+
* measure the platform's content, which arrives a frame late and leaves the
|
|
94
|
+
* control collapsed until something forces a second pass.
|
|
95
|
+
*/
|
|
96
|
+
const NATIVE_HEIGHT = { menu: 44, wheel: 216 } as const;
|
|
97
|
+
|
|
39
98
|
interface SelectContextValue {
|
|
40
99
|
value: string | undefined;
|
|
41
100
|
onSelect: (value: string) => void;
|
|
@@ -48,7 +107,7 @@ export interface SelectItemProps {
|
|
|
48
107
|
label: string;
|
|
49
108
|
}
|
|
50
109
|
|
|
51
|
-
/** Declarative option. Rendered inside
|
|
110
|
+
/** Declarative option. Rendered inside whichever surface is presenting. */
|
|
52
111
|
function SelectItem({ value, label }: SelectItemProps) {
|
|
53
112
|
const context = useContext(SelectContext);
|
|
54
113
|
if (!context) {
|
|
@@ -79,22 +138,45 @@ function SelectItem({ value, label }: SelectItemProps) {
|
|
|
79
138
|
);
|
|
80
139
|
}
|
|
81
140
|
|
|
141
|
+
/** Trigger frame in window coordinates, measured when the list opens. */
|
|
142
|
+
interface Anchor {
|
|
143
|
+
x: number;
|
|
144
|
+
y: number;
|
|
145
|
+
width: number;
|
|
146
|
+
height: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
82
149
|
export interface SelectProps {
|
|
83
150
|
className?: string;
|
|
84
151
|
value?: string;
|
|
85
152
|
onValueChange: (value: string) => void;
|
|
86
153
|
placeholder?: string;
|
|
87
|
-
/** Sheet title shown above the options. */
|
|
88
|
-
title?: string;
|
|
89
154
|
disabled?: boolean;
|
|
90
155
|
/**
|
|
91
|
-
*
|
|
156
|
+
* Where the options appear. `sheet` takes the bottom of the screen, `inline`
|
|
157
|
+
* expands the list in layout flow, `overlay` floats it above the page
|
|
158
|
+
* anchored to the trigger.
|
|
159
|
+
*/
|
|
160
|
+
presentation?: SelectPresentation;
|
|
161
|
+
/** Sheet title shown above the options. `sheet` presentation only. */
|
|
162
|
+
title?: string;
|
|
163
|
+
/**
|
|
164
|
+
* Width of the floating list. `trigger` matches the trigger, `content` sizes
|
|
165
|
+
* to the longest option, or pass a pixel value. `overlay` only.
|
|
166
|
+
*/
|
|
167
|
+
contentWidth?: 'trigger' | 'content' | number;
|
|
168
|
+
/** Gap between the trigger and the floating list. `overlay` only. */
|
|
169
|
+
offset?: number;
|
|
170
|
+
/** Called when the options open or close. */
|
|
171
|
+
onOpenChange?: (open: boolean) => void;
|
|
172
|
+
/**
|
|
173
|
+
* Render the platform's own picker instead of the trigger-and-list pair.
|
|
92
174
|
* Requires the optional `@expo/ui` package; without it this prop does
|
|
93
175
|
* nothing.
|
|
94
176
|
*
|
|
95
177
|
* **Theme tokens do not apply** — the platform draws the picker, so
|
|
96
|
-
* `className` and `
|
|
97
|
-
* the options.
|
|
178
|
+
* `className`, `title` and `presentation` are ignored. `Select.Item`
|
|
179
|
+
* children still declare the options.
|
|
98
180
|
*/
|
|
99
181
|
native?: boolean;
|
|
100
182
|
/**
|
|
@@ -110,13 +192,22 @@ function SelectRoot({
|
|
|
110
192
|
value,
|
|
111
193
|
onValueChange,
|
|
112
194
|
placeholder = 'Select an option',
|
|
113
|
-
title,
|
|
114
195
|
disabled,
|
|
196
|
+
presentation = 'sheet',
|
|
197
|
+
title,
|
|
198
|
+
contentWidth = 'trigger',
|
|
199
|
+
offset = 8,
|
|
200
|
+
onOpenChange,
|
|
115
201
|
native,
|
|
116
202
|
nativeAppearance = 'menu',
|
|
117
203
|
children,
|
|
118
204
|
}: SelectProps) {
|
|
119
205
|
const [open, setOpen] = useState(false);
|
|
206
|
+
const [anchor, setAnchor] = useState<Anchor | null>(null);
|
|
207
|
+
const [listHeight, setListHeight] = useState(0);
|
|
208
|
+
const triggerRef = useRef<View>(null);
|
|
209
|
+
const chevron = useSharedValue(0);
|
|
210
|
+
const { height: screenHeight } = useWindowDimensions();
|
|
120
211
|
const nativeUI = native ? getNativeUI() : null;
|
|
121
212
|
|
|
122
213
|
const options = useMemo(() => {
|
|
@@ -134,28 +225,61 @@ function SelectRoot({
|
|
|
134
225
|
[options, value]
|
|
135
226
|
);
|
|
136
227
|
|
|
228
|
+
const close = useCallback(() => {
|
|
229
|
+
chevron.value = withTiming(0, { duration: 160 });
|
|
230
|
+
setOpen(false);
|
|
231
|
+
onOpenChange?.(false);
|
|
232
|
+
}, [chevron, onOpenChange]);
|
|
233
|
+
|
|
234
|
+
const toggle = useCallback(() => {
|
|
235
|
+
if (open) {
|
|
236
|
+
close();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const show = () => {
|
|
241
|
+
chevron.value = withTiming(1, { duration: 160 });
|
|
242
|
+
setOpen(true);
|
|
243
|
+
onOpenChange?.(true);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (presentation !== 'overlay') {
|
|
247
|
+
show();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// The floating list is positioned in window coordinates, so it has to know
|
|
252
|
+
// where the trigger actually landed — not where layout said it would.
|
|
253
|
+
triggerRef.current?.measureInWindow((x, y, width, height) => {
|
|
254
|
+
setAnchor({ x, y, width, height });
|
|
255
|
+
show();
|
|
256
|
+
});
|
|
257
|
+
}, [chevron, close, onOpenChange, open, presentation]);
|
|
258
|
+
|
|
137
259
|
const context = useMemo<SelectContextValue>(
|
|
138
260
|
() => ({
|
|
139
261
|
value,
|
|
140
262
|
onSelect: (next) => {
|
|
141
263
|
onValueChange(next);
|
|
142
|
-
|
|
264
|
+
close();
|
|
143
265
|
},
|
|
144
266
|
}),
|
|
145
|
-
[value, onValueChange]
|
|
267
|
+
[value, onValueChange, close]
|
|
146
268
|
);
|
|
147
269
|
|
|
148
|
-
const {
|
|
149
|
-
disabled: !!disabled,
|
|
150
|
-
});
|
|
270
|
+
const slots = selectVariants({ disabled: !!disabled, presentation });
|
|
151
271
|
const chevronColor = useCSSVariable('--color-muted-foreground');
|
|
152
272
|
|
|
273
|
+
const chevronStyle = useAnimatedStyle(() => ({
|
|
274
|
+
transform: [{ rotate: `${chevron.value * 180}deg` }],
|
|
275
|
+
}));
|
|
276
|
+
|
|
153
277
|
if (nativeUI) {
|
|
154
278
|
const { Host, Picker } = nativeUI;
|
|
155
279
|
// The native picker has no empty state, so an unset value shows the first
|
|
156
280
|
// option rather than the placeholder.
|
|
157
281
|
return (
|
|
158
|
-
<Host
|
|
282
|
+
<Host style={{ height: NATIVE_HEIGHT[nativeAppearance] }}>
|
|
159
283
|
<Picker
|
|
160
284
|
selectedValue={value ?? options[0]?.value ?? ''}
|
|
161
285
|
onValueChange={(next: string) => onValueChange(next)}
|
|
@@ -163,51 +287,140 @@ function SelectRoot({
|
|
|
163
287
|
enabled={!disabled}
|
|
164
288
|
>
|
|
165
289
|
{options.map((option) => (
|
|
166
|
-
<Picker.Item
|
|
167
|
-
key={option.value}
|
|
168
|
-
label={option.label}
|
|
169
|
-
value={option.value}
|
|
170
|
-
/>
|
|
290
|
+
<Picker.Item key={option.value} label={option.label} value={option.value} />
|
|
171
291
|
))}
|
|
172
292
|
</Picker>
|
|
173
293
|
</Host>
|
|
174
294
|
);
|
|
175
295
|
}
|
|
176
296
|
|
|
177
|
-
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
297
|
+
const trigger = (
|
|
298
|
+
<Pressable
|
|
299
|
+
ref={triggerRef}
|
|
300
|
+
accessibilityRole="button"
|
|
301
|
+
accessibilityState={{ disabled: !!disabled, expanded: open }}
|
|
302
|
+
disabled={disabled}
|
|
303
|
+
onPress={toggle}
|
|
304
|
+
className={slots.trigger()}
|
|
305
|
+
>
|
|
306
|
+
{selectedLabel ? (
|
|
307
|
+
<Text className={slots.triggerLabel()}>{selectedLabel}</Text>
|
|
308
|
+
) : (
|
|
309
|
+
<Text className={slots.placeholder()}>{placeholder}</Text>
|
|
310
|
+
)}
|
|
311
|
+
<Animated.View style={chevronStyle}>
|
|
191
312
|
<ChevronDownIcon
|
|
192
313
|
color={typeof chevronColor === 'string' ? chevronColor : '#737373'}
|
|
193
314
|
/>
|
|
194
|
-
</
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
315
|
+
</Animated.View>
|
|
316
|
+
</Pressable>
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (presentation === 'sheet') {
|
|
320
|
+
return (
|
|
321
|
+
<SelectContext.Provider value={context}>
|
|
322
|
+
<View className={className}>{trigger}</View>
|
|
323
|
+
{/* The sheet only ever reports a close — it is opened from the
|
|
324
|
+
trigger — and close() has the chevron to put back. */}
|
|
325
|
+
<BottomSheet
|
|
326
|
+
open={open}
|
|
327
|
+
onOpenChange={(next) => {
|
|
328
|
+
if (!next) close();
|
|
329
|
+
}}
|
|
330
|
+
>
|
|
331
|
+
<BottomSheet.Content>
|
|
332
|
+
{/* BottomSheet.Content portals its children out of this subtree —
|
|
333
|
+
re-provide the select context so Select.Item keeps working. */}
|
|
334
|
+
<SelectContext.Provider value={context}>
|
|
335
|
+
{title ? (
|
|
336
|
+
<Text size="lg" weight="semibold" className="mb-2 px-3">
|
|
337
|
+
{title}
|
|
338
|
+
</Text>
|
|
339
|
+
) : null}
|
|
340
|
+
<ScrollView bounces={false} className="max-h-96">
|
|
341
|
+
<View className="gap-1 pb-2">{children}</View>
|
|
342
|
+
</ScrollView>
|
|
343
|
+
</SelectContext.Provider>
|
|
344
|
+
</BottomSheet.Content>
|
|
345
|
+
</BottomSheet>
|
|
346
|
+
</SelectContext.Provider>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (presentation === 'inline') {
|
|
351
|
+
return (
|
|
352
|
+
<SelectContext.Provider value={context}>
|
|
353
|
+
<View className={className}>
|
|
354
|
+
{trigger}
|
|
355
|
+
{open ? (
|
|
356
|
+
<Animated.View
|
|
357
|
+
entering={FadeIn.duration(140)}
|
|
358
|
+
exiting={FadeOut.duration(120)}
|
|
359
|
+
className={slots.list()}
|
|
360
|
+
>
|
|
361
|
+
{children}
|
|
362
|
+
</Animated.View>
|
|
363
|
+
) : null}
|
|
364
|
+
</View>
|
|
365
|
+
</SelectContext.Provider>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Flip above the trigger when the list would run off the bottom. listHeight
|
|
370
|
+
// is 0 on the first frame, so the list opens downwards and corrects itself
|
|
371
|
+
// once measured — which is invisible inside the 140ms fade.
|
|
372
|
+
const spaceBelow = anchor ? screenHeight - (anchor.y + anchor.height) - offset : 0;
|
|
373
|
+
const flip = !!anchor && listHeight > 0 && listHeight > spaceBelow;
|
|
374
|
+
|
|
375
|
+
const overlayPosition = anchor
|
|
376
|
+
? {
|
|
377
|
+
position: 'absolute' as const,
|
|
378
|
+
left: anchor.x,
|
|
379
|
+
...(flip
|
|
380
|
+
? { bottom: screenHeight - anchor.y + offset }
|
|
381
|
+
: { top: anchor.y + anchor.height + offset }),
|
|
382
|
+
...(contentWidth === 'trigger'
|
|
383
|
+
? { width: anchor.width }
|
|
384
|
+
: typeof contentWidth === 'number'
|
|
385
|
+
? { width: contentWidth }
|
|
386
|
+
: { minWidth: anchor.width }),
|
|
387
|
+
// Never collapse to nothing in a cramped viewport — the list scrolls.
|
|
388
|
+
maxHeight: Math.max((flip ? anchor.y : spaceBelow) - offset, 160),
|
|
389
|
+
}
|
|
390
|
+
: null;
|
|
391
|
+
|
|
392
|
+
const onListLayout = (event: LayoutChangeEvent) => {
|
|
393
|
+
setListHeight(event.nativeEvent.layout.height);
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<SelectContext.Provider value={context}>
|
|
398
|
+
<View className={className}>{trigger}</View>
|
|
399
|
+
|
|
400
|
+
{open && overlayPosition ? (
|
|
401
|
+
<Portal>
|
|
402
|
+
{/* Full-screen catcher so a press anywhere else dismisses the list. */}
|
|
403
|
+
<Pressable
|
|
404
|
+
accessibilityRole="button"
|
|
405
|
+
accessibilityLabel="Close"
|
|
406
|
+
onPress={close}
|
|
407
|
+
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
|
|
408
|
+
/>
|
|
199
409
|
<SelectContext.Provider value={context}>
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
410
|
+
<Animated.View
|
|
411
|
+
entering={FadeIn.duration(140)}
|
|
412
|
+
exiting={FadeOut.duration(120)}
|
|
413
|
+
onLayout={onListLayout}
|
|
414
|
+
style={overlayPosition}
|
|
415
|
+
className={slots.list()}
|
|
416
|
+
>
|
|
417
|
+
<ScrollView bounces={false} showsVerticalScrollIndicator={false}>
|
|
418
|
+
{children}
|
|
419
|
+
</ScrollView>
|
|
420
|
+
</Animated.View>
|
|
208
421
|
</SelectContext.Provider>
|
|
209
|
-
</
|
|
210
|
-
|
|
422
|
+
</Portal>
|
|
423
|
+
) : null}
|
|
211
424
|
</SelectContext.Provider>
|
|
212
425
|
);
|
|
213
426
|
}
|