Skip to content

[iOS/macOS] [TextInput] Implement ghost text#1897

Merged
Saadnajmi merged 2 commits intomainfrom
user/nakambo/ghost
Aug 2, 2023
Merged

[iOS/macOS] [TextInput] Implement ghost text#1897
Saadnajmi merged 2 commits intomainfrom
user/nakambo/ghost

Conversation

@nakambo
Copy link
Copy Markdown
Collaborator

@nakambo nakambo commented Aug 2, 2023

Please select one of the following

  • I am removing an existing difference between facebook/react-native and microsoft/react-native-macos 👍
  • I am cherry-picking a change from Facebook's react-native into microsoft/react-native-macos 👍
  • I am making a fix / change for the macOS implementation of react-native
  • I am making a change required for Microsoft usage of react-native

Summary

Ghost text is hint text that is inserted inline as a hint to the user (for example predictive text). Unlike autocomplete for combo boxes, which is "visible" to the model, ghost text is "invisible" or transparent to the model (i.e. not observable in any callback or imperative method). Ghost text isn't selectable by the user.

Ideally we do ghost text only in the display layer, meaning it isn't in the model. Unfortunately it doesn't look like we can render ghost text and still have Apple flow text for us (ghost text that is in the middle of regular text will want to reflow the regular text to make room for ghost text). Overriding all text box rendering seems like substantial rework (we need to deal with all drawing, including but not limited to the text box background, text, insertion pointer\selection, ... as well as other ancillary things around positioning related input UI such as IME's), so here we DO introduce it in the model, but try to keep awareness on the native side, and away from Application logic.

Since ghost text is presumed dependent on immediate user context (e.g. text and selection), we reset ghost text automatically when either of these change. We could not do that, but that requires us to deal with any possible edits around\on the ghost text, as well as actually ensure we "hide" ghost text from any callbacks made to the app (something we are getting away with right now since we remove ghost text on pretty much any user interaction).

Plumbing for new imperative method to set ghost text.

  • Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js
  • Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js
  • Libraries/Components/TextInput/TextInput.flow.js
  • Libraries/Components/TextInput/TextInput.js
  • Libraries/Components/TextInput/TextInputNativeCommands.js
  • Libraries/Text/TextInput/RCTBaseTextInputViewManager.m

Introduce a property on the RCTUITextView and RCTUITextField to store state on whether we're currently in the midst of setting\clearing ghost text. We use this to suppress various callbacks to the Application that may be triggered as a result of us changing the ghost text\selection to compensate for addition\removal of ghost text.

  • Libraries/Text/TextInput/Multiline/RCTUITextView.h
  • Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
  • Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h
  • Libraries/Text/TextInput/Singleline/RCTUITextField.h

Actual ghost text management logic. I placed as much as I reasonably could here since that allows us to share most of the functionality between single and multi line text inputs.

  • Libraries/Text/TextInput/RCTBaseTextInputView.h
  • Libraries/Text/TextInput/RCTBaseTextInputView.m

Test page for ghost text. Shows predictive text example.

  • packages/rn-tester/js/examples/GhostText/GhostText.js
  • packages/rn-tester/js/utils/RNTesterList.ios.js

Changelog

General Changed - Initial commit

Test Plan

Testing (macOS\iOS x multi\single line):

  • Type predictable text (see test page for list), see that we get ghost text and that isn't observable in the callbacks
  • Type non-predictable text, check ordering of events consistent with predictable text case
  • Log calls to setGhostText: on native. Ensure we clear ghost text (due to text chagnes)
  • Change selection with ghost text, check that we remove ghost text (due to selection change)
  • Hit Esc in text box with ghost text, check that we remove ghost text (due to focus loss)

Test page example video: https://github.com/microsoft/react-native-macos/assets/72474613/38f466c3-695b-4526-bbb2-a5afb8e333d8

nakambo and others added 2 commits August 2, 2023 12:14
Ghost text is hint text that is inserted inline as a hint to the user (for example predictive text). Unlike autocomplete for combo boxes, which is "visible" to the model, ghost text is "invisible" or transparent to the model (i.e. not observable in any callback or imperative method). Ghost text isn't selectable by the user.

Ideally we do ghost text only in the display layer, meaning it isn't in the model. Unfortunately it doesn't look like we can render ghost text and still have Apple flow text for us (ghost text that is in the middle of regular text will want to reflow the regular text to make room for ghost text). Overriding all text box rendering seems like substantial rework (we need to deal with all drawing, including but not limited to the text box background, text, insertion pointer\selection, ... as well as other ancillary things around positioning related input UI such as IME's), so here we DO introduce it in the model, but try to keep awareness on the native side, and away from Application logic.

Since ghost text is presumed dependent on immediate user context (e.g. text and selection), we reset ghost text automatically when either of these change. We could *not* do that, but that requires us to deal with any possible edits around\on the ghost text, as well as actually ensure we "hide" ghost text from any callbacks made to the app (something we are getting away with right now since we remove ghost text on pretty much any user interaction).

Plumbing for new imperative method to set ghost text.

* Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js
* Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js
* Libraries/Components/TextInput/TextInput.flow.js
* Libraries/Components/TextInput/TextInput.js
* Libraries/Components/TextInput/TextInputNativeCommands.js
* Libraries/Text/TextInput/RCTBaseTextInputViewManager.m

Introduce a property on the `RCTUITextView` and `RCTUITextField` to store state on whether we're currently in the midst of setting\clearing ghost text. We use this to suppress various callbacks to the Application that may be triggered as a result of us changing the ghost text\selection to compensate for addition\removal of ghost text.

* Libraries/Text/TextInput/Multiline/RCTUITextView.h
* Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
* Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h
* Libraries/Text/TextInput/Singleline/RCTUITextField.h

Actual ghost text management logic. I placed as much as I reasonably could here since that allows us to share most of the functionality between single and multi line text inputs.

* Libraries/Text/TextInput/RCTBaseTextInputView.h
* Libraries/Text/TextInput/RCTBaseTextInputView.m

Reintroduce `onTextInput` into Flow typing. This was removed in commit 3f7e0a2, but it looks like it wasn't ever removed on the native side, so the callback still "works" for now. I am using this callback to determine ghost text behavior correctness, and since it is fired, I need it back. When we truly remove this from the native side, we can stop caring about it.

* Libraries/Components/TextInput/TextInput.flow.js
* Libraries/Components/TextInput/TextInput.js

Test page for ghost text. Shows predictive text example.

* packages/rn-tester/js/examples/GhostText/GhostText.js
* packages/rn-tester/js/utils/RNTesterList.ios.js

Testing (macOS\iOS x multi\single line):

* Type predictable text (see test page for list), see that we get ghost text and that isn't observable in the callbacks
* Type non-predictable text, check ordering of events consistent with predictable text case
* Log calls to `setGhostText:` on native. Ensure we clear ghost text (due to text chagnes)
* Change selection with ghost text, check that we remove ghost text (due to selection change)
* Hit Esc in text box with ghost text, check that we remove ghost text (due to focus loss)
@nakambo nakambo requested a review from Saadnajmi August 2, 2023 21:14
@nakambo nakambo requested a review from a team as a code owner August 2, 2023 21:14
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Aug 2, 2023

Fails
🚫

📋 Verify Changelog Format - A changelog entry has the following format: [CATEGORY] [TYPE] - Message.

DetailsCATEGORY may be:
  • General
  • macOS
  • iOS
  • Android
  • JavaScript
  • Internal (for changes that do not need to be called out in the release notes)

TYPE may be:

  • Added, for new features.
  • Changed, for changes in existing functionality.
  • Deprecated, for soon-to-be removed features.
  • Removed, for now removed features.
  • Fixed, for any bug fixes.
  • Security, in case of vulnerabilities.

MESSAGE may answer "what and why" on a feature level. Use this to briefly tell React Native users about notable changes.

Generated by 🚫 dangerJS against 5708151

@Saadnajmi Saadnajmi merged commit ebf1273 into main Aug 2, 2023
@Saadnajmi Saadnajmi deleted the user/nakambo/ghost branch January 12, 2024 21:14
Saadnajmi added a commit that referenced this pull request Apr 22, 2026
…tion fixes (#2912)

## Summary

TextInput commands (`setGhostText`, `blur`, `setSelection`) don't work
correctly under Fabric on macOS. This PR fixes three issues:

- **Add missing `setGhostText` command to Fabric**: Ghost text was
implemented for Paper in #1890 / #1897 (with undo fixes in #2105 /
#2106), but the Fabric command dispatcher and component view never got
the implementation. This ports the ghost text logic (insertion, removal,
attributes, delegate cleanup) from `RCTBaseTextInputView` to
`RCTTextInputComponentView`.
- **Fix `blur` command on macOS Fabric**: `blur` was a silent no-op
because it compared `[window firstResponder]` against the NSTextField,
but on macOS the firstResponder during editing is the field editor
(NSTextView), not the text field itself. Fixed by checking
`currentEditor` on NSTextField.
- **Fix ghost text being selectable on Fabric**: Ghost text could be
selected by the user on Fabric (but not Paper). Added cleanup in
`textInputDidChangeSelection` to clear ghost text when the user changes
selection, matching Paper behavior.
- **Fix `setTextAndSelection` delegate notification**: Changed
`notifyDelegate:YES` to `notifyDelegate:NO` to match Paper's
`setSelectionStart:selectionEnd:` behavior and prevent spurious delegate
callbacks during programmatic selection changes.

## Test plan

- [x] Verified `setGhostText` command works via RNTester on macOS Fabric
- [x] Verified `blur` command works on macOS Fabric  
- [x] Verified ghost text is not selectable on Fabric
- [x] Verified `focus`, `clear`, `setSelection` commands still work
- [x] JS TextInput tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Saadnajmi added a commit that referenced this pull request Apr 22, 2026
…ion fixes (#2937)

## Summary

Backport of #2912 to 0.81-stable.

- **Add missing `setGhostText` command to Fabric**: Ghost text was
implemented for Paper in #1890 / #1897 (with undo fixes in #2105 /
#2106), but the Fabric command dispatcher and component view never got
the implementation. This ports the ghost text logic (insertion, removal,
attributes, delegate cleanup) from `RCTBaseTextInputView` to
`RCTTextInputComponentView`.
- **Fix `blur` command on macOS Fabric**: `blur` was a silent no-op
because it compared `[window firstResponder]` against the NSTextField,
but on macOS the firstResponder during editing is the field editor
(NSTextView), not the text field itself. Fixed by checking
`currentEditor` on NSTextField.
- **Fix ghost text being selectable on Fabric**: Ghost text could be
selected by the user on Fabric (but not Paper). Added cleanup in
`textInputDidChangeSelection` to clear ghost text when the user changes
selection, matching Paper behavior.
- **Fix `setTextAndSelection` delegate notification**: Changed
`notifyDelegate:YES` to `notifyDelegate:NO` to match Paper's
`setSelectionStart:selectionEnd:` behavior and prevent spurious delegate
callbacks during programmatic selection changes.

## Test plan

- [x] Verified `setGhostText` command works via RNTester on macOS Fabric
- [x] Verified `blur` command works on macOS Fabric
- [x] Verified ghost text is not selectable on Fabric
- [x] Verified `focus`, `clear`, `setSelection` commands still work
- [x] JS TextInput tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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