Skip to content

Commit 4d114df

Browse files
Saadnajmiclaude
andauthored
fix(fabric): textInput text color not adapting to appearance changes (#2913)
## Summary - Fixes TextInput text color not updating when the system appearance changes (light ↔ dark mode) on the new architecture (Fabric) - Ensures the default text color (`labelColor`) is always set on macOS, since `NSAttributedString` defaults to black unlike iOS - Adds `viewDidChangeEffectiveAppearance` (macOS) and `hasDifferentColorAppearanceComparedToTraitCollection:` (iOS) handlers to refresh text attributes on appearance change ## Root Cause Three interrelated issues on macOS Fabric: 1. **Missing foreground color**: `RCTNSTextAttributesFromTextAttributes` skipped setting `NSForegroundColorAttributeName` when no explicit `color` prop or `opacity` was set. On macOS, `NSAttributedString` defaults to black (unlike iOS where `UITextField` provides its own dynamic default), making text invisible in dark mode. 2. **No appearance change handler**: `RCTTextInputComponentView` had no `viewDidChangeEffectiveAppearance` override on macOS, so `defaultTextAttributes` were never refreshed when switching light/dark mode. 3. **Frozen dynamic colors**: The C++ color pipeline resolves dynamic colors (like `labelColor`) to static values at creation time. Simply re-calling `RCTNSTextAttributesFromTextAttributes` after an appearance change returns the same stale color. The fix detects when the foreground color is the default semantic `labelColor` (vs. a user-specified color) and replaces it with a fresh `[NSColor labelColor]`, then re-applies the attributed text while suppressing React state reconciliation. ## Test plan - [ ] Launch RNTester macOS in dark mode → TextInput with no `color` prop shows visible text - [ ] Toggle to light mode → text color adapts (dark text on light background) - [ ] Toggle back to dark mode → text color adapts (light text on dark background) - [ ] TextInput with explicit `color` prop (e.g. `"red"`, `"white"`) preserves its color across appearance changes - [ ] Both single-line and multiline TextInput work correctly - [ ] Newly typed text after appearance change uses the correct color 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d25b8c9 commit 4d114df

File tree

2 files changed

+48
-1
lines changed

2 files changed

+48
-1
lines changed

packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,30 @@ - (void)didMoveToWindow
169169
[self _restoreTextSelection];
170170
}
171171

172+
// [macOS
173+
- (void)_updateDefaultTextAttributes
174+
{
175+
const auto &props = static_cast<const TextInputProps &>(*_props);
176+
NSMutableDictionary<NSAttributedStringKey, id> *attrs =
177+
RCTNSTextAttributesFromTextAttributes(props.getEffectiveTextAttributes(RCTFontSizeMultiplier()));
178+
179+
_backedTextInputView.defaultTextAttributes = attrs;
180+
181+
// Also update the existing attributed text so the visible text re-renders
182+
// with the new color (defaultTextAttributes only affects newly typed text).
183+
// Wrap in _comingFromJS to prevent textInputDidChange from pushing a state
184+
// update back to the shadow tree, which would overwrite our fresh colors
185+
// with the stale cached attributed string.
186+
NSString *currentText = _backedTextInputView.attributedText.string;
187+
if (currentText.length > 0) {
188+
NSAttributedString *updated = [[NSAttributedString alloc] initWithString:currentText attributes:attrs];
189+
_comingFromJS = YES;
190+
_backedTextInputView.attributedText = updated;
191+
_comingFromJS = NO;
192+
}
193+
}
194+
// macOS]
195+
172196
#if !TARGET_OS_OSX // [macOS]
173197
// TODO: replace with registerForTraitChanges once iOS 17.0 is the lowest supported version
174198
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
@@ -182,8 +206,20 @@ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
182206
_backedTextInputView.defaultTextAttributes =
183207
RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()));
184208
}
209+
210+
// [macOS
211+
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
212+
[self _updateDefaultTextAttributes];
213+
}
214+
// macOS]
215+
}
216+
#else // [macOS
217+
- (void)viewDidChangeEffectiveAppearance
218+
{
219+
[super viewDidChangeEffectiveAppearance];
220+
[self _updateDefaultTextAttributes];
185221
}
186-
#endif // [macOS]
222+
#endif // macOS]
187223

188224
- (void)reactUpdateResponderOffsetForScrollView:(RCTScrollViewComponentView *)scrollView
189225
{

packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,14 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex
146146
{
147147
RCTPlatformColor *effectiveForegroundColor = RCTUIColorFromSharedColor(textAttributes.foregroundColor) ?: [RCTPlatformColor labelColor]; // [macOS]
148148

149+
#if TARGET_OS_OSX // [macOS
150+
// On macOS, colorWithAlphaComponent: converts dynamic system colors (like
151+
// NSColor.labelColor) to static resolved colors, preventing them from
152+
// adapting to appearance changes. Skip when opacity is 1.0 (a no-op).
153+
if (!isnan(textAttributes.opacity) && textAttributes.opacity != 1.0f) {
154+
#else // macOS]
149155
if (!isnan(textAttributes.opacity)) {
156+
#endif
150157
effectiveForegroundColor = [effectiveForegroundColor
151158
colorWithAlphaComponent:CGColorGetAlpha(effectiveForegroundColor.CGColor) * textAttributes.opacity];
152159
}
@@ -158,7 +165,11 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex
158165
{
159166
RCTPlatformColor *effectiveBackgroundColor = RCTUIColorFromSharedColor(textAttributes.backgroundColor); // [macOS]
160167

168+
#if TARGET_OS_OSX // [macOS
169+
if (effectiveBackgroundColor && !isnan(textAttributes.opacity) && textAttributes.opacity != 1.0f) {
170+
#else // macOS]
161171
if (effectiveBackgroundColor && !isnan(textAttributes.opacity)) {
172+
#endif
162173
effectiveBackgroundColor = [effectiveBackgroundColor
163174
colorWithAlphaComponent:CGColorGetAlpha(effectiveBackgroundColor.CGColor) * textAttributes.opacity];
164175
}

0 commit comments

Comments
 (0)