Skip to content

Commit 720ac15

Browse files
authored
fix(fabric): colors not respecting dark mode appearance, implement platform color (#2867)
1 parent 6103a32 commit 720ac15

File tree

10 files changed

+352
-11
lines changed

10 files changed

+352
-11
lines changed

docsite/api/intro.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ slug: /
77

88
Welcome to the React Native macOS API reference documentation. This section covers macOS-specific props and events that extend the standard React Native components.
99

10-
Most of the additional functionality out of React Native macOS directly is in the form of additional props and callback events implemented on `<View>`, to provide macOS and desktop specific behavior
10+
Most of the additional functionality out of React Native macOS directly is in the form of additional props and callback events implemented on `<View>`, to provide macOS and desktop specific behavior. We also have some additional APIs, like platform specific colors.

docsite/api/platform-color.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
sidebar_label: 'Platform Colors'
3+
sidebar_position: 2
4+
---
5+
6+
# Platform Colors
7+
8+
React Native macOS extends the core `PlatformColor` API with helpers that map directly to AppKit system colors. These helpers make it easier to adopt macOS appearance and accessibility behaviors without writing native code.
9+
10+
## `DynamicColorMacOS`
11+
12+
`DynamicColorMacOS` creates a color that automatically adapts to light, dark, and high-contrast appearances on macOS.
13+
14+
:::note
15+
`DynamicColorIOS` works on macOS too, they are essentially equivalent
16+
:::
17+
18+
| Option | Description |
19+
| -------------------- | --------------------------------------------------------------- |
20+
| `light` | Color used in the standard light appearance. |
21+
| `dark` | Color used in the standard dark appearance. |
22+
| `highContrastLight` | Optional color for high-contrast light mode. Defaults to `light`.|
23+
| `highContrastDark` | Optional color for high-contrast dark mode. Defaults to `dark`. |
24+
25+
## `ColorWithSystemEffectMacOS`
26+
27+
`ColorWithSystemEffectMacOS(color, effect)` wraps an existing color so AppKit can apply control state effects such as pressed, disabled, or rollover.
28+
29+
| Parameter | Description |
30+
| --------- | ----------- |
31+
| `color` | A string produced by `PlatformColor`, `DynamicColorMacOS`, or a CSS color string. |
32+
| `effect` | One of `none`, `pressed`, `deepPressed`, `disabled`, or `rollover`. |
33+
34+
```javascript
35+
import {
36+
ColorWithSystemEffectMacOS,
37+
DynamicColorMacOS,
38+
PlatformColor,
39+
StyleSheet,
40+
} from 'react-native';
41+
42+
const styles = StyleSheet.create({
43+
buttonPressed: {
44+
backgroundColor: ColorWithSystemEffectMacOS(
45+
PlatformColor('controlColor'),
46+
'pressed',
47+
),
48+
},
49+
});
50+
```

docsite/sidebarsApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
33
const sidebars: SidebarsConfig = {
44
apiSidebar: [
55
'intro',
6+
'platform-color',
67
'view-props',
78
'view-events',
89
],

packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1171,7 +1171,7 @@ - (void)invalidateLayer
11711171
#if !TARGET_OS_OSX // [macOS]
11721172
RCTPlatformColor *backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection];
11731173
#else // [macOS
1174-
RCTPlatformColor *backgroundColor = _backgroundColor;
1174+
RCTPlatformColor *backgroundColor = [_backgroundColor resolvedColorWithAppearance:self.effectiveAppearance];
11751175
#endif // macOS]
11761176
// The reason we sometimes do not set self.layer's backgroundColor is because
11771177
// we want to support non-uniform border radii, which apple does not natively
@@ -1202,6 +1202,9 @@ - (void)invalidateLayer
12021202

12031203
layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left;
12041204
RCTPlatformColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left); // [macOS]
1205+
#if TARGET_OS_OSX // [macOS
1206+
borderColor = [borderColor resolvedColorWithAppearance:self.effectiveAppearance];
1207+
#endif // macOS]
12051208
layer.borderColor = borderColor.CGColor;
12061209
layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft.horizontal;
12071210

@@ -1223,6 +1226,12 @@ - (void)invalidateLayer
12231226
layer.cornerRadius = 0;
12241227

12251228
RCTBorderColors borderColors = RCTCreateRCTBorderColorsFromBorderColors(borderMetrics.borderColors);
1229+
#if TARGET_OS_OSX // [macOS
1230+
borderColors.top = [borderColors.top resolvedColorWithAppearance:self.effectiveAppearance];
1231+
borderColors.left = [borderColors.left resolvedColorWithAppearance:self.effectiveAppearance];
1232+
borderColors.bottom = [borderColors.bottom resolvedColorWithAppearance:self.effectiveAppearance];
1233+
borderColors.right = [borderColors.right resolvedColorWithAppearance:self.effectiveAppearance];
1234+
#endif // macOS]
12261235

12271236
RCTAddContourEffectToLayer(
12281237
_borderLayer,
@@ -1249,10 +1258,16 @@ - (void)invalidateLayer
12491258

12501259
if (borderMetrics.borderRadii.isUniform() && borderMetrics.borderRadii.topLeft.horizontal == 0) {
12511260
RCTPlatformColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor); // [macOS]
1261+
#if TARGET_OS_OSX // [macOS
1262+
outlineColor = [outlineColor resolvedColorWithAppearance:self.effectiveAppearance];
1263+
#endif // macOS]
12521264
_outlineLayer.borderWidth = _props->outlineWidth;
12531265
_outlineLayer.borderColor = outlineColor.CGColor;
12541266
} else {
12551267
RCTPlatformColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor); // [macOS]
1268+
#if TARGET_OS_OSX // [macOS
1269+
outlineColor = [outlineColor resolvedColorWithAppearance:self.effectiveAppearance];
1270+
#endif // macOS]
12561271

12571272
RCTAddContourEffectToLayer(
12581273
_outlineLayer,

packages/react-native/React/RCTUIKit/RCTUIKitCompat.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ NS_ASSUME_NONNULL_BEGIN
2727
#else
2828
@compatibility_alias RCTPlatformColor NSColor;
2929
@compatibility_alias RCTUIColor NSColor;
30+
31+
@interface NSColor (RCTAppearanceResolving)
32+
/// Resolve a dynamic/semantic NSColor for a specific appearance, analogous to
33+
/// UIColor's -resolvedColorWithTraitCollection: on iOS.
34+
- (NSColor *)resolvedColorWithAppearance:(NSAppearance *)appearance;
35+
@end
36+
3037
#endif
3138

3239
// MARK: - Event types
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
// [macOS]
9+
10+
#import "RCTUIKitCompat.h"
11+
12+
#if TARGET_OS_OSX
13+
14+
@implementation NSColor (RCTAppearanceResolving)
15+
16+
- (NSColor *)resolvedColorWithAppearance:(NSAppearance *)appearance
17+
{
18+
__block NSColor *resolved = self;
19+
[appearance performAsCurrentDrawingAppearance:^{
20+
CGColorRef cgColor = self.CGColor;
21+
if (cgColor) {
22+
NSColor *fromCG = [NSColor colorWithCGColor:cgColor];
23+
if (fromCG) {
24+
resolved = fromCG;
25+
}
26+
}
27+
}];
28+
return resolved;
29+
}
30+
31+
@end
32+
33+
#endif // TARGET_OS_OSX

packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,19 @@ struct DynamicColor {
2020
int32_t highContrastDarkColor = 0;
2121
};
2222

23+
#if TARGET_OS_OSX // [macOS
24+
struct ColorWithSystemEffect {
25+
int32_t color = 0;
26+
std::string effect;
27+
};
28+
#endif // macOS]
29+
2330
struct Color {
2431
Color(int32_t color);
2532
Color(const DynamicColor& dynamicColor);
33+
#if TARGET_OS_OSX // [macOS
34+
Color(const ColorWithSystemEffect& colorWithSystemEffect);
35+
#endif // macOS]
2636
Color(const ColorComponents& components);
2737
Color() : uiColor_(nullptr){};
2838
int32_t getColor() const;

packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,33 @@
2020

2121
namespace facebook::react {
2222

23+
#if TARGET_OS_OSX // [macOS
24+
RCTPlatformColor *_Nullable UIColorFromColorWithSystemEffect(
25+
RCTUIColor *baseColor,
26+
const std::string &systemEffectString)
27+
{
28+
if (baseColor == nil) {
29+
return nil;
30+
}
31+
32+
NSColor *colorWithEffect = baseColor;
33+
if (!systemEffectString.empty()) {
34+
if (systemEffectString == "none") {
35+
colorWithEffect = [baseColor colorWithSystemEffect:NSColorSystemEffectNone];
36+
} else if (systemEffectString == "pressed") {
37+
colorWithEffect = [baseColor colorWithSystemEffect:NSColorSystemEffectPressed];
38+
} else if (systemEffectString == "deepPressed") {
39+
colorWithEffect = [baseColor colorWithSystemEffect:NSColorSystemEffectDeepPressed];
40+
} else if (systemEffectString == "disabled") {
41+
colorWithEffect = [baseColor colorWithSystemEffect:NSColorSystemEffectDisabled];
42+
} else if (systemEffectString == "rollover") {
43+
colorWithEffect = [baseColor colorWithSystemEffect:NSColorSystemEffectRollover];
44+
}
45+
}
46+
return colorWithEffect;
47+
}
48+
#endif // macOS]
49+
2350
namespace {
2451

2552
bool UIColorIsP3ColorSpace(const std::shared_ptr<void> &uiColor)
@@ -104,7 +131,6 @@ bool UIColorIsP3ColorSpace(const std::shared_ptr<void> &uiColor)
104131
} else {
105132
return nil;
106133
}
107-
108134
return nil;
109135
}
110136

@@ -120,7 +146,11 @@ int32_t ColorFromColorComponents(const facebook::react::ColorComponents &compone
120146
int32_t ColorFromUIColor(RCTPlatformColor *color) // [macOS]
121147
{
122148
CGFloat rgba[4];
149+
#if !TARGET_OS_OSX // [macOS]
123150
[color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]];
151+
#else // [macOS
152+
[[color colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]] getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha: &rgba[3]];
153+
#endif // macOS]
124154
return ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]});
125155
}
126156

@@ -137,15 +167,29 @@ int32_t ColorFromUIColorForSpecificTraitCollection(
137167

138168
return 0;
139169
}
170+
#else // [macOS
171+
int32_t ColorFromUIColorForSpecificAppearance(
172+
const std::shared_ptr<void> &uiColor,
173+
NSAppearance *appearance)
174+
{
175+
RCTPlatformColor *color = (RCTPlatformColor *)unwrapManagedObject(uiColor);
176+
if (color) {
177+
__block int32_t resolvedColorInt = 0;
178+
[appearance performAsCurrentDrawingAppearance:^{
179+
resolvedColorInt = ColorFromUIColor(color);
180+
}];
181+
return resolvedColorInt;
182+
}
183+
return 0;
184+
}
140185
#endif // [macOS]
141186

142187
int32_t ColorFromUIColor(const std::shared_ptr<void> &uiColor)
143188
{
144189
#if !TARGET_OS_OSX // [macOS]
145190
return ColorFromUIColorForSpecificTraitCollection(uiColor, [UITraitCollection currentTraitCollection]);
146191
#else // [macOS
147-
RCTPlatformColor *color = (RCTPlatformColor *)unwrapManagedObject(uiColor);
148-
return ColorFromUIColor(color);
192+
return ColorFromUIColorForSpecificAppearance(uiColor, [NSApp effectiveAppearance]);
149193
#endif // macOS]
150194
}
151195

@@ -170,9 +214,7 @@ int32_t ColorFromUIColor(const std::shared_ptr<void> &uiColor)
170214
return 0;
171215
}
172216

173-
#if TARGET_OS_OSX // [macOS]
174-
return ColorFromUIColor(uiColor);
175-
#else // [macOS
217+
#if !TARGET_OS_OSX // [macOS]
176218
static UITraitCollection *darkModeTraitCollection =
177219
[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
178220
auto darkColor = ColorFromUIColorForSpecificTraitCollection(uiColor, darkModeTraitCollection);
@@ -202,6 +244,15 @@ int32_t ColorFromUIColor(const std::shared_ptr<void> &uiColor)
202244
darkAccessibilityContrastColor,
203245
lightAccessibilityContrastColor,
204246
UIColorIsP3ColorSpace(uiColor));
247+
#else // [macOS
248+
// Hash both light and dark appearance colors to properly distinguish
249+
// dynamic colors that change with appearance.
250+
auto darkColor = ColorFromUIColorForSpecificAppearance(
251+
uiColor, [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]);
252+
auto lightColor = ColorFromUIColorForSpecificAppearance(
253+
uiColor, [NSAppearance appearanceNamed:NSAppearanceNameAqua]);
254+
255+
return facebook::react::hash_combine(darkColor, lightColor);
205256
#endif // macOS]
206257
}
207258

@@ -224,6 +275,21 @@ int32_t ColorFromUIColor(const std::shared_ptr<void> &uiColor)
224275
0);
225276
}
226277

278+
#if TARGET_OS_OSX // [macOS
279+
Color::Color(const ColorWithSystemEffect &colorWithSystemEffect)
280+
{
281+
RCTUIColor *baseColor = UIColorFromInt32(colorWithSystemEffect.color);
282+
RCTUIColor *colorWithEffect =
283+
UIColorFromColorWithSystemEffect(baseColor, colorWithSystemEffect.effect);
284+
if (colorWithEffect != nil) {
285+
uiColor_ = wrapManagedObject(colorWithEffect);
286+
}
287+
uiColorHashValue_ = facebook::react::hash_combine(
288+
colorWithSystemEffect.color,
289+
std::hash<std::string>{}(colorWithSystemEffect.effect));
290+
}
291+
#endif // macOS]
292+
227293
Color::Color(const ColorComponents &components)
228294
{
229295
uiColor_ = wrapManagedObject(UIColorFromComponentsColor(components));

packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/PlatformColorParser.mm

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,24 @@ SharedColor parsePlatformColor(const ContextContainer &contextContainer, int32_t
6262
items.at("dynamic").hasType<std::unordered_map<std::string, RawValue>>()) {
6363
auto dynamicItems = (std::unordered_map<std::string, RawValue>)items.at("dynamic");
6464
return RCTPlatformColorComponentsFromDynamicItems(contextContainer, surfaceId, dynamicItems);
65+
#if TARGET_OS_OSX // [macOS
66+
} else if (
67+
items.find("colorWithSystemEffect") != items.end() &&
68+
items.at("colorWithSystemEffect").hasType<std::unordered_map<std::string, RawValue>>()) {
69+
auto colorWithSystemEffectItems =
70+
(std::unordered_map<std::string, RawValue>)items.at("colorWithSystemEffect");
71+
if (colorWithSystemEffectItems.find("baseColor") != colorWithSystemEffectItems.end() &&
72+
colorWithSystemEffectItems.find("systemEffect") != colorWithSystemEffectItems.end() &&
73+
colorWithSystemEffectItems.at("systemEffect").hasType<std::string>()) {
74+
SharedColor baseColorShared{};
75+
fromRawValue(contextContainer, surfaceId, colorWithSystemEffectItems.at("baseColor"), baseColorShared);
76+
if (baseColorShared) {
77+
std::string systemEffect = (std::string)colorWithSystemEffectItems.at("systemEffect");
78+
auto baseColor = (*baseColorShared).getColor();
79+
return SharedColor(Color(ColorWithSystemEffect{baseColor, systemEffect}));
80+
}
81+
}
82+
#endif // macOS]
6583
}
6684
}
6785

0 commit comments

Comments
 (0)