Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Platform specific colors via PlatformColor #126

Closed
turnrye opened this issue May 13, 2019 · 70 comments
Closed

Platform specific colors via PlatformColor #126

turnrye opened this issue May 13, 2019 · 70 comments
Labels
🗣 Discussion This label identifies an ongoing discussion on a subject

Comments

@turnrye
Copy link
Member

turnrye commented May 13, 2019

Edit: A more general proposal has been posted by @tom-un to support generic platform defined colors. Jump to that proposal. That proposal now supersedes the one in this top post.

Original Proposal

### Introduction

Android Q introduced a Dark theme across the system and applications. From their presentation, "Users will expect apps to have a dark theme". The way we do styling on RN today isn't directly compatible with their solution, but it's close.

The Core of It

I opened facebook/react-native#24790 with a proposed solution, but it admittedly had some issues:

  • It was on Platform, but it's more prone to change than the rest of those values
  • It closely followed Android's implementation, but doesn't anticipate what iOS or out-of-tree platforms might use
  • Android normally recreates the activities when this value changes, but that isn't a safe way to lead to re-rendering (which would be common if the user has enabled it by time-of-day or as part of battery saver)

Discussion Points

@turnrye turnrye changed the title Supporting dark mode and other themes Supporting dark mode and perhaps other themes May 13, 2019
@turnrye turnrye added the 🗣 Discussion This label identifies an ongoing discussion on a subject label May 13, 2019
@acoates-ms
Copy link
Collaborator

@tom-un, might be able to fill in some more details. But for our macOS RN platform we extended the valid values for color. So you can specify either system colors, or be able to specify colors that change based on the native theme Something like:

<View background={{dynamic:{light: 'red', dark:'pink'}}} />
<View background={semantic: "windowBackgroundColor"} />

I believe there was some plan to look at contributing that back for iOS too.

@mmmulani
Copy link

thanks for starting the proposal!

Android normally recreates the activities when this value changes, but that isn't a safe way to lead to re-rendering (which would be common if the user has enabled it by time-of-day or as part of battery saver)

from my reading of the docs, it seem like this changed and depending on how it's implemented, activities might not be recreated when the system theme changed. Because of that, I think a pattern similar to how we treat orientation makes more sense

@turnrye
Copy link
Member Author

turnrye commented May 13, 2019

@acoates-ms will you describe that a bit more? That seems like a good solution for brownfield applications too where they need robust native themes. It's a solution that helps beyond just this light/dark mode use case and instead pushes themes into the native platform, which may or may not be desirable.

@mmmulani that makes a lot of sense. It'd also give more flexibility for other platforms as well that might not recreate automatically.

@TheSavior
Copy link

Also worth mentioning that we recently added an accessibility prop on View called accessibilityIgnoresInvertColors. This is used in iOS inverted mode to keep things like images from being inverted. At Google IO they mentioned that Android will have support for auto dark mode'ing 😄 apps but that images can look weird then. I imagine Android has a similar flag for this. A PR that wires up accessibilityIgnoresInvertColors to the new Android thing would be much appreciated.

@turnrye
Copy link
Member Author

turnrye commented May 13, 2019

@TheSavior I experimented with that some -- it works relatively well, but there are definitely places where people are going to need to manually handle this. In our apps that's the case, and when I tried some other popular RN apps it was mixed as well.

I'll definitely submit that PR -- wasn't familiar with that. I don't think it directly addresses this discussion though.

@necolas
Copy link

necolas commented May 13, 2019

@tom-un
Copy link

tom-un commented May 13, 2019

On macOS Mojave, things works best when using semantic colors: one sets colors on NSView using semantic colors such as [NSColor windowBackgroundColor] etc. Every view in the hierarchy has an 'effectiveAppearance' property that can change when the system changes from light to dark mode or vis versa. When the layers backing the views re-render the conversion of the NSColor to a CGColor is automatically aware of the effectiveAppearance and you'll get a different RGB value in the CGColor. iOS doesn't have appearance changes yet, but its a good bet it will in the same way as Mojave soon -- looking forward to WWDC for more info there.

I prototyped a couple approaches when making our macOS react-native Mojave theme aware. The first approach was to make a JS Api similar to Platform. I called it SemanticColor.js and it behaved similar to Platform: on JS load it used NativeModule constants to initialize a map of semantic color names to RGB string. Then the JS subscribed to an 'theme has changed' event that would be send a new map -- similar to how Platform caches the screen orientation and then gets updated orientation values from an event.

The problem with this solution was that the JS App itself also had to subscribe to the "theme has changed" event and then had to do a hard re-render of the components so that StyleSheets would be reevaluated because the various StyleSheets now contains RGB values fetched from the SematnicColors.js dictionary. Doing such a hard re-render would invariably reset some App state that the App developer didn't bother persisting. A simple example RNTester which does persist which test panel the user has navigated to, and the filter string, but does't persist the scroll position in the list. So with every theme change the list pops back to the top. Also, the time to re-render the whole App can be too slow so that instead of the RN app participating in the nice Mojave theme change animation it would just pop to the new theme a second or two after the rest of the desktop.

So the second approach, which is what is in our macos fork, is the extension to the color type itself, so that a color can be expressed as the current RGB string values or a JSON object like { semantic: 'windowBackground' }. The RCTConvert object turns such JSON objects into a true NSColor semantic color. mac and iOS also have the concept of Asset colors: app defined named colors that have a light and dark variant. I made a RCTDynamic color (a sublcass of NSColor) that works like an Asset color. The JS can express the tuple { dynamic: { light: 'rgb', dark:'rgb' } } and RCTConvert turns it into a RCTDynamic.

The semantic names I came up with for { semantic: 'name' } map 1:1 to the mac NSColor semantic names. And hopefully those same names will be used in a future iOS. But obviously don't map to other platform 1:1. Perhaps we could define a platform agnostic semantic namespace that would serve apple/android/windows.

@DimitarNestorov
Copy link

This should continue the conversation: https://developer.apple.com/videos/play/wwdc2019/214/

@kikisaints
Copy link
Collaborator

kikisaints commented Jun 10, 2019

Hey! Poking this thread a bit with some interest on the React Native for Windows side.

I'm a PM on that team (RN for Windows) and we're looking to enable support for the light and dark themes that we have.

With the introduction of Dark theme in iOS 13 - as @DimitarNestorov mentioned, and the Dark theme mode in Android it's probably time we had something like this for all systems.

We have an internal proposal to support both themes, as they're integral to developing on Windows, but I'd like to append that here to get the opinions of the community before we submit a larger spec PR to the main React Native github.

Here's a look at part of the spec I've written that I would like to introduce to React Native core. I actually work closely with @acoates-ms, so a lot of this may look familiar to his suggested approach above as well as similar to what @tom-un was looking at. I would love feedback 😄

Responding to Light/Dark Themes in React Native

Summary

With the introduction of React Native for Windows, there needs to be a way to utilize/work with the Light and Dark themes at the JavaScript layer.

This feature was pitched in the React Native for Windows repo here.

Motivation

Any React Native developer that wishes to follow the guidance of a Windows app will need to have access to the theme brushes, or at the very least a way to know when the system theme has changed.

There are several reasons why this is important to the React Native developer when they run their app on Windows:

  • Many native Windows components will honor the Light/Dark theme changes automatically. Giving the app-developer access/knowledge of those changes reduces the "zebra" UI effect (zebra UI = a page in an app where some controls honor a theme system, while others do not, or honor a non-complimentary one)
  • Having access to a theme changed event allows a React Native developer to better brand and customize their app for Windows, by accommodating their own styles based on the theme selected by the user (typically set through system settings)
  • Exposing the Windows theme up to the JavaScript layer means React Native apps can blend more seamlessly into the Fluent Design system and native Windows platform more easily
  • Both Android and iOS 13 have or will have support for Dark themes, thus it is a necessity that we expose some way of detecting and handling theme changes at the RN layer.

Scope

There are two core aspects that need to be available to the app developer in order to have a Windows Light/Dark theme sensitive app:

# Feature
1 Expose a way, at the JavaScript layer, to detect current theme and when the theme has changed

Basic examples

Example 1 : Set up and behaviors on theme changed

In this example, we'll look at three things:

  • How to set up your React Native app to be style and event sensitive to the system themes
  • How to switch styles when a theme change has occurred
  • Handling a theme changed event

Setting up your app to be sensitive to theme changes

First import the Platform API into your React Native app.

import { AppThemeState } from 'react-native';

Create a local variable to use in style conditionals or to reference elsewhere easily.

class MyAppClass extends Component {
  state = {
    appThemeState: AppThemeState.currentTheme,
  };
  ...
}

Switching styles based on the app's theme

If the app author wants to switch the style of their component manually based on the system's theme (Dark or Light), they can do so with CSS style conditionals.

<TextInput
    style={[styles.inputStyle, this.state.appThemeState.currentTheme == 'dark' ? styles.darkInput : styles.lightInput]}
</TextInput>

Handling a theme changed event

In this case an app author would like to do something or preform some behavior when the app's theme has changed.

Note: AppThemeState will be a subclass of NativeEventEmitter.

componentDidMount() {
  AppThemeState.currentTheme.addListener('themechanged', this.onAppThemeChanged);
}

onAppThemeChanged = (event) => {
  /*Logic on theme changed goes here*/
  this.setState({appThemeState: AppThemeState.currentTheme});
}

API Design Overview

A look at the APIs for the features and scope described above.

AppThemeState.currentTheme

The specifics of the event or API being fired/used.

API Args Returns Discription
currentTheme none string currentTheme returns the state of the system theme that the user or native app has set.

App theme enum

The currentTheme returns one of the following string values:

Property Type Description
dark string A string value defining that the native app is in Dark theme.
light string A string value defining that the native app is in Light theme.

Could potentially need a AppThemeState.isHighContrast to detect when the system has initiated a theme for accessible users.

@DimitarNestorov
Copy link

DimitarNestorov commented Jun 11, 2019

I thought of a bit different approach on the API though. I was thinking about developer experience and the thought of event emitters just scared me.

Let's start with the basics:
useDarkMode - a hook that returns a boolean whether to use dark mode or not.
useDarkModeContext - the hook that every other hook depends on. Returns 'light' or 'dark'.

I borrowed the idea from Apple about dynamic values (UIColor with a callback, 20:50 of the talk). However React Native doesn't use objects or functions for values, so I thought of dynamic stylesheets. Basically our good old StyleSheet with support for dynamic values.

const dynamicStyles = new DynamicStyleSheet({
	container: {
		backgroundColor: new DynamicValue('white', 'black'),
		flex: 1
	},
	text: {
		color: new DynamicValue('black', 'white'),
		textAlign: 'center'
	}
})

function Component() {
	const styles = useDynamicStyleSheet(dynamicStyles)
	
	return (
		<View style={styles.container}>
			<Text style={styles.text}>My text</Text>
		</View>
	)
}

And here's the benefit to such an approach: let's say that this component is being reused inside a view that has to be only in dark mode. No problem, useDynamicStyleSheet will use context underneath so all you'll have to do is provide the theme you want that component to be in:

function MyScreen() {
	return (
		<>
			<DarkModeProvider mode="dark">
				<Component />{/* will be rendered using dark theme */}
			</DarkModeProvider>

			<DarkModeProvider mode="light">
				<Component />{/* will be rendered using light theme */}
			</DarkModeProvider>

			<Component />{/* will be rendered using current theme */}
		</>
	)
}

DynamicValue would be just an object that holds values for each theme. Literally the source code:

export class DynamicValue<T> {
	constructor(public readonly light: T, public readonly dark: T) { }
}

DynamicStyleSheet creates two stylesheet objects for each theme upon construction. useDynamicStyleSheet returns the appropriate stylesheet.

useDynamicValue which just returns the appropriate value from a DynamicValue object or from args (first arg for light theme, second for dark theme).

const logoUri = new DynamicValue(require('./light.png'), require('./dark.png'))
function Logo() {
	const source = useDynamicValue(logoUri)
	return <Image source={source} />
}
function Input() {
	const placeholderColor = useDynamicValue('black', 'white')
	return <TextInput placeholderTextColor={placeholderColor} />
}

I got excited last week about dark mode on iOS and I already implemented those ideas: https://github.com/codemotionapps/react-native-dark-mode

These hooks currently work because of a native module, which publishes changes to the theme using event emitter. And if you want to subscribe to that event emitter all you have to do is import it (hopefully you don't).

To detect the theme change in iOS I decided to use method swizzling on UIScreens traitCollectionDidChange and a lot of static methods 😅. This approach can be avoided if theming becomes a part of the React Native core 🙌 (override traitCollectionDidChange on the root React Native view controller).


My proposal: A similar high level API, with an alternative for class components. And a better solution for the iOS implementation, something that would require a tiny change to RN core to keep it lean, have it as a module like AsyncStorage and the others.

@tom-un
Copy link

tom-un commented Jun 17, 2019

@DimitarNestorov : that's a really intriguing proposal. The DynamicValues and DynamicStyleSheet approach has advantages over an event emitter approach. (I prototyped an event emitter approach for macOS dark mode and noted the downsides in my post above). I also feel the semantic color values of the platform are also important. They're important for continuity across the device, but really important for continuity in apps that are hybrids contain a mix of pure native views and react-native views.

I finally made the time to extend the work I did in macOS react-native dark mode to iOS. As expected, the iOS mechanism revealed at WWDC is very similar to macOS. Here's what the prototype looks like:

RNTester-ios-dark-mode

The branch with the code changes is here: microsoft/react-native-macos@master...tom-un:tomun/ios-darkmode

My proposed change is a change to core: its a change to the ColorType itself to support a platform specific named color. Every platform has some notion of named colors, and adding them to each platform's implementation I believe is a win for the react-native platform. Dimitar's proposal is also really interesting in that it provides a generic dynamic type that is applicable for things beyond just colors, like dynamic image sets.

@hramos
Copy link
Contributor

hramos commented Aug 2, 2019

👋🏼 I'm tracking issues related to iOS 13 and Android Q, and supporting Dark themes is my current focus. First I'd like to thank everyone in the thread for the thoughtful discussion and for collecting all the relevant information. I've been having discussions around Dark Mode with other members of the core team, and I want to make sure folks here are aware of our current thinking.

@tom-un's proposal is compelling. By exposing the underlying [UI|NS]Color support for adaptive colors, it provides a seamless experience when switching between light and dark themes. It has a higher chance of appealing to iOS developers, and is already in use in the react-native-macos fork due to the introduction of Dark Mode in Mojave last year.

This does bring some questions:

  • What would adaptive color support on Android look like? This is an area I'm less familiar with.
  • Semantic colors work great for apps that use the default system appearance. Do we expect folks building apps with custom color palettes to maintain their own set of named dynamic colors?

For reference,
my commit at hramos/react-native@1cf85c7 takes Tom's work from microsoft/react-native-macos@master...tom-un:tomun/ios-darkmode and applies it on top of the core React Native repository, sans macOS-specific code given macOS is not yet supported in core.

It seems like providing a useDarkMode hook, as @DimitarNestorov suggested above, would be sufficient for supporting dark mode on both iOS and Android. In fact, your DarkModeProvider context approach is similar to how we handle light and dark themes in our components at Facebook. I probably would not go as far as supporting dynamic values / stylesheets yet, though.

How do folks feel about moving forward with a React Hook approach first, and revisit adaptive color support later?

@necolas
Copy link

necolas commented Aug 3, 2019

Why not design something based on the web api that isn't limited to one theme?

@DimitarNestorov
Copy link

If you plan on using context we can then do a dynamic style sheet library similar to my approach in https://github.com/react-native-community

@hramos
Copy link
Contributor

hramos commented Aug 8, 2019

@DimitarNestorov At this point, I am not considering extending StyleSheet or adding a new DynamicStylesheet to core. The hook would be in core, however. It should be possible then to create a dynamic StyleSheet library then.

Nicolas brings up a good point. I had not fully considered the current approach to theme preferences in web yet. Would folks prefer something closer to usePreferredTheme as a hook that returns 'light', 'dark', or 'unspecified'/'no-preference'?

@ide
Copy link
Member

ide commented Aug 8, 2019

A generic theme is definitely better. Dark-mode only will probably look short-sighted in a few years if there are other themes (not necessarily just color-oriented either e.g. 2d theme vs 3d themes).

@brentvatne
Copy link
Contributor

A concern that I have about using the semantic and dynamic platform colors is that this won't translate well over to web because no equivalent exists there. This might be a good option to support but we should be careful about adding it, and we'll need to consider things like how well we can normalize the names across platforms and so on.

usePreferredTheme and an emitter that backs it seem like a good start. Should this be built as an external library that is included in new React Native projects and versioned separately? I think this could be helpful for folks who can't yet update to the latest React Native version but want to add support for the OS dark-mode to their app.

@necolas
Copy link

necolas commented Aug 8, 2019

It might be that we (or RNWindows) can surface whether Windows High Contrast mode is active via this hook too (and hopefully MS will eventually expose it to web apps via the preferred-theme media query)

@hramos
Copy link
Contributor

hramos commented Aug 14, 2019

I've landed on the following approach:

  • Create a Appearance native module that exposes the current appearance, and emits changes.
  • Provide a useAppearance hook (similar to new useWindowDimensions hook).

What do folks think we should do with StatusBar and its barStyle prop? As it is, you could use the new Appearance module to choose the correct light-content/dark-content style as needed.

@necolas
Copy link

necolas commented Aug 14, 2019

FWIW, the web platform has specced several independent queries related to this feature (roughly "user preferences"), including:

  • prefers-color-scheme (light, dark, no-preference)
  • prefers-reduced-motion (reduce, no-preference)
  • prefers-contrast (low, high, no-preference)
  • prefers-reduced-transparency (reduce, no-preference)

The first 2 are supported by modern browsers, the last 2 are not yet supported. In the future, there are likely to be queries related to ambient light level, font-size, audio, and whether the UA/OS is forcing colors (another part of OS-level high contrast modes). This is in addition to all the existing queries related to viewport dimensions, aspect ratio, orientation, resolution, etc.

Each of these queries can be passed to window.matchMedia - https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia. It also allows you to subscribe only to changes you're interested in, rather than being notified every time something like viewport dimensions change.

@brentvatne
Copy link
Contributor

What do folks think we should do with StatusBar and its barStyle prop? As it is, you could use the new Appearance module to choose the correct light-content/dark-content style as needed.

I think it's preferred to leave it as-is for now and discuss different pieces like this in separate RFCs. It's pretty easy to make a wrapper to account for theme in userspace: https://github.com/react-navigation/native/blob/f9334201893f93c81d6900811b481cee3818c748/src/Themed.js#L18-L37.

@brentvatne
Copy link
Contributor

brentvatne commented Aug 14, 2019

@hramos @necolas - do you think UserPreferences would be a better name than Appearance for this module?

type UserPreferencesConfig = {
  colorScheme: 'light' | 'dark' | 'no-preference';
  reducedMotion: 'reduce' | 'no-preference';
  contrast: 'low' | 'high' | 'no-preference';
  reducedTransparency: 'reduce' | 'no-preference';
};

type UserPreferencesListener = (config: UserPreferencesConfig) => void;
type UserPreferencesEvents = 'change';

const get = () => UserPreferencesConfig;
const addChangeListener = (listener: UserPreferencesListener) => EventSubscription;
// or if you prefer this style
const addListener = (event: UserPreferencesEvent, listener: UserPreferencesListener) => EventSubscription;

for this first pass we could just expose the colorScheme property.

edit: perhaps UserPreferences is too generic - it could refer to preferred way to render dates like mm/dd/yyyy vs dd/mm/yyyy) and also clashes with NSUserDefaults a bit. Perhaps Appearance is fine after all, the above config still makes sense under that name.

@hramos
Copy link
Contributor

hramos commented Aug 14, 2019

I'm afraid UserPreferences by itself could be confusing. It's too similar to NSUserDefaults and might imply it's a module for storing user preferences, when in fact this is a read-only module. I don't have a suggestion, other than perhaps PreferredAppearance or UserPreferredAppearance. I'm including Appearance in the name because it looks like the proposed spec so far is all related to the user's preferred interface style or appearance. Can you think of a property that might be needed in the future that does not relate to appearance?

Edit: Naming nits aside, I agree with breaking this down into individual preferences (e.g. colorScheme, contrast). This is more in line with the existing Dimensions module.

@brentvatne
Copy link
Contributor

@hramos - good timing, I just edited my comment with the same note re: UserPreferences being confusing. maybe best to just stick with Appearance and avoid bikeshedding too much on it for now.

@hramos
Copy link
Contributor

hramos commented Aug 14, 2019

This is what I've landed on, roughly:

// Appearance.js
const COLOR_SCHEME_NAME = {
  light: 'light',
  dark: 'dark',
  noPreference: 'no-preference',
};
type ColorSchemeName = $Keys<{
  light: string,
  dark: string,
  noPreference: string,
}>;
type AppearanceListener = (preferences: AppearancePreferences) => void;
type AppearancePreferences = {|
  colorScheme?: string,
/* Potential future keys:
  reducedMotion?: ReducedMotionName,
  contrast?: ContrastName,
  reducedTransparency?: ReducedTransparencyName,
*/
|};

class Appearance {
    static get(preference: string): ColorSchemeName;  
    static set(preferences: AppearancePreferences): void;
    static addChangeListener(listener: AppearanceListener): void;
    static removeChangeListener(listener: AppearanceListener): void;
}
// useColorScheme.js (React Hook)
function useColorScheme(): ColorSchemeName; 

@tom-un
Copy link

tom-un commented Dec 17, 2019

@yungsters : Thanks! Regarding you're feedback on the following:

cross-platform and cross colors:

The idea of PlatformColor() is that it is a way to express a platform specific color. The arguments are not intended to be cross platform -- hence the name PlatformColor. For multiple platforms, the developer would use the Filename.<platform>.js, Platform.OS == 'platform', or Platform.select({}) patterns. I implemented all three of these approaches in RNTester as a proof of concept. For example:

RNTesterApp.ios.js

const styles = StyleSheet.create({
  headerContainer: {
    backgroundColor: PlatformColor('tertiarySystemBackgroundColor'),
...

RNTesterApp.macos.js

const styles = StyleSheet.create({
  headerContainer: {
    backgroundColor: PlatformColor('windowBackgroundColor'),
...

etc.

RNTesterBlock.js

const styles = StyleSheet.create({
  container: {
    ...Platform.select({
      ios: {
        backgroundColor: PlatformColor('tertiarySystemBackgroundColor'),
      },
      macos: {
        backgroundColor: PlatformColor('windowBackgroundColor'),
      },
      default: {
        backgroundColor: '#ffffff',
      },
    }),

Another approach that we use in several projects, and is pretty much the same concept as CSS Variables, is to define color constants files for each platform:
MyAppColors.ios.js

const Background = PlatformColor('tertiarySystemBackgroundColor');
...

MyAppColors.macos.js

const Background = PlatformColor('windowBackgroundColor');
...

etc.
For platform version fallbacks we could certainly do what you suggested (PlatformColor('some-color', 'some-older-color')). For mac and iOS what I implemented was a dictionary of all the system color names that exist to date, and each one has a fallback implemented on the native side. For example, on iOS 'labelColor' is only valid on iOS 13.0 and later, so on earlier OS's RCTConvert will fallback to 'FF000000'. All the fallback RGB colors were "harvested" programmatically by resolving the iOS system named colors to RGB in Light theme. I preferred this over having to specify PlatformColor('labelColor', '#FF000000') everywhere. However, I think there is value in having both. In the future Apple could have a system color name that is not a color: it could be a pattern or some other material that has no single RGB, in which case it would be better to let the developer specify the fallback, i.e. PlatformColor('some-ios-14-color', 'labelColor').

Platform Options

'options' is perhaps too vague of a term. What it really is is a NativeColorValue. It is an object that could describe any platform specific data that describes a color. On mac and ios, NativeColorValue can be { semantic: 'name'} which is what is actually constructed via PlatformColor('name'). It can also be { dynamic: { light:, dark:, ... }} which is a construct that is implemented using + (UIColor *)colorWithDynamicProvider:(UIColor * _Nonnull (^)(UITraitCollection *traitCollection))dynamicProvider;. In the future it could have more keys to leverage other mac or ios color or material traits APIs. On Windows NativeColorValue can contain brush definitions, including gradient colors, etc.

To answer your specific question about the dynamic use case, it is useful to create app specific colors that adjust dynamically to the settings of the system, just like system colors do. In the 'facebook blue' example, the developer could specify light and dark variants of the branded color and the system selects the appropriate one at native render time. It could and should be extended to include high contrast and normal variants as well. I intended to, but wanted to keep the proposal/prototype simple.

@brentvatne : to reply to your comment:

As @acoates-ms replied, it would be problematic to have a function that returns an RGB "flattened" color because system colors on many platforms such as ios, mac, and windows are not limited to simple RGB values: they can be complex materials such as patterns, gradients and other complex rendering compositing constructs. For some apple UIColor/NSColor's it is possible to "extract" an RGB approximation, but for others such as patterns it is impossible.

On iOS, all the current iOS 13 semantic colors can in fact be approximated to an RGB. On macOS, there a few colors that are patterns that cannot even be approximated to RGB. And on both platforms, any RGB value would be just an approximation. On macOS, for example, 'windowBackgroundColor' is at casual observation just a light grey or dark grey depending on the theme. But in fact 'windowBackgroundColor' actually renders by sampling the colors of the desktop behind the window and tinting the color. Apple calls this "Desktop Tinting". More importantly, any future semantic colors on any platform could do anything so I would discourage the idea of ever exposing a function to return the RGB of a NativeColorValue.

The concept of a platform color is by its definition not cross platform. A developer that wants a simple RGB palette this is uniform across all platforms may still do so in react-native. But for complex brownfield apps that need to seamlessly integrate react native views into existing pure native views, it's critical that all the elements in the hierarchy use platform color constructs.

@brentvatne
Copy link
Contributor

thanks for the clarification @tom-un!

@tom-un
Copy link

tom-un commented Dec 17, 2019

Hi @TheSavior , responding to your feedback:

The Flow type checking was working in my prototype branches -- but you got my doubting. So as a test I added a hypothetical Android NativeColorValue. You were right: it didn't validate unless PlatformColor() is defined as PlatformColor(name: string, options?: Object) in all the NativeColorValueType.<platform>.js files. I fixed my facebook prototype branch and pushed it.

In the Microsoft fork of react-native, I discovered that we actually have other hidden Flow validation problems. As in the facebook repo we have .flowconfig (which is really for ios) and .flowconfig.android. The ios config ignores *.android.js files and the android config ignores *.ios.js files. The Microsoft fork really needs a .flowconfig.macos that ignores ios and android and the other two need to ignore macos. I have a local fix in our fork in the works.

Regarding your suggestion to have a native module to register native colors: I believe that would work -- I'll try prototyping it.

Also, regarding the opaque type: I actually wasn't aware of this Flow concept. I will also prototype this suggestion and investigate if there is a TypeScript equivalent.

@tom-un
Copy link

tom-un commented Dec 20, 2019

@TheSavior : I updated the NativeColorValue types to be opaque Flow types. As opaque types the NativeColorValue can now only be touched by code in the defining file, so I had to move the normalizeColorObject() and processColorObject() helper methods into the NativeColorValueTypes.<plat>.js files themselves, which makes sense and is cleaner than what I had originally.

@acoates-ms and I had some concerns about potential performance issues with requiring apps to call a native registerPlatformColor for each custom color -- particularly during App initialization time. There are also cases where we may want to create a color based on runtime conditions: like a named color with an effect applied to it and the effect could vary depending on something like the number of items in a container. It would be cumbersome to have to create a name and register each name for such cases besides the perf problem. Finally it would be tricky to implemented on the native side in such a way so that each react instance didn't have name collisions: A native app could have more than one react native instance in the same app, so each RCTRootView would have to have its own registerPlatformColor namespace.

What if there were simply platform specific factory methods for each platform in addition to PlatformColor()? I prototyped this approach: I created a IOSDynamicColor() method for iOS and just for temporary validation purposes a AndroidHypotheticalColor() method. I like this approach.

@yungsters : I implemented your idea of a fallback array of colors. The definition of PlatformColor() is now:

export const PlatformColor = (...names: Array<string>): ColorValue => {
  ...
}

I updated the PlatformColorExample in RNTester with a test cases for the fallback colors.

I pushed these changes to facebook/react-native@master...tom-un:fb-platformcolor

@MoOx
Copy link

MoOx commented Jan 14, 2020

Concerning cross-platform, pretty sure we will find lot's of people doing very similar things as

const styles = StyleSheet.create({
  container: {
    ...Platform.select({
      ios: {
        backgroundColor: PlatformColor('tertiarySystemBackgroundColor'),
      },
      macos: {
        backgroundColor: PlatformColor('windowBackgroundColor'),
      },
      default: {
        backgroundColor: '#ffffff',
      },
    }),

Shouldn't RN offers a common denominator for some colors? I am myself in the process of doing this & pretty sure tons of people will be interested by this as we are all going to have background, main text, lights text colors etc

@TheSavior
Copy link

I'm thinking I'd expect that to be a potentially separate solution that would be built on top of this. I think we have to support the API you mention with Platform.select, but providing a common denominator for some colors would still require having access to PlatformColor. I'd like to punt that discussion down the road if possible and focus on PlatformColor for now, unless you think changes to PlatformColor need to be made to support such a solution.

@MoOx
Copy link

MoOx commented Jan 14, 2020

Can be built on top for sure.

@TheSavior TheSavior changed the title Supporting dark mode and perhaps other themes Platform specific colors via PlatformColor Jan 14, 2020
@tom-un
Copy link

tom-un commented Jan 14, 2020

Regarding Android colors: I have very limited experience developing on the Android platform, but I jumped in anyway and prototyped an Android PlatformColor() solution in my branch facebook/react-native@master...tom-un:fb-platformcolor.

From my research, Android color resources in XML are expressed as strings:
XML Resource:
@[<package_name>:]<resource_type>/<resource_name>
Style reference from current theme:
?[<package_name>:][<resource_type>/]<resource_name>

These formats seem to be reasonable values for the PlatformColor() arguments.

Conveniently, almost all the color props in the react-native implementation are annotated @ReactProp(... , customType = "Color") (there was only a single exception I could find name = ViewProps.COLOR so I fixed it).

I updated ViewManagersPropertyCache.java with a new ColorPropSetter that extends PropSetter. All the props of customType = "Color" get a ColorPropSetter. In it I parse the resource path string and then resolve the color id against the current theme. I added an RNTester case for Android (results below).

The prototype is not handling the <package_name> or <resource_type> currently.

I would love to hear from Android experts their opinions on this solution.

PlatformColor-ios

@arazabishov
Copy link

Thanks to the guidance of @tom-un , color references that include <package_name> and <resource_type> are now supported by the android prototype as well: tom-un/react-native#1. Now it is possible to reference theme attributes and colors from other packages. For example:

  • ?android:colorError
  • ?android:attr/colorError
  • ?attr/colorPrimary
  • ?colorPrimaryDark
  • @android:color/holo_purple
  • @color/catalyst_redbox_background

@ezranbayantemur
Copy link

Hey everyone, is there any update for this proposal? Will it come soon?

@Dexwell
Copy link

Dexwell commented Feb 7, 2020

@ezranbayantemur Looks like the PR facebook/react-native#27908 is being finalized and will be merged soon :)

@ezranbayantemur
Copy link

ezranbayantemur commented Feb 7, 2020

@Dexwell Sounds amazing !

facebook-github-bot pushed a commit to facebook/react-native that referenced this issue Feb 20, 2020
…Color PR. (#28040)

Summary:
The [PlatformColor PR](#27908) is currently open to implement the [PlatformColor proposal](react-native-community/discussions-and-proposals#126).   When that PR was imported into Facebooks internal builds it was found that the change to the `processColor()` function to return an opaque type or `number` instead of just `number` breaks internal components.

This PR is a simplification of the PlatformColor PR only changing the return type of `processColor()` from `?number` to `?number | NativeColorType` where `NativeColorType` is just an empty but opaque type.   This will allow changes to be made to these internal components but with less risk than the larger PR.

## Changelog

[General] [Changed] - Add NativeColorType opaque type to normalizeColor() ahead of PlatformColor PR
Pull Request resolved: #28040

Test Plan: Flow checks, Jest test, iOS unit tests, iOS integration tests, and manual testing performed on RNTester for iOS and Android.

Differential Revision: D19860205

Pulled By: TheSavior

fbshipit-source-id: 799662c6621d3974158b375ccccfa136982c43b4
facebook-github-bot pushed a commit to facebook/react-native that referenced this issue Mar 2, 2020
Summary:
This Pull Request implements the PlatformColor proposal discussed at react-native-community/discussions-and-proposals#126.   The changes include implementations for iOS and Android as well as a PlatformColorExample page in RNTester.

Every native platform has the concept of system defined colors. Instead of specifying a concrete color value the app developer can choose a system color that varies in appearance depending on a system theme settings such Light or Dark mode, accessibility settings such as a High Contrast mode, and even its context within the app such as the traits of a containing view or window.

The proposal is to add true platform color support to react-native by extending the Flow type `ColorValue` with platform specific color type information for each platform and to provide a convenience function, `PlatformColor()`, for instantiating platform specific ColorValue objects.

`PlatformColor(name [, name ...])` where `name` is a system color name on a given platform.  If `name` does not resolve to a color for any reason, the next `name` in the argument list will be resolved and so on.   If none of the names resolve, a RedBox error occurs.  This allows a latest platform color to be used, but if running on an older platform it will fallback to a previous version.
 The function returns a `ColorValue`.

On iOS the values of `name` is one of the iOS [UI Element](https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors) or [Standard Color](https://developer.apple.com/documentation/uikit/uicolor/standard_colors) names such as `labelColor` or `systemFillColor`.

On Android the `name` values are the same [app resource](https://developer.android.com/guide/topics/resources/providing-resources) path strings that can be expressed in XML:
XML Resource:
`@ [<package_name>:]<resource_type>/<resource_name>`
Style reference from current theme:
`?[<package_name>:][<resource_type>/]<resource_name>`
For example:
- `?android:colorError`
- `?android:attr/colorError`
- `?attr/colorPrimary`
- `?colorPrimaryDark`
- `android:color/holo_purple`
- `color/catalyst_redbox_background`

On iOS another type of system dynamic color can be created using the `IOSDynamicColor({dark: <color>, light:<color>})` method.   The arguments are a tuple containing custom colors for light and dark themes. Such dynamic colors are useful for branding colors or other app specific colors that still respond automatically to system setting changes.

Example: `<View style={{ backgroundColor: IOSDynamicColor({light: 'black', dark: 'white'}) }}/>`

Other platforms could create platform specific functions similar to `IOSDynamicColor` per the needs of those platforms.   For example, macOS has a similar dynamic color type that could be implemented via a `MacDynamicColor`.   On Windows custom brushes that tint or otherwise modify a system brush could be created using a platform specific method.

## Changelog

[General] [Added] - Added PlatformColor implementations for iOS and Android
Pull Request resolved: #27908

Test Plan:
The changes have been tested using the RNTester test app for iOS and Android.   On iOS a set of XCTestCase's were added to the Unit Tests.

<img width="924" alt="PlatformColor-ios-android" src="https://user-images.githubusercontent.com/30053638/73472497-ff183a80-433f-11ea-90d8-2b04338bbe79.png">

In addition `PlatformColor` support has been added to other out-of-tree platforms such as macOS and Windows has been implemented using these changes:

react-native for macOS branch: microsoft/react-native-macos@master...tom-un:tomun/platformcolors

react-native for Windows branch: microsoft/react-native-windows@master...tom-un:tomun/platformcolors

iOS
|Light|Dark|
|{F229354502}|{F229354515}|

Android
|Light|Dark|
|{F230114392}|{F230114490}|

{F230122700}

Reviewed By: hramos

Differential Revision: D19837753

Pulled By: TheSavior

fbshipit-source-id: 82ca70d40802f3b24591bfd4b94b61f3c38ba829
osdnk pushed a commit to osdnk/react-native that referenced this issue Mar 9, 2020
…Color PR. (facebook#28040)

Summary:
The [PlatformColor PR](facebook#27908) is currently open to implement the [PlatformColor proposal](react-native-community/discussions-and-proposals#126).   When that PR was imported into Facebooks internal builds it was found that the change to the `processColor()` function to return an opaque type or `number` instead of just `number` breaks internal components.

This PR is a simplification of the PlatformColor PR only changing the return type of `processColor()` from `?number` to `?number | NativeColorType` where `NativeColorType` is just an empty but opaque type.   This will allow changes to be made to these internal components but with less risk than the larger PR.

## Changelog

[General] [Changed] - Add NativeColorType opaque type to normalizeColor() ahead of PlatformColor PR
Pull Request resolved: facebook#28040

Test Plan: Flow checks, Jest test, iOS unit tests, iOS integration tests, and manual testing performed on RNTester for iOS and Android.

Differential Revision: D19860205

Pulled By: TheSavior

fbshipit-source-id: 799662c6621d3974158b375ccccfa136982c43b4
osdnk pushed a commit to osdnk/react-native that referenced this issue Mar 9, 2020
Summary:
This Pull Request implements the PlatformColor proposal discussed at react-native-community/discussions-and-proposals#126.   The changes include implementations for iOS and Android as well as a PlatformColorExample page in RNTester.

Every native platform has the concept of system defined colors. Instead of specifying a concrete color value the app developer can choose a system color that varies in appearance depending on a system theme settings such Light or Dark mode, accessibility settings such as a High Contrast mode, and even its context within the app such as the traits of a containing view or window.

The proposal is to add true platform color support to react-native by extending the Flow type `ColorValue` with platform specific color type information for each platform and to provide a convenience function, `PlatformColor()`, for instantiating platform specific ColorValue objects.

`PlatformColor(name [, name ...])` where `name` is a system color name on a given platform.  If `name` does not resolve to a color for any reason, the next `name` in the argument list will be resolved and so on.   If none of the names resolve, a RedBox error occurs.  This allows a latest platform color to be used, but if running on an older platform it will fallback to a previous version.
 The function returns a `ColorValue`.

On iOS the values of `name` is one of the iOS [UI Element](https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors) or [Standard Color](https://developer.apple.com/documentation/uikit/uicolor/standard_colors) names such as `labelColor` or `systemFillColor`.

On Android the `name` values are the same [app resource](https://developer.android.com/guide/topics/resources/providing-resources) path strings that can be expressed in XML:
XML Resource:
`@ [<package_name>:]<resource_type>/<resource_name>`
Style reference from current theme:
`?[<package_name>:][<resource_type>/]<resource_name>`
For example:
- `?android:colorError`
- `?android:attr/colorError`
- `?attr/colorPrimary`
- `?colorPrimaryDark`
- `android:color/holo_purple`
- `color/catalyst_redbox_background`

On iOS another type of system dynamic color can be created using the `IOSDynamicColor({dark: <color>, light:<color>})` method.   The arguments are a tuple containing custom colors for light and dark themes. Such dynamic colors are useful for branding colors or other app specific colors that still respond automatically to system setting changes.

Example: `<View style={{ backgroundColor: IOSDynamicColor({light: 'black', dark: 'white'}) }}/>`

Other platforms could create platform specific functions similar to `IOSDynamicColor` per the needs of those platforms.   For example, macOS has a similar dynamic color type that could be implemented via a `MacDynamicColor`.   On Windows custom brushes that tint or otherwise modify a system brush could be created using a platform specific method.

## Changelog

[General] [Added] - Added PlatformColor implementations for iOS and Android
Pull Request resolved: facebook#27908

Test Plan:
The changes have been tested using the RNTester test app for iOS and Android.   On iOS a set of XCTestCase's were added to the Unit Tests.

<img width="924" alt="PlatformColor-ios-android" src="https://user-images.githubusercontent.com/30053638/73472497-ff183a80-433f-11ea-90d8-2b04338bbe79.png">

In addition `PlatformColor` support has been added to other out-of-tree platforms such as macOS and Windows has been implemented using these changes:

react-native for macOS branch: microsoft/react-native-macos@master...tom-un:tomun/platformcolors

react-native for Windows branch: microsoft/react-native-windows@master...tom-un:tomun/platformcolors

iOS
|Light|Dark|
|{F229354502}|{F229354515}|

Android
|Light|Dark|
|{F230114392}|{F230114490}|

{F230122700}

Reviewed By: hramos

Differential Revision: D19837753

Pulled By: TheSavior

fbshipit-source-id: 82ca70d40802f3b24591bfd4b94b61f3c38ba829
@zachgibson
Copy link

Does Android have colors that can be used with PlatformColor that will automatically change when light/dark mode is changed? Similar to something like label color on iOS?

@arazabishov
Copy link

@zachgibson yes. You need to use theme attributes when referencing colors, for example ?attr/colorPrimary. If your android theme supports dark mode, system will pick the right color for you.

@TheSavior
Copy link

I'm going to close this issue as the feature listed in the OP has landed and is now a part of React Native

@a-eid
Copy link

a-eid commented Jan 2, 2022

@zachgibson yes. You need to use theme attributes when referencing colors, for example ?attr/colorPrimary. If your android theme supports dark mode, system will pick the right color for you.

Seems that android react to mode changes only on reload, could you please check here what if I'm doing anything wrong ? facebook/react-native#32823

cc @tom-un

@a-eid
Copy link

a-eid commented Jan 11, 2022

@zachgibson yes. You need to use theme attributes when referencing colors, for example ?attr/colorPrimary. If your android theme supports dark mode, system will pick the right color for you.

Seems that android react to mode changes only on reload, could you please check here what if I'm doing anything wrong ? facebook/react-native#32823

cc @tom-un

seems that wix is working around this issue somehow,
platformColor is working with wix navigation but not with react native.

pc-template.mp4

@a-eid
Copy link

a-eid commented Jan 21, 2022

also platform Colors does not seem to be working with borders on android tested on latest RN 0.67 facebook/react-native#32942

@Nantris
Copy link

Nantris commented May 16, 2022

The PlatformColor code example doesn't show any colors when run on my Android 12 devices?

image

Each color object here only contains a label key and no color key:

https://github.com/facebook/react-native/blob/57d3b9e2ca65f591caa45a8fa6424995cfc399ca/packages/rn-tester/js/examples/PlatformColor/PlatformColorExample.js#L171

What could be the issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🗣 Discussion This label identifies an ongoing discussion on a subject
Projects
None yet
Development

No branches or pull requests