Skip to content

Commit 0be4f94

Browse files
Saadnajmiclaude
andcommitted
Add macOS platform color support to Fabric renderer
Add complete macOS semantic color dictionary (label, text, content, control, window colors etc.) and ColorWithSystemEffect support from PR microsoft#2737. Includes Swift-style aliases, fallback colors, array-based color selectors, and PlatformColorExample for RNTester. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7d97100 commit 0be4f94

6 files changed

Lines changed: 298 additions & 5 deletions

File tree

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/ReactCommon/react/renderer/graphics/React-graphics.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Pod::Spec.new do |s|
4141
if ENV['USE_FRAMEWORKS']
4242
s.module_name = "React_graphics"
4343
s.header_mappings_dir = "../../.."
44-
header_search_paths = header_search_paths + ["\"$(PODS_TARGET_SRCROOT)/platform/ios\""]
44+
header_search_paths = header_search_paths + ["\"$(PODS_TARGET_SRCROOT)/platform/ios\"", "\"$(PODS_TARGET_SRCROOT)/platform/macos\""] # [macOS]
4545
end
4646

4747
s.pod_target_xcconfig = { "USE_HEADERMAP" => "NO",

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

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,33 @@
88
#import "RCTPlatformColorUtils.h"
99

1010
#import <Foundation/Foundation.h>
11+
#import <TargetConditionals.h>
1112
#import <React/RCTUIKit.h> // [macOS]
13+
#if TARGET_OS_OSX // [macOS
14+
#import <AppKit/AppKit.h>
15+
#endif // macOS]
1216
#import <react/renderer/graphics/HostPlatformColor.h>
1317
#import <react/utils/ManagedObjectWrapper.h>
1418

1519
#include <string>
20+
#include <vector>
1621

1722
NS_ASSUME_NONNULL_BEGIN
1823

1924
static NSString *const kColorSuffix = @"Color";
2025
static NSString *const kFallbackARGBKey = @"fallback-argb";
26+
#if TARGET_OS_OSX // [macOS
27+
static NSString *const kFallbackKey = @"fallback";
28+
static NSString *const kSelectorKey = @"selector";
29+
static NSString *const kIndexKey = @"index";
30+
#endif // macOS]
2131

2232
static NSDictionary<NSString *, NSDictionary *> *_PlatformColorSelectorsDict()
2333
{
2434
static NSDictionary<NSString *, NSDictionary *> *dict;
2535
static dispatch_once_t once_token;
2636
dispatch_once(&once_token, ^(void) {
37+
#if !TARGET_OS_OSX // [macOS]
2738
dict = @{
2839
// https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors
2940
// Label Colors
@@ -130,6 +141,105 @@
130141
kFallbackARGBKey : @(0x00000000) // iOS 13.0
131142
},
132143
};
144+
#else // [macOS
145+
NSMutableDictionary<NSString *, NSDictionary *> *map = [@{
146+
// https://developer.apple.com/documentation/appkit/nscolor/ui_element_colors
147+
// Label Colors
148+
@"labelColor": @{},
149+
@"secondaryLabelColor": @{},
150+
@"tertiaryLabelColor": @{},
151+
@"quaternaryLabelColor": @{},
152+
// Text Colors
153+
@"textColor": @{},
154+
@"placeholderTextColor": @{},
155+
@"selectedTextColor": @{},
156+
@"textBackgroundColor": @{},
157+
@"selectedTextBackgroundColor": @{},
158+
@"keyboardFocusIndicatorColor": @{},
159+
@"unemphasizedSelectedTextColor": @{
160+
kFallbackKey: @"selectedTextColor"
161+
},
162+
@"unemphasizedSelectedTextBackgroundColor": @{
163+
kFallbackKey: @"textBackgroundColor"
164+
},
165+
// Content Colors
166+
@"linkColor": @{},
167+
@"separatorColor": @{
168+
kFallbackKey: @"gridColor"
169+
},
170+
@"selectedContentBackgroundColor": @{
171+
kFallbackKey: @"alternateSelectedControlColor"
172+
},
173+
@"unemphasizedSelectedContentBackgroundColor": @{
174+
kFallbackKey: @"secondarySelectedControlColor"
175+
},
176+
// Menu Colors
177+
@"selectedMenuItemTextColor": @{},
178+
// Table Colors
179+
@"gridColor": @{},
180+
@"headerTextColor": @{},
181+
@"alternatingEvenContentBackgroundColor": @{
182+
kSelectorKey: @"alternatingContentBackgroundColors",
183+
kIndexKey: @0,
184+
kFallbackKey: @"controlAlternatingRowBackgroundColors"
185+
},
186+
@"alternatingOddContentBackgroundColor": @{
187+
kSelectorKey: @"alternatingContentBackgroundColors",
188+
kIndexKey: @1,
189+
kFallbackKey: @"controlAlternatingRowBackgroundColors"
190+
},
191+
// Control Colors
192+
@"controlAccentColor": @{
193+
kFallbackKey: @"controlColor"
194+
},
195+
@"controlColor": @{},
196+
@"controlBackgroundColor": @{},
197+
@"controlTextColor": @{},
198+
@"disabledControlTextColor": @{},
199+
@"selectedControlColor": @{},
200+
@"selectedControlTextColor": @{},
201+
@"alternateSelectedControlTextColor": @{},
202+
@"scrubberTexturedBackgroundColor": @{},
203+
// Window Colors
204+
@"windowBackgroundColor": @{},
205+
@"windowFrameTextColor": @{},
206+
@"underPageBackgroundColor": @{},
207+
// Highlights and Shadows
208+
@"findHighlightColor": @{
209+
kFallbackKey: @"highlightColor"
210+
},
211+
@"highlightColor": @{},
212+
@"shadowColor": @{},
213+
// https://developer.apple.com/documentation/appkit/nscolor/standard_colors
214+
// Standard Colors
215+
@"systemBlueColor": @{},
216+
@"systemBrownColor": @{},
217+
@"systemGrayColor": @{},
218+
@"systemGreenColor": @{},
219+
@"systemOrangeColor": @{},
220+
@"systemPinkColor": @{},
221+
@"systemPurpleColor": @{},
222+
@"systemRedColor": @{},
223+
@"systemYellowColor": @{},
224+
// Transparent Color
225+
@"clearColor" : @{},
226+
} mutableCopy];
227+
228+
NSMutableDictionary<NSString *, NSDictionary *> *aliases = [NSMutableDictionary new];
229+
for (NSString *objcSelector in map) {
230+
NSMutableDictionary *entry = [map[objcSelector] mutableCopy];
231+
if ([entry objectForKey:kSelectorKey] == nil) {
232+
entry[kSelectorKey] = objcSelector;
233+
}
234+
if ([objcSelector hasSuffix:kColorSuffix]) {
235+
NSString *swiftSelector = [objcSelector substringToIndex:[objcSelector length] - [kColorSuffix length]];
236+
aliases[swiftSelector] = entry;
237+
}
238+
}
239+
[map addEntriesFromDictionary:aliases];
240+
241+
dict = [map copy];
242+
#endif // macOS]
133243
});
134244
return dict;
135245
}
@@ -154,21 +264,59 @@
154264
NSDictionary<NSString *, NSDictionary *> *platformColorSelectorsDict = _PlatformColorSelectorsDict();
155265
NSDictionary<NSString *, id> *colorInfo = platformColorSelectorsDict[platformColorString];
156266
if (colorInfo) {
267+
#if !TARGET_OS_OSX // [macOS]
157268
SEL objcColorSelector = NSSelectorFromString([platformColorString stringByAppendingString:kColorSuffix]);
158-
if (![RCTPlatformColor respondsToSelector:objcColorSelector]) { // [macOS]
269+
if (![RCTPlatformColor respondsToSelector:objcColorSelector]) {
159270
NSNumber *fallbackRGB = colorInfo[kFallbackARGBKey];
160271
if (fallbackRGB) {
161272
return _UIColorFromHexValue(fallbackRGB);
162273
}
163274
} else {
164-
Class uiColorClass = [RCTPlatformColor class]; // [macOS]
275+
Class uiColorClass = [RCTPlatformColor class];
165276
IMP imp = [uiColorClass methodForSelector:objcColorSelector];
166277
id (*getUIColor)(id, SEL) = ((id(*)(id, SEL))imp);
167278
id colorObject = getUIColor(uiColorClass, objcColorSelector);
168-
if ([colorObject isKindOfClass:[RCTPlatformColor class]]) { // [macOS]
279+
if ([colorObject isKindOfClass:[RCTPlatformColor class]]) {
280+
return colorObject;
281+
}
282+
}
283+
#else // [macOS
284+
NSString *selectorName = colorInfo[kSelectorKey];
285+
if (selectorName == nil) {
286+
selectorName = [platformColorString stringByAppendingString:kColorSuffix];
287+
}
288+
289+
SEL objcColorSelector = NSSelectorFromString(selectorName);
290+
if (![RCTPlatformColor respondsToSelector:objcColorSelector]) {
291+
NSNumber *fallbackRGB = colorInfo[kFallbackARGBKey];
292+
if (fallbackRGB) {
293+
return _UIColorFromHexValue(fallbackRGB);
294+
}
295+
NSString *fallbackColorName = colorInfo[kFallbackKey];
296+
if (fallbackColorName) {
297+
return _UIColorFromSemanticString(fallbackColorName);
298+
}
299+
} else {
300+
Class colorClass = [RCTPlatformColor class];
301+
IMP imp = [colorClass methodForSelector:objcColorSelector];
302+
id (*getColor)(id, SEL) = ((id(*)(id, SEL))imp);
303+
id colorObject = getColor(colorClass, objcColorSelector);
304+
if ([colorObject isKindOfClass:[NSArray class]]) {
305+
NSNumber *index = colorInfo[kIndexKey];
306+
if (index != nil) {
307+
NSArray *colors = colorObject;
308+
NSUInteger idx = [index unsignedIntegerValue];
309+
if (idx < colors.count) {
310+
colorObject = colors[idx];
311+
}
312+
}
313+
}
314+
315+
if ([colorObject isKindOfClass:[RCTPlatformColor class]]) {
169316
return colorObject;
170317
}
171318
}
319+
#endif
172320
}
173321
return nil;
174322
}

packages/rn-tester/js/examples/PlatformColor/PlatformColorExample.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,100 @@ function PlatformColorsExample() {
123123
{label: 'clear', color: PlatformColor('clear')},
124124
{label: 'customColor', color: PlatformColor('customColor')},
125125
];
126+
// [macOS
127+
} else if (Platform.OS === 'macos') {
128+
colors = [
129+
// https://developer.apple.com/documentation/appkit/nscolor
130+
// Text Colors
131+
{label: 'labelColor', color: PlatformColor('labelColor')},
132+
{
133+
label: 'secondaryLabelColor',
134+
color: PlatformColor('secondaryLabelColor'),
135+
},
136+
{
137+
label: 'tertiaryLabelColor',
138+
color: PlatformColor('tertiaryLabelColor'),
139+
},
140+
{
141+
label: 'quaternaryLabelColor',
142+
color: PlatformColor('quaternaryLabelColor'),
143+
},
144+
{label: 'textColor', color: PlatformColor('textColor')},
145+
{
146+
label: 'placeholderTextColor',
147+
color: PlatformColor('placeholderTextColor'),
148+
},
149+
{
150+
label: 'selectedTextColor',
151+
color: PlatformColor('selectedTextColor'),
152+
},
153+
{
154+
label: 'selectedTextBackgroundColor',
155+
color: PlatformColor('selectedTextBackgroundColor'),
156+
},
157+
// Window and Control Colors
158+
{
159+
label: 'windowBackgroundColor',
160+
color: PlatformColor('windowBackgroundColor'),
161+
},
162+
{
163+
label: 'underPageBackgroundColor',
164+
color: PlatformColor('underPageBackgroundColor'),
165+
},
166+
{
167+
label: 'controlBackgroundColor',
168+
color: PlatformColor('controlBackgroundColor'),
169+
},
170+
{
171+
label: 'selectedControlColor',
172+
color: PlatformColor('selectedControlColor'),
173+
},
174+
{
175+
label: 'keyboardFocusIndicatorColor',
176+
color: PlatformColor('keyboardFocusIndicatorColor'),
177+
},
178+
// System Colors
179+
{
180+
label: 'systemBlueColor',
181+
color: PlatformColor('systemBlueColor'),
182+
},
183+
{
184+
label: 'systemBrownColor',
185+
color: PlatformColor('systemBrownColor'),
186+
},
187+
{
188+
label: 'systemGreenColor',
189+
color: PlatformColor('systemGreenColor'),
190+
},
191+
{
192+
label: 'systemOrangeColor',
193+
color: PlatformColor('systemOrangeColor'),
194+
},
195+
{
196+
label: 'systemPinkColor',
197+
color: PlatformColor('systemPinkColor'),
198+
},
199+
{
200+
label: 'systemPurpleColor',
201+
color: PlatformColor('systemPurpleColor'),
202+
},
203+
{
204+
label: 'systemRedColor',
205+
color: PlatformColor('systemRedColor'),
206+
},
207+
{
208+
label: 'systemYellowColor',
209+
color: PlatformColor('systemYellowColor'),
210+
},
211+
// Accents and Grays
212+
{
213+
label: 'controlAccentColor',
214+
color: PlatformColor('controlAccentColor'),
215+
},
216+
{label: 'separatorColor', color: PlatformColor('separatorColor')},
217+
{label: 'gridColor', color: PlatformColor('gridColor')},
218+
{label: 'windowFrameColor', color: PlatformColor('windowFrameColor')},
219+
]; // macOS]
126220
} else if (Platform.OS === 'android') {
127221
colors = [
128222
{label: '?attr/colorAccent', color: PlatformColor('?attr/colorAccent')},

0 commit comments

Comments
 (0)