Skip to content

POC for dark mode support #1147

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

Merged
merged 12 commits into from
Mar 10, 2021
Merged
24 changes: 22 additions & 2 deletions demo/src/configurations.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Assets, Typography, Spacings, Incubator} from 'react-native-ui-lib'; // eslint-disable-line
import {Assets, Colors, Typography, Spacings, Incubator} from 'react-native-ui-lib'; // eslint-disable-line

Assets.loadAssetsGroup('icons.demo', {
add: require('./assets/icons/add.png'),
Expand All @@ -17,11 +17,31 @@ Assets.loadAssetsGroup('images.demo', {
Typography.loadTypographies({
h1: {...Typography.text40},
h2: {...Typography.text50},
h3: {...Typography.text60}
h3: {...Typography.text60},
body: Typography.text70
});

Spacings.loadSpacings({
page: Spacings.s5
});

/* Dark Mode Schemes */
Colors.loadSchemes({
light: {
screenBG: 'transparent',
textColor: Colors.grey10,
moonOrSun: Colors.yellow30,
mountainForeground: Colors.green30,
mountainBackground: Colors.green50
},
dark: {
screenBG: Colors.grey10,
textColor: Colors.white,
moonOrSun: Colors.grey80,
mountainForeground: Colors.violet10,
mountainBackground: Colors.violet20
}
});

/* Components */
Incubator.TextField.defaultProps = {...Incubator.TextField.defaultProps, preset: 'default'};
1 change: 1 addition & 0 deletions demo/src/screens/MenuStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const navigationData = {
screens: [
{title: 'Border Radius', tags: 'corener border radius circle', screen: 'unicorn.style.BorderRadiusesScreen'},
{title: 'Colors', tags: 'palette rgb hex', screen: 'unicorn.style.ColorsScreen'},
{title: 'Dark Mode', tags: 'dark mode colors', screen: 'unicorn.style.DarkModeScreen'},
{title: 'Shadows (iOS)', tags: 'shadow', screen: 'unicorn.style.ShadowsScreen'},
{title: 'Spacings', tags: 'space margins paddings gutter', screen: 'unicorn.style.SpacingsScreen'},
{title: 'Typography', tags: 'fonts text', screen: 'unicorn.style.TypographyScreen'}
Expand Down
54 changes: 54 additions & 0 deletions demo/src/screens/foundationScreens/DarkModeScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, {Component} from 'react';
import {StyleSheet} from 'react-native';
import {View, Text, Constants} from 'react-native-ui-lib';

class DarkModeScreen extends Component {
state = {};
render() {
return (
<View flex padding-page bg-screenBG>
<Text h1 textColor>
Dark Mode
</Text>
{Constants.isIOS ? (
<Text marginT-s2 body textColor>
Change to dark mode in simulator by pressing Cmd+Shift+A
</Text>
) : (
<Text>Change to dark mode</Text>
)}

<View style={styles.moonOrSun} bg-moonOrSun/>
<View style={[styles.mountain, styles.mountainBackground]} bg-mountainBackground/>
<View style={[styles.mountain, styles.mountainForeground]} bg-mountainForeground/>
</View>
);
}
}

const styles = StyleSheet.create({
mountain: {
position: 'absolute',
width: 1000,
height: 1000,
borderRadius: 500
},
mountainForeground: {
left: -500,
bottom: -800
},
mountainBackground: {
right: -500,
bottom: -850
},
moonOrSun: {
position: 'absolute',
right: 50,
bottom: 350,
width: 100,
height: 100,
borderRadius: 50
}
});

export default DarkModeScreen;
1 change: 1 addition & 0 deletions demo/src/screens/foundationScreens/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export function registerScreens(registrar) {
registrar('unicorn.style.BorderRadiusesScreen', () => require('./BorderRadiusesScreen').default);
registrar('unicorn.style.ColorsScreen', () => require('./ColorsScreen').default);
registrar('unicorn.style.DarkModeScreen', () => require('./DarkModeScreen').default);
registrar('unicorn.style.TypographyScreen', () => require('./TypographyScreen').default);
registrar('unicorn.style.ShadowsScreen', () => require('./ShadowsScreen').default);
registrar('unicorn.style.SpacingsScreen', () => require('./SpacingsScreen').default);
Expand Down
4 changes: 1 addition & 3 deletions generatedTypes/components/view/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ export interface ViewProps extends Omit<RNViewProps, 'style'>, ContainerModifier
}
export declare type ViewPropTypes = ViewProps;
declare const _default: React.ComponentClass<ViewProps & {
useCustomTheme?: boolean | undefined; /**
* Use Animate.View as a container
*/
useCustomTheme?: boolean | undefined;
}, any>;
export default _default;
6 changes: 3 additions & 3 deletions generatedTypes/incubator/TextField/usePreset.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default function usePreset({ preset, ...props }: InternalTextFieldProps):
clearTextOnFocus?: boolean | undefined;
dataDetectorTypes?: "none" | "link" | "address" | "phoneNumber" | "calendarEvent" | "all" | import("react-native").DataDetectorTypes[] | undefined;
enablesReturnKeyAutomatically?: boolean | undefined;
keyboardAppearance?: "default" | "light" | "dark" | undefined;
keyboardAppearance?: "light" | "dark" | "default" | undefined;
passwordRules?: string | null | undefined;
rejectResponderTermination?: boolean | null | undefined;
selectionState?: import("react-native").DocumentSelectionState | undefined;
Expand Down Expand Up @@ -498,7 +498,7 @@ export default function usePreset({ preset, ...props }: InternalTextFieldProps):
clearTextOnFocus?: boolean | undefined;
dataDetectorTypes?: "none" | "link" | "address" | "phoneNumber" | "calendarEvent" | "all" | import("react-native").DataDetectorTypes[] | undefined;
enablesReturnKeyAutomatically?: boolean | undefined;
keyboardAppearance?: "default" | "light" | "dark" | undefined;
keyboardAppearance?: "light" | "dark" | "default" | undefined;
passwordRules?: string | null | undefined;
rejectResponderTermination?: boolean | null | undefined;
selectionState?: import("react-native").DocumentSelectionState | undefined;
Expand Down Expand Up @@ -988,7 +988,7 @@ export default function usePreset({ preset, ...props }: InternalTextFieldProps):
clearTextOnFocus?: boolean | undefined;
dataDetectorTypes?: "none" | "link" | "address" | "phoneNumber" | "calendarEvent" | "all" | import("react-native").DataDetectorTypes[] | undefined;
enablesReturnKeyAutomatically?: boolean | undefined;
keyboardAppearance?: "default" | "light" | "dark" | undefined;
keyboardAppearance?: "light" | "dark" | "default" | undefined;
passwordRules?: string | null | undefined;
rejectResponderTermination?: boolean | null | undefined;
selectionState?: import("react-native").DocumentSelectionState | undefined;
Expand Down
23 changes: 15 additions & 8 deletions generatedTypes/style/colors.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import _ from 'lodash';
import tinycolor from 'tinycolor2';
declare type Schemes = {
light: {
[key: string]: string;
};
dark: {
[key: string]: string;
};
};
export declare class Colors {
[key: string]: any;
schemes: Schemes;
constructor();
/**
* Load custom set of colors
Expand All @@ -11,6 +20,12 @@ export declare class Colors {
loadColors(colors: {
[key: string]: string;
}): void;
/**
* Load set of schemes for light/dark mode
* arguments:
* schemes - two sets of map of colors e.g {light: {screen: 'white'}, dark: {screen: 'black'}}
*/
loadSchemes(schemes: Schemes): void;
/**
* Add alpha to hex or rgb color
* arguments:
Expand Down Expand Up @@ -61,14 +76,6 @@ declare const colorObject: Colors & {
blue80: string;
cyan10: string;
cyan20: string;
/**
* Add alpha to hex or rgb color
* arguments:
* p1 - hex color / R part of RGB
* p2 - opacity / G part of RGB
* p3 - B part of RGB
* p4 - opacity
*/
cyan30: string;
cyan40: string;
cyan50: string;
Expand Down
13 changes: 13 additions & 0 deletions src/commons/asBaseComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import {Appearance} from 'react-native';
//@ts-ignore
import hoistStatics from 'hoist-non-react-statics';
//@ts-ignore
Expand Down Expand Up @@ -28,6 +29,18 @@ function asBaseComponent<PROPS, STATICS = {}>(WrappedComponent: React.ComponentT
error: false
};

componentDidMount() {
Appearance.addChangeListener(this.appearanceListener);
}

componentWillUnmount() {
Appearance.removeChangeListener(this.appearanceListener);
}

appearanceListener: Appearance.AppearanceListener = ({colorScheme}) => {
this.setState({colorScheme});
};

static getThemeProps = (props: any, context: any) => {
return Modifiers.getThemeProps.call(WrappedComponent, props, context);
};
Expand Down
16 changes: 9 additions & 7 deletions src/commons/modifiers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import _ from 'lodash';
import {StyleSheet} from 'react-native';
import {Appearance, StyleSheet} from 'react-native';
import {Typography, Colors, BorderRadiuses, Spacings, ThemeManager} from '../style';
import {BorderRadiusesLiterals} from '../style/borderRadiuses';
import TypographyPresets from '../style/typographyPresets';
Expand Down Expand Up @@ -96,26 +96,28 @@ export type ContainerModifiers =
BorderRadiusModifiers &
BackgroundColorModifier;


export function extractColorValue(props: Dictionary<any>) {
// const props = this.getThemeProps();
const allColorsKeys: Array<keyof typeof Colors> = _.keys(Colors);
const scheme = Appearance.getColorScheme() || 'light';
const schemeColors = Colors.schemes[scheme!];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you know scheme in not null or undefined (here and below)?

Copy link
Collaborator Author

@ethanshar ethanshar Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a default to light but i'm pretty it has to return some value and the issue is with the typings.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why but this is the definition in react-native:

type ColorSchemeName = 'light' | 'dark' | null | undefined;

Actually it's more weird because the original code does not have them (does not seem to have changed in the last two years)...

BTW, you can now remove the ! since it has no purpose now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

const allColorsKeys: Array<keyof typeof Colors> = [..._.keys(Colors), ..._.keys(schemeColors)];
const colorPropsKeys = _.chain(props)
.keys()
.filter(key => _.includes(allColorsKeys, key))
.value();
const color = _.findLast(colorPropsKeys, colorKey => props[colorKey] === true)!;
return Colors[color];
const colorKey = _.findLast(colorPropsKeys, colorKey => props[colorKey] === true)!;
return schemeColors[colorKey] || Colors[colorKey];
}

export function extractBackgroundColorValue(props: Dictionary<any>) {
let backgroundColor;
const scheme = Appearance.getColorScheme() || 'light';
const schemeColors = Colors.schemes[scheme!];

const keys = Object.keys(props);
const bgProp = _.findLast(keys, prop => Colors.getBackgroundKeysPattern().test(prop) && !!props[prop])!;
if (props[bgProp]) {
const key = bgProp.replace(Colors.getBackgroundKeysPattern(), '');
backgroundColor = Colors[key];
backgroundColor = schemeColors[key] || Colors[key];
}

return backgroundColor;
Expand Down
19 changes: 19 additions & 0 deletions src/style/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import {colorsPalette, themeColors} from './colorsPalette';
//@ts-ignore
import ColorName from './colorName';

type Schemes = {light: {[key: string]: string}; dark: {[key: string]: string}};

export class Colors {
[key: string]: any;
schemes: Schemes = {light: {}, dark: {}};

constructor() {
const colors = Object.assign(colorsPalette, themeColors);
Expand All @@ -23,6 +26,22 @@ export class Colors {
this[key] = value;
});
}
/**
* Load set of schemes for light/dark mode
* arguments:
* schemes - two sets of map of colors e.g {light: {screen: 'white'}, dark: {screen: 'black'}}
*/
loadSchemes(schemes: Schemes) {
const lightSchemeKeys = Object.keys(schemes.light);
const darkSchemeKeys = Object.keys(schemes.dark);

const missingKeys = _.xor(lightSchemeKeys, darkSchemeKeys);
if (!_.isEmpty(missingKeys)) {
console.error(`There is a mismatch in scheme keys: ${missingKeys.join(', ')}`);
}

this.schemes = schemes;
}

/**
* Add alpha to hex or rgb color
Expand Down