Skip to content

Commit

Permalink
feat: add ability to customize the fonts with the theme (#11243)
Browse files Browse the repository at this point in the history
**Motivation**

React Navigation 5 introduced a theming API. However, the theme only
supported customizing colors. With this change, the theme can be used to
customize font family as well - making it simpler to use a specific font
across all of React Navigation's UI elements.

**Test plan**

Specify a custom font in the theme and check that React Navigation's UI
elements use the new font. The following screenshots show the usage of
the "Lato" font.

<img width="804" alt="SCR-20230224-qy4"
src="https://user-images.githubusercontent.com/1174278/221260433-258083a8-7fba-4d3c-a2ae-da134b0e29f2.png">
<img width="803" alt="SCR-20230224-qyl"
src="https://user-images.githubusercontent.com/1174278/221260454-7e57f7b4-415e-47e5-8a53-a85cd51a5821.png">
<img width="806" alt="SCR-20230224-qz6"
src="https://user-images.githubusercontent.com/1174278/221260478-d698a060-c835-4527-9fdc-3717ea58907f.png">
<img width="388" alt="SCR-20230224-qzt"
src="https://user-images.githubusercontent.com/1174278/221260490-a7e63117-5615-491a-ab0f-1c5f148153fc.png">
  • Loading branch information
satya164 committed Feb 28, 2023
1 parent ee319bb commit 1cd6836
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 39 deletions.
5 changes: 3 additions & 2 deletions packages/bottom-tabs/src/views/Badge.tsx
Expand Up @@ -32,7 +32,7 @@ export function Badge({
const [opacity] = React.useState(() => new Animated.Value(visible ? 1 : 0));
const [rendered, setRendered] = React.useState(visible);

const theme = useTheme();
const { colors, fonts } = useTheme();

React.useEffect(() => {
if (!rendered) {
Expand Down Expand Up @@ -61,7 +61,7 @@ export function Badge({
}

// @ts-expect-error: backgroundColor definitely exists
const { backgroundColor = theme.colors.notification, ...restStyle } =
const { backgroundColor = colors.notification, ...restStyle } =
StyleSheet.flatten(style) || {};
const textColor = color(backgroundColor).isLight() ? 'black' : 'white';

Expand Down Expand Up @@ -90,6 +90,7 @@ export function Badge({
fontSize,
borderRadius,
},
fonts.regular,
styles.container,
restStyle,
]}
Expand Down
5 changes: 3 additions & 2 deletions packages/bottom-tabs/src/views/BottomTabItem.tsx
Expand Up @@ -195,7 +195,7 @@ export function BottomTabItem({
iconStyle,
style,
}: Props) {
const { colors } = useTheme();
const { colors, fonts } = useTheme();

const activeTintColor =
customActiveTintColor === undefined
Expand All @@ -219,8 +219,9 @@ export function BottomTabItem({
<Text
numberOfLines={1}
style={[
styles.label,
{ color },
fonts.regular,
styles.label,
horizontal ? styles.labelBeside : styles.labelBeneath,
labelStyle,
]}
Expand Down
10 changes: 2 additions & 8 deletions packages/drawer/src/views/DrawerItem.tsx
Expand Up @@ -159,7 +159,7 @@ const LinkPressable = ({
* A component used to show an action item with an icon and a label in a navigation drawer.
*/
export function DrawerItem(props: Props) {
const { colors } = useTheme();
const { colors, fonts } = useTheme();

const {
route,
Expand Down Expand Up @@ -220,13 +220,7 @@ export function DrawerItem(props: Props) {
<Text
numberOfLines={1}
allowFontScaling={allowFontScaling}
style={[
{
color,
fontWeight: '500',
},
labelStyle,
]}
style={[{ color }, fonts.medium, labelStyle]}
>
{label}
</Text>
Expand Down
5 changes: 3 additions & 2 deletions packages/elements/src/Header/HeaderBackButton.tsx
Expand Up @@ -33,7 +33,7 @@ export function HeaderBackButton({
testID,
style,
}: HeaderBackButtonProps) {
const { colors } = useTheme();
const { colors, fonts } = useTheme();

const [initialLabelWidth, setInitialLabelWidth] = React.useState<
undefined | number
Expand Down Expand Up @@ -106,8 +106,9 @@ export function HeaderBackButton({
leftLabelText === label ? handleLabelLayout : undefined
}
style={[
styles.label,
tintColor ? { color: tintColor } : null,
fonts.regular,
styles.label,
labelStyle,
]}
numberOfLines={1}
Expand Down
9 changes: 3 additions & 6 deletions packages/elements/src/Header/HeaderTitle.tsx
Expand Up @@ -16,7 +16,7 @@ type Props = Omit<TextProps, 'style'> & {
};

export function HeaderTitle({ tintColor, style, ...rest }: Props) {
const { colors } = useTheme();
const { colors, fonts } = useTheme();

return (
<Animated.Text
Expand All @@ -25,8 +25,9 @@ export function HeaderTitle({ tintColor, style, ...rest }: Props) {
numberOfLines={1}
{...rest}
style={[
styles.title,
{ color: tintColor === undefined ? colors.text : tintColor },
Platform.select({ ios: fonts.bold, default: fonts.medium }),
styles.title,
style,
]}
/>
Expand All @@ -37,16 +38,12 @@ const styles = StyleSheet.create({
title: Platform.select({
ios: {
fontSize: 17,
fontWeight: '600',
},
android: {
fontSize: 20,
fontFamily: 'sans-serif-medium',
fontWeight: 'normal',
},
default: {
fontSize: 18,
fontWeight: '500',
},
}),
});
9 changes: 7 additions & 2 deletions packages/material-top-tabs/src/views/MaterialTopTabBar.tsx
Expand Up @@ -17,7 +17,7 @@ export function MaterialTopTabBar({
descriptors,
...rest
}: MaterialTopTabBarProps) {
const { colors } = useTheme();
const { colors, fonts } = useTheme();

const focusedOptions = descriptors[state.routes[state.index].key].options;

Expand Down Expand Up @@ -103,7 +103,12 @@ export function MaterialTopTabBar({
if (typeof label === 'string') {
return (
<Text
style={[styles.label, { color }, options.tabBarLabelStyle]}
style={[
{ color },
fonts.regular,
styles.label,
options.tabBarLabelStyle,
]}
allowFontScaling={options.tabBarAllowFontScaling}
>
{label}
Expand Down
63 changes: 46 additions & 17 deletions packages/native-stack/src/views/HeaderConfig.tsx
Expand Up @@ -59,15 +59,22 @@ export function HeaderConfig({
title,
canGoBack,
}: Props): JSX.Element {
const { colors } = useTheme();
const { colors, fonts } = useTheme();
const tintColor =
headerTintColor ?? (Platform.OS === 'ios' ? colors.primary : colors.text);

const headerBackTitleStyleFlattened =
StyleSheet.flatten(headerBackTitleStyle) || {};
StyleSheet.flatten([headerBackTitleStyle, fonts.regular]) || {};
const headerLargeTitleStyleFlattened =
StyleSheet.flatten(headerLargeTitleStyle) || {};
const headerTitleStyleFlattened = StyleSheet.flatten(headerTitleStyle) || {};
StyleSheet.flatten([
headerLargeTitleStyle,
Platform.select({ ios: fonts.heavy, default: fonts.medium }),
]) || {};
const headerTitleStyleFlattened =
StyleSheet.flatten([
headerTitleStyle,
Platform.select({ ios: fonts.bold, default: fonts.medium }),
]) || {};
const headerStyleFlattened = StyleSheet.flatten(headerStyle) || {};
const headerLargeStyleFlattened = StyleSheet.flatten(headerLargeStyle) || {};

Expand All @@ -78,12 +85,33 @@ export function HeaderConfig({
headerTitleStyleFlattened.fontFamily,
]);

const backTitleFontSize =
'fontSize' in headerBackTitleStyleFlattened
? headerBackTitleStyleFlattened.fontSize
: undefined;

const titleText = getHeaderTitle({ title, headerTitle }, route.name);
const titleColor =
headerTitleStyleFlattened.color ?? headerTintColor ?? colors.text;
const titleFontSize = headerTitleStyleFlattened.fontSize;
'color' in headerTitleStyleFlattened
? headerTitleStyleFlattened.color
: headerTintColor ?? colors.text;
const titleFontSize =
'fontSize' in headerTitleStyleFlattened
? headerTitleStyleFlattened.fontSize
: undefined;
const titleFontWeight = headerTitleStyleFlattened.fontWeight;

const largeTitleBackgroundColor = headerLargeStyleFlattened.backgroundColor;
const largeTitleColor =
'color' in headerLargeTitleStyleFlattened
? headerLargeTitleStyleFlattened.color
: undefined;
const largeTitleFontSize =
'fontSize' in headerLargeTitleStyleFlattened
? headerLargeTitleStyleFlattened.fontSize
: undefined;
const largeTitleFontWeight = headerLargeTitleStyleFlattened.fontWeight;

const headerTitleStyleSupported: TextStyle = { color: titleColor };

if (headerTitleStyleFlattened.fontFamily != null) {
Expand All @@ -98,6 +126,12 @@ export function HeaderConfig({
headerTitleStyleSupported.fontWeight = titleFontWeight;
}

const headerBackgroundColor =
headerStyleFlattened.backgroundColor ??
(headerBackground != null || headerTransparent
? 'transparent'
: colors.card);

const headerLeftElement = headerLeft?.({
tintColor,
canGoBack,
Expand Down Expand Up @@ -162,15 +196,10 @@ export function HeaderConfig({
) : null}
<ScreenStackHeaderConfig
backButtonInCustomView={backButtonInCustomView}
backgroundColor={
headerStyleFlattened.backgroundColor ??
(headerBackground != null || headerTransparent
? 'transparent'
: colors.card)
}
backgroundColor={headerBackgroundColor}
backTitle={headerBackTitleVisible ? headerBackTitle : ' '}
backTitleFontFamily={backTitleFontFamily}
backTitleFontSize={headerBackTitleStyleFlattened.fontSize}
backTitleFontSize={backTitleFontSize}
blurEffect={headerBlurEffect}
color={tintColor}
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
Expand All @@ -183,11 +212,11 @@ export function HeaderConfig({
(headerTransparent && headerShadowVisible !== true)
}
largeTitle={headerLargeTitle}
largeTitleBackgroundColor={headerLargeStyleFlattened.backgroundColor}
largeTitleColor={headerLargeTitleStyleFlattened.color}
largeTitleBackgroundColor={largeTitleBackgroundColor}
largeTitleColor={largeTitleColor}
largeTitleFontFamily={largeTitleFontFamily}
largeTitleFontSize={headerLargeTitleStyleFlattened.fontSize}
largeTitleFontWeight={headerLargeTitleStyleFlattened.fontWeight}
largeTitleFontSize={largeTitleFontSize}
largeTitleFontWeight={largeTitleFontWeight}
largeTitleHideShadow={headerLargeTitleShadowVisible === false}
title={titleText}
titleColor={titleColor}
Expand Down
2 changes: 2 additions & 0 deletions packages/native/src/theming/DarkTheme.tsx
@@ -1,4 +1,5 @@
import type { Theme } from '../types';
import { fonts } from './fonts';

export const DarkTheme: Theme = {
dark: true,
Expand All @@ -10,4 +11,5 @@ export const DarkTheme: Theme = {
border: 'rgb(39, 39, 41)',
notification: 'rgb(255, 69, 58)',
},
fonts,
};
2 changes: 2 additions & 0 deletions packages/native/src/theming/DefaultTheme.tsx
@@ -1,4 +1,5 @@
import type { Theme } from '../types';
import { fonts } from './fonts';

export const DefaultTheme: Theme = {
dark: false,
Expand All @@ -10,4 +11,5 @@ export const DefaultTheme: Theme = {
border: 'rgb(216, 216, 216)',
notification: 'rgb(255, 59, 48)',
},
fonts,
};
63 changes: 63 additions & 0 deletions packages/native/src/theming/fonts.tsx
@@ -0,0 +1,63 @@
import { Platform } from 'react-native';

import type { Theme } from '../types';

const WEB_FONT_STACK =
'system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"';

export const fonts = Platform.select({
web: {
regular: {
fontFamily: WEB_FONT_STACK,
fontWeight: '400',
},
medium: {
fontFamily: WEB_FONT_STACK,
fontWeight: '500',
},
bold: {
fontFamily: WEB_FONT_STACK,
fontWeight: '600',
},
heavy: {
fontFamily: WEB_FONT_STACK,
fontWeight: '700',
},
},
ios: {
regular: {
fontFamily: 'System',
fontWeight: '400',
},
medium: {
fontFamily: 'System',
fontWeight: '500',
},
bold: {
fontFamily: 'System',
fontWeight: '600',
},
heavy: {
fontFamily: 'System',
fontWeight: '700',
},
},
default: {
regular: {
fontFamily: 'sans-serif',
fontWeight: 'normal',
},
medium: {
fontFamily: 'sans-serif-medium',
fontWeight: 'normal',
},
bold: {
fontFamily: 'sans-serif',
fontWeight: '600',
},
heavy: {
fontFamily: 'sans-serif',
fontWeight: '700',
},
},
} as const satisfies Record<string, Theme['fonts']>);
22 changes: 22 additions & 0 deletions packages/native/src/types.tsx
Expand Up @@ -6,6 +6,22 @@ import type {
Route,
} from '@react-navigation/core';

type FontStyle = {
fontFamily: string;
fontWeight:
| 'normal'
| 'bold'
| '100'
| '200'
| '300'
| '400'
| '500'
| '600'
| '700'
| '800'
| '900';
};

export type Theme = {
dark: boolean;
colors: {
Expand All @@ -16,6 +32,12 @@ export type Theme = {
border: string;
notification: string;
};
fonts: {
regular: FontStyle;
medium: FontStyle;
bold: FontStyle;
heavy: FontStyle;
};
};

export type LinkingOptions<ParamList extends {}> = {
Expand Down

0 comments on commit 1cd6836

Please sign in to comment.