Skip to content

Commit 3e07031

Browse files
Abbondanzofacebook-github-bot
authored andcommitted
Support a lazy raw color fallback for PlatformColor (#57701)
Summary: An implementation for the RFC in react-native-community/discussions-and-proposals#1008 `PlatformColor` previously had no way to specify what color to use when none of the supplied native color tokens resolve on the device. On a miss the behavior was inconsistent across platforms and architectures (transparent, nil, or a thrown error), giving apps no control over the rendered result. This adds an optional trailing `{fallback: '<raw color string>'}` argument to `PlatformColor(...)` and updates the JS entry points to emit it. The fallback is carried lazily to native as a raw, unprocessed string and is only parsed when every provided token fails to resolve (the native side of that behavior landed earlier in this stack). When at least one token resolves the fallback is ignored, so existing call sites are completely unaffected and omitting the argument preserves today's exact behavior. Example: PlatformColor('someSystemToken', {fallback: '#FF0000'}) Each platform entry point (`PlatformColorValueTypes.{android,ios,macos,windows}.js`, plus the vendored macOS/Windows copies) collects the leading string tokens and, when a trailing `{fallback}` object is present, attaches its raw string to the native color object. Non-string arguments in a non-trailing position are filtered out consistently rather than being forwarded to native. The Flow (`.js.flow`), TypeScript (`.d.ts`), and generated (`ReactNativeApi.d.ts`) declarations widen the signature to `Array<string | {fallback: string}>`, which is backward compatible. Changelog: [General][Added] - Support a lazy raw color fallback for PlatformColor when native tokens fail to resolve Differential Revision: D113329139
1 parent a8022d5 commit 3e07031

7 files changed

Lines changed: 122 additions & 9 deletions

File tree

packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.android.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,27 @@
1111
import type {ProcessedColorValue} from './processColor';
1212
import type {NativeColorValue} from './StyleSheet';
1313

14+
import parsePlatformColorArgs from './parsePlatformColorArgs';
15+
1416
/** The actual type of the opaque NativeColorValue on Android platform */
1517
type LocalNativeColorValue = {
1618
resource_paths?: Array<string>,
19+
fallback?: string,
1720
};
1821

19-
export const PlatformColor = (...names: Array<string>): NativeColorValue => {
22+
export const PlatformColor = (
23+
...args: Array<string | {fallback: string}>
24+
): NativeColorValue => {
25+
const {names, fallback} = parsePlatformColorArgs(args);
26+
// Raw fallback (when present) is passed to native untouched and only parsed
27+
// on a token miss.
28+
const color: LocalNativeColorValue =
29+
fallback == null
30+
? {resource_paths: names}
31+
: {resource_paths: names, fallback};
2032
/* $FlowExpectedError[incompatible-type]
2133
* LocalNativeColorValue is the actual type of the opaque NativeColorValue on Android platform */
22-
return {resource_paths: names} as LocalNativeColorValue;
34+
return color as LocalNativeColorValue;
2335
};
2436

2537
export const normalizeColorObject = (

packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ import {OpaqueColorValue} from './StyleSheet';
1515
*
1616
* @see https://reactnative.dev/docs/platformcolor#example
1717
*/
18-
export function PlatformColor(...colors: string[]): OpaqueColorValue;
18+
export function PlatformColor(
19+
...colors: Array<string | {fallback: string}>
20+
): OpaqueColorValue;

packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.ios.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
import type {ProcessedColorValue} from './processColor';
1212
import type {ColorValue, NativeColorValue} from './StyleSheet';
1313

14+
import parsePlatformColorArgs from './parsePlatformColorArgs';
15+
1416
/** The actual type of the opaque NativeColorValue on iOS platform */
1517
type LocalNativeColorValue = {
1618
semantic?: Array<string>,
19+
fallback?: string,
1720
dynamic?: {
1821
light: ?(ColorValue | ProcessedColorValue),
1922
dark: ?(ColorValue | ProcessedColorValue),
@@ -22,9 +25,16 @@ type LocalNativeColorValue = {
2225
},
2326
};
2427

25-
export const PlatformColor = (...names: Array<string>): NativeColorValue => {
28+
export const PlatformColor = (
29+
...args: Array<string | {fallback: string}>
30+
): NativeColorValue => {
31+
const {names, fallback} = parsePlatformColorArgs(args);
32+
// Raw fallback (when present) is passed to native untouched and only parsed
33+
// on a token miss.
34+
const color: LocalNativeColorValue =
35+
fallback == null ? {semantic: names} : {semantic: names, fallback};
2636
// $FlowExpectedError[incompatible-type] LocalNativeColorValue is the iOS LocalNativeColorValue type
27-
return {semantic: names} as LocalNativeColorValue;
37+
return color as LocalNativeColorValue;
2838
};
2939

3040
export type DynamicColorIOSTuplePrivate = {

packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.js.flow

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type {NativeColorValue} from './StyleSheet';
1818
* @see https://reactnative.dev/docs/platformcolor#example
1919
*/
2020
declare export function PlatformColor(
21-
...names: Array<string>
21+
...names: Array<string | {fallback: string}>
2222
): NativeColorValue;
2323

2424
declare export function normalizeColorObject(

packages/react-native/Libraries/StyleSheet/__tests__/processColor-itest.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,21 @@ describe('processColor', () => {
109109
const expectedColor = {dynamic: {light: 0xff000000, dark: 0xffffffff}};
110110
expect(processedColor).toEqual(expectedColor);
111111
});
112+
113+
// The macOS and Windows PlatformColor entry points carry the fallback with
114+
// the same trailing-{fallback} detection as iOS and Android. This
115+
// integration test harness only executes as iOS and Android, so that
116+
// shared behavior is exercised by the iOS and Android cases here rather
117+
// than duplicated for platforms the harness cannot run.
118+
it('should carry an unprocessed fallback on iOS PlatformColor colors', () => {
119+
const color = PlatformColorIOS('systemRedColor', {fallback: '#ff0000'});
120+
const processedColor = processColor(color);
121+
const expectedColor = {
122+
semantic: ['systemRedColor'],
123+
fallback: '#ff0000',
124+
};
125+
expect(processedColor).toEqual(expectedColor);
126+
});
112127
}
113128
});
114129

@@ -120,6 +135,18 @@ describe('processColor', () => {
120135
const expectedColor = {resource_paths: ['?attr/colorPrimary']};
121136
expect(processedColor).toEqual(expectedColor);
122137
});
138+
139+
it('should carry an unprocessed fallback on Android PlatformColor colors', () => {
140+
const color = PlatformColorAndroid('?attr/colorPrimary', {
141+
fallback: '#000000',
142+
});
143+
const processedColor = processColor(color);
144+
const expectedColor = {
145+
resource_paths: ['?attr/colorPrimary'],
146+
fallback: '#000000',
147+
};
148+
expect(processedColor).toEqual(expectedColor);
149+
});
123150
}
124151
});
125152
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
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+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
/**
12+
* Splits the variadic `PlatformColor(...)` arguments into the leading color
13+
* token names and the optional trailing `{fallback}` options object. The
14+
* per-platform `PlatformColor` implementations differ only in the native object
15+
* they build from this result, so the argument parsing is shared here.
16+
*/
17+
export default function parsePlatformColorArgs(
18+
args: Array<string | {fallback: string}>,
19+
): {names: Array<string>, fallback: ?string} {
20+
const lastArg = args[args.length - 1];
21+
if (__DEV__) {
22+
args.forEach((arg, index) => {
23+
if (typeof arg !== 'object' || arg == null) {
24+
return;
25+
}
26+
if (index !== args.length - 1) {
27+
console.error(
28+
'PlatformColor: an options object is only honored as the final argument; one in any other position is ignored.',
29+
);
30+
} else if (typeof arg.fallback !== 'string') {
31+
console.error(
32+
'PlatformColor: the trailing options object must be of the form {fallback: string}; it is ignored.',
33+
);
34+
}
35+
});
36+
}
37+
// The {fallback} options object is only honored as the trailing argument.
38+
const fallback =
39+
lastArg != null &&
40+
typeof lastArg === 'object' &&
41+
typeof lastArg.fallback === 'string'
42+
? lastArg.fallback
43+
: null;
44+
// Collect the leading string tokens; a non-string non-trailing arg (a lint
45+
// error) is dropped.
46+
const names: Array<string> = [];
47+
const nameCount = fallback == null ? args.length : args.length - 1;
48+
for (let i = 0; i < nameCount; i++) {
49+
const arg = args[i];
50+
if (typeof arg === 'string') {
51+
names.push(arg);
52+
}
53+
}
54+
return {names, fallback};
55+
}

packages/react-native/ReactNativeApi.d.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<d895833bed3d3c274a6647773deaa023>>
7+
* @generated SignedSource<<53280bff7703ea0e9e912e22543fd568>>
88
*
99
* This file was generated by scripts/js-api/build-types/index.js.
1010
*/
@@ -3491,7 +3491,14 @@ declare class PixelRatio {
34913491
static startDetecting(): void
34923492
}
34933493
declare type Platform = typeof Platform
3494-
declare function PlatformColor(...names: Array<string>): NativeColorValue
3494+
declare function PlatformColor(
3495+
...names: Array<
3496+
| string
3497+
| {
3498+
fallback: string
3499+
}
3500+
>
3501+
): NativeColorValue
34953502
declare type PlatformConfig = {}
34963503
declare type PlatformOSType =
34973504
"android" | "ios" | "macos" | "native" | "web" | "windows"
@@ -5866,7 +5873,7 @@ export {
58665873
PermissionsAndroid, // 8a0bc8d8
58675874
PixelRatio, // 10d9e32d
58685875
Platform, // b73caa89
5869-
PlatformColor, // 8297ec62
5876+
PlatformColor, // d083d341
58705877
PlatformOSType, // 0a17561e
58715878
PlatformSelectSpec, // 09ed7758
58725879
PointValue, // 69db075f

0 commit comments

Comments
 (0)