Skip to content

Commit

Permalink
Merge pull request #24178 from margelo/@chrispader/theme-switching-ts…
Browse files Browse the repository at this point in the history
…-types

Theme switching: Add TypeScript types
  • Loading branch information
grgia authored Nov 6, 2023
2 parents fd0a701 + b46f0b2 commit f9cf18e
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 72 deletions.
4 changes: 2 additions & 2 deletions src/styles/ThemeStylesContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import styles from './styles';
import styles, {type Styles} from './styles';

const ThemeStylesContext = React.createContext(styles);
const ThemeStylesContext = React.createContext<Styles>(styles);

export default ThemeStylesContext;
7 changes: 2 additions & 5 deletions src/styles/ThemeStylesProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
/* eslint-disable react/jsx-props-no-spreading */
import React, {useMemo} from 'react';
// TODO: Rename this to "styles" once the app is migrated to theme switching hooks and HOCs
import {stylesGenerator as stylesUntyped} from './styles';
import {stylesGenerator} from './styles';
import useTheme from './themes/useTheme';
import ThemeStylesContext from './ThemeStylesContext';

const styles = stylesUntyped;

type ThemeStylesProviderProps = {
children: React.ReactNode;
};

function ThemeStylesProvider({children}: ThemeStylesProviderProps) {
const theme = useTheme();

const themeStyles = useMemo(() => styles(theme), [theme]);
const themeStyles = useMemo(() => stylesGenerator(theme), [theme]);

return <ThemeStylesContext.Provider value={themeStyles}>{children}</ThemeStylesContext.Provider>;
}
Expand Down
11 changes: 9 additions & 2 deletions src/styles/colors.js → src/styles/colors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import {Color} from './themes/types';

/**
* DO NOT import colors.js into files. Use ../themes/default.js instead.
* DO NOT import colors.js into files. Use the theme switching hooks and HOCs instead.
* For functional components, you can use the `useTheme` and `useThemeStyles` hooks
* For class components, you can use the `withTheme` and `withThemeStyles` HOCs
*/
export default {
const colors: Record<string, Color> = {
// Brand Colors
black: '#000000',
white: '#FFFFFF',
ivory: '#fffaf0',
Expand Down Expand Up @@ -91,3 +96,5 @@ export default {
ice700: '#28736D',
ice800: '#134038',
};

export default colors;
18 changes: 7 additions & 11 deletions src/styles/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import overflowXHidden from './overflowXHidden';
import pointerEventsAuto from './pointerEventsAuto';
import pointerEventsNone from './pointerEventsNone';
import defaultTheme from './themes/default';
import {ThemeDefault} from './themes/types';
import {ThemeColors} from './themes/types';
import borders from './utilities/borders';
import cursor from './utilities/cursor';
import display from './utilities/display';
Expand Down Expand Up @@ -80,7 +80,7 @@ const touchCalloutNone: Pick<ViewStyle, 'WebkitTouchCallout'> = Browser.isMobile
// to prevent vertical text offset in Safari for badges, new lineHeight values have been added
const lineHeightBadge: Pick<ViewStyle, 'lineHeight'> = Browser.isSafari() ? {lineHeight: variables.lineHeightXSmall} : {lineHeight: variables.lineHeightNormal};

const picker = (theme: ThemeDefault) =>
const picker = (theme: ThemeColors) =>
({
backgroundColor: theme.transparent,
color: theme.text,
Expand All @@ -96,14 +96,14 @@ const picker = (theme: ThemeDefault) =>
textAlign: 'left',
} satisfies TextStyle);

const link = (theme: ThemeDefault) =>
const link = (theme: ThemeColors) =>
({
color: theme.link,
textDecorationColor: theme.link,
fontFamily: fontFamily.EXP_NEUE,
} satisfies ViewStyle & MixedStyleDeclaration);

const baseCodeTagStyles = (theme: ThemeDefault) =>
const baseCodeTagStyles = (theme: ThemeColors) =>
({
borderWidth: 1,
borderRadius: 5,
Expand All @@ -116,7 +116,7 @@ const headlineFont = {
fontWeight: '500',
} satisfies TextStyle;

const webViewStyles = (theme: ThemeDefault) =>
const webViewStyles = (theme: ThemeColors) =>
({
// As of react-native-render-html v6, don't declare distinct styles for
// custom renderers, the API for custom renderers has changed. Declare the
Expand Down Expand Up @@ -211,7 +211,7 @@ const webViewStyles = (theme: ThemeDefault) =>
},
} satisfies WebViewStyle);

const styles = (theme: ThemeDefault) =>
const styles = (theme: ThemeColors) =>
({
// Add all of our utility and helper styles
...spacing,
Expand Down Expand Up @@ -4014,12 +4014,8 @@ const styles = (theme: ThemeDefault) =>
},
} satisfies Styles);

// For now we need to export the styles function that takes the theme as an argument
// as something named different than "styles", because a lot of files import the "defaultStyles"
// as "styles", which causes ESLint to throw an error.
// TODO: Remove "stylesGenerator" and instead only return "styles" once the app is migrated to theme switching hooks and HOCs and "styles/theme/default.js" is not used anywhere anymore (GH issue: https://github.com/Expensify/App/issues/27337)
const stylesGenerator = styles;
const defaultStyles = styles(defaultTheme);

export default defaultStyles;
export {stylesGenerator};
export {stylesGenerator, type Styles};
6 changes: 0 additions & 6 deletions src/styles/themes/ThemeContext.js

This file was deleted.

7 changes: 7 additions & 0 deletions src/styles/themes/ThemeContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';
import darkTheme from './default';
import {ThemeColors} from './types';

const ThemeContext = React.createContext<ThemeColors>(darkTheme);

export default ThemeContext;
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import PropTypes from 'prop-types';
import React, {useMemo} from 'react';
import CONST from '@src/CONST';
// Going to eventually import the light theme here too
import darkTheme from './default';
import lightTheme from './light';
import ThemeContext from './ThemeContext';
import useThemePreference from './useThemePreference';

Expand All @@ -12,10 +12,10 @@ const propTypes = {
children: PropTypes.node.isRequired,
};

function ThemeProvider(props) {
function ThemeProvider(props: React.PropsWithChildren) {
const themePreference = useThemePreference();

const theme = useMemo(() => (themePreference === CONST.THEME.LIGHT ? /* TODO: replace with light theme */ darkTheme : darkTheme), [themePreference]);
const theme = useMemo(() => (themePreference === CONST.THEME.LIGHT ? lightTheme : darkTheme), [themePreference]);

return <ThemeContext.Provider value={theme}>{props.children}</ThemeContext.Provider>;
}
Expand Down
25 changes: 12 additions & 13 deletions src/styles/themes/default.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import colors from '@styles/colors';
import SCREENS from '@src/SCREENS';
import type {ThemeBase} from './types';
import {ThemeColors} from './types';

const darkTheme = {
// Figma keys
Expand Down Expand Up @@ -83,19 +83,18 @@ const darkTheme = {
starDefaultBG: 'rgb(254, 228, 94)',
loungeAccessOverlay: colors.blue800,
mapAttributionText: colors.black,
PAGE_BACKGROUND_COLORS: {},
white: colors.white,
} satisfies ThemeBase;

darkTheme.PAGE_BACKGROUND_COLORS = {
[SCREENS.HOME]: darkTheme.sidebar,
[SCREENS.SAVE_THE_WORLD.ROOT]: colors.tangerine800,
[SCREENS.SETTINGS.PREFERENCES]: colors.blue500,
[SCREENS.SETTINGS.WORKSPACES]: colors.pink800,
[SCREENS.SETTINGS.WALLET]: colors.darkAppBackground,
[SCREENS.SETTINGS.SECURITY]: colors.ice500,
[SCREENS.SETTINGS.STATUS]: colors.green700,
[SCREENS.SETTINGS.ROOT]: darkTheme.sidebar,
};
PAGE_BACKGROUND_COLORS: {
[SCREENS.HOME]: colors.darkHighlightBackground,
[SCREENS.SAVE_THE_WORLD.ROOT]: colors.tangerine800,
[SCREENS.SETTINGS.PREFERENCES]: colors.blue500,
[SCREENS.SETTINGS.WORKSPACES]: colors.pink800,
[SCREENS.SETTINGS.WALLET]: colors.darkAppBackground,
[SCREENS.SETTINGS.SECURITY]: colors.ice500,
[SCREENS.SETTINGS.STATUS]: colors.green700,
[SCREENS.SETTINGS.ROOT]: colors.darkHighlightBackground,
},
} satisfies ThemeColors;

export default darkTheme;
27 changes: 13 additions & 14 deletions src/styles/themes/light.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import colors from '@styles/colors';
import SCREENS from '@src/SCREENS';
import type {ThemeDefault} from './types';
import {ThemeColors} from './types';

const lightTheme = {
// Figma keys
Expand All @@ -16,9 +16,9 @@ const lightTheme = {
iconSuccessFill: colors.green400,
iconReversed: colors.lightAppBackground,
iconColorfulBackground: `${colors.ivory}cc`,
textColorfulBackground: colors.ivory,
textSupporting: colors.lightSupportingText,
text: colors.lightPrimaryText,
textColorfulBackground: colors.ivory,
link: colors.blue600,
linkHover: colors.blue500,
buttonDefaultBG: colors.lightDefaultButton,
Expand Down Expand Up @@ -83,19 +83,18 @@ const lightTheme = {
starDefaultBG: 'rgb(254, 228, 94)',
loungeAccessOverlay: colors.blue800,
mapAttributionText: colors.black,
PAGE_BACKGROUND_COLORS: {},
white: colors.white,
} satisfies ThemeDefault;

lightTheme.PAGE_BACKGROUND_COLORS = {
[SCREENS.HOME]: lightTheme.sidebar,
[SCREENS.SAVE_THE_WORLD.ROOT]: colors.tangerine800,
[SCREENS.SETTINGS.PREFERENCES]: colors.blue500,
[SCREENS.SETTINGS.WALLET]: colors.darkAppBackground,
[SCREENS.SETTINGS.WORKSPACES]: colors.pink800,
[SCREENS.SETTINGS.SECURITY]: colors.ice500,
[SCREENS.SETTINGS.STATUS]: colors.green700,
[SCREENS.SETTINGS.ROOT]: lightTheme.sidebar,
};
PAGE_BACKGROUND_COLORS: {
[SCREENS.HOME]: colors.lightHighlightBackground,
[SCREENS.SAVE_THE_WORLD.ROOT]: colors.tangerine800,
[SCREENS.SETTINGS.PREFERENCES]: colors.blue500,
[SCREENS.SETTINGS.WORKSPACES]: colors.pink800,
[SCREENS.SETTINGS.WALLET]: colors.darkAppBackground,
[SCREENS.SETTINGS.SECURITY]: colors.ice500,
[SCREENS.SETTINGS.STATUS]: colors.green700,
[SCREENS.SETTINGS.ROOT]: colors.lightHighlightBackground,
},
} satisfies ThemeColors;

export default lightTheme;
91 changes: 86 additions & 5 deletions src/styles/themes/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,89 @@
import DeepRecord from '@src/types/utils/DeepRecord';
import defaultTheme from './default';
type Color = string;

type ThemeBase = DeepRecord<string, string>;
type ThemeColors = {
// Figma keys
appBG: Color;
splashBG: Color;
highlightBG: Color;
border: Color;
borderLighter: Color;
borderFocus: Color;
icon: Color;
iconMenu: Color;
iconHovered: Color;
iconSuccessFill: Color;
iconReversed: Color;
iconColorfulBackground: Color;
textSupporting: Color;
text: Color;
textColorfulBackground: Color;
link: Color;
linkHover: Color;
buttonDefaultBG: Color;
buttonHoveredBG: Color;
buttonPressedBG: Color;
danger: Color;
dangerHover: Color;
dangerPressed: Color;
warning: Color;
success: Color;
successHover: Color;
successPressed: Color;
transparent: Color;
signInPage: Color;
dangerSection: Color;

type ThemeDefault = typeof defaultTheme;
// Additional keys
overlay: Color;
inverse: Color;
shadow: Color;
componentBG: Color;
hoverComponentBG: Color;
activeComponentBG: Color;
signInSidebar: Color;
sidebar: Color;
sidebarHover: Color;
heading: Color;
textLight: Color;
textDark: Color;
textReversed: Color;
textBackground: Color;
textMutedReversed: Color;
textError: Color;
offline: Color;
modalBackground: Color;
cardBG: Color;
cardBorder: Color;
spinner: Color;
unreadIndicator: Color;
placeholderText: Color;
heroCard: Color;
uploadPreviewActivityIndicator: Color;
dropUIBG: Color;
receiptDropUIBG: Color;
checkBox: Color;
pickerOptionsTextColor: Color;
imageCropBackgroundColor: Color;
fallbackIconColor: Color;
reactionActiveBackground: Color;
reactionActiveText: Color;
badgeAdHoc: Color;
badgeAdHocHover: Color;
mentionText: Color;
mentionBG: Color;
ourMentionText: Color;
ourMentionBG: Color;
tooltipSupportingText: Color;
tooltipPrimaryText: Color;
skeletonLHNIn: Color;
skeletonLHNOut: Color;
QRLogo: Color;
starDefaultBG: Color;
loungeAccessOverlay: Color;
mapAttributionText: Color;
white: Color;

export type {ThemeBase, ThemeDefault};
PAGE_BACKGROUND_COLORS: Record<string, Color>;
};

export {type ThemeColors, type Color};
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {useContext} from 'react';
import ThemeContext from './ThemeContext';
import {ThemeColors} from './types';

function useTheme() {
function useTheme(): ThemeColors {
const theme = useContext(ThemeContext);

if (!theme) {
throw new Error('StylesContext was null! Are you sure that you wrapped the component under a <ThemeProvider>?');
throw new Error('ThemeContext was null! Are you sure that you wrapped the component under a <ThemeProvider>?');
}

return theme;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
import {useContext, useEffect, useState} from 'react';
import {Appearance} from 'react-native';
import {Appearance, ColorSchemeName} from 'react-native';
import {PreferredThemeContext} from '@components/OnyxProvider';
import CONST from '@src/CONST';

type ThemePreference = typeof CONST.THEME.LIGHT | typeof CONST.THEME.DARK;

function useThemePreference() {
const [themePreference, setThemePreference] = useState(CONST.THEME.DEFAULT);
const [systemTheme, setSystemTheme] = useState();
const preferredThemeContext = useContext(PreferredThemeContext);
const [themePreference, setThemePreference] = useState<ThemePreference>(CONST.THEME.DEFAULT);
const [systemTheme, setSystemTheme] = useState<ColorSchemeName>();
const preferredThemeFromStorage = useContext(PreferredThemeContext);

useEffect(() => {
// This is used for getting the system theme, that can be set in the OS's theme settings. This will always return either "light" or "dark" and will update automatically if the OS theme changes.
const systemThemeSubscription = Appearance.addChangeListener(({colorScheme}) => setSystemTheme(colorScheme));
return systemThemeSubscription.remove;
return () => systemThemeSubscription.remove();
}, []);

useEffect(() => {
const theme = preferredThemeContext || CONST.THEME.DEFAULT;
const theme = preferredThemeFromStorage ?? CONST.THEME.DEFAULT;

// If the user chooses to use the device theme settings, we need to set the theme preference to the system theme
if (theme === CONST.THEME.SYSTEM) {
setThemePreference(systemTheme);
setThemePreference(systemTheme ?? CONST.THEME.DEFAULT);
} else {
setThemePreference(theme);
}
}, [preferredThemeContext, systemTheme]);
}, [preferredThemeFromStorage, systemTheme]);

return themePreference;
}
Expand Down
Loading

0 comments on commit f9cf18e

Please sign in to comment.