Skip to content

feat: add getCaretRect() to read the caret bounding rect#652

Open
dennistan95 wants to merge 2 commits into
software-mansion:mainfrom
dennistan95:feat/get-caret-rect
Open

feat: add getCaretRect() to read the caret bounding rect#652
dennistan95 wants to merge 2 commits into
software-mansion:mainfrom
dennistan95:feat/get-caret-rect

Conversation

@dennistan95

Copy link
Copy Markdown

Summary

Adds an async getCaretRect() to the EnrichedTextInput imperative handle so consumers can anchor UI — link popovers, floating toolbars, mention menus — to the live caret.

onChangeSelection exposes only character offsets, so there's currently no way to position an overlay at the caret. Native already has the pixel geometry (UITextView caretRectForPosition: on iOS, Layout on Android); this just surfaces it to JS, mirroring the existing getHTML() / requestHTML request→response round-trip exactly.

API

interface CaretRect { x: number; y: number; width: number; height: number; }

// on EnrichedTextInputInstance:
getCaretRect: () => Promise<CaretRect | null>;

Resolves the caret rect in the editor view's coordinate space (RN dp / iOS points), or null when unavailable. A built-in timeout resolves null rather than leaking a pending promise, so callers always get a definite result.

const rect = await ref.current?.getCaretRect();
if (rect) setPopover({ left: rect.x, top: rect.y - POPOVER_HEIGHT }); // e.g. above the caret

Changes

Layer Change
src/spec/EnrichedTextInputNativeComponent.ts OnCaretRectEvent event, onCaretRect prop, requestCaretRect command (+ supportedCommands)
src/types.ts CaretRect type + getCaretRect() on EnrichedTextInputInstance
src/native/EnrichedTextInput.tsx getCaretRect() (requestId→promise with timeout fallback), onCaretRect handler, unmount cleanup
ios/EnrichedTextInputView.mm requestCaretRect:caretRectForPosition: converted into the component view's space; emits a valid flag when the rect is null/infinite
android/.../EnrichedTextInputView.kt requestCaretRect()Layout primary-horizontal + line top/bottom, padding/scroll-adjusted, px→dp for parity with iOS points
android/.../EnrichedTextInputViewManager.kt requestCaretRect override + event registration
android/.../events/OnCaretRectEvent.kt new direct event (requestId, x, y, width, height, valid)

Both platforms emit a valid flag so JS can distinguish "no caret / unknown" from a real zero-origin rect.

Note for maintainers

This package ships pre-generated Fabric codegen (includesGeneratedCode: true) and a compiled lib/. After review, the generated artifacts (ios/generated, android/generated) and lib/ will need regenerating (codegen + bob build) before a release picks up the new command/event.

Testing

  • iOS: caret rect tracks the insertion point across lines/wrapping; valid = false with no selection.
  • Android: rect matches iOS within rounding (dp-normalized).
  • Degradation: on a build without the native side, getCaretRect() resolves null via the timeout (no hang).

Adds an async getCaretRect() to the EnrichedTextInput imperative handle so
consumers can anchor UI (link popovers, floating toolbars, mention menus) to
the live caret. onChangeSelection only exposes character offsets, so there was
no way to position an overlay at the caret; native already has the geometry
(UITextView caretRectForPosition: on iOS, Layout on Android) — this surfaces it
to JS, mirroring the existing getHTML()/requestHTML request-response round-trip.

- spec: OnCaretRectEvent event, onCaretRect prop, requestCaretRect command
- JS: getCaretRect() (requestId->promise with a timeout fallback so it resolves
  null instead of hanging), onCaretRect handler, unmount cleanup
- iOS: caretRectForPosition: converted into the component view's space; emits a
  `valid` flag when the rect is null/infinite
- Android: Layout primary-horizontal + line top/bottom, padding/scroll-adjusted,
  px->dp for parity with iOS points; new OnCaretRectEvent + manager registration

Note: this package ships pre-generated Fabric codegen (includesGeneratedCode)
and a compiled lib/, so the generated artifacts + lib/ need regenerating
(codegen + bob build) before release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 22, 2026 06:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new imperative API, getCaretRect(), to EnrichedTextInput so JS consumers can retrieve the caret’s on-screen bounding rect (in editor view coordinates) and position anchored UI (toolbars/popovers/menus) relative to the live insertion point.

Changes:

  • Adds a new caret-rect request/response round-trip: requestCaretRect command + onCaretRect event (with requestId and valid flag).
  • Implements getCaretRect() in the native JS wrapper with a timeout-based null fallback to avoid hanging promises.
  • Adds native implementations for iOS (caretRectForPosition) and Android (Layout-based caret geometry) and dispatches the result via a direct event.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/types.ts Introduces CaretRect and adds getCaretRect() to the imperative handle type.
src/spec/EnrichedTextInputNativeComponent.ts Adds OnCaretRectEvent, onCaretRect prop, and requestCaretRect command to the codegen spec.
src/native/EnrichedTextInput.tsx Implements the JS-side requestId→promise mapping, timeout fallback, event handler, and unmount cleanup.
ios/EnrichedTextInputView.mm Adds the native command handler and emits caret rect geometry via onCaretRect. Also modifies selection clamping logic.
android/src/main/java/com/swmansion/enriched/textinput/events/OnCaretRectEvent.kt Adds a new direct event payload for caret rect responses.
android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt Registers the new event and wires the requestCaretRect command to the view.
android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt Computes caret rect from Layout, converts px→dp, and dispatches OnCaretRectEvent.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ios/EnrichedTextInputView.mm Outdated
Comment on lines +1341 to +1345
- (void)setCustomSelection:(NSInteger)visibleStart end:(NSInteger)visibleEnd {
NSString *text = textView.textStorage.string;

NSUInteger actualStart = [self getActualIndex:visibleStart text:text];
NSUInteger actualEnd = [self getActualIndex:visibleEnd text:text];
NSUInteger textLength = textView.textStorage.string.length;
NSInteger clampedStart = MAX(visibleStart, 0);
NSInteger clampedEnd = MAX(visibleEnd, clampedStart);
NSUInteger actualStart = MIN((NSUInteger)clampedStart, textLength);
var widthDp = 0.0
var heightDp = 0.0
val currentLayout = layout
val pos = selectionStart
Comment thread src/types.ts
Comment on lines +460 to +466
/**
* Resolves the current caret's bounding rect (for positioning UI like a link
* popover or floating toolbar) in the editor view's coordinate space. Resolves
* `null` if unavailable or the native event doesn't arrive — callers should
* treat `null` as "no anchor".
*/
getCaretRect: () => Promise<CaretRect | null>;
… target

- iOS: revert an unrelated setCustomSelection change that slipped in from a
  packaging diff (restores getActualIndex ZWSP-aware mapping).
- Android: anchor requestCaretRect to selectionEnd (the active caret), matching
  iOS selectedTextRange.end, so a non-empty selection reports the live caret.
- Web: implement getCaretRect via ProseMirror coordsAtPos (selection end),
  converted to editor-element coordinates — so the cross-platform instance type
  is fully satisfied and web/build targets don't break.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dennistan95

Copy link
Copy Markdown
Author

Thanks for the review — all three addressed in the latest commit:

  1. setCustomSelection change — that was unrelated and slipped in from a packaging diff; reverted to the original getActualIndex (ZWSP-aware) mapping.
  2. Android anchoring to selectionStart — switched to selectionEnd so it reports the active caret/insertion point, matching iOS selectedTextRange.end.
  3. Web target missing getCaretRect — implemented it via ProseMirror coordsAtPos(selection.to), converted to editor-element coordinates, so the cross-platform EnrichedTextInputInstance contract is fully satisfied.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants