diff --git a/Libraries/ART/ARTSurfaceView.m b/Libraries/ART/ARTSurfaceView.m index a715efec5b1756..99cb1275933345 100644 --- a/Libraries/ART/ARTSurfaceView.m +++ b/Libraries/ART/ARTSurfaceView.m @@ -47,6 +47,7 @@ - (void)invalidate - (void)drawRect:(CGRect)rect { + [super drawRect:rect]; CGContextRef context = UIGraphicsGetCurrentContext(); for (ARTNode *node in self.subviews) { [node renderTo:context]; diff --git a/Libraries/ActionSheetIOS/ActionSheetIOS.js b/Libraries/ActionSheetIOS/ActionSheetIOS.js index d3b904fbbbbfc5..e330253df81b08 100644 --- a/Libraries/ActionSheetIOS/ActionSheetIOS.js +++ b/Libraries/ActionSheetIOS/ActionSheetIOS.js @@ -14,6 +14,8 @@ import RCTActionSheetManager from './NativeActionSheetManager'; const invariant = require('invariant'); const processColor = require('../StyleSheet/processColor'); +import type {ColorValue} from '../StyleSheet/StyleSheetTypes'; +import type {ProcessedColorValue} from '../StyleSheet/processColor'; /** * Display action sheets and share sheets on iOS. @@ -45,7 +47,7 @@ const ActionSheetIOS = { +destructiveButtonIndex?: ?number | ?Array, +cancelButtonIndex?: ?number, +anchor?: ?number, - +tintColor?: number | string, + +tintColor?: ColorValue | ProcessedColorValue, +userInterfaceStyle?: string, |}, callback: (buttonIndex: number) => void, diff --git a/Libraries/Animated/src/nodes/AnimatedInterpolation.js b/Libraries/Animated/src/nodes/AnimatedInterpolation.js index 49bcca12f648ef..a32bb9c2a7c190 100644 --- a/Libraries/Animated/src/nodes/AnimatedInterpolation.js +++ b/Libraries/Animated/src/nodes/AnimatedInterpolation.js @@ -164,17 +164,17 @@ function interpolate( } function colorToRgba(input: string): string { - let int32Color = normalizeColor(input); - if (int32Color === null) { + let normalizedColor = normalizeColor(input); + if (normalizedColor === null || typeof normalizedColor !== 'number') { return input; } - int32Color = int32Color || 0; + normalizedColor = normalizedColor || 0; - const r = (int32Color & 0xff000000) >>> 24; - const g = (int32Color & 0x00ff0000) >>> 16; - const b = (int32Color & 0x0000ff00) >>> 8; - const a = (int32Color & 0x000000ff) / 255; + const r = (normalizedColor & 0xff000000) >>> 24; + const g = (normalizedColor & 0x00ff0000) >>> 16; + const b = (normalizedColor & 0x0000ff00) >>> 8; + const a = (normalizedColor & 0x000000ff) / 255; return `rgba(${r}, ${g}, ${b}, ${a})`; } diff --git a/Libraries/Components/ActivityIndicator/ActivityIndicator.js b/Libraries/Components/ActivityIndicator/ActivityIndicator.js index 0776f1b1961eea..9d0df8f0a739e7 100644 --- a/Libraries/Components/ActivityIndicator/ActivityIndicator.js +++ b/Libraries/Components/ActivityIndicator/ActivityIndicator.js @@ -16,6 +16,7 @@ const StyleSheet = require('../../StyleSheet/StyleSheet'); const View = require('../View/View'); import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; import type {ViewProps} from '../View/ViewPropTypes'; +import type {ColorValue} from '../../StyleSheet/StyleSheetTypes'; const PlatformActivityIndicator = Platform.OS === 'android' @@ -50,7 +51,7 @@ type Props = $ReadOnly<{| * * See https://reactnative.dev/docs/activityindicator.html#color */ - color?: ?string, + color?: ?ColorValue, /** * Size of the indicator (default is 'small'). diff --git a/Libraries/Components/Button.js b/Libraries/Components/Button.js index 6a2b8244f27266..82ea7a1b0adc33 100644 --- a/Libraries/Components/Button.js +++ b/Libraries/Components/Button.js @@ -21,6 +21,7 @@ const View = require('./View/View'); const invariant = require('invariant'); import type {PressEvent} from '../Types/CoreEventTypes'; +import type {ColorValue} from '../StyleSheet/StyleSheetTypes'; type ButtonProps = $ReadOnly<{| /** @@ -41,7 +42,7 @@ type ButtonProps = $ReadOnly<{| /** * Color of the text (iOS), or background color of the button (Android) */ - color?: ?string, + color?: ?ColorValue, /** * TV preferred focus (see documentation for the View component). diff --git a/Libraries/Components/CheckBox/AndroidCheckBoxNativeComponent.js b/Libraries/Components/CheckBox/AndroidCheckBoxNativeComponent.js index 3062ae85c3347b..2afb9892690d05 100644 --- a/Libraries/Components/CheckBox/AndroidCheckBoxNativeComponent.js +++ b/Libraries/Components/CheckBox/AndroidCheckBoxNativeComponent.js @@ -19,6 +19,7 @@ const requireNativeComponent = require('../../ReactNative/requireNativeComponent import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; import type {ViewProps} from '../View/ViewPropTypes'; import type {SyntheticEvent} from '../../Types/CoreEventTypes'; +import type {ProcessedColorValue} from '../../StyleSheet/processColor'; type CheckBoxEvent = SyntheticEvent< $ReadOnly<{| @@ -47,7 +48,12 @@ type NativeProps = $ReadOnly<{| on?: ?boolean, enabled?: boolean, - tintColors: {|true: ?number, false: ?number|} | typeof undefined, + tintColors: + | {| + true: ?ProcessedColorValue, + false: ?ProcessedColorValue, + |} + | typeof undefined, |}>; type NativeType = HostComponent; diff --git a/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js b/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js index b09f2fc8e8dd38..3758c669fd1de6 100644 --- a/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js +++ b/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js @@ -185,7 +185,7 @@ class DrawerLayoutAndroid extends React.Component { ...props } = this.props; const drawStatusBar = - Platform.Version >= 21 && this.props.statusBarBackgroundColor; + Platform.Version >= 21 && this.props.statusBarBackgroundColor != null; const drawerViewWrapper = ( ; type PickerItemSelectEvent = $ReadOnly<{| diff --git a/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js b/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js index ce4ca119a571c1..ca155bc2e3e759 100644 --- a/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js +++ b/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js @@ -23,11 +23,12 @@ import type { import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; import type {TextStyleProp} from '../../StyleSheet/StyleSheet'; import type {ColorValue} from '../../StyleSheet/StyleSheetTypes'; +import type {ProcessedColorValue} from '../../StyleSheet/processColor'; import type {ViewProps} from '../../Components/View/ViewPropTypes'; type PickerItem = $ReadOnly<{| label: string, - color?: ?Int32, + color?: ?ProcessedColorValue, |}>; type PickerItemSelectEvent = $ReadOnly<{| diff --git a/Libraries/Components/Picker/PickerIOS.ios.js b/Libraries/Components/Picker/PickerIOS.ios.js index e99f2b9f679a2c..c2a5f2946fd07b 100644 --- a/Libraries/Components/Picker/PickerIOS.ios.js +++ b/Libraries/Components/Picker/PickerIOS.ios.js @@ -24,6 +24,7 @@ import RCTPickerNativeComponent, { } from './RCTPickerNativeComponent'; import type {TextStyleProp} from '../../StyleSheet/StyleSheet'; import type {ColorValue} from '../../StyleSheet/StyleSheetTypes'; +import type {ProcessedColorValue} from '../../StyleSheet/processColor'; import type {SyntheticEvent} from '../../Types/CoreEventTypes'; import type {ViewProps} from '../View/ViewPropTypes'; @@ -37,7 +38,7 @@ type PickerIOSChangeEvent = SyntheticEvent< type RCTPickerIOSItemType = $ReadOnly<{| label: ?Label, value: ?(number | string), - textColor: ?number, + textColor: ?ProcessedColorValue, |}>; type Label = Stringish | number; diff --git a/Libraries/Components/Picker/RCTPickerNativeComponent.js b/Libraries/Components/Picker/RCTPickerNativeComponent.js index 14564681382227..ca42471b6025ce 100644 --- a/Libraries/Components/Picker/RCTPickerNativeComponent.js +++ b/Libraries/Components/Picker/RCTPickerNativeComponent.js @@ -15,6 +15,7 @@ const requireNativeComponent = require('../../ReactNative/requireNativeComponent import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; import type {SyntheticEvent} from '../../Types/CoreEventTypes'; import type {TextStyleProp} from '../../StyleSheet/StyleSheet'; +import type {ProcessedColorValue} from '../../StyleSheet/processColor'; import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; import * as React from 'react'; @@ -28,7 +29,7 @@ type PickerIOSChangeEvent = SyntheticEvent< type RCTPickerIOSItemType = $ReadOnly<{| label: ?Label, value: ?(number | string), - textColor: ?number, + textColor: ?ProcessedColorValue, |}>; type Label = Stringish | number; diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js index a2651088bbdc39..7b10a0521522f1 100644 --- a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js @@ -15,6 +15,7 @@ const React = require('react'); import ProgressBarAndroidNativeComponent from './ProgressBarAndroidNativeComponent'; import type {ViewProps} from '../View/ViewPropTypes'; +import type {ColorValue} from '../../StyleSheet/StyleSheetTypes'; export type ProgressBarAndroidProps = $ReadOnly<{| ...ViewProps, @@ -49,7 +50,7 @@ export type ProgressBarAndroidProps = $ReadOnly<{| /** * Color of the progress bar. */ - color?: ?string, + color?: ?ColorValue, /** * Used to locate this view in end-to-end tests. */ diff --git a/Libraries/Components/StatusBar/StatusBar.js b/Libraries/Components/StatusBar/StatusBar.js index eb7e8661d07953..81ab87e4d3747a 100644 --- a/Libraries/Components/StatusBar/StatusBar.js +++ b/Libraries/Components/StatusBar/StatusBar.js @@ -15,6 +15,7 @@ const React = require('react'); const invariant = require('invariant'); const processColor = require('../../StyleSheet/processColor'); +import type {ColorValue} from '../../StyleSheet/StyleSheetTypes'; import NativeStatusBarManagerAndroid from './NativeStatusBarManagerAndroid'; import NativeStatusBarManagerIOS from './NativeStatusBarManagerIOS'; @@ -62,7 +63,7 @@ type AndroidProps = $ReadOnly<{| * The background color of the status bar. * @platform android */ - backgroundColor?: ?string, + backgroundColor?: ?ColorValue, /** * If the status bar is translucent. * When translucent is set to true, the app will draw under the status bar. diff --git a/Libraries/Pressability/PressabilityDebug.js b/Libraries/Pressability/PressabilityDebug.js index 36df2290a73e7c..058b025c5f986d 100644 --- a/Libraries/Pressability/PressabilityDebug.js +++ b/Libraries/Pressability/PressabilityDebug.js @@ -11,12 +11,14 @@ 'use strict'; import normalizeColor from '../StyleSheet/normalizeColor.js'; +import type {ColorValue} from '../StyleSheet/StyleSheetTypes'; + import Touchable from '../Components/Touchable/Touchable'; import View from '../Components/View/View'; import * as React from 'react'; type Props = $ReadOnly<{| - color: string, + color: ColorValue, hitSlop: ?$ReadOnly<{| bottom?: ?number, left?: ?number, @@ -43,8 +45,12 @@ type Props = $ReadOnly<{| export function PressabilityDebugView({color, hitSlop}: Props): React.Node { if (__DEV__) { if (isEnabled()) { + const normalizedColor = normalizeColor(color); + if (typeof normalizedColor !== 'number') { + return null; + } const baseColor = - '#' + (normalizeColor(color) ?? 0).toString(16).padStart(8, '0'); + '#' + (normalizedColor ?? 0).toString(16).padStart(8, '0'); return ( reject(error), diff --git a/Libraries/StyleSheet/PlatformColorValueTypes.android.js b/Libraries/StyleSheet/PlatformColorValueTypes.android.js new file mode 100644 index 00000000000000..1458a9b4396d54 --- /dev/null +++ b/Libraries/StyleSheet/PlatformColorValueTypes.android.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +'use strict'; + +import type {ColorValue} from './StyleSheetTypes'; +import type {ProcessedColorValue} from './processColor'; + +export opaque type NativeColorValue = { + resource_paths?: Array, +}; + +export const PlatformColor = (...names: Array): ColorValue => { + return {resource_paths: names}; +}; + +export const ColorAndroidPrivate = (color: string): ColorValue => { + return {resource_paths: [color]}; +}; + +export const normalizeColorObject = ( + color: NativeColorValue, +): ?ProcessedColorValue => { + if ('resource_paths' in color) { + return color; + } + return null; +}; + +export const processColorObject = ( + color: NativeColorValue, +): ?NativeColorValue => { + return color; +}; diff --git a/Libraries/StyleSheet/PlatformColorValueTypes.ios.js b/Libraries/StyleSheet/PlatformColorValueTypes.ios.js new file mode 100644 index 00000000000000..d329db9316f2d1 --- /dev/null +++ b/Libraries/StyleSheet/PlatformColorValueTypes.ios.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +'use strict'; + +import type {ColorValue} from './StyleSheetTypes'; +import type {ProcessedColorValue} from './processColor'; + +export opaque type NativeColorValue = { + semantic?: Array, + dynamic?: { + light: ?(ColorValue | ProcessedColorValue), + dark: ?(ColorValue | ProcessedColorValue), + }, +}; + +export const PlatformColor = (...names: Array): ColorValue => { + return {semantic: names}; +}; + +export type DynamicColorIOSTuplePrivate = { + light: ColorValue, + dark: ColorValue, +}; + +export const DynamicColorIOSPrivate = ( + tuple: DynamicColorIOSTuplePrivate, +): ColorValue => { + return {dynamic: {light: tuple.light, dark: tuple.dark}}; +}; + +export const normalizeColorObject = ( + color: NativeColorValue, +): ?ProcessedColorValue => { + if ('semantic' in color) { + // an ios semantic color + return color; + } else if ('dynamic' in color && color.dynamic !== undefined) { + const normalizeColor = require('./normalizeColor'); + + // a dynamic, appearance aware color + const dynamic = color.dynamic; + const dynamicColor: NativeColorValue = { + dynamic: { + light: normalizeColor(dynamic.light), + dark: normalizeColor(dynamic.dark), + }, + }; + return dynamicColor; + } + + return null; +}; + +export const processColorObject = ( + color: NativeColorValue, +): ?NativeColorValue => { + if ('dynamic' in color && color.dynamic != null) { + const processColor = require('./processColor'); + const dynamic = color.dynamic; + const dynamicColor: NativeColorValue = { + dynamic: { + light: processColor(dynamic.light), + dark: processColor(dynamic.dark), + }, + }; + return dynamicColor; + } + return color; +}; diff --git a/Libraries/StyleSheet/PlatformColorValueTypesAndroid.android.js b/Libraries/StyleSheet/PlatformColorValueTypesAndroid.android.js new file mode 100644 index 00000000000000..58f551098fb5db --- /dev/null +++ b/Libraries/StyleSheet/PlatformColorValueTypesAndroid.android.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +'use strict'; + +import type {ColorValue} from './StyleSheetTypes'; +import {ColorAndroidPrivate} from './PlatformColorValueTypes'; + +export const ColorAndroid = (color: string): ColorValue => { + return ColorAndroidPrivate(color); +}; diff --git a/Libraries/StyleSheet/PlatformColorValueTypesAndroid.js b/Libraries/StyleSheet/PlatformColorValueTypesAndroid.js new file mode 100644 index 00000000000000..647000b3b1e9d0 --- /dev/null +++ b/Libraries/StyleSheet/PlatformColorValueTypesAndroid.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +'use strict'; + +import type {ColorValue} from './StyleSheetTypes'; + +export const ColorAndroid = (color: string): ColorValue => { + throw new Error('ColorAndroid is not available on this platform.'); +}; diff --git a/Libraries/StyleSheet/PlatformColorValueTypesIOS.ios.js b/Libraries/StyleSheet/PlatformColorValueTypesIOS.ios.js new file mode 100644 index 00000000000000..2b21c61f3df19e --- /dev/null +++ b/Libraries/StyleSheet/PlatformColorValueTypesIOS.ios.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +'use strict'; + +import type {ColorValue} from './StyleSheetTypes'; +import {DynamicColorIOSPrivate} from './PlatformColorValueTypes'; + +export type DynamicColorIOSTuple = { + light: ColorValue, + dark: ColorValue, +}; + +export const DynamicColorIOS = (tuple: DynamicColorIOSTuple): ColorValue => { + return DynamicColorIOSPrivate({light: tuple.light, dark: tuple.dark}); +}; diff --git a/Libraries/StyleSheet/PlatformColorValueTypesIOS.js b/Libraries/StyleSheet/PlatformColorValueTypesIOS.js new file mode 100644 index 00000000000000..cc9aa69e80f96b --- /dev/null +++ b/Libraries/StyleSheet/PlatformColorValueTypesIOS.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +'use strict'; + +import type {ColorValue} from './StyleSheetTypes'; + +export type DynamicColorIOSTuple = { + light: ColorValue, + dark: ColorValue, +}; + +export const DynamicColorIOS = (tuple: DynamicColorIOSTuple): ColorValue => { + throw new Error('DynamicColorIOS is not available on this platform.'); +}; diff --git a/Libraries/StyleSheet/StyleSheetTypes.js b/Libraries/StyleSheet/StyleSheetTypes.js index 7d5d2f59afa46f..81081fab0d3bd6 100644 --- a/Libraries/StyleSheet/StyleSheetTypes.js +++ b/Libraries/StyleSheet/StyleSheetTypes.js @@ -12,7 +12,10 @@ const AnimatedNode = require('../Animated/src/nodes/AnimatedNode'); -export type ColorValue = null | string; +import type {NativeColorValue} from './PlatformColorValueTypes'; + +export type ColorValue = null | string | NativeColorValue; + export type ColorArrayValue = null | $ReadOnlyArray; export type PointValue = {| x: number, diff --git a/Libraries/StyleSheet/__tests__/normalizeColor-test.js b/Libraries/StyleSheet/__tests__/normalizeColor-test.js index 35e8cfd03e8b1c..2a127626b692a3 100644 --- a/Libraries/StyleSheet/__tests__/normalizeColor-test.js +++ b/Libraries/StyleSheet/__tests__/normalizeColor-test.js @@ -10,8 +10,16 @@ 'use strict'; +const {OS} = require('../../Utilities/Platform'); const normalizeColor = require('../normalizeColor'); +const PlatformColorIOS = require('../PlatformColorValueTypes.ios') + .PlatformColor; +const DynamicColorIOS = require('../PlatformColorValueTypesIOS.ios') + .DynamicColorIOS; +const PlatformColorAndroid = require('../PlatformColorValueTypes.android') + .PlatformColor; + describe('normalizeColor', function() { it('should accept only spec compliant colors', function() { expect(normalizeColor('#abc')).not.toBe(null); @@ -128,4 +136,48 @@ describe('normalizeColor', function() { const normalizedColor = normalizeColor('red') || 0; expect(normalizeColor(normalizedColor)).toBe(normalizedColor); }); + + describe('iOS', () => { + if (OS === 'ios') { + it('should normalize iOS PlatformColor colors', () => { + const color = PlatformColorIOS('systemRedColor'); + const normalizedColor = normalizeColor(color); + const expectedColor = {semantic: ['systemRedColor']}; + expect(normalizedColor).toEqual(expectedColor); + }); + + it('should normalize iOS Dynamic colors with named colors', () => { + const color = DynamicColorIOS({light: 'black', dark: 'white'}); + const normalizedColor = normalizeColor(color); + const expectedColor = {dynamic: {light: 'black', dark: 'white'}}; + expect(normalizedColor).toEqual(expectedColor); + }); + + it('should normalize iOS Dynamic colors with PlatformColor colors', () => { + const color = DynamicColorIOS({ + light: PlatformColorIOS('systemBlackColor'), + dark: PlatformColorIOS('systemWhiteColor'), + }); + const normalizedColor = normalizeColor(color); + const expectedColor = { + dynamic: { + light: {semantic: ['systemBlackColor']}, + dark: {semantic: ['systemWhiteColor']}, + }, + }; + expect(normalizedColor).toEqual(expectedColor); + }); + } + }); + + describe('Android', () => { + if (OS === 'android') { + it('should normalize Android PlatformColor colors', () => { + const color = PlatformColorAndroid('?attr/colorPrimary'); + const normalizedColor = normalizeColor(color); + const expectedColor = {resource_paths: ['?attr/colorPrimary']}; + expect(normalizedColor).toEqual(expectedColor); + }); + } + }); }); diff --git a/Libraries/StyleSheet/__tests__/processColor-test.js b/Libraries/StyleSheet/__tests__/processColor-test.js index 0b60130e26ebf3..d428b854e8f50e 100644 --- a/Libraries/StyleSheet/__tests__/processColor-test.js +++ b/Libraries/StyleSheet/__tests__/processColor-test.js @@ -13,6 +13,13 @@ const {OS} = require('../../Utilities/Platform'); const processColor = require('../processColor'); +const PlatformColorIOS = require('../PlatformColorValueTypes.ios') + .PlatformColor; +const DynamicColorIOS = require('../PlatformColorValueTypesIOS.ios') + .DynamicColorIOS; +const PlatformColorAndroid = require('../PlatformColorValueTypes.android') + .PlatformColor; + const platformSpecific = OS === 'android' ? unsigned => unsigned | 0 //eslint-disable-line no-bitwise @@ -84,4 +91,33 @@ describe('processColor', () => { expect(colorFromString).toEqual(platformSpecific(expectedInt)); }); }); + + describe('iOS', () => { + if (OS === 'ios') { + it('should process iOS PlatformColor colors', () => { + const color = PlatformColorIOS('systemRedColor'); + const processedColor = processColor(color); + const expectedColor = {semantic: ['systemRedColor']}; + expect(processedColor).toEqual(expectedColor); + }); + + it('should process iOS Dynamic colors', () => { + const color = DynamicColorIOS({light: 'black', dark: 'white'}); + const processedColor = processColor(color); + const expectedColor = {dynamic: {light: 0xff000000, dark: 0xffffffff}}; + expect(processedColor).toEqual(expectedColor); + }); + } + }); + + describe('Android', () => { + if (OS === 'android') { + it('should process Android PlatformColor colors', () => { + const color = PlatformColorAndroid('?attr/colorPrimary'); + const processedColor = processColor(color); + const expectedColor = {resource_paths: ['?attr/colorPrimary']}; + expect(processedColor).toEqual(expectedColor); + }); + } + }); }); diff --git a/Libraries/StyleSheet/__tests__/processColorArray-test.js b/Libraries/StyleSheet/__tests__/processColorArray-test.js index acd45cdd72a761..1be389cdb92421 100644 --- a/Libraries/StyleSheet/__tests__/processColorArray-test.js +++ b/Libraries/StyleSheet/__tests__/processColorArray-test.js @@ -13,6 +13,13 @@ const {OS} = require('../../Utilities/Platform'); const processColorArray = require('../processColorArray'); +const PlatformColorIOS = require('../PlatformColorValueTypes.ios') + .PlatformColor; +const DynamicColorIOS = require('../PlatformColorValueTypesIOS.ios') + .DynamicColorIOS; +const PlatformColorAndroid = require('../PlatformColorValueTypes.android') + .PlatformColor; + const platformSpecific = OS === 'android' ? unsigned => unsigned | 0 //eslint-disable-line no-bitwise @@ -57,4 +64,48 @@ describe('processColorArray', () => { expect(colorFromNoArray).toEqual(null); }); }); + + describe('iOS', () => { + if (OS === 'ios') { + it('should convert array of iOS PlatformColor colors', () => { + const colorFromArray = processColorArray([ + PlatformColorIOS('systemColorWhite'), + PlatformColorIOS('systemColorBlack'), + ]); + const expectedColorValueArray = [ + {semantic: ['systemColorWhite']}, + {semantic: ['systemColorBlack']}, + ]; + expect(colorFromArray).toEqual(expectedColorValueArray); + }); + + it('should process iOS Dynamic colors', () => { + const colorFromArray = processColorArray([ + DynamicColorIOS({light: 'black', dark: 'white'}), + DynamicColorIOS({light: 'white', dark: 'black'}), + ]); + const expectedColorValueArray = [ + {dynamic: {light: 0xff000000, dark: 0xffffffff}}, + {dynamic: {light: 0xffffffff, dark: 0xff000000}}, + ]; + expect(colorFromArray).toEqual(expectedColorValueArray); + }); + } + }); + + describe('Android', () => { + if (OS === 'android') { + it('should convert array of Android PlatformColor colors', () => { + const colorFromArray = processColorArray([ + PlatformColorAndroid('?attr/colorPrimary'), + PlatformColorAndroid('?colorPrimaryDark'), + ]); + const expectedColorValueArray = [ + {resource_paths: ['?attr/colorPrimary']}, + {resource_paths: ['?colorPrimaryDark']}, + ]; + expect(colorFromArray).toEqual(expectedColorValueArray); + }); + } + }); }); diff --git a/Libraries/StyleSheet/normalizeColor.js b/Libraries/StyleSheet/normalizeColor.js index b14acc3106de64..eaee5813b177e1 100755 --- a/Libraries/StyleSheet/normalizeColor.js +++ b/Libraries/StyleSheet/normalizeColor.js @@ -12,7 +12,12 @@ 'use strict'; -function normalizeColor(color: string | number): ?number { +import type {ColorValue} from './StyleSheetTypes'; +import type {ProcessedColorValue} from './processColor'; + +function normalizeColor( + color: ?(ColorValue | ProcessedColorValue), +): ?ProcessedColorValue { const matchers = getMatchers(); let match; @@ -23,6 +28,21 @@ function normalizeColor(color: string | number): ?number { return null; } + if (typeof color === 'object' && color != null) { + const normalizeColorObject = require('./PlatformColorValueTypes') + .normalizeColorObject; + + const normalizedColorObj = normalizeColorObject(color); + + if (normalizedColorObj != null) { + return color; + } + } + + if (typeof color !== 'string') { + return null; + } + // Ordered based on occurrences on Facebook codebase if ((match = matchers.hex6.exec(color))) { return parseInt(match[1] + 'ff', 16) >>> 0; diff --git a/Libraries/StyleSheet/processColor.js b/Libraries/StyleSheet/processColor.js index 46b298e7be9e84..3bfa9679e00a79 100644 --- a/Libraries/StyleSheet/processColor.js +++ b/Libraries/StyleSheet/processColor.js @@ -14,35 +14,48 @@ const Platform = require('../Utilities/Platform'); const normalizeColor = require('./normalizeColor'); -// TODO: This is an empty object for now, just to enforce that everything using this -// downstream is correct. This will be replaced with an import to other files -// with a platform specific implementation. See the PR for more information -// https://github.com/facebook/react-native/pull/27908 -opaque type NativeColorType = {}; -export type ProcessedColorValue = ?number | NativeColorType; +import type {ColorValue} from './StyleSheetTypes'; +import type {NativeColorValue} from './PlatformColorValueTypes'; + +export type ProcessedColorValue = number | NativeColorValue; /* eslint no-bitwise: 0 */ -function processColor(color?: ?(string | number)): ProcessedColorValue { +function processColor(color?: ?(number | ColorValue)): ?ProcessedColorValue { if (color === undefined || color === null) { return color; } - let int32Color = normalizeColor(color); - if (int32Color === null || int32Color === undefined) { + let normalizedColor = normalizeColor(color); + if (normalizedColor === null || normalizedColor === undefined) { return undefined; } + if (typeof normalizedColor === 'object') { + const processColorObject = require('./PlatformColorValueTypes') + .processColorObject; + + const processedColorObj = processColorObject(normalizedColor); + + if (processedColorObj != null) { + return processedColorObj; + } + } + + if (typeof normalizedColor !== 'number') { + return null; + } + // Converts 0xrrggbbaa into 0xaarrggbb - int32Color = ((int32Color << 24) | (int32Color >>> 8)) >>> 0; + normalizedColor = ((normalizedColor << 24) | (normalizedColor >>> 8)) >>> 0; if (Platform.OS === 'android') { // Android use 32 bit *signed* integer to represent the color // We utilize the fact that bitwise operations in JS also operates on // signed 32 bit integers, so that we can use those to convert from // *unsigned* to *signed* 32bit int that way. - int32Color = int32Color | 0x0; + normalizedColor = normalizedColor | 0x0; } - return int32Color; + return normalizedColor; } module.exports = processColor; diff --git a/Libraries/StyleSheet/processColorArray.js b/Libraries/StyleSheet/processColorArray.js index e84482c8cb681e..9bdb026b5bc0c3 100644 --- a/Libraries/StyleSheet/processColorArray.js +++ b/Libraries/StyleSheet/processColorArray.js @@ -11,11 +11,13 @@ 'use strict'; const processColor = require('./processColor'); + +import type {ColorValue} from './StyleSheetTypes'; import type {ProcessedColorValue} from './processColor'; function processColorArray( - colors: ?Array, -): ?Array { + colors: ?Array, +): ?Array { return colors == null ? null : colors.map(processColor); } diff --git a/Libraries/Text/Text/RCTTextView.m b/Libraries/Text/Text/RCTTextView.m index 3f4382162df1c5..d0d19a988e1306 100644 --- a/Libraries/Text/Text/RCTTextView.m +++ b/Libraries/Text/Text/RCTTextView.m @@ -96,6 +96,7 @@ - (void)setTextStorage:(NSTextStorage *)textStorage - (void)drawRect:(CGRect)rect { + [super drawRect:rect]; if (!_textStorage) { return; } diff --git a/RNTester/RNTesterPods.xcodeproj/project.pbxproj b/RNTester/RNTesterPods.xcodeproj/project.pbxproj index 9ce41dc1357dfa..b79cc14cfa0810 100644 --- a/RNTester/RNTesterPods.xcodeproj/project.pbxproj +++ b/RNTester/RNTesterPods.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 272E6B3F1BEA849E001FCF37 /* UpdatePropertiesExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.m */; }; 27F441EC1BEBE5030039B79C /* FlexibleSizeExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F441E81BEBE5030039B79C /* FlexibleSizeExampleView.m */; }; 2DDEF0101F84BF7B00DBDF73 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2DDEF00F1F84BF7B00DBDF73 /* Images.xcassets */; }; + 383889DA23A7398900D06C3E /* RCTConvert_UIColorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 383889D923A7398900D06C3E /* RCTConvert_UIColorTests.m */; }; 3D2AFAF51D646CF80089D1A3 /* legacy_image@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3D2AFAF41D646CF80089D1A3 /* legacy_image@2x.png */; }; 5C60EB1C226440DB0018C04F /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5C60EB1B226440DB0018C04F /* AppDelegate.mm */; }; 5CB07C9B226467E60039471C /* RNTesterTurboModuleProvider.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5CB07C99226467E60039471C /* RNTesterTurboModuleProvider.mm */; }; @@ -81,6 +82,7 @@ 27F441EA1BEBE5030039B79C /* FlexibleSizeExampleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FlexibleSizeExampleView.h; path = RNTester/NativeExampleViews/FlexibleSizeExampleView.h; sourceTree = ""; }; 2DDEF00F1F84BF7B00DBDF73 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RNTester/Images.xcassets; sourceTree = ""; }; 34028D6B10F47E490042EB27 /* Pods-RNTesterUnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTesterUnitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RNTesterUnitTests/Pods-RNTesterUnitTests.debug.xcconfig"; sourceTree = ""; }; + 383889D923A7398900D06C3E /* RCTConvert_UIColorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_UIColorTests.m; sourceTree = ""; }; 3D2AFAF41D646CF80089D1A3 /* legacy_image@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "legacy_image@2x.png"; path = "RNTester/legacy_image@2x.png"; sourceTree = ""; }; 5BEC8567F3741044B6A5EFC5 /* Pods-RNTester.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTester.release.xcconfig"; path = "Pods/Target Support Files/Pods-RNTester/Pods-RNTester.release.xcconfig"; sourceTree = ""; }; 5C60EB1B226440DB0018C04F /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = RNTester/AppDelegate.mm; sourceTree = ""; }; @@ -324,6 +326,7 @@ E7DB20CC22B2BAA5005AC45F /* RCTComponentPropsTests.m */, E7DB20CA22B2BAA5005AC45F /* RCTConvert_NSURLTests.m */, E7DB20CE22B2BAA5005AC45F /* RCTConvert_YGValueTests.m */, + 383889D923A7398900D06C3E /* RCTConvert_UIColorTests.m */, E7DB20C822B2BAA5005AC45F /* RCTDevMenuTests.m */, E7DB20C022B2BAA4005AC45F /* RCTEventDispatcherTests.m */, E7DB20AF22B2BAA4005AC45F /* RCTFontTests.m */, @@ -686,6 +689,7 @@ E7DB20D322B2BAA6005AC45F /* RCTBlobManagerTests.m in Sources */, E7DB20DC22B2BAA6005AC45F /* RCTUIManagerTests.m in Sources */, E7DB20E322B2BAA6005AC45F /* RCTAllocationTests.m in Sources */, + 383889DA23A7398900D06C3E /* RCTConvert_UIColorTests.m in Sources */, E7DB20E622B2BAA6005AC45F /* RCTImageLoaderHelpers.m in Sources */, E7DB20D622B2BAA6005AC45F /* RCTFontTests.m in Sources */, E7DB20DB22B2BAA6005AC45F /* RCTNativeAnimatedNodesManagerTests.m in Sources */, diff --git a/RNTester/RNTesterUnitTests/RCTConvert_UIColorTests.m b/RNTester/RNTesterUnitTests/RCTConvert_UIColorTests.m new file mode 100644 index 00000000000000..4e9b6754989d65 --- /dev/null +++ b/RNTester/RNTesterUnitTests/RCTConvert_UIColorTests.m @@ -0,0 +1,201 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +#import + +#import + +@interface RCTConvert_NSColorTests : XCTestCase + +@end + +static BOOL CGColorsAreEqual(CGColorRef color1, CGColorRef color2) { + CGFloat rgba1[4]; + CGFloat rgba2[4]; + RCTGetRGBAColorComponents(color1, rgba1); + RCTGetRGBAColorComponents(color2, rgba2); + for (int i = 0; i < 4; i++) { + if (rgba1[i] != rgba2[i]) { + return NO; + } + } + return YES; +} + +@implementation RCTConvert_NSColorTests + +- (void)testColor +{ + id json = RCTJSONParse(@"{ \"semantic\": \"lightTextColor\" }", nil); + UIColor *value = [RCTConvert UIColor:json]; + XCTAssertEqualObjects(value, [UIColor lightTextColor]); +} + +- (void)testColorFailure +{ + id json = RCTJSONParse(@"{ \"semantic\": \"bogusColor\" }", nil); + + __block NSString *errorMessage = nil; + RCTLogFunction defaultLogFunction = RCTGetLogFunction(); + RCTSetLogFunction(^(__unused RCTLogLevel level, __unused RCTLogSource source, __unused NSString *fileName, __unused NSNumber *lineNumber, NSString *message) { + errorMessage = message; + }); + + UIColor *value = [RCTConvert UIColor:json]; + + RCTSetLogFunction(defaultLogFunction); + + XCTAssertEqualObjects(value, nil); + XCTAssertTrue([errorMessage containsString:@"labelColor"]); // the RedBox message will contain a list of the valid color names. +} + +- (void)testFallbackColor +{ + id json = RCTJSONParse(@"{ \"semantic\": \"unitTestFallbackColorIOS\" }", nil); + UIColor *value = [RCTConvert UIColor:json]; + XCTAssertTrue(CGColorsAreEqual([value CGColor], [[UIColor blueColor] CGColor])); +} + +- (void)testDynamicColor +{ + // 0 == 0x00000000 == black + // 16777215 == 0x00FFFFFF == white + id json = RCTJSONParse(@"{ \"dynamic\": { \"light\":0, \"dark\":16777215 } }", nil); + UIColor *value = [RCTConvert UIColor:json]; + XCTAssertNotNil(value); + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + if (@available(iOS 13.0, *)) { + id savedTraitCollection = [UITraitCollection currentTraitCollection]; + + [UITraitCollection setCurrentTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]]; + CGFloat rgba[4]; + RCTGetRGBAColorComponents([value CGColor], rgba); + XCTAssertEqual(rgba[0], 0); + XCTAssertEqual(rgba[1], 0); + XCTAssertEqual(rgba[2], 0); + XCTAssertEqual(rgba[3], 0); + + [UITraitCollection setCurrentTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]]; + RCTGetRGBAColorComponents([value CGColor], rgba); + XCTAssertEqual(rgba[0], 1); + XCTAssertEqual(rgba[1], 1); + XCTAssertEqual(rgba[2], 1); + XCTAssertEqual(rgba[3], 0); + + [UITraitCollection setCurrentTraitCollection:savedTraitCollection]; + } +#endif +} + +- (void)testCompositeDynamicColor +{ + id json = RCTJSONParse(@"{ \"dynamic\": { \"light\": { \"semantic\": \"systemRedColor\" }, \"dark\":{ \"semantic\": \"systemBlueColor\" } } }", nil); + UIColor *value = [RCTConvert UIColor:json]; + XCTAssertNotNil(value); + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + if (@available(iOS 13.0, *)) { + id savedTraitCollection = [UITraitCollection currentTraitCollection]; + + [UITraitCollection setCurrentTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]]; + + XCTAssertTrue(CGColorsAreEqual([value CGColor], [[UIColor systemRedColor] CGColor])); + + [UITraitCollection setCurrentTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]]; + + XCTAssertTrue(CGColorsAreEqual([value CGColor], [[UIColor systemBlueColor] CGColor])); + + [UITraitCollection setCurrentTraitCollection:savedTraitCollection]; + } +#endif +} + +- (void)testGenerateFallbacks +{ + NSDictionary* semanticColors = @{ + // https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors + // Label Colors + @"labelColor": @(0xFF000000), + @"secondaryLabelColor": @(0x993c3c43), + @"tertiaryLabelColor": @(0x4c3c3c43), + @"quaternaryLabelColor": @(0x2d3c3c43), + // Fill Colors + @"systemFillColor": @(0x33787880), + @"secondarySystemFillColor": @(0x28787880), + @"tertiarySystemFillColor": @(0x1e767680), + @"quaternarySystemFillColor": @(0x14747480), + // Text Colors + @"placeholderTextColor": @(0x4c3c3c43), + // Standard Content Background Colors + @"systemBackgroundColor": @(0xFFffffff), + @"secondarySystemBackgroundColor": @(0xFFf2f2f7), + @"tertiarySystemBackgroundColor": @(0xFFffffff), + // Grouped Content Background Colors + @"systemGroupedBackgroundColor": @(0xFFf2f2f7), + @"secondarySystemGroupedBackgroundColor": @(0xFFffffff), + @"tertiarySystemGroupedBackgroundColor": @(0xFFf2f2f7), + // Separator Colors + @"separatorColor": @(0x493c3c43), + @"opaqueSeparatorColor": @(0xFFc6c6c8), + // Link Color + @"linkColor": @(0xFF007aff), + // https://developer.apple.com/documentation/uikit/uicolor/standard_colors + // Adaptable Colors + @"systemBrownColor": @(0xFFa2845e), + @"systemIndigoColor": @(0xFF5856d6), + // Adaptable Gray Colors + @"systemGray2Color": @(0xFFaeaeb2), + @"systemGray3Color": @(0xFFc7c7cc), + @"systemGray4Color": @(0xFFd1d1d6), + @"systemGray5Color": @(0xFFe5e5ea), + @"systemGray6Color": @(0xFFf2f2f7), + }; + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + id savedTraitCollection = nil; + if (@available(iOS 13.0, *)) { + savedTraitCollection = [UITraitCollection currentTraitCollection]; + + [UITraitCollection setCurrentTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]]; + } +#endif + + for (NSString *semanticColor in semanticColors) { + id json = RCTJSONParse([NSString stringWithFormat:@"{ \"semantic\": \"%@\" }", semanticColor], nil); + UIColor *value = [RCTConvert UIColor:json]; + XCTAssertNotNil(value); + + NSNumber *fallback = [semanticColors objectForKey:semanticColor]; + NSUInteger rgbValue = [fallback unsignedIntegerValue]; + NSUInteger alpha1 = ((rgbValue & 0xFF000000) >> 24); + NSUInteger red1 = ((rgbValue & 0x00FF0000) >> 16); + NSUInteger green1 = ((rgbValue & 0x0000FF00) >> 8); + NSUInteger blue1 = ((rgbValue & 0x000000FF) >> 0); + + CGFloat rgba[4]; + RCTGetRGBAColorComponents([value CGColor], rgba); + NSUInteger red2 = rgba[0] * 255; + NSUInteger green2 = rgba[1] * 255; + NSUInteger blue2 = rgba[2] * 255; + NSUInteger alpha2 = rgba[3] * 255; + + XCTAssertEqual(red1, red2); + XCTAssertEqual(green1, green2); + XCTAssertEqual(blue1, blue2); + XCTAssertEqual(alpha1, alpha2); + } + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + if (@available(iOS 13.0, *)) { + [UITraitCollection setCurrentTraitCollection:savedTraitCollection]; + } +#endif +} + +@end diff --git a/RNTester/js/components/RNTesterTheme.js b/RNTester/js/components/RNTesterTheme.js index b78a8344fe29be..7bc14ea3ebd99d 100644 --- a/RNTester/js/components/RNTesterTheme.js +++ b/RNTester/js/components/RNTesterTheme.js @@ -12,28 +12,29 @@ import * as React from 'react'; import {Appearance} from 'react-native'; +import type {ColorValue} from '../../../Libraries/StyleSheet/StyleSheetTypes'; export type RNTesterTheme = { - LabelColor: string, - SecondaryLabelColor: string, - TertiaryLabelColor: string, - QuaternaryLabelColor: string, - PlaceholderTextColor: string, - SystemBackgroundColor: string, - SecondarySystemBackgroundColor: string, - TertiarySystemBackgroundColor: string, - GroupedBackgroundColor: string, - SecondaryGroupedBackgroundColor: string, - TertiaryGroupedBackgroundColor: string, - SystemFillColor: string, - SecondarySystemFillColor: string, - TertiarySystemFillColor: string, - QuaternarySystemFillColor: string, - SeparatorColor: string, - OpaqueSeparatorColor: string, - LinkColor: string, - SystemPurpleColor: string, - ToolbarColor: string, + LabelColor: ColorValue, + SecondaryLabelColor: ColorValue, + TertiaryLabelColor: ColorValue, + QuaternaryLabelColor: ColorValue, + PlaceholderTextColor: ColorValue, + SystemBackgroundColor: ColorValue, + SecondarySystemBackgroundColor: ColorValue, + TertiarySystemBackgroundColor: ColorValue, + GroupedBackgroundColor: ColorValue, + SecondaryGroupedBackgroundColor: ColorValue, + TertiaryGroupedBackgroundColor: ColorValue, + SystemFillColor: ColorValue, + SecondarySystemFillColor: ColorValue, + TertiarySystemFillColor: ColorValue, + QuaternarySystemFillColor: ColorValue, + SeparatorColor: ColorValue, + OpaqueSeparatorColor: ColorValue, + LinkColor: ColorValue, + SystemPurpleColor: ColorValue, + ToolbarColor: ColorValue, ... }; diff --git a/RNTester/js/examples/Appearance/AppearanceExample.js b/RNTester/js/examples/Appearance/AppearanceExample.js index ab99e9213bf256..bc2d074b2be75e 100644 --- a/RNTester/js/examples/Appearance/AppearanceExample.js +++ b/RNTester/js/examples/Appearance/AppearanceExample.js @@ -191,7 +191,9 @@ exports.examples = [ paddingVertical: 2, color: theme.LabelColor, }}> - {theme[key]} + {typeof theme[key] === 'string' + ? theme[key] + : JSON.stringify(theme[key])} diff --git a/RNTester/js/examples/PlatformColor/PlatformColorExample.js b/RNTester/js/examples/PlatformColor/PlatformColorExample.js new file mode 100644 index 00000000000000..611a2fa5b27feb --- /dev/null +++ b/RNTester/js/examples/PlatformColor/PlatformColorExample.js @@ -0,0 +1,355 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +import Platform from '../../../../Libraries/Utilities/Platform'; +const { + ColorAndroid, + DynamicColorIOS, + PlatformColor, + StyleSheet, + Text, + View, +} = ReactNative; + +function PlatformColorsExample() { + function createTable() { + let colors = []; + if (Platform.OS === 'ios') { + colors = [ + // https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors + // Label Colors + {label: 'labelColor', color: PlatformColor('labelColor')}, + { + label: 'secondaryLabelColor', + color: PlatformColor('secondaryLabelColor'), + }, + { + label: 'tertiaryLabelColor', + color: PlatformColor('tertiaryLabelColor'), + }, + { + label: 'quaternaryLabelColor', + color: PlatformColor('quaternaryLabelColor'), + }, + // Fill Colors + {label: 'systemFillColor', color: PlatformColor('systemFillColor')}, + { + label: 'secondarySystemFillColor', + color: PlatformColor('secondarySystemFillColor'), + }, + { + label: 'tertiarySystemFillColor', + color: PlatformColor('tertiarySystemFillColor'), + }, + { + label: 'quaternarySystemFillColor', + color: PlatformColor('quaternarySystemFillColor'), + }, + // Text Colors + { + label: 'placeholderTextColor', + color: PlatformColor('placeholderTextColor'), + }, + // Standard Content Background Colors + { + label: 'systemBackgroundColor', + color: PlatformColor('systemBackgroundColor'), + }, + { + label: 'secondarySystemBackgroundColor', + color: PlatformColor('secondarySystemBackgroundColor'), + }, + { + label: 'tertiarySystemBackgroundColor', + color: PlatformColor('tertiarySystemBackgroundColor'), + }, + // Grouped Content Background Colors + { + label: 'systemGroupedBackgroundColor', + color: PlatformColor('systemGroupedBackgroundColor'), + }, + { + label: 'secondarySystemGroupedBackgroundColor', + color: PlatformColor('secondarySystemGroupedBackgroundColor'), + }, + { + label: 'tertiarySystemGroupedBackgroundColor', + color: PlatformColor('tertiarySystemGroupedBackgroundColor'), + }, + // Separator Colors + {label: 'separatorColor', color: PlatformColor('separatorColor')}, + { + label: 'opaqueSeparatorColor', + color: PlatformColor('opaqueSeparatorColor'), + }, + // Link Color + {label: 'linkColor', color: PlatformColor('linkColor')}, + // Nonadaptable Colors + {label: 'darkTextColor', color: PlatformColor('darkTextColor')}, + {label: 'lightTextColor', color: PlatformColor('lightTextColor')}, + // https://developer.apple.com/documentation/uikit/uicolor/standard_colors + // Adaptable Colors + {label: 'systemBlueColor', color: PlatformColor('systemBlueColor')}, + {label: 'systemBrownColor', color: PlatformColor('systemBrownColor')}, + {label: 'systemGreenColor', color: PlatformColor('systemGreenColor')}, + {label: 'systemIndigoColor', color: PlatformColor('systemIndigoColor')}, + {label: 'systemOrangeColor', color: PlatformColor('systemOrangeColor')}, + {label: 'systemPinkColor', color: PlatformColor('systemPinkColor')}, + {label: 'systemPurpleColor', color: PlatformColor('systemPurpleColor')}, + {label: 'systemRedColor', color: PlatformColor('systemRedColor')}, + {label: 'systemTealColor', color: PlatformColor('systemTealColor')}, + {label: 'systemYellowColor', color: PlatformColor('systemYellowColor')}, + // Adaptable Gray Colors + {label: 'systemGrayColor', color: PlatformColor('systemGrayColor')}, + {label: 'systemGray2Color', color: PlatformColor('systemGray2Color')}, + {label: 'systemGray3Color', color: PlatformColor('systemGray3Color')}, + {label: 'systemGray4Color', color: PlatformColor('systemGray4Color')}, + {label: 'systemGray5Color', color: PlatformColor('systemGray5Color')}, + {label: 'systemGray6Color', color: PlatformColor('systemGray6Color')}, + ]; + } else if (Platform.OS === 'android') { + colors = [ + {label: '?attr/colorAccent', color: PlatformColor('?attr/colorAccent')}, + { + label: '?attr/colorBackgroundFloating', + color: PlatformColor('?attr/colorBackgroundFloating'), + }, + { + label: '?attr/colorButtonNormal', + color: PlatformColor('?attr/colorButtonNormal'), + }, + { + label: '?attr/colorControlActivated', + color: PlatformColor('?attr/colorControlActivated'), + }, + { + label: '?attr/colorControlHighlight', + color: PlatformColor('?attr/colorControlHighlight'), + }, + { + label: '?attr/colorControlNormal', + color: PlatformColor('?attr/colorControlNormal'), + }, + { + label: '?android:colorError', + color: PlatformColor('?android:colorError'), + }, + { + label: '?android:attr/colorError', + color: PlatformColor('?android:attr/colorError'), + }, + { + label: '?attr/colorPrimary', + color: PlatformColor('?attr/colorPrimary'), + }, + {label: '?colorPrimaryDark', color: PlatformColor('?colorPrimaryDark')}, + { + label: '@android:color/holo_purple', + color: PlatformColor('@android:color/holo_purple'), + }, + { + label: '@android:color/holo_green_light', + color: PlatformColor('@android:color/holo_green_light'), + }, + { + label: '@color/catalyst_redbox_background', + color: PlatformColor('@color/catalyst_redbox_background'), + }, + { + label: '@color/catalyst_logbox_background', + color: PlatformColor('@color/catalyst_logbox_background'), + }, + ]; + } + + let table = []; + for (let color of colors) { + table.push( + + {color.label} + + , + ); + } + return table; + } + + return {createTable()}; +} + +function FallbackColorsExample() { + let color = {}; + if (Platform.OS === 'ios') { + color = { + label: "PlatformColor('bogus', 'systemGreenColor')", + color: PlatformColor('bogus', 'systemGreenColor'), + }; + } else if (Platform.OS === 'android') { + color = { + label: "PlatformColor('bogus', '@color/catalyst_redbox_background')", + color: PlatformColor('bogus', '@color/catalyst_redbox_background'), + }; + } else { + throw 'Unexpected Platform.OS: ' + Platform.OS; + } + + return ( + + + {color.label} + + + + ); +} + +function DynamicColorsExample() { + return Platform.OS === 'ios' ? ( + + + + DynamicColorIOS({'{\n'} + {' '}light: 'red', dark: 'blue'{'\n'} + {'}'}) + + + + + + DynamicColorIOS({'{\n'} + {' '}light: PlatformColor('systemBlueColor'),{'\n'} + {' '}dark: PlatformColor('systemRedColor'),{'\n'} + {'}'}) + + + + + ) : ( + Not applicable on this platform + ); +} + +function AndroidColorsExample() { + return Platform.OS === 'android' ? ( + + + ColorAndroid('?attr/colorAccent') + + + + ) : ( + Not applicable on this platform + ); +} + +function VariantColorsExample() { + return ( + + + + {Platform.OS === 'ios' + ? "DynamicColorIOS({light: 'red', dark: 'blue'})" + : "ColorAndroid('?attr/colorAccent')"} + + + + + ); +} + +const styles = StyleSheet.create({ + column: {flex: 1, flexDirection: 'column'}, + row: {flex: 0.75, flexDirection: 'row'}, + labelCell: { + flex: 1, + alignItems: 'stretch', + ...Platform.select({ + ios: {color: PlatformColor('labelColor')}, + default: {color: 'black'}, + }), + }, + colorCell: {flex: 0.25, alignItems: 'stretch'}, +}); + +exports.title = 'PlatformColor'; +exports.description = + 'Examples that show how PlatformColors may be used in an app.'; +exports.examples = [ + { + title: 'Platform Colors', + render(): React.Element { + return ; + }, + }, + { + title: 'Fallback Colors', + render(): React.Element { + return ; + }, + }, + { + title: 'iOS Dynamic Colors', + render(): React.Element { + return ; + }, + }, + { + title: 'Android Colors', + render(): React.Element { + return ; + }, + }, + { + title: 'Variant Colors', + render(): React.Element { + return ; + }, + }, +]; diff --git a/RNTester/js/utils/RNTesterList.android.js b/RNTester/js/utils/RNTesterList.android.js index d4300da1d8f379..1de9d42a00c7b6 100644 --- a/RNTester/js/utils/RNTesterList.android.js +++ b/RNTester/js/utils/RNTesterList.android.js @@ -200,6 +200,10 @@ const APIExamples: Array = [ key: 'PermissionsExampleAndroid', module: require('../examples/PermissionsAndroid/PermissionsExample'), }, + { + key: 'PlatformColorExample', + module: require('../examples/PlatformColor/PlatformColorExample'), + }, { key: 'PointerEventsExample', module: require('../examples/PointerEvents/PointerEventsExample'), diff --git a/RNTester/js/utils/RNTesterList.ios.js b/RNTester/js/utils/RNTesterList.ios.js index 31c9751754e6e7..a3e6e4f71d1167 100644 --- a/RNTester/js/utils/RNTesterList.ios.js +++ b/RNTester/js/utils/RNTesterList.ios.js @@ -279,6 +279,11 @@ const APIExamples: Array = [ module: require('../examples/PanResponder/PanResponderExample'), supportsTVOS: false, }, + { + key: 'PlatformColorExample', + module: require('../examples/PlatformColor/PlatformColorExample'), + supportsTVOS: true, + }, { key: 'PointerEventsExample', module: require('../examples/PointerEvents/PointerEventsExample'), diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index e8b7194af24a7c..9e0791cb8fc614 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -506,6 +506,207 @@ + (type)type:(id)json \ @"a", @"b", @"c", @"d", @"tx", @"ty" ])) +static NSString *const RCTFallback = @"fallback"; +static NSString *const RCTFallbackARGB = @"fallback-argb"; +static NSString *const RCTSelector = @"selector"; +static NSString *const RCTIndex = @"index"; + +/** The following dictionary defines the react-native semantic colors for ios. + * If the value for a given name is empty then the name itself + * is used as the UIColor selector. + * If the RCTSelector key is present then that value is used for a selector instead + * of the key name. + * If the given selector is not available on the running OS version then + * the RCTFallback selector is used instead. + * If the RCTIndex key is present then object returned from UIColor is an + * NSArray and the object at index RCTIndex is to be used. + */ +static NSDictionary* RCTSemanticColorsMap() +{ + static NSDictionary *colorMap = nil; + if (colorMap == nil) { + colorMap = @{ + // https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors + // Label Colors + @"labelColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFF000000) // fallback for iOS<=12: RGBA returned by this semantic color in light mode on iOS 13 + }, + @"secondaryLabelColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0x993c3c43) + }, + @"tertiaryLabelColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0x4c3c3c43) + }, + @"quaternaryLabelColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0x2d3c3c43) + }, + // Fill Colors + @"systemFillColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0x33787880) + }, + @"secondarySystemFillColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0x28787880) + }, + @"tertiarySystemFillColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0x1e767680) + }, + @"quaternarySystemFillColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0x14747480) + }, + // Text Colors + @"placeholderTextColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0x4c3c3c43) + }, + // Standard Content Background Colors + @"systemBackgroundColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFffffff) + }, + @"secondarySystemBackgroundColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFf2f2f7) + }, + @"tertiarySystemBackgroundColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFffffff) + }, + // Grouped Content Background Colors + @"systemGroupedBackgroundColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFf2f2f7) + }, + @"secondarySystemGroupedBackgroundColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFffffff) + }, + @"tertiarySystemGroupedBackgroundColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFf2f2f7) + }, + // Separator Colors + @"separatorColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0x493c3c43) + }, + @"opaqueSeparatorColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFc6c6c8) + }, + // Link Color + @"linkColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFF007aff) + }, + // Nonadaptable Colors + @"darkTextColor": @{}, + @"lightTextColor": @{}, + // https://developer.apple.com/documentation/uikit/uicolor/standard_colors + // Adaptable Colors + @"systemBlueColor": @{}, + @"systemBrownColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFa2845e) + }, + @"systemGreenColor": @{}, + @"systemIndigoColor": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFF5856d6) + }, + @"systemOrangeColor": @{}, + @"systemPinkColor": @{}, + @"systemPurpleColor": @{}, + @"systemRedColor": @{}, + @"systemTealColor": @{}, + @"systemYellowColor": @{}, + // Adaptable Gray Colors + @"systemGrayColor": @{}, + @"systemGray2Color": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFaeaeb2) + }, + @"systemGray3Color": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFc7c7cc) + }, + @"systemGray4Color": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFd1d1d6) + }, + @"systemGray5Color": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFe5e5ea) + }, + @"systemGray6Color": @{ // iOS 13.0 + RCTFallbackARGB: @(0xFFf2f2f7) + }, +#if DEBUG + // The follow exist for Unit Tests + @"unitTestFallbackColor": @{ + RCTFallback: @"gridColor" + }, + @"unitTestFallbackColorIOS": @{ + RCTFallback: @"blueColor" + }, + @"unitTestFallbackColorEven": @{ + RCTSelector: @"unitTestFallbackColorEven", + RCTIndex: @0, + RCTFallback: @"controlAlternatingRowBackgroundColors" + }, + @"unitTestFallbackColorOdd": @{ + RCTSelector: @"unitTestFallbackColorOdd", + RCTIndex: @1, + RCTFallback: @"controlAlternatingRowBackgroundColors" + }, +#endif + }; + } + return colorMap; +} + +/** Returns a UIColor based on a semantic color name. + * Returns nil if the semantic color name is invalid. + */ +static UIColor *RCTColorFromSemanticColorName(NSString *semanticColorName) +{ + NSDictionary *colorMap = RCTSemanticColorsMap(); + UIColor *color = nil; + NSDictionary *colorInfo = colorMap[semanticColorName]; + if (colorInfo) { + NSString *semanticColorSelector = colorInfo[RCTSelector]; + if (semanticColorSelector == nil) { + semanticColorSelector = semanticColorName; + } + SEL selector = NSSelectorFromString(semanticColorSelector); + if (![UIColor respondsToSelector:selector]) { + NSNumber *fallbackRGB = colorInfo[RCTFallbackARGB]; + if (fallbackRGB != nil) { + RCTAssert([fallbackRGB isKindOfClass:[NSNumber class]], @"fallback ARGB is not a number"); + return [RCTConvert UIColor:fallbackRGB]; + } + semanticColorSelector = colorInfo[RCTFallback]; + selector = NSSelectorFromString(semanticColorSelector); + } + RCTAssert ([UIColor respondsToSelector:selector], @"RCTUIColor does not respond to a semantic color selector."); + Class klass = [UIColor class]; + IMP imp = [klass methodForSelector:selector]; + id (*getSemanticColorObject)(id, SEL) = (void *)imp; + id colorObject = getSemanticColorObject(klass, selector); + if ([colorObject isKindOfClass:[UIColor class]]) { + color = colorObject; + } else if ([colorObject isKindOfClass:[NSArray class]]) { + NSArray *colors = colorObject; + NSNumber *index = colorInfo[RCTIndex]; + RCTAssert(index, @"index should not be null"); + color = colors[[index unsignedIntegerValue]]; + } else { + RCTAssert(false, @"selector return an unknown object type"); + } + } + return color; +} + +/** Returns an alphabetically sorted comma seperated list of the valid semantic color names + */ +static NSString *RCTSemanticColorNames() +{ + NSMutableString *names = [[NSMutableString alloc] init]; + NSDictionary *colorMap = RCTSemanticColorsMap(); + NSArray *allKeys = [[[colorMap allKeys] mutableCopy] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; + + for(id key in allKeys) { + if ([names length]) { + [names appendString:@", "]; + } + [names appendString:key]; + } + return names; +} + + (UIColor *)UIColor:(id)json { if (!json) { @@ -525,6 +726,56 @@ + (UIColor *)UIColor:(id)json CGFloat g = ((argb >> 8) & 0xFF) / 255.0; CGFloat b = (argb & 0xFF) / 255.0; return [UIColor colorWithRed:r green:g blue:b alpha:a]; + } else if ([json isKindOfClass:[NSDictionary class]]) { + NSDictionary *dictionary = json; + id value = nil; + if ((value = [dictionary objectForKey:@"semantic"])) { + if ([value isKindOfClass:[NSString class]]) { + NSString *semanticName = value; + UIColor *color = RCTColorFromSemanticColorName(semanticName); + if (color == nil) { + RCTLogConvertError(json, [@"a UIColor. Expected one of the following values: " stringByAppendingString:RCTSemanticColorNames()]); + } + return color; + } else if ([value isKindOfClass:[NSArray class]]) { + for (id name in value) { + UIColor *color = RCTColorFromSemanticColorName(name); + if (color != nil) { + return color; + } + } + RCTLogConvertError(json, [@"a UIColor. None of the names in the array were one of the following values: " stringByAppendingString:RCTSemanticColorNames()]); + return nil; + } + RCTLogConvertError(json, @"a UIColor. Expected either a single name or an array of names but got something else."); + return nil; + } else if ((value = [dictionary objectForKey:@"dynamic"])) { + NSDictionary *appearances = value; + id light = [appearances objectForKey:@"light"]; + UIColor *lightColor = [RCTConvert UIColor:light]; + id dark = [appearances objectForKey:@"dark"]; + UIColor *darkColor = [RCTConvert UIColor:dark]; + if (lightColor != nil && darkColor != nil) { +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + if (@available(iOS 13.0, *)) { + UIColor *color = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull collection) { + return collection.userInterfaceStyle == UIUserInterfaceStyleDark ? darkColor : lightColor; + }]; + return color; + } else { +#endif + return lightColor; +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + } +#endif + } else { + RCTLogConvertError(json, @"a UIColor. Expected an iOS dynamic appearance aware color."); + return nil; + } + } else { + RCTLogConvertError(json, @"a UIColor. Expected an iOS semantic color or dynamic appearance aware color."); + return nil; + } } else { RCTLogConvertError(json, @"a UIColor. Did you forget to call processColor() on the JS side?"); return nil; diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index c9a832d561c134..9159d3d820e177 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -141,6 +141,9 @@ RCT_EXTERN UIImage *__nullable RCTImageFromLocalBundleAssetURL(NSURL *imageURL); // Creates a new, unique temporary file path with the specified extension RCT_EXTERN NSString *__nullable RCTTempFilePath(NSString *__nullable extension, NSError **error); +// Get RGBA components of CGColor +RCT_EXTERN void RCTGetRGBAColorComponents(CGColorRef color, CGFloat rgba[_Nonnull 4]); + // Converts a CGColor to a hex string RCT_EXTERN NSString *RCTColorToHexString(CGColorRef color); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 7a5ef4e172f5fe..c5fd2d8e1ce05b 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -832,7 +832,7 @@ BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL) return [directory stringByAppendingPathComponent:filename]; } -static void RCTGetRGBAColorComponents(CGColorRef color, CGFloat rgba[4]) +RCT_EXTERN void RCTGetRGBAColorComponents(CGColorRef color, CGFloat rgba[4]) { CGColorSpaceModel model = CGColorSpaceGetModel(CGColorGetColorSpace(color)); const CGFloat *components = CGColorGetComponents(color); diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 6936549fa6e653..a4f992712d7fb2 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -608,6 +608,17 @@ - (void)layoutSubviews } } +- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { + [super traitCollectionDidChange: previousTraitCollection]; +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + if (@available(iOS 13.0, *)) { + if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) { + [self.layer setNeedsDisplay]; + } + } +#endif +} + #pragma mark - Borders - (UIColor *)backgroundColor @@ -783,11 +794,22 @@ - (void)displayLayer:(CALayer *)layer // solve this, we'll need to add a container view inside the main view to // correctly clip the subviews. + CGColorRef backgroundColor; +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + if (@available(iOS 13.0, *)) { + backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection].CGColor; + } else { + backgroundColor = _backgroundColor.CGColor; + } +#else + backgroundColor = _backgroundColor.CGColor; +#endif + if (useIOSBorderRendering) { layer.cornerRadius = cornerRadii.topLeft; layer.borderColor = borderColors.left; layer.borderWidth = borderInsets.left; - layer.backgroundColor = _backgroundColor.CGColor; + layer.backgroundColor = backgroundColor; layer.contents = nil; layer.needsDisplayOnBoundsChange = NO; layer.mask = nil; @@ -799,7 +821,7 @@ - (void)displayLayer:(CALayer *)layer cornerRadii, borderInsets, borderColors, - _backgroundColor.CGColor, + backgroundColor, self.clipsToBounds); layer.backgroundColor = NULL; diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index 78a0a0d90f17b0..595bfafb74804f 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -441,6 +441,7 @@ dependencies { api("com.facebook.yoga:proguard-annotations:1.14.1") api("javax.inject:javax.inject:1") api("androidx.appcompat:appcompat:1.0.2") + api("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") api("com.facebook.fresco:fresco:${FRESCO_VERSION}") api("com.facebook.fresco:imagepipeline-okhttp3:${FRESCO_VERSION}") api("com.facebook.soloader:soloader:${SO_LOADER_VERSION}") diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ColorPropConverter.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ColorPropConverter.java new file mode 100644 index 00000000000000..82c90e4ab97edc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ColorPropConverter.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.bridge; + +import android.content.Context; +import android.content.res.Resources; +import android.util.TypedValue; + +import androidx.core.content.res.ResourcesCompat; + +public class ColorPropConverter { + private static final String JSON_KEY = "resource_paths"; + private static final String PREFIX_RESOURCE = "@"; + private static final String PREFIX_ATTR = "?"; + private static final String PACKAGE_DELIMITER = ":"; + private static final String PATH_DELIMITER = "/"; + private static final String ATTR = "attr"; + private static final String ATTR_SEGMENT = "attr/"; + + public static Integer getColor(Object value, Context context) { + if (value == null) { + return null; + } + + if (value instanceof Double) { + return ((Double) value).intValue(); + } + + if (context == null) { + throw new RuntimeException("Context may not be null."); + } + + if (value instanceof ReadableMap) { + ReadableMap map = (ReadableMap) value; + ReadableArray resourcePaths = map.getArray(JSON_KEY); + + if (resourcePaths == null) { + throw new JSApplicationCausedNativeException("ColorValue: The `" + JSON_KEY + "` must be an array of color resource path strings."); + } + + for (int i = 0; i < resourcePaths.size(); i++) { + String resourcePath = resourcePaths.getString(i); + + if (resourcePath == null || resourcePath.isEmpty()) { + continue; + } + + boolean isResource = resourcePath.startsWith(PREFIX_RESOURCE); + boolean isThemeAttribute = resourcePath.startsWith(PREFIX_ATTR); + + resourcePath = resourcePath.substring(1); + + try { + if (isResource) { + return resolveResource(context, resourcePath); + } else if (isThemeAttribute) { + return resolveThemeAttribute(context, resourcePath); + } + } catch (Resources.NotFoundException exception) { + // The resource could not be found so do nothing to allow the for loop to continue and + // try the next fallback resource in the array. If none of the fallbacks are + // found then the exception immediately after the for loop will be thrown. + } + } + + throw new JSApplicationCausedNativeException("ColorValue: None of the paths in the `" + JSON_KEY + "` array resolved to a color resource."); + } + + throw new JSApplicationCausedNativeException("ColorValue: the value must be a number or Object."); + } + + private static int resolveResource(Context context, String resourcePath) { + String[] pathTokens = resourcePath.split(PACKAGE_DELIMITER); + + String packageName = context.getPackageName(); + String resource = resourcePath; + + if (pathTokens.length > 1) { + packageName = pathTokens[0]; + resource = pathTokens[1]; + } + + String[] resourceTokens = resource.split(PATH_DELIMITER); + String resourceType = resourceTokens[0]; + String resourceName = resourceTokens[1]; + + int resourceId = context.getResources().getIdentifier( + resourceName, resourceType, packageName); + + return ResourcesCompat.getColor( + context.getResources(), resourceId, context.getTheme()); + } + + private static int resolveThemeAttribute(Context context, String resourcePath) { + String path = resourcePath.replaceAll(ATTR_SEGMENT, ""); + String[] pathTokens = path.split(PACKAGE_DELIMITER); + + String packageName = context.getPackageName(); + String resourceName = path; + + if (pathTokens.length > 1) { + packageName = pathTokens[0]; + resourceName = pathTokens[1]; + } + + int resourceId = context.getResources().getIdentifier( + resourceName, ATTR, packageName); + + TypedValue outValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + + if (theme.resolveAttribute(resourceId, outValue, true)) { + return outValue.data; + } + + throw new Resources.NotFoundException(); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java index d89882c480f444..8d7b418ab354ac 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.java @@ -9,6 +9,8 @@ import android.view.View; import androidx.annotation.Nullable; + +import com.facebook.react.bridge.ColorPropConverter; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.yoga.YogaConstants; @@ -47,7 +49,7 @@ public void setProperty(T view, String propName, @Nullable Object value) { mViewManager.setViewState(view, (ReadableMap) value); break; case ViewProps.BACKGROUND_COLOR: - mViewManager.setBackgroundColor(view, value == null ? 0 : ((Double) value).intValue()); + mViewManager.setBackgroundColor(view, value == null ? 0 : ColorPropConverter.getColor(value, view.getContext())); break; case ViewProps.BORDER_RADIUS: mViewManager.setBorderRadius( diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java index 9bc456d554450e..2147299eaed98a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java @@ -7,9 +7,13 @@ package com.facebook.react.uimanager; +import android.content.Context; import android.view.View; + import androidx.annotation.Nullable; + import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.ColorPropConverter; import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.DynamicFromObject; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; @@ -17,6 +21,7 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.annotations.ReactPropGroup; + import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; @@ -81,13 +86,13 @@ public void updateViewProp(ViewManager viewManager, View viewToUpdate, Object va try { if (mIndex == null) { VIEW_MGR_ARGS[0] = viewToUpdate; - VIEW_MGR_ARGS[1] = getValueOrDefault(value); + VIEW_MGR_ARGS[1] = getValueOrDefault(value, viewToUpdate.getContext()); mSetter.invoke(viewManager, VIEW_MGR_ARGS); Arrays.fill(VIEW_MGR_ARGS, null); } else { VIEW_MGR_GROUP_ARGS[0] = viewToUpdate; VIEW_MGR_GROUP_ARGS[1] = mIndex; - VIEW_MGR_GROUP_ARGS[2] = getValueOrDefault(value); + VIEW_MGR_GROUP_ARGS[2] = getValueOrDefault(value, viewToUpdate.getContext()); mSetter.invoke(viewManager, VIEW_MGR_GROUP_ARGS); Arrays.fill(VIEW_MGR_GROUP_ARGS, null); } @@ -105,12 +110,12 @@ public void updateViewProp(ViewManager viewManager, View viewToUpdate, Object va public void updateShadowNodeProp(ReactShadowNode nodeToUpdate, Object value) { try { if (mIndex == null) { - SHADOW_ARGS[0] = getValueOrDefault(value); + SHADOW_ARGS[0] = getValueOrDefault(value, nodeToUpdate.getThemedContext()); mSetter.invoke(nodeToUpdate, SHADOW_ARGS); Arrays.fill(SHADOW_ARGS, null); } else { SHADOW_GROUP_ARGS[0] = mIndex; - SHADOW_GROUP_ARGS[1] = getValueOrDefault(value); + SHADOW_GROUP_ARGS[1] = getValueOrDefault(value, nodeToUpdate.getThemedContext()); mSetter.invoke(nodeToUpdate, SHADOW_GROUP_ARGS); Arrays.fill(SHADOW_GROUP_ARGS, null); } @@ -125,7 +130,7 @@ public void updateShadowNodeProp(ReactShadowNode nodeToUpdate, Object value) { } } - protected abstract @Nullable Object getValueOrDefault(Object value); + protected abstract @Nullable Object getValueOrDefault(Object value, Context context); } private static class DynamicPropSetter extends PropSetter { @@ -139,7 +144,7 @@ public DynamicPropSetter(ReactPropGroup prop, Method setter, int index) { } @Override - protected Object getValueOrDefault(Object value) { + protected Object getValueOrDefault(Object value, Context context) { if (value instanceof Dynamic) { return value; } else { @@ -163,7 +168,7 @@ public IntPropSetter(ReactPropGroup prop, Method setter, int index, int defaultV } @Override - protected Object getValueOrDefault(Object value) { + protected Object getValueOrDefault(Object value, Context context) { // All numbers from JS are Doubles which can't be simply cast to Integer return value == null ? mDefaultValue : (Integer) ((Double) value).intValue(); } @@ -184,11 +189,34 @@ public DoublePropSetter(ReactPropGroup prop, Method setter, int index, double de } @Override - protected Object getValueOrDefault(Object value) { + protected Object getValueOrDefault(Object value, Context context) { return value == null ? mDefaultValue : (Double) value; } } + private static class ColorPropSetter extends PropSetter { + + private final int mDefaultValue; + + public ColorPropSetter(ReactProp prop, Method setter) { + this(prop, setter, 0); + } + + public ColorPropSetter(ReactProp prop, Method setter, int defaultValue) { + super(prop, "mixed", setter); + mDefaultValue = defaultValue; + } + + @Override + protected Object getValueOrDefault(Object value, Context context) { + if (value == null) { + return mDefaultValue; + } + + return ColorPropConverter.getColor(value, context); + } + } + private static class BooleanPropSetter extends PropSetter { private final boolean mDefaultValue; @@ -199,7 +227,7 @@ public BooleanPropSetter(ReactProp prop, Method setter, boolean defaultValue) { } @Override - protected Object getValueOrDefault(Object value) { + protected Object getValueOrDefault(Object value, Context context) { boolean val = value == null ? mDefaultValue : (boolean) value; return val ? Boolean.TRUE : Boolean.FALSE; } @@ -220,7 +248,7 @@ public FloatPropSetter(ReactPropGroup prop, Method setter, int index, float defa } @Override - protected Object getValueOrDefault(Object value) { + protected Object getValueOrDefault(Object value, Context context) { // All numbers from JS are Doubles which can't be simply cast to Float return value == null ? mDefaultValue : (Float) ((Double) value).floatValue(); } @@ -233,7 +261,7 @@ public ArrayPropSetter(ReactProp prop, Method setter) { } @Override - protected @Nullable Object getValueOrDefault(Object value) { + protected @Nullable Object getValueOrDefault(Object value, Context context) { return (ReadableArray) value; } } @@ -245,7 +273,7 @@ public MapPropSetter(ReactProp prop, Method setter) { } @Override - protected @Nullable Object getValueOrDefault(Object value) { + protected @Nullable Object getValueOrDefault(Object value, Context context) { return (ReadableMap) value; } } @@ -257,7 +285,7 @@ public StringPropSetter(ReactProp prop, Method setter) { } @Override - protected @Nullable Object getValueOrDefault(Object value) { + protected @Nullable Object getValueOrDefault(Object value, Context context) { return (String) value; } } @@ -269,7 +297,7 @@ public BoxedBooleanPropSetter(ReactProp prop, Method setter) { } @Override - protected @Nullable Object getValueOrDefault(Object value) { + protected @Nullable Object getValueOrDefault(Object value, Context context) { if (value != null) { return (boolean) value ? Boolean.TRUE : Boolean.FALSE; } @@ -288,7 +316,7 @@ public BoxedIntPropSetter(ReactPropGroup prop, Method setter, int index) { } @Override - protected @Nullable Object getValueOrDefault(Object value) { + protected @Nullable Object getValueOrDefault(Object value, Context context) { if (value != null) { if (value instanceof Double) { return ((Double) value).intValue(); @@ -379,6 +407,9 @@ private static PropSetter createPropSetter( } else if (propTypeClass == boolean.class) { return new BooleanPropSetter(annotation, method, annotation.defaultBoolean()); } else if (propTypeClass == int.class) { + if ("Color".equals(annotation.customType())) { + return new ColorPropSetter(annotation, method, annotation.defaultInt()); + } return new IntPropSetter(annotation, method, annotation.defaultInt()); } else if (propTypeClass == float.class) { return new FloatPropSetter(annotation, method, annotation.defaultFloat()); @@ -389,6 +420,9 @@ private static PropSetter createPropSetter( } else if (propTypeClass == Boolean.class) { return new BoxedBooleanPropSetter(annotation, method); } else if (propTypeClass == Integer.class) { + if ("Color".equals(annotation.customType())) { + return new ColorPropSetter(annotation, method); + } return new BoxedIntPropSetter(annotation, method); } else if (propTypeClass == ReadableArray.class) { return new ArrayPropSetter(annotation, method); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java index c9f4b21360a8b8..69a26b3b1a0f91 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java @@ -14,6 +14,8 @@ import androidx.annotation.Nullable; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener; + +import com.facebook.react.bridge.ColorPropConverter; import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableType; @@ -68,7 +70,11 @@ public void setColors(ReactSwipeRefreshLayout view, @Nullable ReadableArray colo if (colors != null) { int[] colorValues = new int[colors.size()]; for (int i = 0; i < colors.size(); i++) { - colorValues[i] = colors.getInt(i); + if (colors.getType(i) == ReadableType.Map) { + colorValues[i] = ColorPropConverter.getColor(colors.getMap(i), view.getContext()); + } else { + colorValues[i] = colors.getInt(i); + } } view.setColorSchemeColors(colorValues); } else { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java index e36cfa0c4ee5d4..68a8550deee893 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java @@ -469,7 +469,7 @@ public void setFontSize(float fontSize) { markUpdated(); } - @ReactProp(name = ViewProps.COLOR) + @ReactProp(name = ViewProps.COLOR, customType = "Color") public void setColor(@Nullable Integer color) { mIsColorSet = (color != null); if (mIsColorSet) { diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropForShadowNodeSetterTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropForShadowNodeSetterTest.java index ee15d06ebb6296..347c4190892be8 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropForShadowNodeSetterTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropForShadowNodeSetterTest.java @@ -25,6 +25,8 @@ import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.RobolectricTestRunner; +import androidx.annotation.Nullable; + /** * Test {@link ReactProp} annotation for {@link ReactShadowNode}. More comprehensive test of this * annotation can be found in {@link ReactPropAnnotationSetterTest} where we test all possible types diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java index 0e507faafcdbab..2af226ff63894d 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java @@ -12,9 +12,11 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import android.annotation.TargetApi; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.Drawable; +import android.os.Build; import android.text.Layout; import android.text.Spanned; import android.text.TextUtils; diff --git a/index.js b/index.js index bc185e7f178152..529a64e87dafed 100644 --- a/index.js +++ b/index.js @@ -94,6 +94,9 @@ import typeof RCTNativeAppEventEmitter from './Libraries/EventEmitter/RCTNativeA import typeof NativeModules from './Libraries/BatchedBridge/NativeModules'; import typeof Platform from './Libraries/Utilities/Platform'; import typeof processColor from './Libraries/StyleSheet/processColor'; +import typeof {PlatformColor} from './Libraries/StyleSheet/PlatformColorValueTypes'; +import typeof {DynamicColorIOS} from './Libraries/StyleSheet/PlatformColorValueTypesIOS'; +import typeof {ColorAndroid} from './Libraries/StyleSheet/PlatformColorValueTypesAndroid'; import typeof RootTagContext from './Libraries/ReactNative/RootTagContext'; import typeof DeprecatedColorPropType from './Libraries/DeprecatedPropTypes/DeprecatedColorPropType'; import typeof DeprecatedEdgeInsetsPropType from './Libraries/DeprecatedPropTypes/DeprecatedEdgeInsetsPropType'; @@ -462,6 +465,18 @@ module.exports = { get processColor(): processColor { return require('./Libraries/StyleSheet/processColor'); }, + get PlatformColor(): PlatformColor { + return require('./Libraries/StyleSheet/PlatformColorValueTypes') + .PlatformColor; + }, + get DynamicColorIOS(): DynamicColorIOS { + return require('./Libraries/StyleSheet/PlatformColorValueTypesIOS') + .DynamicColorIOS; + }, + get ColorAndroid(): ColorAndroid { + return require('./Libraries/StyleSheet/PlatformColorValueTypesAndroid') + .ColorAndroid; + }, get requireNativeComponent(): ( uiViewClassName: string, ) => HostComponent { diff --git a/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/fixtures.js index d4a9f2efbe7db8..5a6e24d43dc864 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/fixtures.js @@ -275,6 +275,12 @@ type ModuleProps = $ReadOnly<{| color_array_optional_value: ?ColorArrayValue, color_array_optional_both?: ?ColorArrayValue, + // ProcessedColorValue props + processed_color_required: ProcessedColorValue, + processed_color_optional_key?: ProcessedColorValue, + processed_color_optional_value: ?ProcessedColorValue, + processed_color_optional_both?: ?ProcessedColorValue, + // PointValue props point_required: PointValue, point_optional_key?: PointValue, @@ -310,7 +316,7 @@ const codegenNativeComponent = require('codegenNativeComponent'); import type {Int32, Double, Float, WithDefault} from 'CodegenTypes'; import type {ImageSource} from 'ImageSource'; -import type {ColorValue, PointValue, EdgeInsetsValue} from 'StyleSheetTypes'; +import type {ColorValue, PointValue, ProcessColorValue, EdgeInsetsValue} from 'StyleSheetTypes'; import type {ViewProps} from 'ViewPropTypes'; import type {HostComponent} from 'react-native'; @@ -508,6 +514,12 @@ type ModuleProps = $ReadOnly<{| color_optional_value: $ReadOnly<{|prop: ?ColorValue|}>, color_optional_both: $ReadOnly<{|prop?: ?ColorValue|}>, + // ProcessedColorValue props + processed_color_required: $ReadOnly<{|prop: ProcessedColorValue|}>, + processed_color_optional_key: $ReadOnly<{|prop?: ProcessedColorValue|}>, + processed_color_optional_value: $ReadOnly<{|prop: ?ProcessedColorValue|}>, + processed_color_optional_both: $ReadOnly<{|prop?: ?ProcessedColorValue|}>, + // PointValue props point_required: $ReadOnly<{|prop: PointValue|}>, point_optional_key: $ReadOnly<{|prop?: PointValue|}>, diff --git a/packages/react-native-codegen/src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap b/packages/react-native-codegen/src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap index 712aca291934ce..5fdbb373abe9a9 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap +++ b/packages/react-native-codegen/src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap @@ -456,6 +456,38 @@ Object { "type": "ArrayTypeAnnotation", }, }, + Object { + "name": "processed_color_required", + "optional": false, + "typeAnnotation": Object { + "name": "ColorPrimitive", + "type": "NativePrimitiveTypeAnnotation", + }, + }, + Object { + "name": "processed_color_optional_key", + "optional": true, + "typeAnnotation": Object { + "name": "ColorPrimitive", + "type": "NativePrimitiveTypeAnnotation", + }, + }, + Object { + "name": "processed_color_optional_value", + "optional": true, + "typeAnnotation": Object { + "name": "ColorPrimitive", + "type": "NativePrimitiveTypeAnnotation", + }, + }, + Object { + "name": "processed_color_optional_both", + "optional": true, + "typeAnnotation": Object { + "name": "ColorPrimitive", + "type": "NativePrimitiveTypeAnnotation", + }, + }, Object { "name": "point_required", "optional": false, @@ -5480,6 +5512,74 @@ Object { "type": "ObjectTypeAnnotation", }, }, + Object { + "name": "processed_color_required", + "optional": false, + "typeAnnotation": Object { + "properties": Array [ + Object { + "name": "prop", + "optional": false, + "typeAnnotation": Object { + "name": "ColorPrimitive", + "type": "NativePrimitiveTypeAnnotation", + }, + }, + ], + "type": "ObjectTypeAnnotation", + }, + }, + Object { + "name": "processed_color_optional_key", + "optional": false, + "typeAnnotation": Object { + "properties": Array [ + Object { + "name": "prop", + "optional": true, + "typeAnnotation": Object { + "name": "ColorPrimitive", + "type": "NativePrimitiveTypeAnnotation", + }, + }, + ], + "type": "ObjectTypeAnnotation", + }, + }, + Object { + "name": "processed_color_optional_value", + "optional": false, + "typeAnnotation": Object { + "properties": Array [ + Object { + "name": "prop", + "optional": true, + "typeAnnotation": Object { + "name": "ColorPrimitive", + "type": "NativePrimitiveTypeAnnotation", + }, + }, + ], + "type": "ObjectTypeAnnotation", + }, + }, + Object { + "name": "processed_color_optional_both", + "optional": false, + "typeAnnotation": Object { + "properties": Array [ + Object { + "name": "prop", + "optional": true, + "typeAnnotation": Object { + "name": "ColorPrimitive", + "type": "NativePrimitiveTypeAnnotation", + }, + }, + ], + "type": "ObjectTypeAnnotation", + }, + }, Object { "name": "point_required", "optional": false, diff --git a/packages/react-native-codegen/src/parsers/flow/components/props.js b/packages/react-native-codegen/src/parsers/flow/components/props.js index 6531a2c9b433fa..404289cc8a7dc7 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/props.js +++ b/packages/react-native-codegen/src/parsers/flow/components/props.js @@ -94,6 +94,7 @@ function getTypeAnnotationForArray(name, typeAnnotation, defaultValue, types) { name: 'ImageSourcePrimitive', }; case 'ColorValue': + case 'ProcessedColorValue': return { type: 'NativePrimitiveTypeAnnotation', name: 'ColorPrimitive', @@ -217,6 +218,7 @@ function getTypeAnnotation( name: 'ImageSourcePrimitive', }; case 'ColorValue': + case 'ProcessedColorValue': return { type: 'NativePrimitiveTypeAnnotation', name: 'ColorPrimitive',