Commit ac7dee1
authored
fix: wrong selection coordinates on focus (#1234)
## 📜 Description
Fixed non-working `KeyboardAwareScrollView` on iOS.
## 💡 Motivation and Context
Turns out that selection is not available straight after input focus. To
fix this problem I decided to query selection in next frame:
```swift
DispatchQueue.main.async {
updateSelectionPosition(textInput: textInput, sendEvent: self.onSelectionChange)
}
```
And it works, and works pretty well, but it introduces a regression on
iOS < 16. The thing is that starting from iOS 16 Apple started to use
TextKit 2. And in TextKit 2 all operations are async, so for iOS 16+
we'll get events like layout updated -> selection updated -> onStart ->
onMove, but on iOS 15 it will be layout updated -> onStart -> selection
updated -> onMove.
The original JS code assumed `selection` always arrives before onStart.
On iOS < 16, `onStart` now fires first, so `updateLayoutFromSelection()`
reads stale `lastSelection.value` from the previous focus session -
producing a wrong `ayout.value` used by `maybeScroll` throughout the
keyboard animation.
So I added `pendingSelectionForFocus` flag - a shared value that tracks
whether `onStart` fired for a focus change but the corresponding
selection event hasn't arrived yet.
In `onStart` - when focus changes, check if
`lastSelection.value?.target` matches the new target:
- **matches** (iOS 16+ flow — selection arrived first): call
`updateLayoutFromSelection()` as before
- **doesn't match** (iOS < 16 flow — selection is late): set
`layout.value = input.value` as a safe fallback (full input height
instead of cursor-precise height), set the pending flag
The immediate scroll for focus-change-without-keyboard-appearing
(`focusWasChanged && !keyboardWillAppear`) is skipped when pending,
since we don't have accurate layout data yet.
In `onSelectionChange` - when the target changes and the pending flag is
set:
- clear the flag
- call `updateLayoutFromSelection()` with the now-correct selection data
- If keyboard was already visible (no `onMove` expected), perform the
deferred scroll
This ensures `layout.value` is correct before `onMove` starts using it
for scroll interpolation.
In onEnd — `lastSelection.value` is cleared to `null `when the keyboard
fully hides (`e.height === 0)`. This prevents a subtle bug: when
re-focusing the same input at a different cursor position,
`lastSelection.value?.target` would still match `e.target` from the
previous session, making `onStart` incorrectly think fresh selection
data was available.
Closes
#1218
## 📢 Changelog
<!-- High level overview of important changes -->
<!-- For example: fixed status bar manipulation; added new types
declarations; -->
<!-- If your changes don't affect one of platform/language below - then
remove this platform/language -->
### JS
- added `pendingSelectionForFocus` flag;
- added logic for proper selection updates depending on the flag state;
### iOS
- dispatch selection in next frame on focus so that it becomes
available;
### E2E
- re-generated Android 28 test assets;
## 🤔 How Has This Been Tested?
Tested manually on:
- iPhone 16 Pro (iOS 26.2, simulator);
- e2e_emulator_28 (API 28, emulator);
- iPhone 13 Pro (iOS 15.5, simulator).
## 📸 Screenshots (if appropriate):
|iOS 15|iOS 26|
|-------|------|
|<video
src="https://github.com/user-attachments/assets/36f4cc76-74be-4190-8ca6-f80b341333d9">|<video
src="https://github.com/user-attachments/assets/cbeb2c96-541d-447a-844c-8082399737d1">|
## 📝 Checklist
- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed1 parent 5ef74d9 commit ac7dee1
3 files changed
Lines changed: 46 additions & 8 deletions
File tree
- e2e/kit/assets/android/e2e_emulator_28
- ios/observers
- src/components/KeyboardAwareScrollView
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
142 | 142 | | |
143 | 143 | | |
144 | 144 | | |
145 | | - | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
146 | 148 | | |
147 | 149 | | |
148 | 150 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
149 | 149 | | |
150 | 150 | | |
151 | 151 | | |
| 152 | + | |
152 | 153 | | |
153 | 154 | | |
154 | 155 | | |
| |||
324 | 325 | | |
325 | 326 | | |
326 | 327 | | |
327 | | - | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
328 | 340 | | |
329 | 341 | | |
330 | 342 | | |
| |||
342 | 354 | | |
343 | 355 | | |
344 | 356 | | |
345 | | - | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
346 | 363 | | |
347 | 364 | | |
348 | 365 | | |
| |||
375 | 392 | | |
376 | 393 | | |
377 | 394 | | |
| 395 | + | |
378 | 396 | | |
379 | 397 | | |
380 | 398 | | |
| |||
391 | 409 | | |
392 | 410 | | |
393 | 411 | | |
394 | | - | |
395 | | - | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
396 | 426 | | |
397 | 427 | | |
398 | 428 | | |
399 | 429 | | |
400 | 430 | | |
401 | 431 | | |
402 | | - | |
403 | | - | |
404 | | - | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
405 | 437 | | |
406 | 438 | | |
407 | 439 | | |
| |||
435 | 467 | | |
436 | 468 | | |
437 | 469 | | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
438 | 474 | | |
439 | 475 | | |
440 | 476 | | |
| |||
0 commit comments