Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 40 additions & 48 deletions apps/native-component-list/src/screens/UI/CommunityPickerScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { Picker, type PickerProps, type PickerRef } from '@expo/ui/community/picker';
import React, { useRef, useState } from 'react';
import { Button, Text } from 'react-native';
import { Button, Platform, Text } from 'react-native';

import { ScrollPage, Section } from '../../components/Page';

const monospace = Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' });
const serif = Platform.select({ ios: 'Georgia', android: 'serif', default: 'serif' });
const sansSerif = Platform.select({
ios: 'Helvetica',
android: 'sansSerif',
default: 'sansSerif',
});
const cursive = Platform.select({ ios: 'Snell Roundhand', android: 'cursive', default: 'cursive' });

export default function CommunityPickerScreen() {
return (
<ScrollPage>
Expand All @@ -15,16 +24,8 @@ export default function CommunityPickerScreen() {
<GenericPicker style={{ backgroundColor: '#e0e7ff', borderRadius: 12 }} />
</Section>

<Section title="Item color (iOS)">
<ColoredPicker />
</Section>

<Section title="Item fontFamily (iOS)">
<FontFamilyPicker />
</Section>

<Section title="Item enabled (Android)">
<ItemEnabledPicker />
<Section title="Per-item styling and state">
<StyledPicker />
</Section>

<Section title="Imperative focus and blur (Android)">
Expand All @@ -42,48 +43,39 @@ CommunityPickerScreen.navigationOptions = {
title: 'Community Picker',
};

function ColoredPicker() {
function StyledPicker() {
const [value, setValue] = useState<string>('java');

return (
<>
<Picker selectedValue={value} onValueChange={(itemValue) => setValue(itemValue)}>
<Picker.Item label="Java" value="java" color="#e11d48" />
<Picker.Item label="JavaScript" value="js" color="#2563eb" />
<Picker.Item label="Objective C" value="objc" color="#059669" />
<Picker.Item label="Swift" value="swift" color="#d97706" />
</Picker>
<Text>Selected: {value}</Text>
</>
);
}

function FontFamilyPicker() {
const [value, setValue] = useState<string>('java');

return (
<>
<Picker selectedValue={value} onValueChange={(itemValue) => setValue(itemValue)}>
<Picker.Item label="Java" value="java" fontFamily="Courier New" />
<Picker.Item label="JavaScript" value="js" fontFamily="Georgia" />
<Picker.Item label="Objective C" value="objc" fontFamily="Helvetica" />
<Picker.Item label="Swift" value="swift" fontFamily="Menlo" />
</Picker>
<Text>Selected: {value}</Text>
</>
);
}

function ItemEnabledPicker() {
const [value, setValue] = useState<string>('java');

return (
<>
<Picker selectedValue={value} onValueChange={(itemValue) => setValue(itemValue)}>
<Picker.Item label="Java" value="java" />
<Picker.Item label="JavaScript" value="js" enabled={false} />
<Picker.Item label="Objective C" value="objc" />
<Picker.Item label="Swift" value="swift" enabled={false} />
<Picker.Item
label="Java"
value="java"
style={{
color: '#e11d48',
fontFamily: monospace,
fontSize: 14,
backgroundColor: 'black',
}}
/>
<Picker.Item
label="JavaScript"
value="js"
style={{ color: '#2563eb', fontFamily: serif, fontSize: 33 }}
enabled={false}
/>
<Picker.Item
label="Objective C"
value="objc"
style={{ color: '#059669', fontFamily: sansSerif, fontSize: 16 }}
/>
<Picker.Item
label="Swift"
value="swift"
style={{ color: '#d97706', fontFamily: cursive, fontSize: 30 }}
enabled={false}
/>
</Picker>
<Text>Selected: {value}</Text>
</>
Expand Down
28 changes: 12 additions & 16 deletions docs/pages/develop/user-interface/system-bars.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ System bars are fundamental to the mobile experience, and understanding how to w
<ContentSpotlight
alt="System bars and navigation bars on Android and iOS."
src="/static/images/system-bars.png"
className="max-w-[480px]"
className="max-w-120"
/>

## Handling overlaps using safe areas
Expand Down Expand Up @@ -62,25 +62,21 @@ export default function RootLayout() {

To control the `StatusBar` visibility, you can set the [`hidden`](/versions/latest/sdk/status-bar/#hidden) property to `true` or use the [`setStatusBarHidden`](/versions/latest/sdk/status-bar/#statusbarsetstatusbarhiddenhidden-animation) method.

**With edge-to-edge enabled on Android, features from `expo-status-bar` that depend on an opaque status bar [are unavailable](https://developer.android.com/about/versions/15/behavior-changes-15#edge-to-edge)**. It's only possible to customize the style and visibility. Other properties will no-op and warn.

### Navigation bar configuration (Android only)

On Android devices, the Navigation Bar appears at the bottom of the screen. You can customize it using the [`expo-navigation-bar`](/versions/latest/sdk/navigation-bar) library. It provides a `NavigationBar` component that you can use to set the style of the navigation bar using the [`setStyle`](/versions/latest/sdk/navigation-bar/#navigationbarsetstylestyle) method:

```tsx src/app/_layout.tsx
import { Platform } from 'react-native';
import * as NavigationBar from 'expo-navigation-bar';
import { useEffect } from 'react';

useEffect(() => {
if (Platform.OS === 'android') {
// Set the navigation bar style
NavigationBar.setStyle('dark');
}
}, []);
```
import { NavigationBar } from 'expo-navigation-bar';

To control the `NavigationBar` visibility, you can use the [`setVisibilityAsync`](/versions/latest/sdk/navigation-bar/#navigationbarsetvisibilityasyncvisibility) method.
export default function RootLayout() {
return (
<>
{/* Set the navigation bar style */}
<NavigationBar style="dark" />
</>
);
}
```

**With edge-to-edge enabled on Android, features from `expo-navigation-bar` that depend on an opaque navigation bar [are unavailable](https://developer.android.com/about/versions/15/behavior-changes-15#edge-to-edge)**. It's only possible to customize the style and visibility. Other properties will no-op and warn.
To control the `NavigationBar` visibility, you can use the [`hidden`](/versions/unversioned/sdk/navigation-bar/#hidden) prop or the [`NavigationBar.setHidden`](/versions/unversioned/sdk/navigation-bar/#navigationbarsethiddenhidden) method.
6 changes: 4 additions & 2 deletions docs/pages/eas/observe/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ ExpoObserve.configure({
});
```

A build is treated as a debug build if either the native app is a debug build or the JS bundle is a development bundle (`__DEV__` is `true`). This detection is independent of the `environment` value (see [Environments](#environments)).

`dispatchInDebug` has no effect on release builds, which always dispatch (subject to [`dispatchingEnabled`](#configure) and [`sampleRate`](#sampling)). If `dispatchingEnabled` is `false` or this installation is out-of-sample, nothing dispatches regardless of `dispatchInDebug`.

> **warning** Enable this only when testing your Expo Observe integration. Development/debug performance differs significantly from production, so collecting development/debug metrics may distort the results shown in your dashboard.
Expand All @@ -97,6 +99,6 @@ The endpoint URL is baked into the native layer of the app at build time, so cha

## Environments

All metrics are grouped by environment. The environment value is derived from `process.env.NODE_ENV` by default. To override it, use [`configure({ environment })`](#configure).
All metrics are grouped by environment. The environment value is derived from `process.env.NODE_ENV` by default (falling back to `'production'` if unset). To override it, use [`configure({ environment })`](#configure).

Metrics collected from debug builds are dropped before dispatching unless [`dispatchInDebug` is set to `true`](#enable-metrics-in-development) via `configure()`. You can also disable all dispatching globally using `configure({ dispatchingEnabled: false })`.
The environment is a metadata tag attached to each metric and is independent of how the bundle was built. To control whether debug-build metrics are dispatched, see [Enable metrics in development](#enable-metrics-in-development). To disable all dispatching globally, use `configure({ dispatchingEnabled: false })`.
10 changes: 9 additions & 1 deletion docs/pages/eas/observe/reference/metrics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,12 @@ By default, all installations dispatch their metrics. To dispatch from a fractio

### Environment

All metrics are grouped by environment. The environment value is derived from `process.env.NODE_ENV` by default, or can be overridden via [`configure({ environment })`](/eas/observe/configuration/#configure). Metrics collected from debug builds are dropped before dispatching unless `dispatchInDebug` is set to `true` via [`configure()`](/eas/observe/configuration/#configure) (for information, see [Enable metrics in development](/eas/observe/configuration/#enable-metrics-in-development)). You can also disable all dispatching globally using `configure({ dispatchingEnabled: false })`.
All metrics are grouped by environment. The environment value is derived from `process.env.NODE_ENV` by default (falling back to `'production'` if unset), or can be overridden via [`configure({ environment })`](/eas/observe/configuration/#configure). The environment is a metadata tag attached to each metric and does not affect whether metrics are dispatched.

### Debug builds

Metrics collected from debug builds are dropped before dispatching unless `dispatchInDebug` is set to `true` via [`configure()`](/eas/observe/configuration/#configure) (for information, see [Enable metrics in development](/eas/observe/configuration/#enable-metrics-in-development)). A build is treated as a debug build if either the native app is a debug build or the JS bundle is a development bundle (`__DEV__` is `true`). This detection is independent of the `environment` value.

### Disabling dispatching

You can disable all dispatching globally using `configure({ dispatchingEnabled: false })`. While disabled, any pending metrics are dropped without being dispatched and no further metrics are dispatched until it is set back to `true`.
27 changes: 23 additions & 4 deletions docs/pages/router/advanced/drawer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,39 @@ A navigation drawer is a common pattern in mobile apps, it allows users to swipe

## Installation

In **SDK 56 and later**, the drawer navigator is bundled in `expo-router` and uses [`react-native-drawer-layout`](https://www.npmjs.com/package/react-native-drawer-layout) under the hood.

On Android and iOS, the drawer requires `react-native-reanimated` and `react-native-worklets` to drive its animations. On web, animation is handled by CSS.

<Tabs>
<Tab label="SDK 54 and later">

To use [drawer navigator](https://reactnavigation.org/docs/drawer-navigator) you'll need to install some additional dependencies if you do not have them already. On Android and iOS, the drawer navigator requires `react-native-reanimated` and `react-native-worklets` to drive its animations. On web, this is handled by CSS animations.
<Tab label="SDK 56 and later">

To use the drawer navigator, install these dependencies if you do not have them already:

<Terminal
cmd={[
'$ npx expo install react-native-reanimated react-native-worklets react-native-gesture-handler',
]}
/>

</Tab>

<Tab label="SDK 54 and 55">

To use [drawer navigator](https://reactnavigation.org/docs/drawer-navigator), install these dependencies if you do not have them already:

<Terminal
cmd={[
'$ npx expo install @react-navigation/drawer react-native-reanimated react-native-worklets',
'$ npx expo install @react-navigation/drawer react-native-reanimated react-native-worklets react-native-gesture-handler',
]}
/>

</Tab>

<Tab label="SDK 53 and earlier">

To use [drawer navigator](https://reactnavigation.org/docs/drawer-navigator) you'll need to install some additional dependencies if you do not have them already. On Android and iOS, the drawer navigator requires `react-native-reanimated` and `react-native-gesture-handler` to drive its animations. On web, this is handled by CSS animations.
To use [drawer navigator](https://reactnavigation.org/docs/drawer-navigator), install these dependencies if you do not have them already:

<Terminal
cmd={[
Expand All @@ -39,6 +57,7 @@ To use [drawer navigator](https://reactnavigation.org/docs/drawer-navigator) you
/>

</Tab>

</Tabs>

## Usage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ The following components provide API-compatible replacements for popular React N

- **[DateTimePicker](datetimepicker)**: Compatible with `@react-native-community/datetimepicker`
- **[BottomSheet](bottomsheet)**: Compatible with `@gorhom/bottom-sheet`
- **[Picker](picker)**: Compatible with `@react-native-picker/picker`
- **[SegmentedControl](segmentedcontrol)**: Compatible with `@react-native-segmented-control/segmented-control`
142 changes: 142 additions & 0 deletions docs/pages/versions/unversioned/sdk/ui/drop-in-replacements/picker.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
title: Picker
description: A picker compatible with @react-native-picker/picker.
sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-ui'
packageName: '@expo/ui'
platforms: ['android', 'ios', 'web', 'expo-go']
---

import APISection from '~/components/plugins/APISection';
import { APIInstallSection } from '~/components/plugins/InstallSection';

A `Picker` component with an API compatible with `@react-native-picker/picker`. It uses a SwiftUI wheel `Picker` on iOS, a Material 3 `ExposedDropdownMenuBox` on Android, and a native `<select>` element on web.

Under the hood this component wraps the platform-specific `@expo/ui` primitives:

- **Android**: [Jetpack Compose ExposedDropdownMenuBox](../jetpack-compose/exposeddropdownmenubox)
- **iOS**: [SwiftUI Picker](../swift-ui/picker) with `pickerStyle('wheel')`

If you need lower-level control, use those primitives directly.

## Installation

<APIInstallSection />

## Migrating from `@react-native-picker/picker`

- Update the import from `import { Picker } from '@react-native-picker/picker'` to `import { Picker } from '@expo/ui/community/picker'`.
- `mode`, `prompt`, `dropdownIconColor`, `dropdownIconRippleColor`, `numberOfLines`, `selectionColor`, `itemStyle`, and `accessibilityLabel` props are not supported.
- On `Picker.Item`, the `style` prop only applies `color`, `backgroundColor`, `fontFamily`, and `fontSize`. The top-level `color` and `fontFamily` props are still supported as aliases for the corresponding `style` values.
- `enabled` on `Picker.Item` only applies on Android.
- The `ref` `focus()` and `blur()` methods only have an effect on Android (open/close the dropdown). On iOS, the wheel picker is always visible.

## Basic usage

```tsx PickerExample.tsx
import { useState } from 'react';
import { Text, View } from 'react-native';
import { Picker } from '@expo/ui/community/picker';

export default function PickerExample() {
const [language, setLanguage] = useState('java');

return (
<View>
<Picker selectedValue={language} onValueChange={value => setLanguage(value)}>
<Picker.Item label="Java" value="java" />
<Picker.Item label="JavaScript" value="js" />
<Picker.Item label="Objective C" value="objc" />
<Picker.Item label="Swift" value="swift" />
</Picker>
<Text>Selected: {language}</Text>
</View>
);
}
```

## Per-item styling and state

Pass a `style` to `Picker.Item` to control `color`, `backgroundColor`, `fontFamily`, and `fontSize` per item, and `enabled={false}` to disable specific items on Android.

`fontFamily` accepts iOS font names (for example, `'Menlo'`) on iOS, and Compose generic families (`'monospace'`, `'serif'`, `'sansSerif'`, `'cursive'`) or fonts loaded with [`expo-font`](../../font) on Android.

```tsx StyledPickerExample.tsx
import { useState } from 'react';
import { Platform } from 'react-native';
import { Picker } from '@expo/ui/community/picker';

const monospace = Platform.select({ ios: 'Menlo', android: 'monospace' });
const serif = Platform.select({ ios: 'Georgia', android: 'serif' });

export default function StyledPickerExample() {
const [language, setLanguage] = useState('java');

return (
<Picker selectedValue={language} onValueChange={value => setLanguage(value)}>
<Picker.Item
label="Java"
value="java"
style={{ color: '#e11d48', fontFamily: monospace, fontSize: 14 }}
/>
<Picker.Item
label="JavaScript"
value="js"
style={{ color: '#2563eb', fontFamily: serif, fontSize: 18 }}
enabled={false}
/>
<Picker.Item
label="Objective C"
value="objc"
style={{ color: '#059669', fontFamily: monospace, fontSize: 16 }}
/>
<Picker.Item
label="Swift"
value="swift"
style={{ color: '#d97706', fontFamily: serif, fontSize: 30 }}
enabled={false}
/>
</Picker>
);
}
```

## Imperative focus and blur (Android)

Use a ref to programmatically open and close the dropdown on Android. On iOS, these methods are no-ops because the wheel picker is always visible.

```tsx RefPickerExample.tsx
import { useRef, useState } from 'react';
import { Button } from 'react-native';
import { Picker, type PickerRef } from '@expo/ui/community/picker';

export default function RefPickerExample() {
const [language, setLanguage] = useState('java');
const pickerRef = useRef<PickerRef>(null);

return (
<>
<Button
title="Open and close after 2s"
onPress={() => {
pickerRef.current?.focus();
setTimeout(() => pickerRef.current?.blur(), 2000);
}}
/>
<Picker ref={pickerRef} selectedValue={language} onValueChange={setLanguage}>
<Picker.Item label="Java" value="java" />
<Picker.Item label="JavaScript" value="js" />
<Picker.Item label="Objective C" value="objc" />
<Picker.Item label="Swift" value="swift" />
</Picker>
</>
);
}
```

## API

```tsx
import { Picker } from '@expo/ui/community/picker';
```

<APISection packageName="expo-ui/community/picker" apiName="Picker" />
Loading
Loading