@expo/ui 56.0.8 → 56.0.9
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/CHANGELOG.md +22 -1
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +38 -1
- package/android/src/main/java/expo/modules/ui/LoadingView.kt +80 -0
- package/android/src/main/java/expo/modules/ui/ModifierRegistry.kt +31 -3
- package/android/src/main/java/expo/modules/ui/SnackbarView.kt +126 -0
- package/android/src/main/java/expo/modules/ui/state/ObservableState.kt +10 -0
- package/assets/keyboard_arrow_down.xml +10 -0
- package/build/State/useNativeState.d.ts +32 -3
- package/build/State/useNativeState.d.ts.map +1 -1
- package/build/community/bottom-sheet/BottomSheet.ios.d.ts.map +1 -1
- package/build/jetpack-compose/LoadingIndicator/index.d.ts +41 -0
- package/build/jetpack-compose/LoadingIndicator/index.d.ts.map +1 -0
- package/build/jetpack-compose/Snackbar/index.d.ts +94 -0
- package/build/jetpack-compose/Snackbar/index.d.ts.map +1 -0
- package/build/jetpack-compose/index.d.ts +2 -0
- package/build/jetpack-compose/index.d.ts.map +1 -1
- package/build/jetpack-compose/modifiers/index.d.ts +6 -2
- package/build/jetpack-compose/modifiers/index.d.ts.map +1 -1
- package/build/swift-ui/BottomSheet/index.d.ts +5 -1
- package/build/swift-ui/BottomSheet/index.d.ts.map +1 -1
- package/build/swift-ui/index.d.ts +1 -0
- package/build/swift-ui/index.d.ts.map +1 -1
- package/build/swift-ui/withAnimation.d.ts +26 -0
- package/build/swift-ui/withAnimation.d.ts.map +1 -0
- package/build/universal/BottomSheet/index.android.d.ts +1 -1
- package/build/universal/BottomSheet/index.android.d.ts.map +1 -1
- package/build/universal/BottomSheet/index.d.ts +1 -1
- package/build/universal/BottomSheet/index.d.ts.map +1 -1
- package/build/universal/BottomSheet/index.ios.d.ts +1 -1
- package/build/universal/BottomSheet/index.ios.d.ts.map +1 -1
- package/build/universal/BottomSheet/types.d.ts +27 -0
- package/build/universal/BottomSheet/types.d.ts.map +1 -1
- package/build/universal/Collapsible/index.android.d.ts +8 -0
- package/build/universal/Collapsible/index.android.d.ts.map +1 -0
- package/build/universal/Collapsible/index.d.ts +8 -0
- package/build/universal/Collapsible/index.d.ts.map +1 -0
- package/build/universal/Collapsible/index.ios.d.ts +7 -0
- package/build/universal/Collapsible/index.ios.d.ts.map +1 -0
- package/build/universal/Collapsible/types.d.ts +23 -0
- package/build/universal/Collapsible/types.d.ts.map +1 -0
- package/build/universal/Column/index.d.ts.map +1 -1
- package/build/universal/Host/index.d.ts +5 -18
- package/build/universal/Host/index.d.ts.map +1 -1
- package/build/universal/Host/types.d.ts +72 -0
- package/build/universal/Host/types.d.ts.map +1 -0
- package/build/universal/List/index.android.d.ts +9 -0
- package/build/universal/List/index.android.d.ts.map +1 -0
- package/build/universal/List/index.d.ts +8 -0
- package/build/universal/List/index.d.ts.map +1 -0
- package/build/universal/List/index.ios.d.ts +8 -0
- package/build/universal/List/index.ios.d.ts.map +1 -0
- package/build/universal/List/types.d.ts +26 -0
- package/build/universal/List/types.d.ts.map +1 -0
- package/build/universal/ListItem/ListItem.android.d.ts +8 -0
- package/build/universal/ListItem/ListItem.android.d.ts.map +1 -0
- package/build/universal/ListItem/ListItem.d.ts +9 -0
- package/build/universal/ListItem/ListItem.d.ts.map +1 -0
- package/build/universal/ListItem/ListItem.ios.d.ts +8 -0
- package/build/universal/ListItem/ListItem.ios.d.ts.map +1 -0
- package/build/universal/ListItem/ListItemSlots.d.ts +21 -0
- package/build/universal/ListItem/ListItemSlots.d.ts.map +1 -0
- package/build/universal/ListItem/index.d.ts +10 -0
- package/build/universal/ListItem/index.d.ts.map +1 -0
- package/build/universal/ListItem/types.d.ts +59 -0
- package/build/universal/ListItem/types.d.ts.map +1 -0
- package/build/universal/Picker/Picker.android.d.ts +9 -0
- package/build/universal/Picker/Picker.android.d.ts.map +1 -0
- package/build/universal/Picker/Picker.d.ts +8 -0
- package/build/universal/Picker/Picker.d.ts.map +1 -0
- package/build/universal/Picker/Picker.ios.d.ts +9 -0
- package/build/universal/Picker/Picker.ios.d.ts.map +1 -0
- package/build/universal/Picker/PickerItem.d.ts +9 -0
- package/build/universal/Picker/PickerItem.d.ts.map +1 -0
- package/build/universal/Picker/index.d.ts +8 -0
- package/build/universal/Picker/index.d.ts.map +1 -0
- package/build/universal/Picker/types.d.ts +69 -0
- package/build/universal/Picker/types.d.ts.map +1 -0
- package/build/universal/index.d.ts +4 -0
- package/build/universal/index.d.ts.map +1 -1
- package/expo-module.config.json +1 -1
- package/ios/BottomSheetView.swift +4 -1
- package/ios/ExpoUIModule.swift +41 -1
- package/ios/Modifiers/AnimationConfig.swift +109 -0
- package/ios/Modifiers/ViewModifierRegistry.swift +1 -112
- package/ios/State/ObservableState.swift +12 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.8/expo.modules.ui-56.0.8-sources.jar → 56.0.9/expo.modules.ui-56.0.9-sources.jar} +0 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar +0 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.8/expo.modules.ui-56.0.8.module → 56.0.9/expo.modules.ui-56.0.9.module} +22 -22
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.8/expo.modules.ui-56.0.8.pom → 56.0.9/expo.modules.ui-56.0.9.pom} +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
- package/package.json +3 -3
- package/src/State/useNativeState.ts +70 -10
- package/src/community/bottom-sheet/BottomSheet.ios.tsx +0 -17
- package/src/jetpack-compose/LoadingIndicator/index.tsx +92 -0
- package/src/jetpack-compose/Snackbar/index.tsx +135 -0
- package/src/jetpack-compose/index.ts +2 -0
- package/src/jetpack-compose/modifiers/index.ts +5 -2
- package/src/swift-ui/BottomSheet/index.tsx +32 -15
- package/src/swift-ui/index.tsx +1 -0
- package/src/swift-ui/withAnimation.ts +71 -0
- package/src/ts-declarations/react-native-web.d.ts +7 -0
- package/src/universal/BottomSheet/index.android.tsx +27 -3
- package/src/universal/BottomSheet/index.ios.tsx +30 -12
- package/src/universal/BottomSheet/index.tsx +46 -4
- package/src/universal/BottomSheet/types.ts +25 -0
- package/src/universal/Collapsible/index.android.tsx +72 -0
- package/src/universal/Collapsible/index.ios.tsx +16 -0
- package/src/universal/Collapsible/index.tsx +58 -0
- package/src/universal/Collapsible/types.ts +25 -0
- package/src/universal/Column/index.tsx +3 -1
- package/src/universal/Host/index.tsx +9 -10
- package/src/universal/Host/types.ts +70 -0
- package/src/universal/List/index.android.tsx +44 -0
- package/src/universal/List/index.ios.tsx +19 -0
- package/src/universal/List/index.tsx +26 -0
- package/src/universal/List/types.ts +28 -0
- package/src/universal/ListItem/ListItem.android.tsx +52 -0
- package/src/universal/ListItem/ListItem.ios.tsx +58 -0
- package/src/universal/ListItem/ListItem.tsx +72 -0
- package/src/universal/ListItem/ListItemSlots.tsx +66 -0
- package/src/universal/ListItem/index.ts +15 -0
- package/src/universal/ListItem/types.ts +67 -0
- package/src/universal/Picker/Picker.android.tsx +69 -0
- package/src/universal/Picker/Picker.ios.tsx +45 -0
- package/src/universal/Picker/Picker.tsx +52 -0
- package/src/universal/Picker/PickerItem.tsx +27 -0
- package/src/universal/Picker/index.ts +11 -0
- package/src/universal/Picker/types.ts +79 -0
- package/src/universal/index.ts +4 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8-sources.jar.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8-sources.jar.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8-sources.jar.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8-sources.jar.sha512 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar +0 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar.sha512 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.module.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.module.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.module.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.module.sha512 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.pom.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.pom.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.pom.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.pom.sha512 +0 -1
- package/src/community/bottom-sheet/CLAUDE.md +0 -55
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ColorSchemeName, ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Props for the [`Host`](#host) component.
|
|
5
|
+
*/
|
|
6
|
+
export interface UniversalHostProps extends ViewProps {
|
|
7
|
+
/**
|
|
8
|
+
* When `true`, the host updates its size in the React Native view tree to match the content's layout from the underlying platform UI toolkit.
|
|
9
|
+
* Can only be set once on mount.
|
|
10
|
+
*
|
|
11
|
+
* @default false
|
|
12
|
+
* @platform android
|
|
13
|
+
* @platform ios
|
|
14
|
+
* @platform web
|
|
15
|
+
*/
|
|
16
|
+
matchContents?: boolean | { vertical?: boolean; horizontal?: boolean };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The color scheme to apply to descendant native views.
|
|
20
|
+
* `'light'` / `'dark'` force a specific appearance; omitted follows the device setting.
|
|
21
|
+
*
|
|
22
|
+
* @platform android
|
|
23
|
+
* @platform ios
|
|
24
|
+
*/
|
|
25
|
+
colorScheme?: ColorSchemeName;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Layout direction for the platform UI content.
|
|
29
|
+
* Defaults to the current locale direction from `I18nManager`.
|
|
30
|
+
*
|
|
31
|
+
* @platform android
|
|
32
|
+
* @platform ios
|
|
33
|
+
* @platform web
|
|
34
|
+
*/
|
|
35
|
+
layoutDirection?: 'leftToRight' | 'rightToLeft';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Controls which safe area regions the hosting view should ignore. Can only be set once on mount.
|
|
39
|
+
* - `'all'`- ignores all safe area insets.
|
|
40
|
+
* - `'keyboard'` - ignores only the keyboard safe area.
|
|
41
|
+
*
|
|
42
|
+
* @platform android
|
|
43
|
+
* @platform ios
|
|
44
|
+
* @platform web
|
|
45
|
+
*/
|
|
46
|
+
ignoreSafeArea?: 'all' | 'keyboard';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* When true and no explicit size is provided, the host will use the viewport size as the proposed size for layout.
|
|
50
|
+
* This is particularly useful for views that need to fill their available space, such as `List`.
|
|
51
|
+
* @default false
|
|
52
|
+
*
|
|
53
|
+
* @platform android
|
|
54
|
+
* @platform ios
|
|
55
|
+
* @platform web
|
|
56
|
+
*/
|
|
57
|
+
useViewportSizeMeasurement?: boolean;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Callback function that is triggered when the content completes its layout.
|
|
61
|
+
* Provides the current dimensions of the content, which may change as the content updates.
|
|
62
|
+
*
|
|
63
|
+
* @platform android
|
|
64
|
+
* @platform ios
|
|
65
|
+
* @platform web
|
|
66
|
+
*/
|
|
67
|
+
onLayoutContent?: (event: { nativeEvent: { width: number; height: number } }) => void;
|
|
68
|
+
|
|
69
|
+
children?: React.ReactNode;
|
|
70
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { LazyColumn, PullToRefreshBox } from '@expo/ui/jetpack-compose';
|
|
2
|
+
import { testID as testIDModifier, type ModifierConfig } from '@expo/ui/jetpack-compose/modifiers';
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { ListProps } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Android implementation of `List`.
|
|
9
|
+
* Composes `LazyColumn` and wraps with `PullToRefreshBox` when `onRefresh` is provided.
|
|
10
|
+
* The returned promise drives the refresh indicator's visibility.
|
|
11
|
+
*/
|
|
12
|
+
export function List({ children, onRefresh, testID }: ListProps) {
|
|
13
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
14
|
+
|
|
15
|
+
const handleRefresh = useCallback(async () => {
|
|
16
|
+
if (!onRefresh) return;
|
|
17
|
+
setIsRefreshing(true);
|
|
18
|
+
try {
|
|
19
|
+
await onRefresh();
|
|
20
|
+
} finally {
|
|
21
|
+
setIsRefreshing(false);
|
|
22
|
+
}
|
|
23
|
+
}, [onRefresh]);
|
|
24
|
+
|
|
25
|
+
const listModifiers: ModifierConfig[] | undefined = testID ? [testIDModifier(testID)] : undefined;
|
|
26
|
+
const listContent = <LazyColumn modifiers={listModifiers}>{children}</LazyColumn>;
|
|
27
|
+
|
|
28
|
+
if (!onRefresh) {
|
|
29
|
+
return listContent;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
// `contentAlignment="topCenter"` keeps the refresh indicator centered.
|
|
34
|
+
// The indicator's own `Modifier.align` resolves outside `BoxScope` and becomes a no-op, so we set it on the parent.
|
|
35
|
+
<PullToRefreshBox
|
|
36
|
+
isRefreshing={isRefreshing}
|
|
37
|
+
onRefresh={handleRefresh}
|
|
38
|
+
contentAlignment="topCenter">
|
|
39
|
+
{listContent}
|
|
40
|
+
</PullToRefreshBox>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export * from './types';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { List as SwiftUIList } from '@expo/ui/swift-ui';
|
|
2
|
+
import { refreshable, type ModifierConfig } from '@expo/ui/swift-ui/modifiers';
|
|
3
|
+
|
|
4
|
+
import type { ListProps } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* iOS implementation of `List`.
|
|
8
|
+
* Delegates to SwiftUI's `List` and applies `.refreshable` when `onRefresh` is provided.
|
|
9
|
+
*/
|
|
10
|
+
export function List({ children, onRefresh, testID }: ListProps) {
|
|
11
|
+
const modifiers: ModifierConfig[] | undefined = onRefresh ? [refreshable(onRefresh)] : undefined;
|
|
12
|
+
return (
|
|
13
|
+
<SwiftUIList modifiers={modifiers} testID={testID}>
|
|
14
|
+
{children}
|
|
15
|
+
</SwiftUIList>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export * from './types';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { StyleSheet, View } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import type { ListProps } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A vertical container of rows.
|
|
7
|
+
* Typically populated with [`ListItem`](#listitem) children.
|
|
8
|
+
*/
|
|
9
|
+
export function List({ children, testID }: ListProps) {
|
|
10
|
+
return (
|
|
11
|
+
<View style={styles.container} testID={testID}>
|
|
12
|
+
{children}
|
|
13
|
+
</View>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const styles = StyleSheet.create({
|
|
18
|
+
container: {
|
|
19
|
+
flexDirection: 'column',
|
|
20
|
+
width: '100%',
|
|
21
|
+
overflowX: 'auto',
|
|
22
|
+
overflowY: 'auto',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export * from './types';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Props for the [`List`](#list) component.
|
|
5
|
+
* A virtualized vertical container of rows.
|
|
6
|
+
* Typically populated with [`ListItem`](#listitem) children, though any node is accepted.
|
|
7
|
+
*/
|
|
8
|
+
export interface ListProps {
|
|
9
|
+
/**
|
|
10
|
+
* The list rows. Usually `<ListItem>` elements.
|
|
11
|
+
*/
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Optional pull-to-refresh handler.
|
|
16
|
+
* When provided, the list shows the platform-native refresh affordance.
|
|
17
|
+
* The returned promise drives the indicator's visibility.
|
|
18
|
+
*
|
|
19
|
+
* @platform android
|
|
20
|
+
* @platform ios
|
|
21
|
+
*/
|
|
22
|
+
onRefresh?: () => Promise<void>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Identifier used to locate the component in end-to-end tests.
|
|
26
|
+
*/
|
|
27
|
+
testID?: string;
|
|
28
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ListItem as ComposeListItem, Text } from '@expo/ui/jetpack-compose';
|
|
2
|
+
import { clickable } from '@expo/ui/jetpack-compose/modifiers';
|
|
3
|
+
import { Children, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import { extractListItemSlots } from './ListItemSlots';
|
|
6
|
+
import type { ListItemProps } from './types';
|
|
7
|
+
|
|
8
|
+
// Compose hosts can't render raw strings — they need a `Text` composable.
|
|
9
|
+
// Wrap any string/number node so consumers can pass plain strings via shorthand props or compound children.
|
|
10
|
+
function wrapStrings(node: ReactNode): ReactNode {
|
|
11
|
+
if (node == null || typeof node === 'boolean') return node;
|
|
12
|
+
if (typeof node === 'string' || typeof node === 'number') return <Text>{node}</Text>;
|
|
13
|
+
if (Array.isArray(node)) return Children.map(node, wrapStrings);
|
|
14
|
+
return node;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Android implementation of `ListItem`.
|
|
19
|
+
* Delegates to Material 3 `ListItem` and applies `clickable` for tap handling.
|
|
20
|
+
*/
|
|
21
|
+
export function ListItem(props: ListItemProps) {
|
|
22
|
+
const { children, onPress, leading: leadingProp, trailing: trailingProp, supportingText } = props;
|
|
23
|
+
const slots = extractListItemSlots(children);
|
|
24
|
+
const leading = slots.leading ?? leadingProp;
|
|
25
|
+
const trailing = slots.trailing ?? trailingProp;
|
|
26
|
+
const supporting = slots.supporting ?? supportingText;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<ComposeListItem modifiers={onPress ? [clickable(onPress)] : undefined}>
|
|
30
|
+
<ComposeListItem.HeadlineContent>
|
|
31
|
+
<>{wrapStrings(slots.headline)}</>
|
|
32
|
+
</ComposeListItem.HeadlineContent>
|
|
33
|
+
{supporting != null ? (
|
|
34
|
+
<ComposeListItem.SupportingContent>
|
|
35
|
+
{typeof supporting === 'string' || typeof supporting === 'number' ? (
|
|
36
|
+
<Text>{supporting}</Text>
|
|
37
|
+
) : (
|
|
38
|
+
supporting
|
|
39
|
+
)}
|
|
40
|
+
</ComposeListItem.SupportingContent>
|
|
41
|
+
) : null}
|
|
42
|
+
{leading != null ? (
|
|
43
|
+
<ComposeListItem.LeadingContent>{wrapStrings(leading)}</ComposeListItem.LeadingContent>
|
|
44
|
+
) : null}
|
|
45
|
+
{trailing != null ? (
|
|
46
|
+
<ComposeListItem.TrailingContent>{wrapStrings(trailing)}</ComposeListItem.TrailingContent>
|
|
47
|
+
) : null}
|
|
48
|
+
</ComposeListItem>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export * from './types';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Button, HStack, Spacer, Text, VStack } from '@expo/ui/swift-ui';
|
|
2
|
+
import { buttonStyle, contentShape, foregroundStyle, shapes } from '@expo/ui/swift-ui/modifiers';
|
|
3
|
+
import { Children, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import { extractListItemSlots } from './ListItemSlots';
|
|
6
|
+
import type { ListItemProps } from './types';
|
|
7
|
+
|
|
8
|
+
// Wrap raw strings/numbers in a SwiftUI `Text` — SwiftUI hosts can't render bare strings.
|
|
9
|
+
function wrapStrings(node: ReactNode): ReactNode {
|
|
10
|
+
if (node == null || typeof node === 'boolean') return node;
|
|
11
|
+
if (typeof node === 'string' || typeof node === 'number') return <Text>{node}</Text>;
|
|
12
|
+
if (Array.isArray(node)) return Children.map(node, wrapStrings);
|
|
13
|
+
return node;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function renderSupporting(node: ReactNode): ReactNode {
|
|
17
|
+
if (typeof node === 'string' || typeof node === 'number') {
|
|
18
|
+
return (
|
|
19
|
+
<Text modifiers={[foregroundStyle({ type: 'color', color: 'secondaryLabel' })]}>{node}</Text>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return node;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* iOS implementation of `ListItem`.
|
|
27
|
+
* Wraps a plain SwiftUI `Button` and applies `contentShape(.rectangle())` so the full row rectangle registers taps, including the gap between slots.
|
|
28
|
+
*/
|
|
29
|
+
export function ListItem(props: ListItemProps) {
|
|
30
|
+
const {
|
|
31
|
+
children,
|
|
32
|
+
onPress,
|
|
33
|
+
leading: leadingProp,
|
|
34
|
+
trailing: trailingProp,
|
|
35
|
+
supportingText,
|
|
36
|
+
testID,
|
|
37
|
+
} = props;
|
|
38
|
+
const slots = extractListItemSlots(children);
|
|
39
|
+
const leading = slots.leading ?? leadingProp;
|
|
40
|
+
const trailing = slots.trailing ?? trailingProp;
|
|
41
|
+
const supporting = slots.supporting ?? supportingText;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Button onPress={onPress} modifiers={[buttonStyle('plain')]} testID={testID}>
|
|
45
|
+
<HStack alignment="center" spacing={12} modifiers={[contentShape(shapes.rectangle())]}>
|
|
46
|
+
{wrapStrings(leading)}
|
|
47
|
+
<VStack alignment="leading" spacing={2}>
|
|
48
|
+
<>{wrapStrings(slots.headline)}</>
|
|
49
|
+
{supporting != null ? renderSupporting(supporting) : null}
|
|
50
|
+
</VStack>
|
|
51
|
+
<Spacer />
|
|
52
|
+
{wrapStrings(trailing)}
|
|
53
|
+
</HStack>
|
|
54
|
+
</Button>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export * from './types';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { extractListItemSlots } from './ListItemSlots';
|
|
5
|
+
import type { ListItemProps } from './types';
|
|
6
|
+
|
|
7
|
+
function renderSupporting(node: ReactNode): ReactNode {
|
|
8
|
+
if (typeof node === 'string') {
|
|
9
|
+
return <Text style={styles.supportingText}>{node}</Text>;
|
|
10
|
+
}
|
|
11
|
+
return node;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A tappable row in a list.
|
|
16
|
+
* Composes with [`List`](#list).
|
|
17
|
+
* Pass row content via the `leading` / `trailing` / `supportingText` shorthand props or the compound `<ListItem.Leading>` / `<ListItem.Trailing>` / `<ListItem.Supporting>` slot children.
|
|
18
|
+
*/
|
|
19
|
+
export function ListItem(props: ListItemProps) {
|
|
20
|
+
const {
|
|
21
|
+
children,
|
|
22
|
+
onPress,
|
|
23
|
+
leading: leadingProp,
|
|
24
|
+
trailing: trailingProp,
|
|
25
|
+
supportingText,
|
|
26
|
+
testID,
|
|
27
|
+
} = props;
|
|
28
|
+
const slots = extractListItemSlots(children);
|
|
29
|
+
const leading = slots.leading ?? leadingProp;
|
|
30
|
+
const trailing = slots.trailing ?? trailingProp;
|
|
31
|
+
const supporting = slots.supporting ?? supportingText;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Pressable onPress={onPress} style={styles.row} testID={testID}>
|
|
35
|
+
{leading != null ? <View style={styles.slot}>{leading}</View> : null}
|
|
36
|
+
<View style={styles.main}>
|
|
37
|
+
<Text>{slots.headline}</Text>
|
|
38
|
+
{supporting != null ? renderSupporting(supporting) : null}
|
|
39
|
+
</View>
|
|
40
|
+
{trailing != null ? <View style={styles.slot}>{trailing}</View> : null}
|
|
41
|
+
</Pressable>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const styles = StyleSheet.create({
|
|
46
|
+
row: {
|
|
47
|
+
flexDirection: 'row',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
gap: 12,
|
|
50
|
+
width: '100%',
|
|
51
|
+
paddingVertical: 12,
|
|
52
|
+
paddingHorizontal: 16,
|
|
53
|
+
cursor: 'pointer',
|
|
54
|
+
},
|
|
55
|
+
main: {
|
|
56
|
+
flexDirection: 'column',
|
|
57
|
+
gap: 2,
|
|
58
|
+
flex: 1,
|
|
59
|
+
minWidth: 0,
|
|
60
|
+
},
|
|
61
|
+
slot: {
|
|
62
|
+
flexDirection: 'row',
|
|
63
|
+
alignItems: 'center',
|
|
64
|
+
flexShrink: 0,
|
|
65
|
+
},
|
|
66
|
+
supportingText: {
|
|
67
|
+
fontSize: 13,
|
|
68
|
+
color: '#6b7280',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export * from './types';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Children, Fragment, isValidElement, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { ListItemLeadingProps, ListItemSupportingProps, ListItemTrailingProps } from './types';
|
|
4
|
+
|
|
5
|
+
/** Leading-slot marker for [`ListItem`](#listitem). */
|
|
6
|
+
export function Leading(props: ListItemLeadingProps) {
|
|
7
|
+
return props.children;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Trailing-slot marker for [`ListItem`](#listitem). */
|
|
11
|
+
export function Trailing(props: ListItemTrailingProps) {
|
|
12
|
+
return props.children;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Supporting-text-slot marker for [`ListItem`](#listitem), rendered below the headline. */
|
|
16
|
+
export function Supporting(props: ListItemSupportingProps) {
|
|
17
|
+
return props.children;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ExtractedListItemSlots = {
|
|
21
|
+
leading?: ReactNode;
|
|
22
|
+
trailing?: ReactNode;
|
|
23
|
+
supporting?: ReactNode;
|
|
24
|
+
headline: ReactNode[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Walks `children`, pulls out any `<ListItem.Leading>` /
|
|
29
|
+
* `<ListItem.Trailing>` / `<ListItem.Supporting>` slots, and returns the
|
|
30
|
+
* remaining nodes as the headline content. Recurses into `React.Fragment`.
|
|
31
|
+
*/
|
|
32
|
+
export function extractListItemSlots(children: ReactNode): ExtractedListItemSlots {
|
|
33
|
+
let leading: ReactNode | undefined;
|
|
34
|
+
let trailing: ReactNode | undefined;
|
|
35
|
+
let supporting: ReactNode | undefined;
|
|
36
|
+
const headline: ReactNode[] = [];
|
|
37
|
+
|
|
38
|
+
const walk = (node: ReactNode) => {
|
|
39
|
+
Children.forEach(node, (child) => {
|
|
40
|
+
if (!isValidElement(child)) {
|
|
41
|
+
headline.push(child);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (child.type === Leading) {
|
|
45
|
+
leading = (child.props as { children?: ReactNode }).children;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (child.type === Trailing) {
|
|
49
|
+
trailing = (child.props as { children?: ReactNode }).children;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (child.type === Supporting) {
|
|
53
|
+
supporting = (child.props as { children?: ReactNode }).children;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (child.type === Fragment) {
|
|
57
|
+
walk((child.props as { children?: ReactNode }).children);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
headline.push(child);
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
walk(children);
|
|
65
|
+
return { leading, trailing, supporting, headline };
|
|
66
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ListItem as ListItemBase } from './ListItem';
|
|
2
|
+
import { Leading, Supporting, Trailing } from './ListItemSlots';
|
|
3
|
+
import type { ListItemLeadingProps, ListItemSupportingProps, ListItemTrailingProps } from './types';
|
|
4
|
+
|
|
5
|
+
const ListItem = ListItemBase as typeof ListItemBase & {
|
|
6
|
+
Leading: React.FC<ListItemLeadingProps>;
|
|
7
|
+
Trailing: React.FC<ListItemTrailingProps>;
|
|
8
|
+
Supporting: React.FC<ListItemSupportingProps>;
|
|
9
|
+
};
|
|
10
|
+
ListItem.Leading = Leading;
|
|
11
|
+
ListItem.Trailing = Trailing;
|
|
12
|
+
ListItem.Supporting = Supporting;
|
|
13
|
+
|
|
14
|
+
export { ListItem };
|
|
15
|
+
export * from './types';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Props for the [`ListItem.Leading`](#listitemleading) slot marker.
|
|
5
|
+
*/
|
|
6
|
+
export interface ListItemLeadingProps {
|
|
7
|
+
/** Content rendered in the leading (start) slot. */
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Props for the [`ListItem.Trailing`](#listitemtrailing) slot marker.
|
|
13
|
+
*/
|
|
14
|
+
export interface ListItemTrailingProps {
|
|
15
|
+
/** Content rendered in the trailing (end) slot. */
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Props for the [`ListItem.Supporting`](#listitemsupporting) slot marker.
|
|
21
|
+
*/
|
|
22
|
+
export interface ListItemSupportingProps {
|
|
23
|
+
/** Content rendered below the headline. */
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Props for the [`ListItem`](#listitem) component.
|
|
29
|
+
* A tappable row in a list.
|
|
30
|
+
*/
|
|
31
|
+
export interface ListItemProps {
|
|
32
|
+
/**
|
|
33
|
+
* Headline content of the row.
|
|
34
|
+
* The remaining (non-slot) children are rendered in the headline area.
|
|
35
|
+
*/
|
|
36
|
+
children?: ReactNode;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Tap handler.
|
|
40
|
+
* Activates over the entire row rectangle, including the empty gap between leading/headline/trailing.
|
|
41
|
+
*/
|
|
42
|
+
onPress?: () => void;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Shorthand for the leading slot.
|
|
46
|
+
* Overridden by `<ListItem.Leading>` if both are provided.
|
|
47
|
+
*/
|
|
48
|
+
leading?: ReactNode;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Shorthand for the trailing slot.
|
|
52
|
+
* Overridden by `<ListItem.Trailing>` if both are provided.
|
|
53
|
+
*/
|
|
54
|
+
trailing?: ReactNode;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Shorthand for the supporting (sub-)text slot.
|
|
58
|
+
* Strings are rendered with platform-appropriate secondary styling; pass a `ReactNode` for richer content.
|
|
59
|
+
* Overridden by `<ListItem.Supporting>` if both are provided.
|
|
60
|
+
*/
|
|
61
|
+
supportingText?: string | ReactNode;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Identifier used to locate the component in end-to-end tests.
|
|
65
|
+
*/
|
|
66
|
+
testID?: string;
|
|
67
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { extractPickerItems } from './PickerItem';
|
|
4
|
+
import type { PickerItemValue, PickerProps } from './types';
|
|
5
|
+
import { DropdownMenuItem } from '../../jetpack-compose/DropdownMenu/DropdownMenuItem';
|
|
6
|
+
import {
|
|
7
|
+
ExposedDropdownMenuBox,
|
|
8
|
+
ExposedDropdownMenu,
|
|
9
|
+
} from '../../jetpack-compose/ExposedDropdownMenuBox';
|
|
10
|
+
import { Text } from '../../jetpack-compose/Text';
|
|
11
|
+
import { TextField, type TextFieldRef } from '../../jetpack-compose/TextField';
|
|
12
|
+
import { menuAnchor, onVisibilityChanged } from '../../jetpack-compose/modifiers';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Android implementation of `Picker`.
|
|
16
|
+
* Renders a Material 3 `ExposedDropdownMenuBox`.
|
|
17
|
+
* `appearance` is accepted for API parity but ignored — Material 3 has no wheel-style picker.
|
|
18
|
+
*/
|
|
19
|
+
export function Picker<T extends PickerItemValue>({
|
|
20
|
+
selectedValue,
|
|
21
|
+
onValueChange,
|
|
22
|
+
enabled = true,
|
|
23
|
+
children,
|
|
24
|
+
}: PickerProps<T>) {
|
|
25
|
+
const items = extractPickerItems<T>(children);
|
|
26
|
+
const [expanded, setExpanded] = useState(false);
|
|
27
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
28
|
+
const textFieldRef = useRef<TextFieldRef>(null);
|
|
29
|
+
|
|
30
|
+
const selectedLabel = items.find((item) => item.value === selectedValue)?.label ?? '';
|
|
31
|
+
// The anchor `TextField` is uncontrolled — push the current label imperatively once the view is on screen.
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!isVisible) return;
|
|
34
|
+
textFieldRef.current?.setText(selectedLabel);
|
|
35
|
+
}, [selectedLabel, isVisible]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<ExposedDropdownMenuBox
|
|
39
|
+
expanded={expanded}
|
|
40
|
+
onExpandedChange={enabled ? setExpanded : undefined}>
|
|
41
|
+
<TextField
|
|
42
|
+
ref={textFieldRef}
|
|
43
|
+
readOnly
|
|
44
|
+
enabled={enabled}
|
|
45
|
+
modifiers={[menuAnchor(), onVisibilityChanged((visible) => setIsVisible(visible))]}
|
|
46
|
+
/>
|
|
47
|
+
<ExposedDropdownMenu expanded={expanded} onDismissRequest={() => setExpanded(false)}>
|
|
48
|
+
{items.map((item) => (
|
|
49
|
+
<DropdownMenuItem
|
|
50
|
+
key={String(item.value)}
|
|
51
|
+
onClick={
|
|
52
|
+
enabled
|
|
53
|
+
? () => {
|
|
54
|
+
onValueChange(item.value);
|
|
55
|
+
setExpanded(false);
|
|
56
|
+
}
|
|
57
|
+
: undefined
|
|
58
|
+
}>
|
|
59
|
+
<DropdownMenuItem.Text>
|
|
60
|
+
<Text>{item.label}</Text>
|
|
61
|
+
</DropdownMenuItem.Text>
|
|
62
|
+
</DropdownMenuItem>
|
|
63
|
+
))}
|
|
64
|
+
</ExposedDropdownMenu>
|
|
65
|
+
</ExposedDropdownMenuBox>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export * from './types';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Picker as SwiftUIPicker, Text } from '@expo/ui/swift-ui';
|
|
2
|
+
import {
|
|
3
|
+
disabled as disabledModifier,
|
|
4
|
+
pickerStyle,
|
|
5
|
+
tag,
|
|
6
|
+
type ModifierConfig,
|
|
7
|
+
} from '@expo/ui/swift-ui/modifiers';
|
|
8
|
+
|
|
9
|
+
import { extractPickerItems } from './PickerItem';
|
|
10
|
+
import type { PickerItemValue, PickerProps } from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* iOS implementation of `Picker`.
|
|
14
|
+
* Wraps SwiftUI's `Picker` and applies the matching `.pickerStyle` for the requested `appearance`.
|
|
15
|
+
* Embed inside a parent `<Host>` (same as `Column` / `Row`).
|
|
16
|
+
*/
|
|
17
|
+
export function Picker<T extends PickerItemValue>({
|
|
18
|
+
selectedValue,
|
|
19
|
+
onValueChange,
|
|
20
|
+
appearance = 'menu',
|
|
21
|
+
enabled = true,
|
|
22
|
+
children,
|
|
23
|
+
testID,
|
|
24
|
+
}: PickerProps<T>) {
|
|
25
|
+
const items = extractPickerItems<T>(children);
|
|
26
|
+
const swiftUIStyle = appearance === 'wheel' ? 'wheel' : 'menu';
|
|
27
|
+
const modifiers: ModifierConfig[] = [pickerStyle(swiftUIStyle)];
|
|
28
|
+
if (!enabled) modifiers.push(disabledModifier(true));
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<SwiftUIPicker
|
|
32
|
+
selection={selectedValue}
|
|
33
|
+
onSelectionChange={(value) => onValueChange(value as T)}
|
|
34
|
+
modifiers={modifiers}
|
|
35
|
+
testID={testID}>
|
|
36
|
+
{items.map((item) => (
|
|
37
|
+
<Text key={String(item.value)} modifiers={[tag(item.value)]}>
|
|
38
|
+
{item.label}
|
|
39
|
+
</Text>
|
|
40
|
+
))}
|
|
41
|
+
</SwiftUIPicker>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export * from './types';
|