From 4b9d9dda270acd4e0314f40490c699ffd0f6e30e Mon Sep 17 00:00:00 2001 From: Birkir Gudjonsson Date: Tue, 8 Jun 2021 11:50:00 -0700 Subject: [PATCH] Accessible colors for DynamicColorIOS (#31651) Summary: Allow you to harvest the `UIAccessibilityContrastHigh` trait from iOS to show accessible colors when high contrast mode is enabled. ```jsx // usage PlatformColorIOS({ light: '#eeeeee', dark: '#333333', highContrastLight: '#ffffff', highContrastDark: '#000000', }); // { // "dynamic": { // "light": "#eeeeee", // "dark": "#333333", // "highContrastLight": "#ffffff", // "highContrastDark": "#000000", // } // } ``` This is how apple's own dynamic system colors work under the hood (https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/color/#dynamic-system-colors) --- The react native docs mention that more keys may become available in the future, which this PR is adding: > In the future, more keys might become available for different user preferences, like high contrast. https://reactnative.dev/docs/dynamiccolorios ## Changelog [iOS] [Added] - High contrast dynamic color options for dark and light mode. Pull Request resolved: https://github.com/facebook/react-native/pull/31651 Test Plan: Added unit tests for `normalizeColor` to pass the high contrast colors downstream to RCTConvert Reviewed By: lunaleaps Differential Revision: D28922536 Pulled By: p-sun fbshipit-source-id: f81417f003c3adefac50e994e62b9be14ffa91a1 --- .../StyleSheet/PlatformColorValueTypes.ios.js | 17 ++++++++++++- .../PlatformColorValueTypesIOS.ios.js | 9 ++++++- .../StyleSheet/PlatformColorValueTypesIOS.js | 2 ++ .../__tests__/normalizeColor-test.js | 19 +++++++++++++++ React/Base/RCTConvert.m | 24 +++++++++++++++---- .../platform-colors.js | 1 - 6 files changed, 65 insertions(+), 7 deletions(-) diff --git a/Libraries/StyleSheet/PlatformColorValueTypes.ios.js b/Libraries/StyleSheet/PlatformColorValueTypes.ios.js index 883f86e97cd316..193ea338bdffc5 100644 --- a/Libraries/StyleSheet/PlatformColorValueTypes.ios.js +++ b/Libraries/StyleSheet/PlatformColorValueTypes.ios.js @@ -16,6 +16,8 @@ export opaque type NativeColorValue = { dynamic?: { light: ?(ColorValue | ProcessedColorValue), dark: ?(ColorValue | ProcessedColorValue), + highContrastLight?: ?(ColorValue | ProcessedColorValue), + highContrastDark?: ?(ColorValue | ProcessedColorValue), }, }; @@ -26,12 +28,21 @@ export const PlatformColor = (...names: Array): ColorValue => { export type DynamicColorIOSTuplePrivate = { light: ColorValue, dark: ColorValue, + highContrastLight?: ColorValue, + highContrastDark?: ColorValue, }; export const DynamicColorIOSPrivate = ( tuple: DynamicColorIOSTuplePrivate, ): ColorValue => { - return {dynamic: {light: tuple.light, dark: tuple.dark}}; + return { + dynamic: { + light: tuple.light, + dark: tuple.dark, + highContrastLight: tuple.highContrastLight, + highContrastDark: tuple.highContrastDark, + }, + }; }; export const normalizeColorObject = ( @@ -49,6 +60,8 @@ export const normalizeColorObject = ( dynamic: { light: normalizeColor(dynamic.light), dark: normalizeColor(dynamic.dark), + highContrastLight: normalizeColor(dynamic.highContrastLight), + highContrastDark: normalizeColor(dynamic.highContrastDark), }, }; return dynamicColor; @@ -67,6 +80,8 @@ export const processColorObject = ( dynamic: { light: processColor(dynamic.light), dark: processColor(dynamic.dark), + highContrastLight: processColor(dynamic.highContrastLight), + highContrastDark: processColor(dynamic.highContrastDark), }, }; return dynamicColor; diff --git a/Libraries/StyleSheet/PlatformColorValueTypesIOS.ios.js b/Libraries/StyleSheet/PlatformColorValueTypesIOS.ios.js index c9835a0ce140c5..912236c7359de3 100644 --- a/Libraries/StyleSheet/PlatformColorValueTypesIOS.ios.js +++ b/Libraries/StyleSheet/PlatformColorValueTypesIOS.ios.js @@ -14,8 +14,15 @@ import {DynamicColorIOSPrivate} from './PlatformColorValueTypes'; export type DynamicColorIOSTuple = { light: ColorValue, dark: ColorValue, + highContrastLight?: ColorValue, + highContrastDark?: ColorValue, }; export const DynamicColorIOS = (tuple: DynamicColorIOSTuple): ColorValue => { - return DynamicColorIOSPrivate({light: tuple.light, dark: tuple.dark}); + return DynamicColorIOSPrivate({ + light: tuple.light, + dark: tuple.dark, + highContrastLight: tuple.highContrastLight, + highContrastDark: tuple.highContrastDark, + }); }; diff --git a/Libraries/StyleSheet/PlatformColorValueTypesIOS.js b/Libraries/StyleSheet/PlatformColorValueTypesIOS.js index cfdf4ae0c0ac29..ead8c4fca2f59f 100644 --- a/Libraries/StyleSheet/PlatformColorValueTypesIOS.js +++ b/Libraries/StyleSheet/PlatformColorValueTypesIOS.js @@ -13,6 +13,8 @@ import type {ColorValue} from './StyleSheet'; export type DynamicColorIOSTuple = { light: ColorValue, dark: ColorValue, + highContrastLight?: ColorValue, + highContrastDark?: ColorValue, }; export const DynamicColorIOS = (tuple: DynamicColorIOSTuple): ColorValue => { diff --git a/Libraries/StyleSheet/__tests__/normalizeColor-test.js b/Libraries/StyleSheet/__tests__/normalizeColor-test.js index 80979e0ec2c75c..5421edccd23319 100644 --- a/Libraries/StyleSheet/__tests__/normalizeColor-test.js +++ b/Libraries/StyleSheet/__tests__/normalizeColor-test.js @@ -43,6 +43,25 @@ describe('iOS', () => { expect(normalizedColor).toEqual(expectedColor); }); + it('should normalize iOS Dynamic colors with accessible colors', () => { + const color = DynamicColorIOS({ + light: 'black', + dark: 'white', + highContrastLight: 'red', + highContrastDark: 'blue', + }); + const normalizedColor = normalizeColor(color); + const expectedColor = { + dynamic: { + light: 'black', + dark: 'white', + highContrastLight: 'red', + highContrastDark: 'blue', + }, + }; + expect(normalizedColor).toEqual(expectedColor); + }); + it('should normalize iOS Dynamic colors with PlatformColor colors', () => { const color = DynamicColorIOS({ light: PlatformColor('systemBlackColor'), diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index af47b0d353ed1a..d60b80b2dc7069 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -879,13 +879,29 @@ + (UIColor *)UIColor:(id)json UIColor *lightColor = [RCTConvert UIColor:light]; id dark = [appearances objectForKey:@"dark"]; UIColor *darkColor = [RCTConvert UIColor:dark]; + id highContrastLight = [appearances objectForKey:@"highContrastLight"]; + UIColor *highContrastLightColor = [RCTConvert UIColor:highContrastLight]; + id highContrastDark = [appearances objectForKey:@"highContrastDark"]; + UIColor *highContrastDarkColor = [RCTConvert UIColor:highContrastDark]; 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; - }]; + UIColor *color = [UIColor colorWithDynamicProvider:^UIColor *_Nonnull( + UITraitCollection *_Nonnull collection) { + if (collection.userInterfaceStyle == UIUserInterfaceStyleDark) { + if (collection.accessibilityContrast == UIAccessibilityContrastHigh && highContrastDarkColor != nil) { + return highContrastDarkColor; + } else { + return darkColor; + } + } else { + if (collection.accessibilityContrast == UIAccessibilityContrastHigh && highContrastLightColor != nil) { + return highContrastLightColor; + } else { + return lightColor; + } + } + }]; return color; } else { #endif diff --git a/packages/eslint-plugin-react-native-community/platform-colors.js b/packages/eslint-plugin-react-native-community/platform-colors.js index d2d720527e64a6..12e8f908f90a5d 100644 --- a/packages/eslint-plugin-react-native-community/platform-colors.js +++ b/packages/eslint-plugin-react-native-community/platform-colors.js @@ -60,7 +60,6 @@ module.exports = { const properties = args[0].properties; if ( !( - properties.length === 2 && properties[0].type === 'Property' && properties[0].key.name === 'light' && properties[1].type === 'Property' &&