@expo/ui 57.0.13 → 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.
- package/CHANGELOG.md +21 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +17 -3
- package/android/src/main/java/expo/modules/ui/ProgressView.kt +31 -0
- package/android/src/main/java/expo/modules/ui/RNHostView.kt +141 -11
- package/build/PresentedContentContext.d.ts +18 -0
- package/build/PresentedContentContext.d.ts.map +1 -0
- package/build/community/pager-view/PagerView.android.d.ts.map +1 -1
- package/build/jetpack-compose/AlertDialog/index.d.ts.map +1 -1
- package/build/jetpack-compose/BasicAlertDialog/index.d.ts.map +1 -1
- package/build/jetpack-compose/DropdownMenu/index.d.ts.map +1 -1
- package/build/jetpack-compose/ExposedDropdownMenuBox/ExposedDropdownMenu.d.ts.map +1 -1
- package/build/jetpack-compose/Host/index.d.ts +3 -0
- package/build/jetpack-compose/Host/index.d.ts.map +1 -1
- package/build/jetpack-compose/ModalBottomSheet/index.d.ts.map +1 -1
- package/build/jetpack-compose/Progress/index.d.ts +25 -2
- package/build/jetpack-compose/Progress/index.d.ts.map +1 -1
- package/build/jetpack-compose/RNHostView/index.d.ts +0 -6
- package/build/jetpack-compose/RNHostView/index.d.ts.map +1 -1
- package/build/jetpack-compose/TextField/shared.d.ts +2 -1
- package/build/jetpack-compose/TextField/shared.d.ts.map +1 -1
- package/build/jetpack-compose/Tooltip/index.d.ts.map +1 -1
- package/build/keyboard/index.d.ts +43 -0
- package/build/keyboard/index.d.ts.map +1 -0
- package/build/keyboard/interop.d.ts +16 -0
- package/build/keyboard/interop.d.ts.map +1 -0
- package/build/keyboard/textInputState.d.ts +2 -0
- package/build/keyboard/textInputState.d.ts.map +1 -0
- package/build/keyboard/textInputState.web.d.ts +11 -0
- package/build/keyboard/textInputState.web.d.ts.map +1 -0
- package/build/swift-ui/Background/index.d.ts +18 -0
- package/build/swift-ui/Background/index.d.ts.map +1 -0
- package/build/swift-ui/BottomSheet/index.d.ts.map +1 -1
- package/build/swift-ui/Host/index.d.ts +3 -0
- package/build/swift-ui/Host/index.d.ts.map +1 -1
- package/build/swift-ui/Popover/index.d.ts.map +1 -1
- package/build/swift-ui/RNHostView.d.ts +2 -1
- package/build/swift-ui/RNHostView.d.ts.map +1 -1
- package/build/swift-ui/SecureField/index.d.ts.map +1 -1
- package/build/swift-ui/TextField/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/utils/useMergeRefs.d.ts +12 -0
- package/build/utils/useMergeRefs.d.ts.map +1 -0
- package/expo-module.config.json +1 -1
- package/ios/BackgroundView.swift +34 -0
- package/ios/ColorPickerView.swift +1 -1
- package/ios/ExpoUIModule.swift +4 -0
- package/ios/HostView.swift +7 -0
- package/ios/RNHostView.swift +60 -4
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/{57.0.13/expo.modules.ui-57.0.13-sources.jar → 57.0.14/expo.modules.ui-57.0.14-sources.jar} +0 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14-sources.jar.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14-sources.jar.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14-sources.jar.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14-sources.jar.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.aar +0 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.aar.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.aar.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.aar.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.aar.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/{57.0.13/expo.modules.ui-57.0.13.module → 57.0.14/expo.modules.ui-57.0.14.module} +22 -22
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.module.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.module.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.module.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.module.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/{57.0.13/expo.modules.ui-57.0.13.pom → 57.0.14/expo.modules.ui-57.0.14.pom} +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.pom.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.pom.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.pom.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.14/expo.modules.ui-57.0.14.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 +4 -4
- package/src/PresentedContentContext.tsx +22 -0
- package/src/community/pager-view/PagerView.android.tsx +1 -7
- package/src/jetpack-compose/AlertDialog/index.tsx +6 -1
- package/src/jetpack-compose/BasicAlertDialog/index.tsx +7 -1
- package/src/jetpack-compose/DropdownMenu/index.tsx +7 -1
- package/src/jetpack-compose/ExposedDropdownMenuBox/ExposedDropdownMenu.tsx +3 -1
- package/src/jetpack-compose/Host/index.tsx +28 -17
- package/src/jetpack-compose/ModalBottomSheet/index.tsx +8 -1
- package/src/jetpack-compose/Progress/index.tsx +32 -2
- package/src/jetpack-compose/RNHostView/index.tsx +16 -13
- package/src/jetpack-compose/TextField/shared.ts +7 -1
- package/src/jetpack-compose/Tooltip/index.tsx +2 -1
- package/src/keyboard/index.tsx +243 -0
- package/src/keyboard/interop.ts +99 -0
- package/src/keyboard/textInputState.ts +2 -0
- package/src/keyboard/textInputState.web.ts +16 -0
- package/src/swift-ui/Background/index.tsx +38 -0
- package/src/swift-ui/BottomSheet/index.tsx +4 -1
- package/src/swift-ui/Host/index.tsx +33 -18
- package/src/swift-ui/Popover/index.tsx +7 -1
- package/src/swift-ui/RNHostView.tsx +17 -3
- package/src/swift-ui/SecureField/index.tsx +8 -2
- package/src/swift-ui/TextField/index.tsx +8 -1
- package/src/swift-ui/index.tsx +1 -0
- package/src/ts-declarations/react-native-text-input-state.d.ts +26 -0
- package/src/utils/useMergeRefs.ts +47 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13-sources.jar.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13-sources.jar.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13-sources.jar.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13-sources.jar.sha512 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.aar +0 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.aar.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.aar.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.aar.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.aar.sha512 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.module.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.module.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.module.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.module.sha512 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.pom.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.pom.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.pom.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.13/expo.modules.ui-57.0.13.pom.sha512 +0 -1
|
@@ -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,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
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { requireNativeView } from 'expo';
|
|
2
|
+
|
|
3
|
+
import { Slot } from '../SlotView';
|
|
4
|
+
import { createViewModifierEventListener } from '../modifiers/utils';
|
|
5
|
+
import { type Alignment, type CommonViewModifierProps } from '../types';
|
|
6
|
+
|
|
7
|
+
export interface BackgroundProps extends CommonViewModifierProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* The alignment of the background content relative to the base content.
|
|
11
|
+
* @default 'center'
|
|
12
|
+
*/
|
|
13
|
+
alignment?: Alignment;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const BackgroundNativeView: React.ComponentType<BackgroundProps> = requireNativeView(
|
|
17
|
+
'ExpoUI',
|
|
18
|
+
'BackgroundView'
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
function BackgroundContent(props: { children: React.ReactNode }) {
|
|
22
|
+
return <Slot name="content">{props.children}</Slot>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Background.Content = BackgroundContent;
|
|
26
|
+
|
|
27
|
+
export function Background(props: BackgroundProps) {
|
|
28
|
+
const { modifiers, children, ...restProps } = props;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<BackgroundNativeView
|
|
32
|
+
{...(modifiers ? createViewModifierEventListener(modifiers) : undefined)}
|
|
33
|
+
modifiers={modifiers}
|
|
34
|
+
{...restProps}>
|
|
35
|
+
{children}
|
|
36
|
+
</BackgroundNativeView>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -2,6 +2,7 @@ import { requireNativeView } from 'expo';
|
|
|
2
2
|
import { useState, type ComponentType, type ReactNode } from 'react';
|
|
3
3
|
import { type NativeSyntheticEvent } from 'react-native';
|
|
4
4
|
|
|
5
|
+
import { PresentedContent } from '../../PresentedContentContext';
|
|
5
6
|
import { Slot } from '../SlotView';
|
|
6
7
|
import { createViewModifierEventListener } from '../modifiers/utils';
|
|
7
8
|
import { type CommonViewModifierProps } from '../types';
|
|
@@ -76,7 +77,9 @@ function BottomSheet(props: BottomSheetProps) {
|
|
|
76
77
|
onDismiss?.();
|
|
77
78
|
}}>
|
|
78
79
|
{anchor != null ? <Slot name="anchor">{anchor}</Slot> : null}
|
|
79
|
-
{
|
|
80
|
+
{/* Only the sheet's content is presented in its own view controller. The anchor renders in
|
|
81
|
+
place, in the React Native surface, so it stays outside `PresentedContent`. */}
|
|
82
|
+
{isMounted ? <PresentedContent>{children}</PresentedContent> : null}
|
|
80
83
|
</BottomSheetNativeView>
|
|
81
84
|
);
|
|
82
85
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { requireNativeView } from 'expo';
|
|
2
|
+
import type { Ref } from 'react';
|
|
2
3
|
import { I18nManager, type ColorValue, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
4
|
|
|
5
|
+
import { TextInputHostProvider, useTextInputHostRef } from '../../keyboard';
|
|
6
|
+
import { useMergeRefs } from '../../utils/useMergeRefs';
|
|
4
7
|
import { createViewModifierEventListener } from '../modifiers/utils';
|
|
5
8
|
import { type CommonViewModifierProps } from '../types';
|
|
6
9
|
|
|
@@ -54,10 +57,16 @@ export interface HostProps extends CommonViewModifierProps {
|
|
|
54
57
|
children: React.ReactNode;
|
|
55
58
|
style?: StyleProp<ViewStyle>;
|
|
56
59
|
pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto';
|
|
60
|
+
/** @hidden */
|
|
61
|
+
ref?: Ref<any>;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
const HostNativeView: React.ComponentType<
|
|
60
|
-
HostProps & {
|
|
65
|
+
HostProps & {
|
|
66
|
+
matchContentsVertical?: boolean;
|
|
67
|
+
matchContentsHorizontal?: boolean;
|
|
68
|
+
ref?: Ref<any>;
|
|
69
|
+
}
|
|
61
70
|
> = requireNativeView('ExpoUI', 'HostView');
|
|
62
71
|
|
|
63
72
|
/**
|
|
@@ -71,26 +80,32 @@ export function Host(props: HostProps) {
|
|
|
71
80
|
modifiers,
|
|
72
81
|
layoutDirection,
|
|
73
82
|
seedColor,
|
|
83
|
+
ref,
|
|
74
84
|
...restProps
|
|
75
85
|
} = props;
|
|
86
|
+
const hostRef = useTextInputHostRef();
|
|
87
|
+
const mergedRef = useMergeRefs(ref, hostRef);
|
|
76
88
|
|
|
77
89
|
return (
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
layoutDirection
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
<TextInputHostProvider hostRef={hostRef}>
|
|
91
|
+
<HostNativeView
|
|
92
|
+
modifiers={modifiers}
|
|
93
|
+
{...(modifiers ? createViewModifierEventListener(modifiers) : undefined)}
|
|
94
|
+
matchContentsVertical={
|
|
95
|
+
typeof matchContents === 'object' ? matchContents.vertical : matchContents
|
|
96
|
+
}
|
|
97
|
+
matchContentsHorizontal={
|
|
98
|
+
typeof matchContents === 'object' ? matchContents.horizontal : matchContents
|
|
99
|
+
}
|
|
100
|
+
onLayoutContent={onLayoutContent}
|
|
101
|
+
layoutDirection={
|
|
102
|
+
layoutDirection ?? (I18nManager.getConstants().isRTL ? 'rightToLeft' : 'leftToRight')
|
|
103
|
+
}
|
|
104
|
+
ignoreSafeArea={ignoreSafeArea}
|
|
105
|
+
seedColor={seedColor}
|
|
106
|
+
{...restProps}
|
|
107
|
+
ref={mergedRef}
|
|
108
|
+
/>
|
|
109
|
+
</TextInputHostProvider>
|
|
95
110
|
);
|
|
96
111
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { requireNativeView } from 'expo';
|
|
2
2
|
import type { NativeSyntheticEvent } from 'react-native';
|
|
3
3
|
|
|
4
|
+
import { PresentedContent } from '../../PresentedContentContext';
|
|
4
5
|
import { Slot } from '../SlotView';
|
|
5
6
|
import { createViewModifierEventListener } from '../modifiers/utils';
|
|
6
7
|
import type { CommonViewModifierProps } from '../types';
|
|
@@ -40,7 +41,12 @@ function PopoverTrigger(props: { children: React.ReactNode }) {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
function PopoverContent(props: { children: React.ReactNode }) {
|
|
43
|
-
|
|
44
|
+
// Presented in its own view controller; the trigger renders in place, so only this side is wrapped.
|
|
45
|
+
return (
|
|
46
|
+
<Slot name="popover">
|
|
47
|
+
<PresentedContent>{props.children}</PresentedContent>
|
|
48
|
+
</Slot>
|
|
49
|
+
);
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
Popover.Trigger = PopoverTrigger;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { requireNativeView } from 'expo';
|
|
2
2
|
|
|
3
|
+
import { PresentedContentContext, useIsPresentedInOwnWindow } from '../PresentedContentContext';
|
|
4
|
+
|
|
3
5
|
const RNHostNativeView: React.ComponentType<any> = requireNativeView('ExpoUI', 'RNHostView');
|
|
4
6
|
|
|
5
7
|
export interface RNHostViewProps {
|
|
6
8
|
/**
|
|
7
|
-
* When `true`, the RNHost will update its size in the React Native view tree to match the
|
|
9
|
+
* When `true`, the RNHost will update its size in the React Native view tree to match the
|
|
10
|
+
* children's size.
|
|
8
11
|
* When `false`, the RNHost will use the size of the parent SwiftUI View.
|
|
9
12
|
* Can be only set once on mount.
|
|
10
13
|
* @default false
|
|
@@ -17,12 +20,23 @@ export interface RNHostViewProps {
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function RNHostView(props: RNHostViewProps) {
|
|
23
|
+
// A sheet or popover presents its content in its own view controller, where the React Native
|
|
24
|
+
// surface root is not an ancestor and so touch dispatches don't work.
|
|
25
|
+
// Hosted content there owns its touches and is the origin it is measured from; native reads
|
|
26
|
+
// this one prop for both.
|
|
27
|
+
const layoutRoot = useIsPresentedInOwnWindow();
|
|
28
|
+
|
|
20
29
|
return (
|
|
21
30
|
<RNHostNativeView
|
|
22
31
|
{...props}
|
|
32
|
+
layoutRoot={layoutRoot}
|
|
23
33
|
// `matchContents` can only be used once on mount
|
|
24
34
|
// So we force unmount when it changes to prevent unexpected layout
|
|
25
|
-
key={props.matchContents ? 'matchContents' : 'noMatchContents'}
|
|
26
|
-
|
|
35
|
+
key={props.matchContents ? 'matchContents' : 'noMatchContents'}>
|
|
36
|
+
{/* Reset context here so only nearest RNHostView becomes the layout root for its children, and not any other RNHostView above it in the tree. */}
|
|
37
|
+
<PresentedContentContext.Provider value={false}>
|
|
38
|
+
{props.children}
|
|
39
|
+
</PresentedContentContext.Provider>
|
|
40
|
+
</RNHostNativeView>
|
|
27
41
|
);
|
|
28
42
|
}
|
|
@@ -2,6 +2,7 @@ import { requireNativeView } from 'expo';
|
|
|
2
2
|
import type { Ref } from 'react';
|
|
3
3
|
|
|
4
4
|
import { getStateId, type ObservableState, useWorkletProp, worklets } from '../../State';
|
|
5
|
+
import { useHostedTextInput } from '../../keyboard';
|
|
5
6
|
import type { ViewEvent } from '../../types';
|
|
6
7
|
import { Slot } from '../SlotView';
|
|
7
8
|
import { createViewModifierEventListener } from '../modifiers/utils';
|
|
@@ -74,14 +75,19 @@ function Placeholder({ children }: { children: React.ReactNode }) {
|
|
|
74
75
|
* Renders a SwiftUI `SecureField` for password input.
|
|
75
76
|
*/
|
|
76
77
|
export function SecureField(props: SecureFieldProps) {
|
|
77
|
-
const { text, onTextChange, onFocusChange, modifiers, ...restProps } = props;
|
|
78
|
+
const { text, onTextChange, onFocusChange, modifiers, ref, ...restProps } = props;
|
|
78
79
|
|
|
79
80
|
const isWorklet = !!onTextChange && !!worklets?.isWorkletFunction?.(onTextChange);
|
|
80
81
|
const workletCallback = useWorkletProp(isWorklet ? onTextChange : undefined, 'onTextChange');
|
|
82
|
+
// `blurOnUnmount`: a field left first responder while its row is removed makes UIKit
|
|
83
|
+
// assert ("refused to resign"); native teardown hooks run too late to clear SwiftUI's
|
|
84
|
+
// `@FocusState`. https://github.com/expo/expo/issues/49348
|
|
85
|
+
const hosted = useHostedTextInput<SecureFieldRef>(ref, onFocusChange, { blurOnUnmount: true });
|
|
81
86
|
|
|
82
87
|
return (
|
|
83
88
|
<SecureFieldNativeView
|
|
84
89
|
{...restProps}
|
|
90
|
+
ref={hosted.ref}
|
|
85
91
|
modifiers={modifiers}
|
|
86
92
|
{...(modifiers ? createViewModifierEventListener(modifiers) : undefined)}
|
|
87
93
|
text={getStateId(text)}
|
|
@@ -89,7 +95,7 @@ export function SecureField(props: SecureFieldProps) {
|
|
|
89
95
|
onTextChange={
|
|
90
96
|
!isWorklet && onTextChange ? (event) => onTextChange(event.nativeEvent.value) : undefined
|
|
91
97
|
}
|
|
92
|
-
onFocusChange={
|
|
98
|
+
onFocusChange={(event) => hosted.onFocusChange(event.nativeEvent.value)}
|
|
93
99
|
/>
|
|
94
100
|
);
|
|
95
101
|
}
|
|
@@ -2,6 +2,7 @@ import { requireNativeView } from 'expo';
|
|
|
2
2
|
import type { Ref } from 'react';
|
|
3
3
|
|
|
4
4
|
import { getStateId, type ObservableState, useWorkletProp, worklets } from '../../State';
|
|
5
|
+
import { useHostedTextInput } from '../../keyboard';
|
|
5
6
|
import type { ViewEvent } from '../../types';
|
|
6
7
|
import { Slot } from '../SlotView';
|
|
7
8
|
import { createViewModifierEventListener } from '../modifiers/utils';
|
|
@@ -114,15 +115,21 @@ export function TextField(props: TextFieldProps) {
|
|
|
114
115
|
onFocusChange,
|
|
115
116
|
onSelectionChange,
|
|
116
117
|
modifiers,
|
|
118
|
+
ref,
|
|
117
119
|
...restProps
|
|
118
120
|
} = props;
|
|
119
121
|
|
|
120
122
|
const isWorklet = !!onTextChange && !!worklets?.isWorkletFunction?.(onTextChange);
|
|
121
123
|
const workletCallback = useWorkletProp(isWorklet ? onTextChange : undefined, 'onTextChange');
|
|
124
|
+
// `blurOnUnmount`: a field left first responder while its row is removed makes UIKit
|
|
125
|
+
// assert ("refused to resign"); native teardown hooks run too late to clear SwiftUI's
|
|
126
|
+
// `@FocusState`. https://github.com/expo/expo/issues/49348
|
|
127
|
+
const hosted = useHostedTextInput<TextFieldRef>(ref, onFocusChange, { blurOnUnmount: true });
|
|
122
128
|
|
|
123
129
|
return (
|
|
124
130
|
<TextFieldNativeView
|
|
125
131
|
{...restProps}
|
|
132
|
+
ref={hosted.ref}
|
|
126
133
|
modifiers={modifiers}
|
|
127
134
|
{...(modifiers ? createViewModifierEventListener(modifiers) : undefined)}
|
|
128
135
|
text={getStateId(text)}
|
|
@@ -131,7 +138,7 @@ export function TextField(props: TextFieldProps) {
|
|
|
131
138
|
onTextChange={
|
|
132
139
|
!isWorklet && onTextChange ? (event) => onTextChange(event.nativeEvent.value) : undefined
|
|
133
140
|
}
|
|
134
|
-
onFocusChange={
|
|
141
|
+
onFocusChange={(event) => hosted.onFocusChange(event.nativeEvent.value)}
|
|
135
142
|
onSelectionChange={
|
|
136
143
|
onSelectionChange
|
|
137
144
|
? (event) =>
|
package/src/swift-ui/index.tsx
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
declare module 'react-native/Libraries/Components/TextInput/TextInputState' {
|
|
2
|
+
import type { ReactNativeElement } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React Native's registry of focusable text inputs. `ScrollView` reads it to
|
|
6
|
+
* decide whether a touch landed on the focused input, and `Keyboard.dismiss()`
|
|
7
|
+
* blurs whatever it points at. `TextInput.State` re-exports only a read-only
|
|
8
|
+
* subset publicly, so participating in the registry requires this module.
|
|
9
|
+
*/
|
|
10
|
+
const TextInputState: {
|
|
11
|
+
currentlyFocusedInput(): ReactNativeElement | null;
|
|
12
|
+
/** Marks an instance as focused. Does not dispatch a native focus command. */
|
|
13
|
+
focusInput(instance: ReactNativeElement | null): void;
|
|
14
|
+
/** Clears the focused instance, but only if it is the one passed in. */
|
|
15
|
+
blurInput(instance: ReactNativeElement | null): void;
|
|
16
|
+
registerInput(instance: ReactNativeElement): void;
|
|
17
|
+
unregisterInput(instance: ReactNativeElement): void;
|
|
18
|
+
isTextInput(instance: ReactNativeElement): boolean;
|
|
19
|
+
/** Marks an instance as focused, then dispatches a native `focus` command. */
|
|
20
|
+
focusTextInput(instance: ReactNativeElement | null): void;
|
|
21
|
+
/** Clears the focused instance, then dispatches a native `blur` command. */
|
|
22
|
+
blurTextInput(instance: ReactNativeElement | null): void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default TextInputState;
|
|
26
|
+
}
|