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>
) : (
<Tex marginT-s2 body textColort>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];
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