Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>,
fallback?: string,
};

export const PlatformColor = (...names: Array<string>): NativeColorValue => {
export const PlatformColor = (
...args: Array<string | {fallback: string}>
): 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 = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | {fallback: string}>
): OpaqueColorValue;
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>,
fallback?: string,
dynamic?: {
light: ?(ColorValue | ProcessedColorValue),
dark: ?(ColorValue | ProcessedColorValue),
Expand All @@ -22,9 +25,16 @@ type LocalNativeColorValue = {
},
};

export const PlatformColor = (...names: Array<string>): NativeColorValue => {
export const PlatformColor = (
...args: Array<string | {fallback: string}>
): 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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type {NativeColorValue} from './StyleSheet';
* @see https://reactnative.dev/docs/platformcolor#example
*/
declare export function PlatformColor(
...names: Array<string>
...names: Array<string | {fallback: string}>
): NativeColorValue;

declare export function normalizeColorObject(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
});

Expand All @@ -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);
});
}
});
});
Original file line number Diff line number Diff line change
@@ -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<string | {fallback: string}>,
): {names: Array<string>, 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<string> = [];
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};
}
2 changes: 1 addition & 1 deletion packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -2252,7 +2252,7 @@ public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/brid
public fun dispatchCommand (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V
public fun dispatchCommand (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V
public fun findNextFocusableElement (III)Ljava/lang/Integer;
public fun getColor (I[Ljava/lang/String;)I
public fun getColor (I[Ljava/lang/String;)Ljava/lang/Integer;
public fun getEventDispatcher ()Lcom/facebook/react/uimanager/events/EventDispatcher;
public fun getPerformanceCounters ()Ljava/util/Map;
public fun getRelativeAncestorList (II)[I
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,12 +565,12 @@ private NativeArray measureLines(
mTextEffectRegistry);
}

public int getColor(int surfaceId, String[] resourcePaths) {
public @Nullable Integer getColor(int surfaceId, String[] resourcePaths) {
ThemedReactContext context =
mMountingManager.getSurfaceManagerEnforced(surfaceId, "getColor").getContext();
// Surface may have been stopped
if (context == null) {
return 0;
return null;
}

for (String resourcePath : resourcePaths) {
Expand All @@ -579,7 +579,9 @@ public int getColor(int surfaceId, String[] resourcePaths) {
return color;
}
}
return 0;
// Null (explicit miss), not 0, so native can tell an unresolved color from
// one that resolves to transparent black (ARGB 0).
return null;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ inline static void updateNativeDrawableProp(
}
folly::dynamic platformColorMap = folly::dynamic::object();
platformColorMap["resource_paths"] = resourcePaths;
if (nativeDrawableValue.ripple.colorFallback.has_value()) {
platformColorMap["fallback"] =
nativeDrawableValue.ripple.colorFallback.value();
}
nativeDrawableResult["color"] = platformColorMap;
} else {
nativeDrawableResult["color"] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,21 @@ struct NativeDrawable {
struct Ripple {
std::optional<SharedColor> color{};
std::optional<std::vector<std::string>> colorResourcePaths{};
std::optional<std::string> colorFallback{};
std::optional<Float> rippleRadius{};
bool borderless{false};
std::optional<Float> alpha{};

bool operator==(const Ripple &rhs) const
{
return std::tie(this->color, this->colorResourcePaths, this->borderless, this->rippleRadius, this->alpha) ==
std::tie(rhs.color, rhs.colorResourcePaths, rhs.borderless, rhs.rippleRadius, rhs.alpha);
return std::tie(
this->color,
this->colorResourcePaths,
this->colorFallback,
this->borderless,
this->rippleRadius,
this->alpha) ==
std::tie(rhs.color, rhs.colorResourcePaths, rhs.colorFallback, rhs.borderless, rhs.rippleRadius, rhs.alpha);
}
};

Expand Down Expand Up @@ -87,14 +94,25 @@ static inline void fromRawValue(const PropsParserContext &context, const RawValu

std::optional<SharedColor> parsedColor{};
std::optional<std::vector<std::string>> parsedColorResourcePaths{};
std::optional<std::string> parsedColorFallback{};
if (color != map.end()) {
if (color->second.hasType<std::unordered_map<std::string, std::vector<std::string>>>()) {
auto colorMap = (std::unordered_map<std::string, std::vector<std::string>>)color->second;
// The color object mixes an array (`resource_paths`) with an optional
// string (`fallback`), so read it as a map of RawValue rather than a map
// of vector<string> (which would assert on the string value).
bool handledAsResourcePaths = false;
if (color->second.hasType<std::unordered_map<std::string, RawValue>>()) {
auto colorMap = (std::unordered_map<std::string, RawValue>)color->second;
auto pathsIt = colorMap.find("resource_paths");
if (pathsIt != colorMap.end()) {
parsedColorResourcePaths = pathsIt->second;
if (pathsIt != colorMap.end() && pathsIt->second.hasType<std::vector<std::string>>()) {
parsedColorResourcePaths = (std::vector<std::string>)pathsIt->second;
auto fallbackIt = colorMap.find("fallback");
if (fallbackIt != colorMap.end() && fallbackIt->second.hasType<std::string>()) {
parsedColorFallback = (std::string)fallbackIt->second;
}
handledAsResourcePaths = true;
}
} else {
}
if (!handledAsResourcePaths) {
SharedColor resolved;
fromRawValue(context, color->second, resolved);
if (resolved) {
Expand All @@ -114,6 +132,7 @@ static inline void fromRawValue(const PropsParserContext &context, const RawValu
NativeDrawable::Ripple{
.color = parsedColor,
.colorResourcePaths = parsedColorResourcePaths,
.colorFallback = parsedColorFallback,
.rippleRadius = rippleRadius != map.end() && rippleRadius->second.hasType<Float>()
? (Float)rippleRadius->second
: std::optional<Float>{},
Expand Down
Loading
Loading