@expo/ui 57.0.12 → 57.0.14

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.
Files changed (130) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +17 -3
  4. package/android/src/main/java/expo/modules/ui/ModalBottomSheetView.kt +50 -0
  5. package/android/src/main/java/expo/modules/ui/ProgressView.kt +31 -0
  6. package/android/src/main/java/expo/modules/ui/RNHostView.kt +141 -11
  7. package/build/PresentedContentContext.d.ts +18 -0
  8. package/build/PresentedContentContext.d.ts.map +1 -0
  9. package/build/community/pager-view/PagerView.android.d.ts.map +1 -1
  10. package/build/jetpack-compose/AlertDialog/index.d.ts.map +1 -1
  11. package/build/jetpack-compose/BasicAlertDialog/index.d.ts.map +1 -1
  12. package/build/jetpack-compose/DropdownMenu/index.d.ts.map +1 -1
  13. package/build/jetpack-compose/ExposedDropdownMenuBox/ExposedDropdownMenu.d.ts.map +1 -1
  14. package/build/jetpack-compose/Host/index.d.ts +3 -0
  15. package/build/jetpack-compose/Host/index.d.ts.map +1 -1
  16. package/build/jetpack-compose/ModalBottomSheet/index.d.ts.map +1 -1
  17. package/build/jetpack-compose/Progress/index.d.ts +25 -2
  18. package/build/jetpack-compose/Progress/index.d.ts.map +1 -1
  19. package/build/jetpack-compose/RNHostView/index.d.ts +0 -6
  20. package/build/jetpack-compose/RNHostView/index.d.ts.map +1 -1
  21. package/build/jetpack-compose/TextField/shared.d.ts +2 -1
  22. package/build/jetpack-compose/TextField/shared.d.ts.map +1 -1
  23. package/build/jetpack-compose/Tooltip/index.d.ts.map +1 -1
  24. package/build/keyboard/index.d.ts +43 -0
  25. package/build/keyboard/index.d.ts.map +1 -0
  26. package/build/keyboard/interop.d.ts +16 -0
  27. package/build/keyboard/interop.d.ts.map +1 -0
  28. package/build/keyboard/textInputState.d.ts +2 -0
  29. package/build/keyboard/textInputState.d.ts.map +1 -0
  30. package/build/keyboard/textInputState.web.d.ts +11 -0
  31. package/build/keyboard/textInputState.web.d.ts.map +1 -0
  32. package/build/swift-ui/Background/index.d.ts +18 -0
  33. package/build/swift-ui/Background/index.d.ts.map +1 -0
  34. package/build/swift-ui/BottomSheet/index.d.ts.map +1 -1
  35. package/build/swift-ui/Host/index.d.ts +3 -0
  36. package/build/swift-ui/Host/index.d.ts.map +1 -1
  37. package/build/swift-ui/Popover/index.d.ts.map +1 -1
  38. package/build/swift-ui/RNHostView.d.ts +2 -1
  39. package/build/swift-ui/RNHostView.d.ts.map +1 -1
  40. package/build/swift-ui/SecureField/index.d.ts.map +1 -1
  41. package/build/swift-ui/TextField/index.d.ts.map +1 -1
  42. package/build/swift-ui/index.d.ts +1 -0
  43. package/build/swift-ui/index.d.ts.map +1 -1
  44. package/build/swift-ui/modifiers/index.d.ts +21 -1
  45. package/build/swift-ui/modifiers/index.d.ts.map +1 -1
  46. package/build/universal/BottomSheet/index.android.d.ts +1 -1
  47. package/build/universal/BottomSheet/index.android.d.ts.map +1 -1
  48. package/build/universal/BottomSheet/types.d.ts +14 -0
  49. package/build/universal/BottomSheet/types.d.ts.map +1 -1
  50. package/build/utils/useMergeRefs.d.ts +12 -0
  51. package/build/utils/useMergeRefs.d.ts.map +1 -0
  52. package/expo-module.config.json +1 -1
  53. package/ios/BackgroundView.swift +34 -0
  54. package/ios/ColorPickerView.swift +1 -1
  55. package/ios/ExpoUIModule.swift +4 -0
  56. package/ios/HostView.swift +7 -0
  57. package/ios/Modifiers/ViewModifierRegistry.swift +48 -0
  58. package/ios/RNHostView.swift +65 -4
  59. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{57.0.12/expo.modules.ui-57.0.12-sources.jar → 57.0.14/expo.modules.ui-57.0.14-sources.jar} +0 -0
  60. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14-sources.jar.md5 +1 -0
  61. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14-sources.jar.sha1 +1 -0
  62. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14-sources.jar.sha256 +1 -0
  63. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14-sources.jar.sha512 +1 -0
  64. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.aar +0 -0
  65. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.aar.md5 +1 -0
  66. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.aar.sha1 +1 -0
  67. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.aar.sha256 +1 -0
  68. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.aar.sha512 +1 -0
  69. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{57.0.12/expo.modules.ui-57.0.12.module → 57.0.14/expo.modules.ui-57.0.14.module} +22 -22
  70. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.module.md5 +1 -0
  71. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.module.sha1 +1 -0
  72. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.module.sha256 +1 -0
  73. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.module.sha512 +1 -0
  74. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{57.0.12/expo.modules.ui-57.0.12.pom → 57.0.14/expo.modules.ui-57.0.14.pom} +1 -1
  75. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.pom.md5 +1 -0
  76. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.pom.sha1 +1 -0
  77. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.pom.sha256 +1 -0
  78. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.pom.sha512 +1 -0
  79. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
  80. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
  81. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
  82. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
  83. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
  84. package/package.json +4 -4
  85. package/src/PresentedContentContext.tsx +22 -0
  86. package/src/community/pager-view/PagerView.android.tsx +1 -7
  87. package/src/jetpack-compose/AlertDialog/index.tsx +6 -1
  88. package/src/jetpack-compose/BasicAlertDialog/index.tsx +7 -1
  89. package/src/jetpack-compose/DropdownMenu/index.tsx +7 -1
  90. package/src/jetpack-compose/ExposedDropdownMenuBox/ExposedDropdownMenu.tsx +3 -1
  91. package/src/jetpack-compose/Host/index.tsx +28 -17
  92. package/src/jetpack-compose/ModalBottomSheet/index.tsx +8 -1
  93. package/src/jetpack-compose/Progress/index.tsx +32 -2
  94. package/src/jetpack-compose/RNHostView/index.tsx +16 -13
  95. package/src/jetpack-compose/TextField/shared.ts +7 -1
  96. package/src/jetpack-compose/Tooltip/index.tsx +2 -1
  97. package/src/keyboard/index.tsx +243 -0
  98. package/src/keyboard/interop.ts +99 -0
  99. package/src/keyboard/textInputState.ts +2 -0
  100. package/src/keyboard/textInputState.web.ts +16 -0
  101. package/src/swift-ui/Background/index.tsx +38 -0
  102. package/src/swift-ui/BottomSheet/index.tsx +4 -1
  103. package/src/swift-ui/Host/index.tsx +33 -18
  104. package/src/swift-ui/Popover/index.tsx +7 -1
  105. package/src/swift-ui/RNHostView.tsx +17 -3
  106. package/src/swift-ui/SecureField/index.tsx +8 -2
  107. package/src/swift-ui/TextField/index.tsx +8 -1
  108. package/src/swift-ui/index.tsx +1 -0
  109. package/src/swift-ui/modifiers/index.ts +25 -0
  110. package/src/ts-declarations/react-native-text-input-state.d.ts +26 -0
  111. package/src/universal/BottomSheet/index.android.tsx +4 -1
  112. package/src/universal/BottomSheet/types.ts +17 -0
  113. package/src/utils/useMergeRefs.ts +47 -0
  114. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12-sources.jar.md5 +0 -1
  115. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12-sources.jar.sha1 +0 -1
  116. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12-sources.jar.sha256 +0 -1
  117. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12-sources.jar.sha512 +0 -1
  118. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.aar +0 -0
  119. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.aar.md5 +0 -1
  120. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.aar.sha1 +0 -1
  121. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.aar.sha256 +0 -1
  122. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.aar.sha512 +0 -1
  123. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.module.md5 +0 -1
  124. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.module.sha1 +0 -1
  125. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.module.sha256 +0 -1
  126. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.module.sha512 +0 -1
  127. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.pom.md5 +0 -1
  128. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.pom.sha1 +0 -1
  129. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.pom.sha256 +0 -1
  130. package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.pom.sha512 +0 -1
@@ -1,5 +1,5 @@
1
1
  import { requireNativeView } from 'expo';
2
- import { useMemo } from 'react';
2
+ import { useMemo, type Ref } from 'react';
3
3
  import {
4
4
  type ColorSchemeName,
5
5
  type ColorValue,
@@ -9,6 +9,8 @@ import {
9
9
  useColorScheme as useRNColorScheme,
10
10
  } from 'react-native';
11
11
 
12
+ import { TextInputHostProvider, useTextInputHostRef } from '../../keyboard';
13
+ import { useMergeRefs } from '../../utils/useMergeRefs';
12
14
  import { getMaterialColors, HostPaletteContext } from '../colors';
13
15
  import { type PrimitiveBaseProps } from '../layout';
14
16
 
@@ -65,6 +67,8 @@ export interface HostProps extends PrimitiveBaseProps {
65
67
  children: React.ReactNode;
66
68
  style?: StyleProp<ViewStyle>;
67
69
  pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto';
70
+ /** @hidden */
71
+ ref?: Ref<any>;
68
72
  }
69
73
 
70
74
  type NativeHostProps = Omit<HostProps, 'colorScheme'> & {
@@ -72,6 +76,7 @@ type NativeHostProps = Omit<HostProps, 'colorScheme'> & {
72
76
  matchContentsHorizontal?: boolean;
73
77
  colorScheme?: ColorSchemeName;
74
78
  seedColor?: ColorValue;
79
+ ref?: Ref<any>;
75
80
  };
76
81
 
77
82
  const HostNativeView: React.ComponentType<NativeHostProps> = requireNativeView(
@@ -87,6 +92,7 @@ export function Host(props: HostProps) {
87
92
  layoutDirection,
88
93
  colorScheme,
89
94
  seedColor,
95
+ ref,
90
96
  ...restProps
91
97
  } = props;
92
98
  const schemeString = colorScheme === 'light' || colorScheme === 'dark' ? colorScheme : undefined;
@@ -97,25 +103,30 @@ export function Host(props: HostProps) {
97
103
  () => getMaterialColors({ scheme: resolvedScheme, seedColor }),
98
104
  [resolvedScheme, seedColor]
99
105
  );
106
+ const hostRef = useTextInputHostRef();
107
+ const mergedRef = useMergeRefs(ref, hostRef);
100
108
 
101
109
  return (
102
110
  <HostPaletteContext.Provider value={palette}>
103
- <HostNativeView
104
- {...restProps}
105
- modifiers={modifiers}
106
- matchContentsVertical={
107
- typeof matchContents === 'object' ? matchContents.vertical : matchContents
108
- }
109
- matchContentsHorizontal={
110
- typeof matchContents === 'object' ? matchContents.horizontal : matchContents
111
- }
112
- colorScheme={schemeString}
113
- seedColor={seedColor}
114
- onLayoutContent={onLayoutContent}
115
- layoutDirection={
116
- layoutDirection ?? (I18nManager.getConstants().isRTL ? 'rightToLeft' : 'leftToRight')
117
- }
118
- />
111
+ <TextInputHostProvider hostRef={hostRef}>
112
+ <HostNativeView
113
+ {...restProps}
114
+ modifiers={modifiers}
115
+ matchContentsVertical={
116
+ typeof matchContents === 'object' ? matchContents.vertical : matchContents
117
+ }
118
+ matchContentsHorizontal={
119
+ typeof matchContents === 'object' ? matchContents.horizontal : matchContents
120
+ }
121
+ colorScheme={schemeString}
122
+ seedColor={seedColor}
123
+ onLayoutContent={onLayoutContent}
124
+ layoutDirection={
125
+ layoutDirection ?? (I18nManager.getConstants().isRTL ? 'rightToLeft' : 'leftToRight')
126
+ }
127
+ ref={mergedRef}
128
+ />
129
+ </TextInputHostProvider>
119
130
  </HostPaletteContext.Provider>
120
131
  );
121
132
  }
@@ -2,6 +2,7 @@ import { requireNativeView } from 'expo';
2
2
  import type { Ref, ReactNode, ComponentType } from 'react';
3
3
  import type { ColorValue } from 'react-native';
4
4
 
5
+ import { PresentedContent } from '../../PresentedContentContext';
5
6
  import { type ModifierConfig } from '../../types';
6
7
  import { createViewModifierEventListener } from '../modifiers/utils';
7
8
 
@@ -138,7 +139,13 @@ function DragHandle(props: { children: ReactNode }) {
138
139
  * A Material Design modal bottom sheet.
139
140
  */
140
141
  function ModalBottomSheetComponent(props: ModalBottomSheetProps) {
141
- return <ModalBottomSheetNativeView {...transformProps(props)} />;
142
+ const { children, ...rest } = props;
143
+ // The whole sheet, drag handle included, is presented in its own window, so all of it is marked.
144
+ return (
145
+ <ModalBottomSheetNativeView {...transformProps(rest as ModalBottomSheetProps)}>
146
+ <PresentedContent>{children}</PresentedContent>
147
+ </ModalBottomSheetNativeView>
148
+ );
142
149
  }
143
150
 
144
151
  ModalBottomSheetComponent.DragHandle = DragHandle;
@@ -128,9 +128,38 @@ export const CircularProgressIndicator = createProgressComponent<CircularProgres
128
128
 
129
129
  // endregion
130
130
 
131
+ // region WavyProgressIndicators
132
+
133
+ /**
134
+ * Wave configuration shared by the wavy progress indicator variants.
135
+ */
136
+ export type WavyProgressCommonConfig = {
137
+ /**
138
+ * Height of the wave, where `0` draws a flat line and `1` draws the full wave height.
139
+ * When set, the amplitude stays the same across the whole track.
140
+ * When omitted, a determinate indicator uses the Material 3 default, which flattens the wave as
141
+ * progress approaches `0` and `1`.
142
+ * An indeterminate indicator defaults to the full wave height.
143
+ */
144
+ amplitude?: number;
145
+ /**
146
+ * Length of a single wave in dp. `CircularWavyProgressIndicator` rounds this to fit a whole
147
+ * number of waves around the ring, so the wave it draws can be slightly shorter or longer.
148
+ */
149
+ wavelength?: number;
150
+ /**
151
+ * Speed of the wave in dp per second. Defaults to `wavelength`, which advances the wave by one
152
+ * wavelength per second. Set it to `0` to stop the wave from moving.
153
+ */
154
+ waveSpeed?: number;
155
+ };
156
+
157
+ // endregion
158
+
131
159
  // region LinearWavyProgressIndicator
132
160
 
133
- export interface LinearWavyProgressIndicatorProps extends ProgressCommonConfig {
161
+ export interface LinearWavyProgressIndicatorProps
162
+ extends ProgressCommonConfig, WavyProgressCommonConfig {
134
163
  /**
135
164
  * Size of the stop indicator in dp at the end of the determinate progress track.
136
165
  */
@@ -149,7 +178,8 @@ export const LinearWavyProgressIndicator =
149
178
 
150
179
  // region CircularWavyProgressIndicator
151
180
 
152
- export interface CircularWavyProgressIndicatorProps extends ProgressCommonConfig {}
181
+ export interface CircularWavyProgressIndicatorProps
182
+ extends ProgressCommonConfig, WavyProgressCommonConfig {}
153
183
 
154
184
  /**
155
185
  * A circular progress indicator with wavy animation style.
@@ -1,7 +1,7 @@
1
1
  import { requireNativeView } from 'expo';
2
2
  import type { ReactElement, ComponentType } from 'react';
3
- import type { StyleProp, ViewStyle } from 'react-native';
4
3
 
4
+ import { PresentedContentContext, useIsPresentedInOwnWindow } from '../../PresentedContentContext';
5
5
  import type { ModifierConfig } from '../../types';
6
6
  import type { PrimitiveBaseProps } from '../layout';
7
7
  import { createViewModifierEventListener } from '../modifiers/utils';
@@ -22,11 +22,6 @@ export interface RNHostProps extends PrimitiveBaseProps {
22
22
  * Modifiers for the component.
23
23
  */
24
24
  modifiers?: ModifierConfig[];
25
- /**
26
- * Style applied to the host view's React Native shadow node. Useful for
27
- * controlling its layout position (e.g. `position: 'absolute'`).
28
- */
29
- style?: StyleProp<ViewStyle>;
30
25
  }
31
26
 
32
27
  type NativeRNHostProps = RNHostProps & {
@@ -37,25 +32,33 @@ const NativeRNHostView: ComponentType<NativeRNHostProps> = requireNativeView(
37
32
  'RNHostView'
38
33
  );
39
34
 
40
- function transformProps(props: RNHostProps): NativeRNHostProps {
35
+ function transformProps(props: RNHostProps, layoutRoot: boolean): NativeRNHostProps {
41
36
  const { modifiers, ...restProps } = props;
42
37
  return {
43
38
  modifiers,
44
39
  ...(modifiers ? createViewModifierEventListener(modifiers) : undefined),
45
40
  ...restProps,
46
- // Touches on hosted content are dispatched relative to the host, so `measure()` must report
47
- // the same coordinate space; otherwise `Pressable` cancels the press on any finger movement.
48
- layoutRoot: true,
41
+ layoutRoot,
49
42
  };
50
43
  }
51
44
 
52
45
  export function RNHostView(props: RNHostProps) {
46
+ // Content presented in its own window — a modal bottom sheet, a dialog — has no React root above
47
+ // it, so it dispatches its own touches and is measured from itself. Everywhere else the surface
48
+ // root already streams this subtree's touches, and a second stream in a second coordinate space
49
+ // is what makes a `Pressable` drop its press on the first finger movement.
50
+ const layoutRoot = useIsPresentedInOwnWindow();
51
+
53
52
  return (
54
53
  <NativeRNHostView
55
- {...transformProps(props)}
54
+ {...transformProps(props, layoutRoot)}
56
55
  // `matchContents` can only be used once on mount
57
56
  // So we force unmount when it changes to prevent unexpected layout
58
- key={props.matchContents ? 'matchContents' : 'noMatchContents'}
59
- />
57
+ key={props.matchContents ? 'matchContents' : 'noMatchContents'}>
58
+ {/* Reset context here so only nearest RNHostView becomes the layout root for its children, and not any other RNHostView above it in the tree. */}
59
+ <PresentedContentContext.Provider value={false}>
60
+ {props.children}
61
+ </PresentedContentContext.Provider>
62
+ </NativeRNHostView>
60
63
  );
61
64
  }
@@ -2,6 +2,7 @@ import type { ReactNode, Ref } from 'react';
2
2
  import type { ColorValue } from 'react-native';
3
3
 
4
4
  import { getStateId, type ObservableState, useWorkletProp, worklets } from '../../State';
5
+ import { useHostedTextInput } from '../../keyboard';
5
6
  import type { ModifierConfig, ViewEvent } from '../../types';
6
7
  import { createViewModifierEventListener } from '../modifiers/utils';
7
8
 
@@ -180,6 +181,7 @@ export type CommonTextFieldProperties = {
180
181
  * else on the props passes through untouched.
181
182
  */
182
183
  type TransformedKeys =
184
+ | 'ref'
183
185
  | 'value'
184
186
  | 'selection'
185
187
  | 'modifiers'
@@ -195,6 +197,7 @@ type TransformedKeys =
195
197
  * callbacks become `nativeEvent`-wrapped listeners.
196
198
  */
197
199
  export type CommonNativeTextFieldProps = {
200
+ ref?: Ref<any>;
198
201
  modifiers?: ModifierConfig[];
199
202
  children?: ReactNode;
200
203
  value?: number | null;
@@ -209,6 +212,7 @@ export function useCommonTextFieldProps<T extends CommonTextFieldProperties>(
209
212
  props: T
210
213
  ): CommonNativeTextFieldProps & Omit<T, TransformedKeys> {
211
214
  const {
215
+ ref,
212
216
  value,
213
217
  selection,
214
218
  modifiers,
@@ -222,9 +226,11 @@ export function useCommonTextFieldProps<T extends CommonTextFieldProperties>(
222
226
 
223
227
  const isWorklet = !!onValueChange && !!worklets?.isWorkletFunction?.(onValueChange);
224
228
  const workletCallback = useWorkletProp(isWorklet ? onValueChange : undefined, 'onValueChange');
229
+ const hosted = useHostedTextInput<TextFieldRef>(ref, onFocusChanged);
225
230
 
226
231
  return {
227
232
  ...rest,
233
+ ref: hosted.ref,
228
234
  modifiers,
229
235
  ...(modifiers ? createViewModifierEventListener(modifiers) : undefined),
230
236
  children,
@@ -233,7 +239,7 @@ export function useCommonTextFieldProps<T extends CommonTextFieldProperties>(
233
239
  onValueChangeSync: getStateId(workletCallback),
234
240
  onValueChange:
235
241
  !isWorklet && onValueChange ? (event) => onValueChange(event.nativeEvent.text) : undefined,
236
- onFocusChanged: onFocusChanged ? (event) => onFocusChanged(event.nativeEvent.value) : undefined,
242
+ onFocusChanged: (event) => hosted.onFocusChange(event.nativeEvent.value),
237
243
  onSelectionChange: onSelectionChange
238
244
  ? (event) => onSelectionChange({ start: event.nativeEvent.start, end: event.nativeEvent.end })
239
245
  : undefined,
@@ -2,6 +2,7 @@ import { requireNativeView } from 'expo';
2
2
  import { type Ref } from 'react';
3
3
  import { type ColorValue } from 'react-native';
4
4
 
5
+ import { PresentedContent } from '../../PresentedContentContext';
5
6
  import { type ModifierConfig } from '../../types';
6
7
  import { createViewModifierEventListener } from '../modifiers/utils';
7
8
 
@@ -146,7 +147,7 @@ function RichTooltipComponent(props: RichTooltipProps) {
146
147
  modifiers={modifiers}
147
148
  {...(modifiers ? createViewModifierEventListener(modifiers) : undefined)}
148
149
  {...restProps}>
149
- {children}
150
+ <PresentedContent>{children}</PresentedContent>
150
151
  </RichTooltipNativeView>
151
152
  </SlotNativeView>
152
153
  );
@@ -0,0 +1,243 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useRef,
9
+ type Ref,
10
+ type RefObject,
11
+ } from 'react';
12
+ import type { ReactNativeElement } from 'react-native';
13
+
14
+ import { attachHostController, detachHostController } from './interop';
15
+ import { TextInputState } from './textInputState';
16
+ import { useMergeRefs } from '../utils/useMergeRefs';
17
+
18
+ /**
19
+ * React Native decides whether a touch dismissed the keyboard by comparing the
20
+ * focused instance in `TextInputState` against the `event.target` of the touch.
21
+ * A hosted field can never be that target — SwiftUI children are virtual and
22
+ * Compose children carry no React tag — so every touch inside a host resolves to
23
+ * the host, and the host is what React Native has to be told about.
24
+ *
25
+ * Fields drive all of it. The host only lends its native view as the address.
26
+ */
27
+
28
+ /** A hosted field, identified by object identity and able to act on itself. */
29
+ type HostedField = {
30
+ focus: () => void;
31
+ blur: () => void;
32
+ };
33
+
34
+ /** One host's fields: which are mounted, which report focus. */
35
+ type HostEntry = {
36
+ mounted: Set<HostedField>;
37
+ focused: Set<HostedField>;
38
+ lastFocused: HostedField | null;
39
+ };
40
+
41
+ /** The wrapper component that `requireNativeView` returns. */
42
+ type NativeViewWrapper = {
43
+ getNativeRef?: () => ReactNativeElement | null;
44
+ };
45
+
46
+ /** The subset of a field's imperative handle a host needs. */
47
+ type TextInputHandle = {
48
+ focus: () => unknown;
49
+ blur: () => unknown;
50
+ };
51
+
52
+ // Weak so a host whose fields never unregister cannot outlive its view tree.
53
+ // Entries are still removed explicitly, since unregistering from React Native
54
+ // matters for correctness, not only for memory.
55
+ const hosts = new WeakMap<ReactNativeElement, HostEntry>();
56
+
57
+ const HostInstanceContext = createContext<RefObject<NativeViewWrapper | null> | null>(null);
58
+
59
+ /** Used by `<Host>`. Attach the returned ref to the native host view. */
60
+ export function useTextInputHostRef() {
61
+ return useRef<NativeViewWrapper | null>(null);
62
+ }
63
+
64
+ /**
65
+ * Lends the host's native view to the fields it hosts, at any depth.
66
+ *
67
+ * A field inside a nested host uses the inner one, which is also what the hit
68
+ * test reports. Presented content is the exception: a field inside a `BottomSheet`
69
+ * or `Popover` is nested in the React tree but not in the host's view, so it
70
+ * registers a view the touch never lands on.
71
+ */
72
+ export function TextInputHostProvider({
73
+ hostRef,
74
+ children,
75
+ }: {
76
+ hostRef: RefObject<NativeViewWrapper | null>;
77
+ children: React.ReactNode;
78
+ }) {
79
+ return <HostInstanceContext.Provider value={hostRef}>{children}</HostInstanceContext.Provider>;
80
+ }
81
+
82
+ function addField(instance: ReactNativeElement, field: HostedField) {
83
+ let entry = hosts.get(instance);
84
+ if (!entry) {
85
+ entry = { mounted: new Set(), focused: new Set(), lastFocused: null };
86
+ hosts.set(instance, entry);
87
+ TextInputState.registerInput(instance);
88
+ const hostEntry = entry;
89
+ attachHostController(instance, {
90
+ blurFocusedFields: () => hostEntry.focused.forEach((focusedField) => focusedField.blur()),
91
+ focusLastField: () => hostEntry.lastFocused?.focus(),
92
+ });
93
+ }
94
+ entry.mounted.add(field);
95
+ }
96
+
97
+ function removeField(instance: ReactNativeElement, field: HostedField) {
98
+ const entry = hosts.get(instance);
99
+ if (!entry) {
100
+ return;
101
+ }
102
+ entry.mounted.delete(field);
103
+ if (entry.lastFocused === field) {
104
+ entry.lastFocused = null;
105
+ }
106
+ // A field can unmount while it still holds focus.
107
+ if (entry.focused.delete(field) && entry.focused.size === 0) {
108
+ TextInputState.blurInput(instance);
109
+ }
110
+ if (entry.mounted.size === 0) {
111
+ // Leaving a stale focused instance behind makes `ScrollView` swallow the
112
+ // first tap of every gesture. `blurInput` no-ops unless it still points here.
113
+ TextInputState.blurInput(instance);
114
+ TextInputState.unregisterInput(instance);
115
+ detachHostController(instance);
116
+ hosts.delete(instance);
117
+ }
118
+ }
119
+
120
+ function setFieldFocused(instance: ReactNativeElement, field: HostedField, focused: boolean) {
121
+ const entry = hosts.get(instance);
122
+ if (!entry) {
123
+ return;
124
+ }
125
+ if (focused) {
126
+ entry.focused.add(field);
127
+ entry.lastFocused = field;
128
+ } else {
129
+ entry.focused.delete(field);
130
+ }
131
+ // Two fields swapping focus emit independent events that can arrive in either
132
+ // order, so the host stays focused while any of its fields is.
133
+ if (entry.focused.size > 0) {
134
+ TextInputState.focusInput(instance);
135
+ } else {
136
+ TextInputState.blurInput(instance);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Used by hosted text fields. Joins React Native's keyboard coordination through
142
+ * the surrounding host, and returns the ref to attach to the native view plus
143
+ * the focus handler to wire to its focus event. The app's own ref and callback
144
+ * keep working.
145
+ */
146
+ export function useHostedTextInput<T extends TextInputHandle>(
147
+ ref: Ref<T> | undefined,
148
+ onFocusChange: ((focused: boolean) => void) | undefined,
149
+ options?: {
150
+ /**
151
+ * Blur the field when it unmounts. SwiftUI only: `blur` there affects just this field, while
152
+ * the Compose handler clears focus for whichever component currently holds it in the host.
153
+ */
154
+ blurOnUnmount?: boolean;
155
+ }
156
+ ) {
157
+ const hostRef = useContext(HostInstanceContext);
158
+ const nativeRef = useRef<T | null>(null);
159
+ const hostInstance = useRef<ReactNativeElement | null>(null);
160
+ // An `autoFocus` field can report focus before its registration effect runs.
161
+ const focusBeforeRegistered = useRef<boolean | null>(null);
162
+ // Last focus state this field reported, so unmount only blurs a field that holds focus.
163
+ const isFocused = useRef(false);
164
+
165
+ // Identifies this field to its host, and lets the host act on it.
166
+ const field = useMemo<HostedField>(
167
+ () => ({
168
+ focus: () => callHandle(nativeRef.current, 'focus'),
169
+ blur: () => callHandle(nativeRef.current, 'blur'),
170
+ }),
171
+ []
172
+ );
173
+
174
+ // Blur before React removes the field's native views: a field left first responder while its
175
+ // row is deleted makes UIKit assert ("refused to resign"), and every native teardown hook runs
176
+ // too late to clear SwiftUI's `@FocusState`. https://github.com/expo/expo/issues/49348
177
+ const blurOnUnmount = options?.blurOnUnmount ?? false;
178
+ useLayoutEffect(() => {
179
+ if (!blurOnUnmount) {
180
+ return;
181
+ }
182
+ return () => {
183
+ if (isFocused.current) {
184
+ callHandle(nativeRef.current, 'blur');
185
+ }
186
+ };
187
+ }, [blurOnUnmount]);
188
+
189
+ useEffect(() => {
190
+ // Resolved here rather than during render: the host's ref is attached by the
191
+ // time a hosted field's effects run.
192
+ const instance = hostRef?.current?.getNativeRef?.() ?? null;
193
+ hostInstance.current = instance;
194
+ if (!instance) {
195
+ return;
196
+ }
197
+ addField(instance, field);
198
+ if (focusBeforeRegistered.current != null) {
199
+ setFieldFocused(instance, field, focusBeforeRegistered.current);
200
+ focusBeforeRegistered.current = null;
201
+ }
202
+ return () => {
203
+ removeField(instance, field);
204
+ hostInstance.current = null;
205
+ };
206
+ }, [hostRef, field]);
207
+
208
+ return {
209
+ ref: useMergeRefs(ref, nativeRef),
210
+ onFocusChange: useCallback(
211
+ (focused: boolean) => {
212
+ isFocused.current = focused;
213
+ if (hostInstance.current) {
214
+ setFieldFocused(hostInstance.current, field, focused);
215
+ } else {
216
+ focusBeforeRegistered.current = focused;
217
+ }
218
+ onFocusChange?.(focused);
219
+ },
220
+ [field, onFocusChange]
221
+ ),
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Calls one of the field's imperative view functions. They are async functions,
227
+ * so a rejection would otherwise surface as an unhandled rejection inside React
228
+ * Native's touch handling.
229
+ */
230
+ function callHandle(handle: TextInputHandle | null, method: 'focus' | 'blur') {
231
+ const call = handle?.[method];
232
+ if (typeof call !== 'function') {
233
+ return;
234
+ }
235
+ try {
236
+ const result = call.call(handle);
237
+ if (result && typeof (result as Promise<unknown>).catch === 'function') {
238
+ (result as Promise<unknown>).catch(() => {});
239
+ }
240
+ } catch {
241
+ // A field whose native view has gone cannot be focused or blurred.
242
+ }
243
+ }
@@ -0,0 +1,99 @@
1
+ import { TextInput } from 'react-native';
2
+
3
+ import { TextInputState } from './textInputState';
4
+
5
+ /**
6
+ * How a `<Host>` acts on the field that holds focus, once React Native asks the
7
+ * host to blur or focus.
8
+ */
9
+ export type HostedFocusController = {
10
+ blurFocusedFields: () => void;
11
+ /** Refocuses the field that held focus last, if it is still there. */
12
+ focusLastField: () => void;
13
+ };
14
+
15
+ type FocusEntryPoints = {
16
+ blurTextInput: (instance: any) => void;
17
+ focusTextInput: (instance: any) => void;
18
+ };
19
+
20
+ // Weak so a host that unmounts without detaching cannot keep its instance alive.
21
+ const controllers = new WeakMap<object, HostedFocusController>();
22
+ let installed = false;
23
+
24
+ /**
25
+ * Starts routing React Native's blur and focus requests for this host to its
26
+ * hosted fields.
27
+ */
28
+ export function attachHostController(instance: object, controller: HostedFocusController) {
29
+ controllers.set(instance, controller);
30
+ install();
31
+ }
32
+
33
+ export function detachHostController(instance: object) {
34
+ controllers.delete(instance);
35
+ }
36
+
37
+ /**
38
+ * React Native blurs the focused input by dispatching a native `blur` view
39
+ * command to it. A hosted SwiftUI or Compose field cannot receive one: it has no
40
+ * host component of its own, so the command would land on the `<Host>` and stop
41
+ * there. Taking over the two entry points instead lets the host forward the
42
+ * request to the field that actually holds focus, through the same imperative
43
+ * methods `ref.blur()` and `ref.focus()` already use.
44
+ *
45
+ * Inputs React Native owns are passed through untouched, and the takeover only
46
+ * happens once a hosted text field exists.
47
+ *
48
+ * Re-evaluating this module, which only Fast Refresh does, adds one more wrapper
49
+ * in front of the previous one. That stays correct: a host this copy does not know
50
+ * falls through to the wrapper before it.
51
+ */
52
+ function install() {
53
+ if (installed) {
54
+ return;
55
+ }
56
+ installed = true;
57
+ takeOver(TextInputState);
58
+ // `TextInput.State` holds its own copies of these two functions, so it needs
59
+ // the same treatment to stay consistent with the module it copied them from.
60
+ takeOver(TextInput?.State as unknown as FocusEntryPoints);
61
+ }
62
+
63
+ function takeOver(target: FocusEntryPoints | undefined) {
64
+ // If React Native ever reshapes these, hosted fields quietly stop taking part
65
+ // rather than breaking the app.
66
+ if (typeof target?.blurTextInput !== 'function' || typeof target?.focusTextInput !== 'function') {
67
+ return;
68
+ }
69
+
70
+ const blurTextInput = target.blurTextInput;
71
+ const focusTextInput = target.focusTextInput;
72
+
73
+ target.blurTextInput = (instance: any) => {
74
+ const controller = instance ? controllers.get(instance) : undefined;
75
+ if (!controller) {
76
+ blurTextInput(instance);
77
+ return;
78
+ }
79
+ // Same guard as React Native: blurring an input that does not hold focus is a
80
+ // no-op, so a host never blurs its fields on behalf of a different input.
81
+ if (TextInputState.currentlyFocusedInput() !== instance) {
82
+ return;
83
+ }
84
+ TextInputState.blurInput(instance);
85
+ controller.blurFocusedFields();
86
+ };
87
+
88
+ target.focusTextInput = (instance: any) => {
89
+ const controller = instance ? controllers.get(instance) : undefined;
90
+ if (!controller) {
91
+ focusTextInput(instance);
92
+ return;
93
+ }
94
+ // No `focusInput` here: the field's own focus event sets the focused instance
95
+ // once focus actually lands, so React Native is never told about focus that
96
+ // did not happen.
97
+ controller.focusLastField();
98
+ };
99
+ }
@@ -0,0 +1,2 @@
1
+ /// <reference path="../ts-declarations/react-native-text-input-state.d.ts" />
2
+ export { default as TextInputState } from 'react-native/Libraries/Components/TextInput/TextInputState';
@@ -0,0 +1,16 @@
1
+ // React Native for web has no `TextInputState`: the browser owns focus. This file
2
+ // is also what keeps the deep import in `textInputState.ts` out of the web bundle.
3
+ // `@expo/ui/swift-ui` and `@expo/ui/jetpack-compose` have no web entry point, so a
4
+ // cross-platform file importing either one puts this module in the web graph, and
5
+ // without this variant the whole bundle fails to resolve rather than reaching the
6
+ // `UnavailabilityError` those entry points already throw on web.
7
+ export const TextInputState = {
8
+ currentlyFocusedInput: () => null,
9
+ focusInput: () => {},
10
+ blurInput: () => {},
11
+ registerInput: () => {},
12
+ unregisterInput: () => {},
13
+ isTextInput: () => false,
14
+ focusTextInput: () => {},
15
+ blurTextInput: () => {},
16
+ };