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
1 change: 1 addition & 0 deletions apps/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
1 change: 1 addition & 0 deletions apps/notification-tester/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
2 changes: 2 additions & 0 deletions docs/components/plugins/api/APIStaticData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export const hardcodedTypeLinks: Record<string, string> = {
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',
Expand Down Expand Up @@ -273,6 +274,7 @@ export const hardcodedTypeLinks: Record<string, string> = {
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',
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/eas/workflows/pre-packaged-jobs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`. |
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/eas/workflows/syntax.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 2 additions & 0 deletions docs/pages/versions/unversioned/sdk/router/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,32 +260,36 @@ 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 (
<Host matchContents>
<TextField
value={phone}
selection={selection}
keyboardOptions={{ keyboardType: 'phone' }}
modifiers={[fillMaxWidth()]}
onValueChange={v => {
'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}>
<TextField.Placeholder>
<Text>(555) 123-4567</Text>
</TextField.Placeholder>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,40 @@ 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 (
<Host matchContents>
<TextField
value={maskedPhone}
selection={selection}
keyboardOptions={{ keyboardType: 'phone' }}
modifiers={[fillMaxWidth()]}
onValueChange={v => {
'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}>
<TextField.Placeholder>
<ComposeText>(555) 123-4567</ComposeText>
</TextField.Placeholder>
Expand Down
32 changes: 18 additions & 14 deletions docs/pages/versions/unversioned/sdk/ui/swift-ui/textfield.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -184,32 +184,36 @@ 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 (
<Host matchContents>
<TextField
text={phone}
selection={selection}
placeholder="(555) 123-4567"
modifiers={[keyboardType('phone-pad')]}
onTextChange={v => {
'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}
/>
</Host>
);
Expand Down
52 changes: 25 additions & 27 deletions docs/pages/versions/unversioned/sdk/ui/swift-ui/usenativestate.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TextFieldRef>(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 (
<Host matchContents>
<TextField
ref={phoneRef}
text={maskedPhone}
selection={selection}
placeholder="(555) 123-4567"
modifiers={[keyboardType('phone-pad')]}
onTextChange={v => {
'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}
/>
</Host>
);
Expand Down
Loading
Loading