From d6dde1abf733df12aaaa9729e8a6733f5719c715 Mon Sep 17 00:00:00 2001 From: Dimitar Nestorov Date: Tue, 28 Feb 2023 14:21:35 +0200 Subject: [PATCH 1/2] fix: allow for custom variants in `Text` --- docs/docs/guides/04-fonts.md | 12 ++- example/src/Examples/TextExample.tsx | 36 ++++++++- src/components/Appbar/AppbarContent.tsx | 4 +- src/components/Typography/AnimatedText.tsx | 55 ++++++-------- src/components/Typography/Text.tsx | 74 +++++++++---------- src/components/Typography/types.tsx | 5 ++ .../__tests__/Typography/Text.test.tsx | 43 ++++++++++- .../__snapshots__/Text.test.tsx.snap | 20 +++++ src/index.tsx | 10 ++- 9 files changed, 180 insertions(+), 79 deletions(-) create mode 100644 src/components/Typography/types.tsx diff --git a/docs/docs/guides/04-fonts.md b/docs/docs/guides/04-fonts.md index dd5067ede8..3430bfdf88 100644 --- a/docs/docs/guides/04-fonts.md +++ b/docs/docs/guides/04-fonts.md @@ -393,7 +393,6 @@ Platform.select({ #### Using `configureFonts` helper -
* 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 `variant` name as `config` into the params object: ```js @@ -428,7 +427,16 @@ export default function Main() { ); } ``` -
+ +If you're using TypeScript you will need to create a custom `Text` component which accepts your custom variants: + +```typescript +import { customText } from 'react-native-paper' + +// Use this instead of importing `Text` from `react-native-paper` +export const Text = customText<'customVariant'>() +``` + * In order to override one of the available `variant`'s font properties, pass the modified `fontConfig` under specific `variant` name as `config` into the params object: diff --git a/example/src/Examples/TextExample.tsx b/example/src/Examples/TextExample.tsx index dd3c8f3825..8a49f03638 100644 --- a/example/src/Examples/TextExample.tsx +++ b/example/src/Examples/TextExample.tsx @@ -1,20 +1,46 @@ import * as React from 'react'; -import { StyleSheet, View } from 'react-native'; +import { Platform, StyleSheet, View } from 'react-native'; import { Caption, + configureFonts, Headline, + MD3LightTheme, Paragraph, + Provider, Subheading, - Text, + customText, Title, } from 'react-native-paper'; import { useExampleTheme } from '..'; import ScreenWrapper from '../ScreenWrapper'; +const Text = customText<'customVariant'>(); + const TextExample = () => { const { isV3 } = useExampleTheme(); + + const fontConfig = { + customVariant: { + fontFamily: Platform.select({ + ios: 'Noteworthy', + default: 'serif', + }), + fontWeight: '400', + letterSpacing: Platform.select({ + ios: 7, + default: 4.6, + }), + lineHeight: 54, + fontSize: 40, + }, + } as const; + + const theme = { + ...MD3LightTheme, + fonts: configureFonts({ config: fontConfig }), + }; return ( @@ -79,6 +105,12 @@ const TextExample = () => { Body Small + + + + Custom Variant + + )} diff --git a/src/components/Appbar/AppbarContent.tsx b/src/components/Appbar/AppbarContent.tsx index e89a370dab..73ad08c76e 100644 --- a/src/components/Appbar/AppbarContent.tsx +++ b/src/components/Appbar/AppbarContent.tsx @@ -15,7 +15,7 @@ import color from 'color'; import { useInternalTheme } from '../../core/theming'; import { white } from '../../styles/themes/v2/colors'; import type { $RemoveChildren, MD3TypescaleKey, ThemeProp } from '../../types'; -import Text from '../Typography/Text'; +import Text, { TextRef } from '../Typography/Text'; import { modeTextVariant } from './utils'; type TitleString = { @@ -39,7 +39,7 @@ export type Props = $RemoveChildren & { /** * Reference for the title. */ - titleRef?: React.RefObject; + titleRef?: React.RefObject; /** * @deprecated Deprecated in v5.x * Text for the subtitle. diff --git a/src/components/Typography/AnimatedText.tsx b/src/components/Typography/AnimatedText.tsx index aa22d80d72..b1b36b596a 100644 --- a/src/components/Typography/AnimatedText.tsx +++ b/src/components/Typography/AnimatedText.tsx @@ -2,9 +2,10 @@ import * as React from 'react'; import { Animated, I18nManager, StyleSheet, TextStyle } from 'react-native'; import { useInternalTheme } from '../../core/theming'; -import { Font, ThemeProp, MD3TypescaleKey } from '../../types'; +import type { ThemeProp } from '../../types'; +import type { VariantProp } from './types'; -type Props = React.ComponentPropsWithRef & { +type Props = React.ComponentPropsWithRef & { /** * Variant defines appropriate text styles for type role and its size. * Available variants: @@ -19,7 +20,7 @@ type Props = React.ComponentPropsWithRef & { * * Body: `bodyLarge`, `bodyMedium`, `bodySmall` */ - variant?: keyof typeof MD3TypescaleKey; + variant?: VariantProp; style?: TextStyle; /** * @optional @@ -37,44 +38,29 @@ function AnimatedText({ theme: themeOverrides, variant, ...rest -}: Props) { +}: Props) { const theme = useInternalTheme(themeOverrides); const writingDirection = I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'; if (theme.isV3 && variant) { - const stylesByVariant = Object.keys(MD3TypescaleKey).reduce( - (acc, key) => { - const { fontSize, fontWeight, lineHeight, letterSpacing, fontFamily } = - theme.fonts[key as keyof typeof MD3TypescaleKey]; - - return { - ...acc, - [key]: { - fontFamily, - fontSize, - fontWeight, - lineHeight: lineHeight, - letterSpacing, - color: theme.colors.onSurface, - }, - }; - }, - {} as { - [key in MD3TypescaleKey]: { - fontSize: number; - fontWeight: Font['fontWeight']; - lineHeight: number; - letterSpacing: number; - }; - } - ); - - const styleForVariant = stylesByVariant[variant]; + const font = theme.fonts[variant]; + if (typeof font !== 'object') { + throw new Error( + `Variant ${variant} was not provided properly. Valid variants are ${Object.keys( + theme.fonts + ).join(', ')}.` + ); + } return ( ); } else { @@ -105,4 +91,7 @@ const styles = StyleSheet.create({ }, }); +export const customAnimatedText = () => + AnimatedText as (props: Props) => JSX.Element; + export default AnimatedText; diff --git a/src/components/Typography/Text.tsx b/src/components/Typography/Text.tsx index 01f9403b4b..a2357dd323 100644 --- a/src/components/Typography/Text.tsx +++ b/src/components/Typography/Text.tsx @@ -8,10 +8,11 @@ import { } from 'react-native'; import { useInternalTheme } from '../../core/theming'; -import { Font, MD3TypescaleKey, ThemeProp } from '../../types'; +import type { ThemeProp } from '../../types'; import { forwardRef } from '../../utils/forwardRef'; +import type { VariantProp } from './types'; -export type Props = React.ComponentProps & { +export type Props = React.ComponentProps & { /** * @supported Available in v5.x with theme version 3 * @@ -28,12 +29,16 @@ export type Props = React.ComponentProps & { * * Body: `bodyLarge`, `bodyMedium`, `bodySmall` */ - variant?: keyof typeof MD3TypescaleKey; + variant?: VariantProp; children: React.ReactNode; theme?: ThemeProp; style?: StyleProp; }; +export type TextRef = React.ForwardedRef<{ + setNativeProps(args: Object): void; +}>; + // @component-group Typography /** @@ -77,10 +82,9 @@ export type Props = React.ComponentProps & { * * @extends Text props https://reactnative.dev/docs/text#props */ - -const Text: React.ForwardRefRenderFunction<{}, Props> = ( - { style, variant, theme: initialTheme, ...rest }: Props, - ref +const Text = ( + { style, variant, theme: initialTheme, ...rest }: Props, + ref: TextRef ) => { const root = React.useRef(null); // FIXME: destructure it in TS 4.6+ @@ -92,39 +96,27 @@ const Text: React.ForwardRefRenderFunction<{}, Props> = ( })); if (theme.isV3 && variant) { - const stylesByVariant = Object.keys(MD3TypescaleKey).reduce( - (acc, key) => { - const { fontSize, fontWeight, lineHeight, letterSpacing, fontFamily } = - theme.fonts[key as keyof typeof MD3TypescaleKey]; - - return { - ...acc, - [key]: { - fontFamily, - fontSize, - fontWeight, - lineHeight, - letterSpacing, - color: theme.colors.onSurface, - }, - }; - }, - {} as { - [key in MD3TypescaleKey]: { - fontSize: number; - fontWeight: Font['fontWeight']; - lineHeight: number; - letterSpacing: number; - }; - } - ); - - const styleForVariant = stylesByVariant[variant]; + const font = theme.fonts[variant]; + if (typeof font !== 'object') { + throw new Error( + `Variant ${variant} was not provided properly. Valid variants are ${Object.keys( + theme.fonts + ).join(', ')}.` + ); + } return ( ); @@ -150,4 +142,12 @@ const styles = StyleSheet.create({ }, }); -export default forwardRef(Text); +type TextComponent = ( + props: Props & { ref?: React.RefObject } +) => JSX.Element; + +const Component = forwardRef(Text) as TextComponent; + +export const customText = () => Component as unknown as TextComponent; + +export default Component; diff --git a/src/components/Typography/types.tsx b/src/components/Typography/types.tsx new file mode 100644 index 0000000000..61a6dc7c1e --- /dev/null +++ b/src/components/Typography/types.tsx @@ -0,0 +1,5 @@ +import type { MD3TypescaleKey } from '../../types'; + +export type VariantProp = + | (T extends string ? (string extends T ? never : T) : never) + | keyof typeof MD3TypescaleKey; diff --git a/src/components/__tests__/Typography/Text.test.tsx b/src/components/__tests__/Typography/Text.test.tsx index 0939e6af79..c349c4e8f2 100644 --- a/src/components/__tests__/Typography/Text.test.tsx +++ b/src/components/__tests__/Typography/Text.test.tsx @@ -3,8 +3,11 @@ import * as React from 'react'; import { render } from '@testing-library/react-native'; import renderer from 'react-test-renderer'; +import Provider from '../../../core/Provider'; +import configureFonts from '../../../styles/fonts'; +import { MD3LightTheme } from '../../../styles/themes'; import { tokens } from '../../../styles/themes/v3/tokens'; -import Text from '../../Typography/Text'; +import Text, { customText } from '../../Typography/Text'; const content = 'Something rendered as a child content'; @@ -49,3 +52,41 @@ it('renders v3 Text component without variant with default fontWeight and fontFa fontWeight: weightRegular, }); }); + +it('renders v3 Text component with custom variant correctly', () => { + const fontConfig = { + customVariant: { + fontFamily: 'Montserrat-Regular', + fontWeight: '400', + letterSpacing: 0.51, + lineHeight: 54.1, + fontSize: 41, + }, + } as const; + + const theme = { + ...MD3LightTheme, + fonts: configureFonts({ config: fontConfig }), + }; + const Text = customText<'customVariant'>(); + const { getByTestId } = render( + + + {content} + + + ); + + expect(getByTestId('text-with-custom-variant').props.style).toMatchSnapshot(); +}); + +it('throws when custom variant not provided', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const Text = customText<'myCustomVariant'>(); + expect(() => + render({content}) + ).toThrow(/myCustomVariant was not provided/); + + jest.clearAllMocks(); +}); diff --git a/src/components/__tests__/Typography/__snapshots__/Text.test.tsx.snap b/src/components/__tests__/Typography/__snapshots__/Text.test.tsx.snap index d744a11961..79933c6847 100644 --- a/src/components/__tests__/Typography/__snapshots__/Text.test.tsx.snap +++ b/src/components/__tests__/Typography/__snapshots__/Text.test.tsx.snap @@ -349,3 +349,23 @@ Array [ , ] `; + +exports[`renders v3 Text component with custom variant correctly 1`] = ` +Array [ + Object { + "color": "rgba(28, 27, 31, 1)", + "fontFamily": "Montserrat-Regular", + "fontSize": 41, + "fontWeight": "400", + "letterSpacing": 0.51, + "lineHeight": 54.1, + }, + Object { + "textAlign": "left", + }, + Object { + "writingDirection": "ltr", + }, + undefined, +] +`; diff --git a/src/index.tsx b/src/index.tsx index da07c7794f..1f97921563 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -63,7 +63,7 @@ export { Subheading, Title, } from './components/Typography/v2'; -export { default as Text } from './components/Typography/Text'; +export { default as Text, customText } from './components/Typography/Text'; // Types export type { Props as ActivityIndicatorProps } from './components/ActivityIndicator'; @@ -149,4 +149,10 @@ export type { Props as SegmentedButtonsProps } from './components/SegmentedButto export type { Props as ListImageProps } from './components/List/ListImage'; export type { Props as TooltipProps } from './components/Tooltip/Tooltip'; -export type { MD2Theme, MD3Theme, ThemeBase, MD3Elevation } from './types'; +export type { + MD2Theme, + MD3Theme, + ThemeBase, + MD3Elevation, + MD3TypescaleKey, +} from './types'; From 66e403b3367f8586a9d776bf54f75253d3f22fb8 Mon Sep 17 00:00:00 2001 From: Dimitar Nestorov Date: Tue, 28 Feb 2023 14:21:36 +0200 Subject: [PATCH 2/2] refactor: avoid spreading in `Text` styles --- src/components/Typography/AnimatedText.tsx | 4 +- src/components/Typography/Text.tsx | 7 +- .../Appbar/__snapshots__/Appbar.test.tsx.snap | 2 +- .../__snapshots__/CheckboxItem.test.tsx.snap | 8 +-- .../RadioButtonItem.test.tsx.snap | 8 +-- .../__snapshots__/Text.test.tsx.snap | 32 ++++----- .../__snapshots__/Banner.test.tsx.snap | 8 +-- .../BottomNavigation.test.tsx.snap | 70 +++++++++---------- .../__snapshots__/Button.test.tsx.snap | 26 +++---- .../__snapshots__/Chip.test.tsx.snap | 12 ++-- .../__snapshots__/DataTable.test.tsx.snap | 2 +- .../__snapshots__/DrawerItem.test.tsx.snap | 6 +- .../__tests__/__snapshots__/FAB.test.tsx.snap | 4 +- .../__snapshots__/ListItem.test.tsx.snap | 2 +- .../__snapshots__/ListSection.test.tsx.snap | 4 +- .../__snapshots__/Menu.test.tsx.snap | 14 ++-- .../__snapshots__/MenuItem.test.tsx.snap | 10 +-- .../SegmentedButton.test.tsx.snap | 12 ++-- .../__snapshots__/Snackbar.test.tsx.snap | 6 +- 19 files changed, 117 insertions(+), 120 deletions(-) diff --git a/src/components/Typography/AnimatedText.tsx b/src/components/Typography/AnimatedText.tsx index b1b36b596a..f662103ec9 100644 --- a/src/components/Typography/AnimatedText.tsx +++ b/src/components/Typography/AnimatedText.tsx @@ -56,9 +56,9 @@ function AnimatedText({ diff --git a/src/components/Typography/Text.tsx b/src/components/Typography/Text.tsx index a2357dd323..0a16760912 100644 --- a/src/components/Typography/Text.tsx +++ b/src/components/Typography/Text.tsx @@ -109,12 +109,9 @@ const Text = (