diff --git a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.android.js b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.android.js index 2647776e354..08a3b86bb08 100644 --- a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.android.js +++ b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.android.js @@ -11,15 +11,27 @@ import type {ProcessedColorValue} from './processColor'; import type {NativeColorValue} from './StyleSheet'; +import parsePlatformColorArgs from './parsePlatformColorArgs'; + /** The actual type of the opaque NativeColorValue on Android platform */ type LocalNativeColorValue = { resource_paths?: Array, + fallback?: string, }; -export const PlatformColor = (...names: Array): NativeColorValue => { +export const PlatformColor = ( + ...args: Array +): NativeColorValue => { + const {names, fallback} = parsePlatformColorArgs(args); + // Raw fallback (when present) is passed to native untouched and only parsed + // on a token miss. + const color: LocalNativeColorValue = + fallback == null + ? {resource_paths: names} + : {resource_paths: names, fallback}; /* $FlowExpectedError[incompatible-type] * LocalNativeColorValue is the actual type of the opaque NativeColorValue on Android platform */ - return {resource_paths: names} as LocalNativeColorValue; + return color as LocalNativeColorValue; }; export const normalizeColorObject = ( diff --git a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.d.ts b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.d.ts index 909f73d596e..02be4f88770 100644 --- a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.d.ts +++ b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.d.ts @@ -15,4 +15,6 @@ import {OpaqueColorValue} from './StyleSheet'; * * @see https://reactnative.dev/docs/platformcolor#example */ -export function PlatformColor(...colors: string[]): OpaqueColorValue; +export function PlatformColor( + ...colors: Array +): OpaqueColorValue; diff --git a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.ios.js b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.ios.js index 50af1b60828..c008435227a 100644 --- a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.ios.js +++ b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.ios.js @@ -11,9 +11,12 @@ import type {ProcessedColorValue} from './processColor'; import type {ColorValue, NativeColorValue} from './StyleSheet'; +import parsePlatformColorArgs from './parsePlatformColorArgs'; + /** The actual type of the opaque NativeColorValue on iOS platform */ type LocalNativeColorValue = { semantic?: Array, + fallback?: string, dynamic?: { light: ?(ColorValue | ProcessedColorValue), dark: ?(ColorValue | ProcessedColorValue), @@ -22,9 +25,16 @@ type LocalNativeColorValue = { }, }; -export const PlatformColor = (...names: Array): NativeColorValue => { +export const PlatformColor = ( + ...args: Array +): NativeColorValue => { + const {names, fallback} = parsePlatformColorArgs(args); + // Raw fallback (when present) is passed to native untouched and only parsed + // on a token miss. + const color: LocalNativeColorValue = + fallback == null ? {semantic: names} : {semantic: names, fallback}; // $FlowExpectedError[incompatible-type] LocalNativeColorValue is the iOS LocalNativeColorValue type - return {semantic: names} as LocalNativeColorValue; + return color as LocalNativeColorValue; }; export type DynamicColorIOSTuplePrivate = { diff --git a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.js.flow b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.js.flow index e76df70da96..94c595e6a8b 100644 --- a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.js.flow +++ b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.js.flow @@ -18,7 +18,7 @@ import type {NativeColorValue} from './StyleSheet'; * @see https://reactnative.dev/docs/platformcolor#example */ declare export function PlatformColor( - ...names: Array + ...names: Array ): NativeColorValue; declare export function normalizeColorObject( diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processColor-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/processColor-itest.js index 5084f878778..2054ef112a2 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processColor-itest.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processColor-itest.js @@ -109,6 +109,21 @@ describe('processColor', () => { const expectedColor = {dynamic: {light: 0xff000000, dark: 0xffffffff}}; expect(processedColor).toEqual(expectedColor); }); + + // The macOS and Windows PlatformColor entry points carry the fallback with + // the same trailing-{fallback} detection as iOS and Android. This + // integration test harness only executes as iOS and Android, so that + // shared behavior is exercised by the iOS and Android cases here rather + // than duplicated for platforms the harness cannot run. + it('should carry an unprocessed fallback on iOS PlatformColor colors', () => { + const color = PlatformColorIOS('systemRedColor', {fallback: '#ff0000'}); + const processedColor = processColor(color); + const expectedColor = { + semantic: ['systemRedColor'], + fallback: '#ff0000', + }; + expect(processedColor).toEqual(expectedColor); + }); } }); @@ -120,6 +135,18 @@ describe('processColor', () => { const expectedColor = {resource_paths: ['?attr/colorPrimary']}; expect(processedColor).toEqual(expectedColor); }); + + it('should carry an unprocessed fallback on Android PlatformColor colors', () => { + const color = PlatformColorAndroid('?attr/colorPrimary', { + fallback: '#000000', + }); + const processedColor = processColor(color); + const expectedColor = { + resource_paths: ['?attr/colorPrimary'], + fallback: '#000000', + }; + expect(processedColor).toEqual(expectedColor); + }); } }); }); diff --git a/packages/react-native/Libraries/StyleSheet/parsePlatformColorArgs.js b/packages/react-native/Libraries/StyleSheet/parsePlatformColorArgs.js new file mode 100644 index 00000000000..5dcfb6d754d --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/parsePlatformColorArgs.js @@ -0,0 +1,55 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +/** + * Splits the variadic `PlatformColor(...)` arguments into the leading color + * token names and the optional trailing `{fallback}` options object. The + * per-platform `PlatformColor` implementations differ only in the native object + * they build from this result, so the argument parsing is shared here. + */ +export default function parsePlatformColorArgs( + args: Array, +): {names: Array, fallback: ?string} { + const lastArg = args[args.length - 1]; + if (__DEV__) { + args.forEach((arg, index) => { + if (typeof arg !== 'object' || arg == null) { + return; + } + if (index !== args.length - 1) { + console.error( + 'PlatformColor: an options object is only honored as the final argument; one in any other position is ignored.', + ); + } else if (typeof arg.fallback !== 'string') { + console.error( + 'PlatformColor: the trailing options object must be of the form {fallback: string}; it is ignored.', + ); + } + }); + } + // The {fallback} options object is only honored as the trailing argument. + const fallback = + lastArg != null && + typeof lastArg === 'object' && + typeof lastArg.fallback === 'string' + ? lastArg.fallback + : null; + // Collect the leading string tokens; a non-string non-trailing arg (a lint + // error) is dropped. + const names: Array = []; + const nameCount = fallback == null ? args.length : args.length - 1; + for (let i = 0; i < nameCount; i++) { + const arg = args[i]; + if (typeof arg === 'string') { + names.push(arg); + } + } + return {names, fallback}; +} diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 24f632825a8..556c8117198 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<53280bff7703ea0e9e912e22543fd568>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -3491,7 +3491,14 @@ declare class PixelRatio { static startDetecting(): void } declare type Platform = typeof Platform -declare function PlatformColor(...names: Array): NativeColorValue +declare function PlatformColor( + ...names: Array< + | string + | { + fallback: string + } + > +): NativeColorValue declare type PlatformConfig = {} declare type PlatformOSType = "android" | "ios" | "macos" | "native" | "web" | "windows" @@ -5866,7 +5873,7 @@ export { PermissionsAndroid, // 8a0bc8d8 PixelRatio, // 10d9e32d Platform, // b73caa89 - PlatformColor, // 8297ec62 + PlatformColor, // d083d341 PlatformOSType, // 0a17561e PlatformSelectSpec, // 09ed7758 PointValue, // 69db075f