diff --git a/apps/common/package.json b/apps/common/package.json index 9cb00b0293de02..815273bf5c8e36 100644 --- a/apps/common/package.json +++ b/apps/common/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@expo/styleguide-base": "^1.0.1", + "@expo/vector-icons": "^15.0.2", "react-native": "0.85.3", "react": "19.2.3" }, diff --git a/apps/notification-tester/package.json b/apps/notification-tester/package.json index 096524a4060f25..81044000d4ab4b 100644 --- a/apps/notification-tester/package.json +++ b/apps/notification-tester/package.json @@ -22,6 +22,7 @@ "@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/native": "^7.1.33", "@expo/ui": "workspace:*", + "@expo/vector-icons": "^15.0.2", "expo": "workspace:*", "expo-font": "workspace:*", "expo-linking": "workspace:*", diff --git a/docs/components/plugins/api/APIStaticData.ts b/docs/components/plugins/api/APIStaticData.ts index 3a742331af4385..96fe054453cb90 100644 --- a/docs/components/plugins/api/APIStaticData.ts +++ b/docs/components/plugins/api/APIStaticData.ts @@ -241,6 +241,7 @@ export const hardcodedTypeLinks: Record = { IterableIterator: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator', KeepAwakeListener: '/versions/latest/sdk/keep-awake/#keepawakelistenerevent', + KeyboardTypeOptions: 'https://reactnative.dev/docs/textinput#keyboardtype', LocationCallback: '/versions/latest/sdk/location/#locationcallbacklocation', LocationErrorCallback: '/versions/latest/sdk/location/#locationerrorcallbackreason', LocationHeadingCallback: '/versions/latest/sdk/location/#locationheadingcallbacklocation', @@ -273,6 +274,7 @@ export const hardcodedTypeLinks: Record = { RefObject: 'https://react.dev/reference/react/useRef', RegExp: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp', Required: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#requiredtype', + ReturnKeyTypeOptions: 'https://reactnative.dev/docs/textinput#returnkeytype', Response: 'https://developer.mozilla.org/en-US/docs/Web/API/Response', RootParamList: 'https://reactnavigation.org/docs/typescript/#navigator-specific-types', RouteProp: 'https://reactnavigation.org/docs/glossary-of-terms/#route-object', diff --git a/docs/pages/eas/workflows/pre-packaged-jobs.mdx b/docs/pages/eas/workflows/pre-packaged-jobs.mdx index 0ba9a811b003dc..0f46b3af22dce8 100644 --- a/docs/pages/eas/workflows/pre-packaged-jobs.mdx +++ b/docs/pages/eas/workflows/pre-packaged-jobs.mdx @@ -856,7 +856,7 @@ jobs: flow_path: string | string[] # required shards: number # optional - defaults to 1 retries: number # optional - defaults to 0 - smart_retry: boolean # optional - defaults to true + retry_failed_only: boolean # optional - defaults to true record_screen: boolean # optional - defaults to false include_tags: string | string[] # optional exclude_tags: string | string[] # optional @@ -876,7 +876,7 @@ You can pass the following parameters into the `params` list: | flow_path | string or string[] | Required. The path to the Maestro flow file(s) or directory to run. | | shards | number | Optional and experimental. The number of shards to split the tests into. Defaults to 1. | | retries | number | Optional. The number of times to retry the tests if they fail. Defaults to 0. | -| smart_retry | boolean | Optional. When true (default), retries will attempt to re-run only the flows that failed on the previous attempt when applicable. Set to false to re-run all flows on every retry. | +| retry_failed_only | boolean | Optional. When true (default), retries will attempt to re-run only the flows that failed on the previous attempt when applicable. Set to false to re-run all flows on every retry. | | record_screen | boolean | Optional. Whether to record the screen. Defaults to false. Note: recording screen may impact emulator performance. You may want to use large runners when recording screen. | | include_tags | string or string[] | Optional. Flow tags to include in the tests. Will be passed to Maestro as `--include-tags`. | | exclude_tags | string or string[] | Optional. Flow tags to exclude from the tests. Will be passed to Maestro as `--exclude-tags`. | diff --git a/docs/pages/eas/workflows/syntax.mdx b/docs/pages/eas/workflows/syntax.mdx index 88a330a7f16a40..73a3b29b4f8cfb 100644 --- a/docs/pages/eas/workflows/syntax.mdx +++ b/docs/pages/eas/workflows/syntax.mdx @@ -1240,7 +1240,7 @@ jobs: flow_path: string | string[] # required shards: number # optional, defaults to 1 retries: number # optional, defaults to 0 - smart_retry: boolean # optional, defaults to true. When true, retries will attempt to re-run only the flows that failed on the previous attempt when applicable. + retry_failed_only: boolean # optional, defaults to true. When true, retries will attempt to re-run only the flows that failed on the previous attempt when applicable. record_screen: boolean # optional, defaults to false. If true, uploads a screen recording of the tests. include_tags: string | string[] # optional. Tags to include in the tests. Will be passed to Maestro as `--include-tags`. exclude_tags: string | string[] # optional. Tags to exclude from the tests. Will be passed to Maestro as `--exclude-tags`. diff --git a/docs/pages/versions/unversioned/sdk/router/index.mdx b/docs/pages/versions/unversioned/sdk/router/index.mdx index bf74aa4f135093..722a7d8a535d6e 100644 --- a/docs/pages/versions/unversioned/sdk/router/index.mdx +++ b/docs/pages/versions/unversioned/sdk/router/index.mdx @@ -23,6 +23,8 @@ import { ConfigPluginExample, ConfigPluginProperties } from '~/ui/components/Con Icon={BookOpen02Icon} /> +> **Important** In **SDK 56 and later**, Expo Router no longer supports importing from external `@react-navigation/*` packages in application code. Repoint those imports to the matching `expo-router` entry points. Run the [codemod](/router/migrate/sdk-55-to-56#automated-migration) or follow the [SDK 55 to 56 migration guide](/router/migrate/sdk-55-to-56) to update your project. + ## Installation To use Expo Router in your project, you need to install. Follow the instructions from the Expo Router's installation guide: diff --git a/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/textfield.mdx b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/textfield.mdx index 42e834179ea073..8f38d10866dfbb 100644 --- a/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/textfield.mdx +++ b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/textfield.mdx @@ -260,11 +260,28 @@ When `onValueChange` is marked with the `'worklet'` directive, it runs synchrono ```tsx WorkletPhoneMaskExample.tsx import { Host, TextField, Text, useNativeState } from '@expo/ui/jetpack-compose'; import { fillMaxWidth } from '@expo/ui/jetpack-compose/modifiers'; +import { useEffectEvent } from 'react'; export default function WorkletPhoneMaskExample() { const phone = useNativeState(''); const selection = useNativeState({ start: 0, end: 0 }); + const handleValueChange = useEffectEvent((v: string) => { + 'worklet'; + const digits = v.replace(/\D/g, '').slice(0, 10); + let formatted = digits; + if (digits.length > 6) { + formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; + } else if (digits.length > 3) { + formatted = `(${digits.slice(0, 3)}) ${digits.slice(3)}`; + } + if (formatted !== v) { + phone.value = formatted; + // Snaps to end for demo. Real masks need smarter cursor handling. + selection.value = { start: formatted.length, end: formatted.length }; + } + }); + return ( { - 'worklet'; - const digits = v.replace(/\D/g, '').slice(0, 10); - let formatted = digits; - if (digits.length > 6) { - formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; - } else if (digits.length > 3) { - formatted = `(${digits.slice(0, 3)}) ${digits.slice(3)}`; - } - if (formatted !== v) { - phone.value = formatted; - selection.value = { start: formatted.length, end: formatted.length }; - } - }}> + onValueChange={handleValueChange}> (555) 123-4567 diff --git a/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/usenativestate.mdx b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/usenativestate.mdx index 70767a69360fc0..aa9a1217fa4a7c 100644 --- a/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/usenativestate.mdx +++ b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/usenativestate.mdx @@ -24,11 +24,32 @@ The example below masks a phone number as the user types. The formatting and the ```tsx WorkletPhoneMaskExample.tsx import { Host, TextField, Text as ComposeText, useNativeState } from '@expo/ui/jetpack-compose'; import { fillMaxWidth } from '@expo/ui/jetpack-compose/modifiers'; +import { useEffectEvent } from 'react'; export default function WorkletPhoneMaskExample() { const maskedPhone = useNativeState(''); const selection = useNativeState({ start: 0, end: 0 }); + const handleValueChange = useEffectEvent((v: string) => { + 'worklet'; + const digits = v.replace(/\D/g, '').slice(0, 10); + let formatted: string; + if (digits.length === 0) { + formatted = ''; + } else if (digits.length <= 3) { + formatted = digits; + } else if (digits.length <= 6) { + formatted = `(${digits.slice(0, 3)}) ${digits.slice(3)}`; + } else { + formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; + } + if (formatted !== v) { + maskedPhone.value = formatted; + // Snaps to end for demo. Real masks need smarter cursor handling. + selection.value = { start: formatted.length, end: formatted.length }; + } + }); + return ( { - 'worklet'; - const digits = v.replace(/\D/g, '').slice(0, 10); - let formatted: string; - if (digits.length === 0) { - formatted = ''; - } else if (digits.length <= 3) { - formatted = digits; - } else if (digits.length <= 6) { - formatted = `(${digits.slice(0, 3)}) ${digits.slice(3)}`; - } else { - formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; - } - if (formatted !== v) { - maskedPhone.value = formatted; - selection.value = { start: formatted.length, end: formatted.length }; - } - }}> + onValueChange={handleValueChange}> (555) 123-4567 diff --git a/docs/pages/versions/unversioned/sdk/ui/swift-ui/textfield.mdx b/docs/pages/versions/unversioned/sdk/ui/swift-ui/textfield.mdx index 7dd6e92b4721fb..fadb32028a87da 100644 --- a/docs/pages/versions/unversioned/sdk/ui/swift-ui/textfield.mdx +++ b/docs/pages/versions/unversioned/sdk/ui/swift-ui/textfield.mdx @@ -184,11 +184,28 @@ When `onTextChange` is marked with the `'worklet'` directive, it runs synchronou ```tsx WorkletPhoneMaskExample.tsx import { Host, TextField, useNativeState } from '@expo/ui/swift-ui'; import { keyboardType } from '@expo/ui/swift-ui/modifiers'; +import { useEffectEvent } from 'react'; export default function WorkletPhoneMaskExample() { const phone = useNativeState(''); const selection = useNativeState({ start: 0, end: 0 }); + const handleTextChange = useEffectEvent((v: string) => { + 'worklet'; + const digits = v.replace(/\D/g, '').slice(0, 10); + let formatted = digits; + if (digits.length > 6) { + formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; + } else if (digits.length > 3) { + formatted = `(${digits.slice(0, 3)}) ${digits.slice(3)}`; + } + if (formatted !== v) { + phone.value = formatted; + // Snaps to end for demo. Real masks need smarter cursor handling. + selection.value = { start: formatted.length, end: formatted.length }; + } + }); + return ( { - 'worklet'; - const digits = v.replace(/\D/g, '').slice(0, 10); - let formatted = digits; - if (digits.length > 6) { - formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; - } else if (digits.length > 3) { - formatted = `(${digits.slice(0, 3)}) ${digits.slice(3)}`; - } - if (formatted !== v) { - phone.value = formatted; - selection.value = { start: formatted.length, end: formatted.length }; - } - }} + onTextChange={handleTextChange} /> ); diff --git a/docs/pages/versions/unversioned/sdk/ui/swift-ui/usenativestate.mdx b/docs/pages/versions/unversioned/sdk/ui/swift-ui/usenativestate.mdx index b3d13813f6f9bb..2f9fe889bb0ec8 100644 --- a/docs/pages/versions/unversioned/sdk/ui/swift-ui/usenativestate.mdx +++ b/docs/pages/versions/unversioned/sdk/ui/swift-ui/usenativestate.mdx @@ -19,47 +19,45 @@ import { APIInstallSection } from '~/components/plugins/InstallSection'; > **Note:** Using worklets requires installing [`react-native-reanimated`](https://docs.swmansion.com/react-native-reanimated/) and [`react-native-worklets`](https://docs.swmansion.com/react-native-worklets/) in your project. `useNativeState` itself works without them, but the synchronous UI-thread updates shown below depend on the worklet runtime. -The example below masks a phone number as the user types. The formatting and the write to `maskedPhone.value` both happen synchronously on the UI thread, so there is no flicker between the typed value and the masked value. +The example below masks a phone number as the user types. The formatting and the writes to `maskedPhone.value` (text) and `selection.value` (cursor position) all happen synchronously on the UI thread, so there is no flicker between the typed value and the masked value. ```tsx WorkletPhoneMaskExample.tsx -import { Host, TextField, TextFieldRef, useNativeState } from '@expo/ui/swift-ui'; +import { Host, TextField, useNativeState } from '@expo/ui/swift-ui'; import { keyboardType } from '@expo/ui/swift-ui/modifiers'; -import { useCallback, useRef } from 'react'; -import { runOnJS } from 'react-native-worklets'; +import { useEffectEvent } from 'react'; export default function WorkletPhoneMaskExample() { - const phoneRef = useRef(null); const maskedPhone = useNativeState(''); + const selection = useNativeState({ start: 0, end: 0 }); - const setPhoneCursor = useCallback((position: number) => { - phoneRef.current?.setSelection(position, position); - }, []); + const handleTextChange = useEffectEvent((v: string) => { + 'worklet'; + const digits = v.replace(/\D/g, '').slice(0, 10); + let formatted: string; + if (digits.length === 0) { + formatted = ''; + } else if (digits.length <= 3) { + formatted = digits; + } else if (digits.length <= 6) { + formatted = `(${digits.slice(0, 3)}) ${digits.slice(3)}`; + } else { + formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; + } + if (formatted !== v) { + maskedPhone.value = formatted; + // Snaps to end for demo. Real masks need smarter cursor handling. + selection.value = { start: formatted.length, end: formatted.length }; + } + }); return ( { - 'worklet'; - const digits = v.replace(/\D/g, '').slice(0, 10); - let formatted: string; - if (digits.length === 0) { - formatted = ''; - } else if (digits.length <= 3) { - formatted = digits; - } else if (digits.length <= 6) { - formatted = `(${digits.slice(0, 3)}) ${digits.slice(3)}`; - } else { - formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; - } - if (formatted !== v) { - maskedPhone.value = formatted; - runOnJS(setPhoneCursor)(formatted.length); - } - }} + onTextChange={handleTextChange} /> ); diff --git a/docs/pages/versions/unversioned/sdk/ui/universal/textinput.mdx b/docs/pages/versions/unversioned/sdk/ui/universal/textinput.mdx new file mode 100644 index 00000000000000..228f0d5334d8c7 --- /dev/null +++ b/docs/pages/versions/unversioned/sdk/ui/universal/textinput.mdx @@ -0,0 +1,129 @@ +--- +title: TextInput +description: A text input backed by native SwiftUI and Jetpack Compose components, with a React Native-compatible API. +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 text input that routes to [`TextField`](../jetpack-compose/textfield) from `@expo/ui/jetpack-compose` on Android, [`TextField`](../swift-ui/textfield) from `@expo/ui/swift-ui` on iOS, and React Native's [`TextInput`](https://reactnative.dev/docs/textinput) on web. + +The API mirrors React Native's [`TextInput`](https://reactnative.dev/docs/textinput), with two changes: [`value`](#value) and [`selection`](#selection) are observable state objects (created with `useNativeState`), and [`onChangeText`](#onchangetext) can be a worklet for synchronously updating the state on the UI thread. + +## Installation + + + +## Usage + +### Uncontrolled + +Omit [`value`](#value) and the field manages its own text internally. Use [`onChangeText`](#onchangetext) to observe edits, and use the [ref](#textinputref) for imperative actions like `focus`, `blur`, and `clear`. + +```tsx UncontrolledTextInputExample.tsx +import { Button, Column, Host, TextInput, type TextInputRef } from '@expo/ui'; +import { useRef } from 'react'; + +export default function UncontrolledTextInputExample() { + const inputRef = useRef(null); + + return ( + + + console.log(value)} + /> +