Skip to content

Commit eb5ce8d

Browse files
fix: allow for custom variants in Text (with factory) (#3660)
1 parent f4c87e9 commit eb5ce8d

25 files changed

+289
-191
lines changed

docs/docs/guides/04-fonts.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,6 @@ Platform.select({
393393
394394
#### Using `configureFonts` helper
395395
396-
<div style={{display: 'none'}}>
397396
* If there is a need to create a custom font variant, prepare its config object including required all fonts properties. After that, defined `fontConfig` has to be passed under the <b>`variant`</b> name as `config` into the params object:
398397
399398
```js
@@ -428,7 +427,16 @@ export default function Main() {
428427
);
429428
}
430429
```
431-
</div>
430+
431+
If you're using TypeScript you will need to create a custom `Text` component which accepts your custom variants:
432+
433+
```typescript
434+
import { customText } from 'react-native-paper'
435+
436+
// Use this instead of importing `Text` from `react-native-paper`
437+
export const Text = customText<'customVariant'>()
438+
```
439+
432440
433441
* In order to override one of the available `variant`'s font properties, pass the modified `fontConfig` under specific <b>`variant`</b> name as `config` into the params object:
434442

example/src/Examples/TextExample.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,46 @@
11
import * as React from 'react';
2-
import { StyleSheet, View } from 'react-native';
2+
import { Platform, StyleSheet, View } from 'react-native';
33

44
import {
55
Caption,
6+
configureFonts,
67
Headline,
8+
MD3LightTheme,
79
Paragraph,
10+
Provider,
811
Subheading,
9-
Text,
12+
customText,
1013
Title,
1114
} from 'react-native-paper';
1215

1316
import { useExampleTheme } from '..';
1417
import ScreenWrapper from '../ScreenWrapper';
1518

19+
const Text = customText<'customVariant'>();
20+
1621
const TextExample = () => {
1722
const { isV3 } = useExampleTheme();
23+
24+
const fontConfig = {
25+
customVariant: {
26+
fontFamily: Platform.select({
27+
ios: 'Noteworthy',
28+
default: 'serif',
29+
}),
30+
fontWeight: '400',
31+
letterSpacing: Platform.select({
32+
ios: 7,
33+
default: 4.6,
34+
}),
35+
lineHeight: 54,
36+
fontSize: 40,
37+
},
38+
} as const;
39+
40+
const theme = {
41+
...MD3LightTheme,
42+
fonts: configureFonts({ config: fontConfig }),
43+
};
1844
return (
1945
<ScreenWrapper>
2046
<View style={styles.container}>
@@ -79,6 +105,12 @@ const TextExample = () => {
79105
<Text style={styles.text} variant="bodySmall">
80106
Body Small
81107
</Text>
108+
109+
<Provider theme={theme}>
110+
<Text style={styles.text} variant="customVariant">
111+
Custom Variant
112+
</Text>
113+
</Provider>
82114
</>
83115
)}
84116
</View>

src/components/Appbar/AppbarContent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import color from 'color';
1515
import { useInternalTheme } from '../../core/theming';
1616
import { white } from '../../styles/themes/v2/colors';
1717
import type { $RemoveChildren, MD3TypescaleKey, ThemeProp } from '../../types';
18-
import Text from '../Typography/Text';
18+
import Text, { TextRef } from '../Typography/Text';
1919
import { modeTextVariant } from './utils';
2020

2121
type TitleString = {
@@ -39,7 +39,7 @@ export type Props = $RemoveChildren<typeof View> & {
3939
/**
4040
* Reference for the title.
4141
*/
42-
titleRef?: React.RefObject<Text>;
42+
titleRef?: React.RefObject<TextRef>;
4343
/**
4444
* @deprecated Deprecated in v5.x
4545
* Text for the subtitle.

src/components/Typography/AnimatedText.tsx

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import * as React from 'react';
22
import { Animated, I18nManager, StyleSheet, TextStyle } from 'react-native';
33

44
import { useInternalTheme } from '../../core/theming';
5-
import { Font, ThemeProp, MD3TypescaleKey } from '../../types';
5+
import type { ThemeProp } from '../../types';
6+
import type { VariantProp } from './types';
67

7-
type Props = React.ComponentPropsWithRef<typeof Animated.Text> & {
8+
type Props<T> = React.ComponentPropsWithRef<typeof Animated.Text> & {
89
/**
910
* Variant defines appropriate text styles for type role and its size.
1011
* Available variants:
@@ -19,7 +20,7 @@ type Props = React.ComponentPropsWithRef<typeof Animated.Text> & {
1920
*
2021
* Body: `bodyLarge`, `bodyMedium`, `bodySmall`
2122
*/
22-
variant?: keyof typeof MD3TypescaleKey;
23+
variant?: VariantProp<T>;
2324
style?: TextStyle;
2425
/**
2526
* @optional
@@ -37,44 +38,29 @@ function AnimatedText({
3738
theme: themeOverrides,
3839
variant,
3940
...rest
40-
}: Props) {
41+
}: Props<never>) {
4142
const theme = useInternalTheme(themeOverrides);
4243
const writingDirection = I18nManager.getConstants().isRTL ? 'rtl' : 'ltr';
4344

4445
if (theme.isV3 && variant) {
45-
const stylesByVariant = Object.keys(MD3TypescaleKey).reduce(
46-
(acc, key) => {
47-
const { fontSize, fontWeight, lineHeight, letterSpacing, fontFamily } =
48-
theme.fonts[key as keyof typeof MD3TypescaleKey];
49-
50-
return {
51-
...acc,
52-
[key]: {
53-
fontFamily,
54-
fontSize,
55-
fontWeight,
56-
lineHeight: lineHeight,
57-
letterSpacing,
58-
color: theme.colors.onSurface,
59-
},
60-
};
61-
},
62-
{} as {
63-
[key in MD3TypescaleKey]: {
64-
fontSize: number;
65-
fontWeight: Font['fontWeight'];
66-
lineHeight: number;
67-
letterSpacing: number;
68-
};
69-
}
70-
);
71-
72-
const styleForVariant = stylesByVariant[variant];
46+
const font = theme.fonts[variant];
47+
if (typeof font !== 'object') {
48+
throw new Error(
49+
`Variant ${variant} was not provided properly. Valid variants are ${Object.keys(
50+
theme.fonts
51+
).join(', ')}.`
52+
);
53+
}
7354

7455
return (
7556
<Animated.Text
7657
{...rest}
77-
style={[styleForVariant, styles.text, { writingDirection }, style]}
58+
style={[
59+
font,
60+
styles.text,
61+
{ writingDirection, color: theme.colors.onSurface },
62+
style,
63+
]}
7864
/>
7965
);
8066
} else {
@@ -105,4 +91,7 @@ const styles = StyleSheet.create({
10591
},
10692
});
10793

94+
export const customAnimatedText = <T,>() =>
95+
AnimatedText as (props: Props<T>) => JSX.Element;
96+
10897
export default AnimatedText;

src/components/Typography/Text.tsx

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import {
88
} from 'react-native';
99

1010
import { useInternalTheme } from '../../core/theming';
11-
import { Font, MD3TypescaleKey, ThemeProp } from '../../types';
11+
import type { ThemeProp } from '../../types';
1212
import { forwardRef } from '../../utils/forwardRef';
13+
import type { VariantProp } from './types';
1314

14-
export type Props = React.ComponentProps<typeof NativeText> & {
15+
export type Props<T> = React.ComponentProps<typeof NativeText> & {
1516
/**
1617
* @supported Available in v5.x with theme version 3
1718
*
@@ -28,12 +29,16 @@ export type Props = React.ComponentProps<typeof NativeText> & {
2829
*
2930
* Body: `bodyLarge`, `bodyMedium`, `bodySmall`
3031
*/
31-
variant?: keyof typeof MD3TypescaleKey;
32+
variant?: VariantProp<T>;
3233
children: React.ReactNode;
3334
theme?: ThemeProp;
3435
style?: StyleProp<TextStyle>;
3536
};
3637

38+
export type TextRef = React.ForwardedRef<{
39+
setNativeProps(args: Object): void;
40+
}>;
41+
3742
// @component-group Typography
3843

3944
/**
@@ -77,10 +82,9 @@ export type Props = React.ComponentProps<typeof NativeText> & {
7782
*
7883
* @extends Text props https://reactnative.dev/docs/text#props
7984
*/
80-
81-
const Text: React.ForwardRefRenderFunction<{}, Props> = (
82-
{ style, variant, theme: initialTheme, ...rest }: Props,
83-
ref
85+
const Text = (
86+
{ style, variant, theme: initialTheme, ...rest }: Props<string>,
87+
ref: TextRef
8488
) => {
8589
const root = React.useRef<NativeText | null>(null);
8690
// FIXME: destructure it in TS 4.6+
@@ -92,39 +96,24 @@ const Text: React.ForwardRefRenderFunction<{}, Props> = (
9296
}));
9397

9498
if (theme.isV3 && variant) {
95-
const stylesByVariant = Object.keys(MD3TypescaleKey).reduce(
96-
(acc, key) => {
97-
const { fontSize, fontWeight, lineHeight, letterSpacing, fontFamily } =
98-
theme.fonts[key as keyof typeof MD3TypescaleKey];
99-
100-
return {
101-
...acc,
102-
[key]: {
103-
fontFamily,
104-
fontSize,
105-
fontWeight,
106-
lineHeight,
107-
letterSpacing,
108-
color: theme.colors.onSurface,
109-
},
110-
};
111-
},
112-
{} as {
113-
[key in MD3TypescaleKey]: {
114-
fontSize: number;
115-
fontWeight: Font['fontWeight'];
116-
lineHeight: number;
117-
letterSpacing: number;
118-
};
119-
}
120-
);
121-
122-
const styleForVariant = stylesByVariant[variant];
99+
const font = theme.fonts[variant];
100+
if (typeof font !== 'object') {
101+
throw new Error(
102+
`Variant ${variant} was not provided properly. Valid variants are ${Object.keys(
103+
theme.fonts
104+
).join(', ')}.`
105+
);
106+
}
123107

124108
return (
125109
<NativeText
126110
ref={root}
127-
style={[styleForVariant, styles.text, { writingDirection }, style]}
111+
style={[
112+
font,
113+
styles.text,
114+
{ writingDirection, color: theme.colors.onSurface },
115+
style,
116+
]}
128117
{...rest}
129118
/>
130119
);
@@ -150,4 +139,12 @@ const styles = StyleSheet.create({
150139
},
151140
});
152141

153-
export default forwardRef(Text);
142+
type TextComponent<T> = (
143+
props: Props<T> & { ref?: React.RefObject<TextRef> }
144+
) => JSX.Element;
145+
146+
const Component = forwardRef(Text) as TextComponent<never>;
147+
148+
export const customText = <T,>() => Component as unknown as TextComponent<T>;
149+
150+
export default Component;

src/components/Typography/types.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { MD3TypescaleKey } from '../../types';
2+
3+
export type VariantProp<T> =
4+
| (T extends string ? (string extends T ? never : T) : never)
5+
| keyof typeof MD3TypescaleKey;

src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,6 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A
741741
style={
742742
Array [
743743
Object {
744-
"color": "rgba(28, 27, 31, 1)",
745744
"fontFamily": "System",
746745
"fontSize": 22,
747746
"fontWeight": "400",
@@ -752,6 +751,7 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A
752751
"textAlign": "left",
753752
},
754753
Object {
754+
"color": "rgba(28, 27, 31, 1)",
755755
"writingDirection": "ltr",
756756
},
757757
Array [

src/components/__tests__/Checkbox/__snapshots__/CheckboxItem.test.tsx.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@ exports[`can render leading checkbox control 1`] = `
129129
style={
130130
Array [
131131
Object {
132-
"color": "rgba(28, 27, 31, 1)",
133132
"fontFamily": "System",
134133
"fontSize": 16,
135134
"fontWeight": "400",
@@ -140,6 +139,7 @@ exports[`can render leading checkbox control 1`] = `
140139
"textAlign": "left",
141140
},
142141
Object {
142+
"color": "rgba(28, 27, 31, 1)",
143143
"writingDirection": "ltr",
144144
},
145145
Array [
@@ -213,7 +213,6 @@ exports[`can render the Android checkbox on different platforms 1`] = `
213213
style={
214214
Array [
215215
Object {
216-
"color": "rgba(28, 27, 31, 1)",
217216
"fontFamily": "System",
218217
"fontSize": 16,
219218
"fontWeight": "400",
@@ -224,6 +223,7 @@ exports[`can render the Android checkbox on different platforms 1`] = `
224223
"textAlign": "left",
225224
},
226225
Object {
226+
"color": "rgba(28, 27, 31, 1)",
227227
"writingDirection": "ltr",
228228
},
229229
Array [
@@ -412,7 +412,6 @@ exports[`can render the iOS checkbox on different platforms 1`] = `
412412
style={
413413
Array [
414414
Object {
415-
"color": "rgba(28, 27, 31, 1)",
416415
"fontFamily": "System",
417416
"fontSize": 16,
418417
"fontWeight": "400",
@@ -423,6 +422,7 @@ exports[`can render the iOS checkbox on different platforms 1`] = `
423422
"textAlign": "left",
424423
},
425424
Object {
425+
"color": "rgba(28, 27, 31, 1)",
426426
"writingDirection": "ltr",
427427
},
428428
Array [
@@ -575,7 +575,6 @@ exports[`renders unchecked 1`] = `
575575
style={
576576
Array [
577577
Object {
578-
"color": "rgba(28, 27, 31, 1)",
579578
"fontFamily": "System",
580579
"fontSize": 16,
581580
"fontWeight": "400",
@@ -586,6 +585,7 @@ exports[`renders unchecked 1`] = `
586585
"textAlign": "left",
587586
},
588587
Object {
588+
"color": "rgba(28, 27, 31, 1)",
589589
"writingDirection": "ltr",
590590
},
591591
Array [

0 commit comments

Comments
 (0)