From 116d3c988c6df0e16a6029824ec512e002a1ddc8 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Thu, 10 Sep 2026 13:00:08 +0200 Subject: [PATCH] refactor: remove splitStyles and StyleSheet.flatten --- docs/6.x/docs/guides/migration.md | 81 +++++++++++- eslint.config.mjs | 12 ++ example/src/Examples/AppbarExample.tsx | 9 +- example/src/Examples/AvatarExample.tsx | 16 +-- example/src/Examples/ButtonExample.tsx | 13 +- src/components/Appbar/Appbar.tsx | 116 ++++++++-------- src/components/Appbar/AppbarHeader.tsx | 45 +++---- src/components/Appbar/utils.ts | 31 ----- src/components/Avatar/AvatarIcon.tsx | 19 ++- src/components/Avatar/AvatarImage.tsx | 17 ++- src/components/Avatar/AvatarText.tsx | 22 ++- .../BottomNavigation/BottomNavigationBar.tsx | 14 +- src/components/Button/Button.tsx | 71 +++++----- src/components/Button/utils.tsx | 36 ++--- src/components/Card/Card.tsx | 7 +- src/components/Card/CardCover.tsx | 16 +-- src/components/Card/utils.tsx | 14 -- .../DataTable/DataTablePagination.tsx | 5 +- src/components/Surface.tsx | 2 +- src/components/Tooltip/Tooltip.tsx | 58 +++++--- src/components/Tooltip/utils.ts | 57 ++------ .../__tests__/Appbar/Appbar.test.tsx | 69 +++++----- .../Appbar/__snapshots__/Appbar.test.tsx.snap | 12 +- src/components/__tests__/Avatar.test.tsx | 12 +- src/components/__tests__/Button.test.tsx | 10 +- src/components/__tests__/Card/Card.test.tsx | 28 +--- .../Card/__snapshots__/Card.test.tsx.snap | 125 ------------------ src/components/__tests__/Tooltip.test.tsx | 10 ++ .../__snapshots__/Avatar.test.tsx.snap | 12 +- .../BottomNavigation.test.tsx.snap | 41 ++---- .../__snapshots__/Button.test.tsx.snap | 6 +- .../__snapshots__/DataTable.test.tsx.snap | 6 +- src/utils/__tests__/splitStyles.test.ts | 86 ------------ src/utils/splitStyles.ts | 62 --------- 34 files changed, 416 insertions(+), 724 deletions(-) delete mode 100644 src/utils/__tests__/splitStyles.test.ts delete mode 100644 src/utils/splitStyles.ts diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md index 35a8f74830..cf226ba980 100644 --- a/docs/6.x/docs/guides/migration.md +++ b/docs/6.x/docs/guides/migration.md @@ -10,6 +10,7 @@ React Native Paper 6 uses [Reanimated](https://docs.swmansion.com/react-native-r The following props now accept animated styles returned from `useAnimatedStyle`. They no longer accept `Animated.Value` or `Animated.AnimatedInterpolation` where these were previously supported: +- `Appbar` and `Appbar.Header`: `style` - `Appbar.Action` and `Appbar.BackAction`: `style` - `Badge`: `style` - `Banner`: `style` @@ -58,6 +59,8 @@ You can use an elevation level from `0` to `5` instead. Changes to the elevation The following component style props no longer support overriding their background color or border radius: +- `Appbar` and `Appbar.Header` +- `Avatar.Icon`, `Avatar.Image` and `Avatar.Text`: background color - `Banner` - `Button` - `Card` @@ -67,7 +70,12 @@ The following component style props no longer support overriding their backgroun - `Searchbar` - `Snackbar` -You can use the component's color prop where available, or override the corresponding theme colors. +You can use the component's color prop where available, or override the corresponding theme colors. See the component sections below for details. + +Components no longer read values from the `style` prop to derive other styles, since this is not possible with animated styles. The affected props have been replaced with dedicated props: + +- `Button`: `iconPosition` replaces `contentStyle={{ flexDirection: 'row-reverse' }}` +- `Card`: the outline color is controlled by `theme.colors.outline` instead of `style.borderColor` ### Test IDs @@ -111,9 +119,76 @@ Some components now accept explicit `testID` props for their interactable elemen ### Appbar -The `style` props for `Appbar` and `Appbar.Header` no longer accept `Animated.Value` or `Animated.AnimatedInterpolation`. They only accept static styles. +- The `style` props for `Appbar` and `Appbar.Header` no longer configure the background color or border radius. Use the `backgroundColor` prop and the border radius props (`borderRadius`, `borderTopLeftRadius`, `borderCurve` etc.) instead. +- The `style.elevation` property is no longer supported. Use the `elevated` prop to control Appbar elevation. +- The default height of `Appbar` now includes the `safeAreaInsets`, so the content area keeps the height of the selected `mode`. +- The `height` specified in `style` for `Appbar.Header` now includes the status bar height. Previously, the status bar height was added to it automatically. + +e.g.: + +```diff + + + +``` + +### Avatar + +The `style` prop for `Avatar.Icon`, `Avatar.Image` and `Avatar.Text` no longer configures the background color. Use the `backgroundColor` prop instead. The text or icon color is still derived from the background color unless `color` is specified. + +e.g.: + +```diff + +``` + +### Button + +- The `iconPosition` prop controls the placement of the icon. Use `iconPosition="trailing"` to display the icon after the label instead of `contentStyle={{ flexDirection: 'row-reverse' }}`. +- The `color` and `fontSize` in `labelStyle` no longer apply to the icon and loading indicator. Use the `textColor` prop to customize the icon color along with the label color. + +e.g.: + +```diff + +``` + +### Card + +- The `borderColor` in `style` no longer changes the outline color in `outlined` mode. Override `theme.colors.outline` using the `theme` prop instead. +- `Card.Cover` no longer applies the border radius from `style` to the image directly. The image is clipped by the container, so any border radius passed in `style` still applies. + +e.g.: + +```diff + + ... + +``` + +### Tooltip -The `style.elevation` property is no longer supported. Use the `elevated` prop to control Appbar elevation. +`Tooltip` now passes a `ref` to the wrapped element to measure its position on the screen. When wrapping a custom component, make sure it forwards the `ref` to its root view. Otherwise the tooltip falls back to measuring its own wrapper. ### Surface diff --git a/eslint.config.mjs b/eslint.config.mjs index ac6e65951f..d415994fde 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -119,6 +119,16 @@ export default defineConfig( }, ], + 'no-restricted-properties': [ + 'error', + { + object: 'StyleSheet', + property: 'flatten', + message: + 'Do not flatten styles. Animated styles from `useAnimatedStyle` cannot be flattened. Accept the values you need as props instead.', + }, + ], + '@react-native/platform-colors': 'error', 'promise/no-callback-in-promise': 'error', @@ -174,6 +184,8 @@ export default defineConfig( rules: { ...testingLibraryReact.rules, + 'no-restricted-properties': 'off', + 'no-restricted-syntax': [ 'error', { diff --git a/example/src/Examples/AppbarExample.tsx b/example/src/Examples/AppbarExample.tsx index 2d4eb0bbc4..08bbbcbc1b 100644 --- a/example/src/Examples/AppbarExample.tsx +++ b/example/src/Examples/AppbarExample.tsx @@ -45,7 +45,7 @@ const AppbarExample = () => { navigation.setOptions({ header: () => ( @@ -169,10 +169,8 @@ const AppbarExample = () => { { height: height + bottom, }, - { - backgroundColor: theme.colors.surfaceContainerHigh, - }, ]} + backgroundColor={theme.colors.surfaceContainerHigh} safeAreaInsets={{ bottom, left, right }} > {}} /> @@ -217,7 +215,4 @@ const styles = StyleSheet.create({ position: 'absolute', right: 16, }, - customColor: { - backgroundColor: Palette.secondary80, - }, }); diff --git a/example/src/Examples/AvatarExample.tsx b/example/src/Examples/AvatarExample.tsx index 6d6349cbba..03a7576337 100644 --- a/example/src/Examples/AvatarExample.tsx +++ b/example/src/Examples/AvatarExample.tsx @@ -10,12 +10,8 @@ const AvatarExample = () => { @@ -26,12 +22,8 @@ const AvatarExample = () => { diff --git a/example/src/Examples/ButtonExample.tsx b/example/src/Examples/ButtonExample.tsx index 2a164e8e77..9a048c4559 100644 --- a/example/src/Examples/ButtonExample.tsx +++ b/example/src/Examples/ButtonExample.tsx @@ -32,7 +32,7 @@ const ButtonExample = () => { icon="camera" onPress={() => {}} style={styles.button} - contentStyle={styles.flexReverse} + iconPosition="trailing" > Icon right @@ -84,7 +84,7 @@ const ButtonExample = () => { icon="camera" onPress={() => {}} style={styles.button} - contentStyle={styles.flexReverse} + iconPosition="trailing" > Icon right @@ -132,7 +132,7 @@ const ButtonExample = () => { icon="camera" onPress={() => {}} style={styles.button} - contentStyle={styles.flexReverse} + iconPosition="trailing" > Icon right @@ -180,7 +180,7 @@ const ButtonExample = () => { icon="camera" onPress={() => {}} style={styles.button} - contentStyle={styles.flexReverse} + iconPosition="trailing" > Icon right @@ -228,7 +228,7 @@ const ButtonExample = () => { icon="camera" onPress={() => {}} style={styles.button} - contentStyle={styles.flexReverse} + iconPosition="trailing" > Icon right @@ -366,9 +366,6 @@ const styles = StyleSheet.create({ button: { margin: 4, }, - flexReverse: { - flexDirection: 'row-reverse', - }, md3FontStyles: { lineHeight: 32, }, diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx index a324a61d5c..df41fa07bc 100644 --- a/src/components/Appbar/Appbar.tsx +++ b/src/components/Appbar/Appbar.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; -import type { ColorValue, StyleProp, ViewProps, ViewStyle } from 'react-native'; +import type { ColorValue, StyleProp, ViewProps } from 'react-native'; import AppbarContent from './AppbarContent'; import { getAppbarBackgroundColor, - getAppbarBorders, modeAppbarHeight, renderAppbarContent, filterAppbarActions, @@ -14,50 +13,61 @@ import type { AppbarModes, AppbarChildProps } from './utils'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../theme/types'; import Surface from '../Surface'; +import type { SurfaceStyle, SurfaceVisualProps } from '../Surface'; const APPBAR_HORIZONTAL_PADDING = 4; -export type AppbarStyle = Omit; +export type AppbarStyle = SurfaceStyle; -export type Props = Omit, 'style'> & { - /** - * Whether the background color is a dark color. A dark appbar will render light text and vice-versa. - */ - dark?: boolean; - /** - * Content of the `Appbar`. - */ - children: React.ReactNode; - /** - * @supported Available in v5.x with theme version 3 - * - * Mode of the Appbar. - * - `small` - Appbar with default height (64). - * - `medium` - Appbar with medium height (112). - * - `large` - Appbar with large height (152). - * - `center-aligned` - Appbar with default height and center-aligned title. - */ - mode?: 'small' | 'medium' | 'large' | 'center-aligned'; - /** - * @supported Available in v5.x with theme version 3 - * Whether Appbar background should have the elevation along with primary color pigment. - */ - elevated?: boolean; - /** - * Safe area insets for the Appbar. This can be used to avoid elements like the navigation bar on Android and bottom safe area on iOS. - */ - safeAreaInsets?: { - bottom?: number; - top?: number; - left?: number; - right?: number; +export type Props = Omit, 'style'> & + Omit & { + /** + * Whether the background color is a dark color. A dark appbar will render light text and vice-versa. + */ + dark?: boolean; + /** + * Background color of the Appbar. Overrides the color derived from the `elevated` prop. + */ + backgroundColor?: ColorValue; + /** + * Content of the `Appbar`. + */ + children: React.ReactNode; + /** + * @supported Available in v5.x with theme version 3 + * + * Mode of the Appbar. + * - `small` - Appbar with default height (64). + * - `medium` - Appbar with medium height (112). + * - `large` - Appbar with large height (152). + * - `center-aligned` - Appbar with default height and center-aligned title. + */ + mode?: 'small' | 'medium' | 'large' | 'center-aligned'; + /** + * @supported Available in v5.x with theme version 3 + * Whether Appbar background should have the elevation along with primary color pigment. + */ + elevated?: boolean; + /** + * Safe area insets for the Appbar. This can be used to avoid elements like the navigation bar on Android and bottom safe area on iOS. + */ + safeAreaInsets?: { + bottom?: number; + top?: number; + left?: number; + right?: number; + }; + /** + * @optional + */ + theme?: ThemeProp; + /** + * Style of the Appbar. + * + * Background color and border radius should be specified via props instead. + */ + style?: StyleProp; }; - /** - * @optional - */ - theme?: ThemeProp; - style?: StyleProp; -}; /** * A component to display action items in a bar. It can be placed at the top or bottom. @@ -153,14 +163,10 @@ const Appbar = ({ elevated = false, safeAreaInsets, theme: themeOverrides, + backgroundColor: customBackground, ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); - const flattenedStyle = StyleSheet.flatten(style); - const { backgroundColor: customBackground, ...restStyle } = (flattenedStyle || - {}) as Exclude & { - backgroundColor?: ColorValue; - }; const backgroundColor = getAppbarBackgroundColor( theme, @@ -168,8 +174,6 @@ const Appbar = ({ customBackground ); - const borderStyles = getAppbarBorders(restStyle); - const isMode = (modeToCompare: AppbarModes) => { return mode === modeToCompare; }; @@ -215,18 +219,16 @@ const Appbar = ({ paddingRight: (safeAreaInsets?.right ?? 0) + APPBAR_HORIZONTAL_PADDING, }; + // The safe area insets are applied as padding, so they need to be included in the height + const height = + modeAppbarHeight[mode] + + (safeAreaInsets?.top ?? 0) + + (safeAreaInsets?.bottom ?? 0); + return ( & { * @optional */ theme?: ThemeProp; + /** + * Style of the header. + * + * Background color and border radius should be specified via props instead. + */ style?: StyleProp; }; @@ -90,40 +94,14 @@ const AppbarHeader = ({ }: Props) => { const theme = useInternalTheme(themeOverrides); - const flattenedStyle = StyleSheet.flatten(style); - const { - height = modeAppbarHeight[mode], - backgroundColor: customBackground, - zIndex = elevated ? 1 : 0, - ...restStyle - } = (flattenedStyle || {}) as Exclude & { - height?: AppbarStyle['height']; - backgroundColor?: ColorValue; - zIndex?: number; - }; - - const backgroundColor = getAppbarBackgroundColor( - theme, - elevated, - customBackground - ); - const { top, left, right } = useSafeAreaInsets(); const topInset = statusBarHeight ?? top; const horizontalInset = Math.max(left, right); - const headerHeight = typeof height === 'number' ? height + topInset : height; return ( ; }; -const borderStyleProperties = [ - 'borderRadius', - 'borderBottomEndRadius', - 'borderBottomStartRadius', - 'borderEndEndRadius', - 'borderEndStartRadius', - 'borderStartEndRadius', - 'borderStartStartRadius', - 'borderTopEndRadius', - 'borderTopStartRadius', - 'borderTopLeftRadius', - 'borderTopRightRadius', - 'borderBottomRightRadius', - 'borderBottomLeftRadius', - 'borderCurve', -] satisfies readonly (keyof ViewStyle)[]; - export const getAppbarBackgroundColor = ( theme: InternalTheme, elevated: boolean, @@ -62,20 +45,6 @@ export const getAppbarColor = ({ return undefined; }; -export const getAppbarBorders = (style: ViewStyle) => { - let borders: ViewStyle = {}; - - for (const property of borderStyleProperties) { - const value = style[property]; - - if (typeof value === 'number' || typeof value === 'string') { - borders = { ...borders, [property]: value }; - } - } - - return borders; -}; - type BaseProps = { isDark: boolean; }; diff --git a/src/components/Avatar/AvatarIcon.tsx b/src/components/Avatar/AvatarIcon.tsx index 2d2f08760b..42c0747eff 100644 --- a/src/components/Avatar/AvatarIcon.tsx +++ b/src/components/Avatar/AvatarIcon.tsx @@ -1,5 +1,5 @@ import { StyleSheet, View } from 'react-native'; -import type { StyleProp, ViewProps, ViewStyle } from 'react-native'; +import type { ColorValue, StyleProp, ViewProps, ViewStyle } from 'react-native'; import { useInternalTheme } from '../../core/theming'; import { white } from '../../theme/colors'; @@ -23,7 +23,16 @@ export type Props = ViewProps & { * Custom color for the icon. */ color?: string; - style?: StyleProp; + /** + * Background color of the avatar. + */ + backgroundColor?: ColorValue; + /** + * Style for the icon container. + * + * Background color should be specified via the `backgroundColor` prop instead. + */ + style?: StyleProp>; /** * @optional */ @@ -47,12 +56,12 @@ const Avatar = ({ icon, size = defaultSize, style, + backgroundColor: customBackgroundColor, theme: themeOverrides, ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); - const { backgroundColor = theme.colors?.primary, ...restStyle } = - StyleSheet.flatten(style) || {}; + const backgroundColor = customBackgroundColor ?? theme.colors.primary; const textColor = rest.color ?? getContrastingColor(backgroundColor, white, 'rgba(0, 0, 0, .54)'); @@ -67,7 +76,7 @@ const Avatar = ({ backgroundColor, }, styles.container, - restStyle, + style, ]} {...rest} > diff --git a/src/components/Avatar/AvatarImage.tsx b/src/components/Avatar/AvatarImage.tsx index e8b6e501b1..4407420bd2 100644 --- a/src/components/Avatar/AvatarImage.tsx +++ b/src/components/Avatar/AvatarImage.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import { Image, StyleSheet, View } from 'react-native'; +import { Image, View } from 'react-native'; import type { + ColorValue, ImageProps, ImageSourcePropType, StyleProp, @@ -28,7 +29,16 @@ export type Props = ViewProps & { * Size of the avatar. */ size?: number; - style?: StyleProp; + /** + * Background color of the avatar. + */ + backgroundColor?: ColorValue; + /** + * Style for the image container. + * + * Background color should be specified via the `backgroundColor` prop instead. + */ + style?: StyleProp>; /** * Invoked on load error. */ @@ -83,12 +93,13 @@ const AvatarImage = ({ onLoadEnd, onLoadStart, onProgress, + backgroundColor: customBackgroundColor, theme: themeOverrides, testID, ...rest }: Props) => { const { colors } = useInternalTheme(themeOverrides); - const { backgroundColor = colors?.primary } = StyleSheet.flatten(style) || {}; + const backgroundColor = customBackgroundColor ?? colors.primary; return ( ; + style?: StyleProp>; /** * Style for the title. */ @@ -59,13 +71,13 @@ const AvatarText = ({ style, labelStyle, color: customColor, + backgroundColor: customBackgroundColor, theme: themeOverrides, maxFontSizeMultiplier, ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); - const { backgroundColor = theme.colors?.primary, ...restStyle } = - StyleSheet.flatten(style) || {}; + const backgroundColor = customBackgroundColor ?? theme.colors.primary; const textColor = customColor ?? getContrastingColor(backgroundColor, white, 'rgba(0, 0, 0, .54)'); @@ -81,7 +93,7 @@ const AvatarText = ({ backgroundColor, }, styles.container, - restStyle, + style, ]} {...rest} > diff --git a/src/components/BottomNavigation/BottomNavigationBar.tsx b/src/components/BottomNavigation/BottomNavigationBar.tsx index c53545b39a..63aaf74df9 100644 --- a/src/components/BottomNavigation/BottomNavigationBar.tsx +++ b/src/components/BottomNavigation/BottomNavigationBar.tsx @@ -433,15 +433,6 @@ const BottomNavigationBar = ({ const { routes } = navigationState; - const { - backgroundColor: customBackground, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - } = (StyleSheet.flatten(style) || {}) as { - backgroundColor?: ColorValue; - }; - - const backgroundColor = customBackground || colors.surfaceContainer; - const activeTintColor = getActiveTintColor({ activeColor, theme, @@ -472,6 +463,7 @@ const BottomNavigationBar = ({ testID={testID} style={[ styles.bar, + { backgroundColor: colors.surfaceContainer }, keyboardHidesNavigationBar // eslint-disable-next-line react-native/no-inline-styles ? { // When the keyboard is shown, slide down the navigation bar @@ -493,7 +485,7 @@ const BottomNavigationBar = ({ ]} onLayout={onLayout} > - + ({ }); })} - + ); }; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 82b327dc74..831bd15c27 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -61,6 +61,12 @@ export type Props = Omit & { * Icon to display for the `Button`. */ icon?: IconSource; + /** + * Position of the icon relative to the label. + * - `leading` - icon is displayed before the label (default). + * - `trailing` - icon is displayed after the label. + */ + iconPosition?: 'leading' | 'trailing'; /** * Whether the button is disabled. A disabled button is greyed out and `onPress` is not called on touch. */ @@ -112,7 +118,7 @@ export type Props = Omit & { delayLongPress?: number; /** * Style of button's inner content. - * Use this prop to apply custom height and width, to set a custom padding or to set the icon on the right with `flexDirection: 'row-reverse'`. + * Use this prop to apply custom height and width or to set a custom padding. */ contentStyle?: StyleProp; /** @@ -167,6 +173,7 @@ const Button = ({ dark, loading, icon, + iconPosition = 'leading', buttonColor: customButtonColor, textColor: customTextColor, children, @@ -263,9 +270,6 @@ const Button = ({ const touchableStyle = { borderRadius }; - const { color: customLabelColor, fontSize: customLabelSize } = - StyleSheet.flatten(labelStyle) || {}; - const font = theme.fonts.labelLarge; const textStyle = { @@ -273,20 +277,20 @@ const Button = ({ ...font, }; - const iconStyle = - StyleSheet.flatten(contentStyle)?.flexDirection === 'row-reverse' - ? [ - styles.iconReverse, - styles[`md3IconReverse${compact ? 'Compact' : ''}`], - isMode('text') && - styles[`md3IconReverseTextMode${compact ? 'Compact' : ''}`], - ] - : [ - styles.icon, - styles[`md3Icon${compact ? 'Compact' : ''}`], - isMode('text') && - styles[`md3IconTextMode${compact ? 'Compact' : ''}`], - ]; + const isIconTrailing = iconPosition === 'trailing'; + + const iconStyle = isIconTrailing + ? [ + styles.iconReverse, + styles[`md3IconReverse${compact ? 'Compact' : ''}`], + isMode('text') && + styles[`md3IconReverseTextMode${compact ? 'Compact' : ''}`], + ] + : [ + styles.icon, + styles[`md3Icon${compact ? 'Compact' : ''}`], + isMode('text') && styles[`md3IconTextMode${compact ? 'Compact' : ''}`], + ]; return ( - + {icon && loading !== true ? ( - + ) : null} {loading ? ( ) : null} @@ -403,6 +401,11 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, + contentReverse: { + flexDirection: 'row-reverse', + alignItems: 'center', + justifyContent: 'center', + }, icon: { marginLeft: 12, marginRight: -4, diff --git a/src/components/Button/utils.tsx b/src/components/Button/utils.tsx index ec3503c82b..1b09c53cab 100644 --- a/src/components/Button/utils.tsx +++ b/src/components/Button/utils.tsx @@ -3,7 +3,6 @@ import type { ColorValue, ViewStyle } from 'react-native'; import { black, white } from '../../theme/colors'; import { tokens } from '../../theme/tokens'; import type { InternalTheme } from '../../theme/types'; -import { splitStyles } from '../../utils/splitStyles'; const stateOpacity = tokens.md.sys.state.opacity; @@ -191,20 +190,22 @@ export const getButtonColors = ({ }; }; +const borderRadiusKeys = [ + 'borderBottomEndRadius', + 'borderBottomLeftRadius', + 'borderBottomRightRadius', + 'borderBottomStartRadius', + 'borderTopEndRadius', + 'borderTopLeftRadius', + 'borderTopRightRadius', + 'borderTopStartRadius', + 'borderRadius', +] as const; + type ViewStyleBorderRadiusStyles = Partial< - Pick< - ViewStyle, - | 'borderBottomEndRadius' - | 'borderBottomLeftRadius' - | 'borderBottomRightRadius' - | 'borderBottomStartRadius' - | 'borderTopEndRadius' - | 'borderTopLeftRadius' - | 'borderTopRightRadius' - | 'borderTopStartRadius' - | 'borderRadius' - > + Pick >; + export const getButtonTouchableRippleStyle = ( style?: ViewStyle, borderWidth: number = 0 @@ -212,15 +213,6 @@ export const getButtonTouchableRippleStyle = ( if (!style) return {}; const touchableRippleStyle: ViewStyleBorderRadiusStyles = {}; - const [, borderRadiusStyles] = splitStyles( - style, - (style) => style.startsWith('border') && style.endsWith('Radius') - ); - - const borderRadiusKeys = - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - Object.keys(borderRadiusStyles) as Array; - borderRadiusKeys.forEach((key) => { const value = style[key]; if (typeof value === 'number') { diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx index 712110883b..cb5e2c3c67 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -191,16 +191,11 @@ const Card = ({ : null ); - const { backgroundColor, borderColor: themedBorderColor } = getCardColors({ + const { backgroundColor, borderColor } = getCardColors({ theme, mode: cardMode, }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const flattenedStyles = (StyleSheet.flatten(style) || {}) as ViewStyle; - - const { borderColor = themedBorderColor } = flattenedStyles; - const borderRadius = theme.shapes.corner.medium; const content = ( diff --git a/src/components/Card/CardCover.tsx b/src/components/Card/CardCover.tsx index 4542aa7c99..c55cddedc8 100644 --- a/src/components/Card/CardCover.tsx +++ b/src/components/Card/CardCover.tsx @@ -5,7 +5,6 @@ import { getCardCoverStyle } from './utils'; import { useInternalTheme } from '../../core/theming'; import { grey200 } from '../../theme/colors'; import type { ThemeProp } from '../../theme/types'; -import { splitStyles } from '../../utils/splitStyles'; export type Props = ImageProps & { /** @@ -51,26 +50,17 @@ const CardCover = ({ }: Props) => { const theme = useInternalTheme(themeOverrides); - const flattenedStyles = StyleSheet.flatten(style) || {}; - const [, borderRadiusStyles] = splitStyles( - flattenedStyles, - (style) => style.startsWith('border') && style.endsWith('Radius') - ); - const coverStyle = getCardCoverStyle({ theme, index, total, - borderRadiusStyles, }); + // The container clips the image with `overflow: 'hidden'`, + // so any border radius passed in `style` is applied to the image as well return ( - + ); }; diff --git a/src/components/Card/utils.tsx b/src/components/Card/utils.tsx index fc0faa945a..b642a3f855 100644 --- a/src/components/Card/utils.tsx +++ b/src/components/Card/utils.tsx @@ -4,11 +4,6 @@ import type { InternalTheme } from '../../theme/types'; type CardMode = 'elevated' | 'outlined' | 'contained'; -type BorderRadiusStyles = Pick< - ViewStyle, - Extract ->; - export type CardActionChildProps = { compact?: boolean; mode?: string; @@ -19,20 +14,11 @@ export const getCardCoverStyle = ({ theme, index: _index, total: _total, - borderRadiusStyles, }: { theme: InternalTheme; - borderRadiusStyles: BorderRadiusStyles; index?: number; total?: number; }) => { - if (Object.keys(borderRadiusStyles).length > 0) { - return { - borderRadius: theme.shapes.corner.medium, - ...borderRadiusStyles, - }; - } - return { borderRadius: theme.shapes.corner.medium, }; diff --git a/src/components/DataTable/DataTablePagination.tsx b/src/components/DataTable/DataTablePagination.tsx index 0fd2cb7a04..da165c5dc6 100644 --- a/src/components/DataTable/DataTablePagination.tsx +++ b/src/components/DataTable/DataTablePagination.tsx @@ -182,7 +182,7 @@ const PaginationDropdown = ({ onPress={() => toggleSelect(true)} style={styles.button} icon="menu-down" - contentStyle={styles.contentStyle} + iconPosition="trailing" theme={theme} > {`${numberOfItemsPerPage}`} @@ -357,9 +357,6 @@ const styles = StyleSheet.create({ iconsContainer: { flexDirection: 'row', }, - contentStyle: { - flexDirection: 'row-reverse', - }, }); export default DataTablePagination; diff --git a/src/components/Surface.tsx b/src/components/Surface.tsx index 48df1a4d4a..719d74d31b 100644 --- a/src/components/Surface.tsx +++ b/src/components/Surface.tsx @@ -20,7 +20,7 @@ type AnimatedStyleProp = Extract< type BorderRadius = AnimatedStyleProp<'borderRadius'>; -type SurfaceVisualProps = { +export type SurfaceVisualProps = { /** * Background color of the Surface. Overrides the color derived from * `elevation`. diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index d0dc180115..06d9f35e51 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -83,6 +83,7 @@ const Tooltip = ({ const hideTooltipTimer = React.useRef[]>([]); const childrenWrapperRef = React.useRef(null); + const childRef = React.useRef(null); const touched = React.useRef(false); const isValidChild = React.useMemo( @@ -90,6 +91,26 @@ const Tooltip = ({ [children] ); + const childOwnRef = isValidChild + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (children.props as TooltipChildProps).ref + : undefined; + + // Keep a ref to the wrapped element so it can be measured directly, + // while still forwarding the ref passed by the user (if any) + const setChildRef = React.useCallback( + (node: View | null) => { + childRef.current = node; + + if (typeof childOwnRef === 'function') { + childOwnRef(node); + } else if (childOwnRef) { + childOwnRef.current = node; + } + }, + [childOwnRef] + ); + React.useEffect(() => { return () => { if (showTooltipTimer.current.length) { @@ -173,15 +194,17 @@ const Tooltip = ({ }, [children.props, handleTouchEnd, isValidChild]); const handleOnLayout = ({ nativeEvent: { layout } }: LayoutChangeEvent) => { - childrenWrapperRef.current?.measure( - (_x, _y, width, height, pageX, pageY) => { - setMeasurement({ - children: { pageX, pageY, height, width }, - tooltip: { ...layout }, - measured: true, - }); - } - ); + // Measure the wrapped element itself when possible, since the wrapper + // doesn't reflect its layout if the element is absolutely positioned + const target = childRef.current ?? childrenWrapperRef.current; + + target?.measure((_x, _y, width, height, pageX, pageY) => { + setMeasurement({ + children: { pageX, pageY, height, width }, + tooltip: { ...layout }, + measured: true, + }); + }); }; const mobilePressProps = { @@ -208,9 +231,7 @@ const Tooltip = ({ backgroundColor: theme.colors.onSurface, ...getTooltipPosition( // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - measurement as Measurement, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - children as React.ReactElement + measurement as Measurement ), borderRadius: theme.shapes.corner.extraSmall, ...(measurement.measured ? styles.visible : styles.hidden), @@ -235,10 +256,15 @@ const Tooltip = ({ style={styles.pressContainer} {...(Platform.OS === 'web' ? webPressProps : mobilePressProps)} > - {React.cloneElement(children, { - ...rest, - ...(Platform.OS === 'web' ? webPressProps : mobilePressProps), - })} + {React.cloneElement( + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + children as React.ReactElement, + { + ...rest, + ...(Platform.OS === 'web' ? webPressProps : mobilePressProps), + ref: setChildRef, + } + )} ); diff --git a/src/components/Tooltip/utils.ts b/src/components/Tooltip/utils.ts index 43baf684fd..80699a896c 100644 --- a/src/components/Tooltip/utils.ts +++ b/src/components/Tooltip/utils.ts @@ -1,5 +1,5 @@ -import { Dimensions, StyleSheet } from 'react-native'; -import type { LayoutRectangle, StyleProp, ViewStyle } from 'react-native'; +import { Dimensions } from 'react-native'; +import type { LayoutRectangle, View } from 'react-native'; type ChildrenMeasurement = { width: number; @@ -17,7 +17,7 @@ export type Measurement = { }; export type TooltipChildProps = { - style: StyleProp; + ref?: React.Ref; disabled?: boolean; onPress?: () => void; onHoverIn?: () => void; @@ -85,52 +85,15 @@ const getTooltipYPosition = ( return childrenY + childrenHeight; }; -const getChildrenMeasures = ( - style: StyleProp, - measures: ChildrenMeasurement -): ChildrenMeasurement => { - const { position, top, bottom, left, right } = StyleSheet.flatten(style); - - if (position === 'absolute') { - let pageX = 0; - let pageY = measures.pageY; - let height = 0; - let width = 0; - if (typeof left === 'number') { - pageX = left; - width = 0; - } - if (typeof right === 'number') { - pageX = measures.width - right; - width = 0; - } - if (typeof top === 'number') { - pageY = pageY + top; - } - if (typeof bottom === 'number') { - pageY = pageY - bottom; - } - - return { pageX, pageY, width, height }; - } - - return measures; -}; - -export const getTooltipPosition = ( - { children, tooltip, measured }: Measurement, - component: React.ReactElement<{ - style: StyleProp; - }> -): {} | { left: number; top: number } => { +export const getTooltipPosition = ({ + children, + tooltip, + measured, +}: Measurement): {} | { left: number; top: number } => { if (!measured) return {}; - let measures = children; - if (component.props.style) { - measures = getChildrenMeasures(component.props.style, children); - } return { - left: getTooltipXPosition(measures, tooltip), - top: getTooltipYPosition(measures, tooltip), + left: getTooltipXPosition(children, tooltip), + top: getTooltipYPosition(children, tooltip), }; }; diff --git a/src/components/__tests__/Appbar/Appbar.test.tsx b/src/components/__tests__/Appbar/Appbar.test.tsx index 95aec82ddd..78e24191c5 100644 --- a/src/components/__tests__/Appbar/Appbar.test.tsx +++ b/src/components/__tests__/Appbar/Appbar.test.tsx @@ -7,7 +7,6 @@ import { tokens } from '../../../theme/tokens'; import Appbar from '../../Appbar'; import { getAppbarBackgroundColor, - getAppbarBorders, modeTextVariant, renderAppbarContent as utilRenderAppbarContent, } from '../../Appbar/utils'; @@ -46,6 +45,31 @@ describe('Appbar', () => { expect(tree).toMatchSnapshot(); }); + + it('renders custom background color passed to backgroundColor prop', async () => { + await render( + + + + ); + + expect(screen.getByTestId('appbar')).toHaveStyle({ + backgroundColor: '#FF0000', + }); + }); + + it('renders border radius passed to props', async () => { + await render( + + + + ); + + expect(screen.getByTestId('appbar')).toHaveStyle({ + borderRadius: 8, + borderBottomLeftRadius: 4, + }); + }); }); describe('renderAppbarContent', () => { @@ -151,6 +175,20 @@ describe('renderAppbarContent', () => { ); }); + it('renders custom background color passed to Appbar.Header', async () => { + await render( + + + + + + ); + + expect(screen.getByTestId('appbar-header')).toHaveStyle({ + backgroundColor: '#FF0000', + }); + }); + it('Is recognized as a heading when no onPress callback has been passed', async () => { await render( @@ -280,32 +318,3 @@ describe('getAppbarColors', () => { ); }); }); - -describe('getAppbarBorders', () => { - const borderStyles = { - borderRadius: 1, - borderBottomEndRadius: 2, - borderBottomStartRadius: 3, - borderEndEndRadius: 4, - borderEndStartRadius: 5, - borderStartEndRadius: 6, - borderStartStartRadius: 7, - borderTopEndRadius: 8, - borderTopStartRadius: 9, - borderTopLeftRadius: 10, - borderTopRightRadius: 11, - borderBottomRightRadius: 12, - borderBottomLeftRadius: 13, - borderCurve: 'continuous' as const, - }; - - it('returns every border style and excludes unrelated styles', () => { - expect(getAppbarBorders({ ...borderStyles, height: 60, top: 13 })).toEqual( - borderStyles - ); - }); - - it('returns an empty object when no border styles are passed', () => { - expect(getAppbarBorders({ height: 60, top: 13 })).toEqual({}); - }); -}); diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap index faeb89916b..5ff794c9c0 100644 --- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap +++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap @@ -19,7 +19,7 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` "paddingRight": 4, "paddingTop": undefined, }, - {}, + undefined, {}, { "backgroundColor": "rgba(254, 247, 255, 1)", @@ -496,7 +496,7 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A "paddingRight": 4, "paddingTop": undefined, }, - {}, + undefined, {}, { "backgroundColor": "rgba(254, 247, 255, 1)", @@ -976,7 +976,7 @@ exports[`AppbarAction should be rendered with custom color 1`] = ` "paddingRight": 4, "paddingTop": undefined, }, - {}, + undefined, {}, { "backgroundColor": "rgba(254, 247, 255, 1)", @@ -1234,7 +1234,7 @@ exports[`AppbarAction should be rendered with default theme color 1`] = ` "paddingRight": 4, "paddingTop": undefined, }, - {}, + undefined, {}, { "backgroundColor": "rgba(254, 247, 255, 1)", @@ -1492,7 +1492,7 @@ exports[`AppbarAction should be rendered with specific theme color if is leading "paddingRight": 4, "paddingTop": undefined, }, - {}, + undefined, {}, { "backgroundColor": "rgba(254, 247, 255, 1)", @@ -1750,7 +1750,7 @@ exports[`AppbarAction should render AppbarBackAction with custom color 1`] = ` "paddingRight": 4, "paddingTop": undefined, }, - {}, + undefined, {}, { "backgroundColor": "rgba(254, 247, 255, 1)", diff --git a/src/components/__tests__/Avatar.test.tsx b/src/components/__tests__/Avatar.test.tsx index dc437b2dd9..23b6d63a0e 100644 --- a/src/components/__tests__/Avatar.test.tsx +++ b/src/components/__tests__/Avatar.test.tsx @@ -1,5 +1,3 @@ -import { StyleSheet } from 'react-native'; - import { describe, expect, it, jest } from '@jest/globals'; import { fireEvent } from '@testing-library/react-native'; @@ -7,12 +5,6 @@ import { render, screen } from '../../test-utils'; import { red500 } from '../../theme/colors'; import * as Avatar from '../Avatar/Avatar'; -const styles = StyleSheet.create({ - bgColor: { - backgroundColor: red500, - }, -}); - it('renders avatar with text', async () => { const tree = (await render()).toJSON(); @@ -27,7 +19,7 @@ it('renders avatar with text and custom size', async () => { it('renders avatar with text and custom background color', async () => { const tree = ( - await render() + await render() ).toJSON(); expect(tree).toMatchSnapshot(); @@ -49,7 +41,7 @@ it('renders avatar with icon', async () => { it('renders avatar with icon and custom background color', async () => { const tree = ( - await render() + await render() ).toJSON(); expect(tree).toMatchSnapshot(); diff --git a/src/components/__tests__/Button.test.tsx b/src/components/__tests__/Button.test.tsx index 7ea496a419..7f49ce6a8d 100644 --- a/src/components/__tests__/Button.test.tsx +++ b/src/components/__tests__/Button.test.tsx @@ -1,5 +1,3 @@ -import { StyleSheet } from 'react-native'; - import { describe, expect, it, jest } from '@jest/globals'; import { userEvent } from '@testing-library/react-native'; @@ -12,12 +10,6 @@ import { getButtonColors } from '../Button/utils'; const stateOpacity = tokens.md.sys.state.opacity; -const styles = StyleSheet.create({ - flexing: { - flexDirection: 'row-reverse', - }, -}); - it('renders text button by default', async () => { const tree = (await render()).toJSON(); @@ -59,7 +51,7 @@ it('renders button with icon', async () => { it('renders button with icon in reverse order', async () => { const tree = ( await render( - ) diff --git a/src/components/__tests__/Card/Card.test.tsx b/src/components/__tests__/Card/Card.test.tsx index 7d84f0e44f..f678194529 100644 --- a/src/components/__tests__/Card/Card.test.tsx +++ b/src/components/__tests__/Card/Card.test.tsx @@ -4,7 +4,6 @@ import { afterEach, describe, expect, it, jest } from '@jest/globals'; import { render, screen } from '../../../test-utils'; import { LightTheme } from '../../../theme/schemes'; -import { Palette } from '../../../theme/tokens'; import Button from '../../Button/Button'; import Card from '../../Card/Card'; import { getCardColors, getCardCoverStyle } from '../../Card/utils'; @@ -46,20 +45,6 @@ describe('Card', () => { expect(toJSON()).toMatchSnapshot(); }); - it('renders an outlined card with custom border color', async () => { - const { toJSON } = await render( - - {null} - - ); - - expect(toJSON()).toMatchSnapshot(); - }); - it('renders with a custom theme background color', async () => { jest.replaceProperty(Platform, 'OS', 'web'); @@ -116,7 +101,8 @@ describe('CardCover', () => { ); - expect(screen.getByTestId('card-cover')).toHaveStyle( + // The border radius is applied to the container which clips the image + expect(screen.getByTestId('card-cover').parent).toHaveStyle( styles.customCoverRadius ); }); @@ -183,20 +169,10 @@ describe('getCardColors - border color', () => { }); describe('getCardCoverStyle - border radius', () => { - it('should return custom border radius', () => { - expect( - getCardCoverStyle({ - theme: LightTheme, - borderRadiusStyles: styles.customCoverRadius, - }) - ).toMatchObject(styles.customCoverRadius); - }); - it('should return correct border radius based on roundness, for theme version 3', () => { expect( getCardCoverStyle({ theme: LightTheme, - borderRadiusStyles: {}, }) ).toMatchObject({ borderRadius: LightTheme.shapes.corner.medium }); }); diff --git a/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap b/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap index 216e5b5d08..0df270a4a6 100644 --- a/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap +++ b/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap @@ -244,128 +244,3 @@ exports[`Card renders an outlined card with a custom outline color 1`] = ` /> `; - -exports[`Card renders an outlined card with custom border color 1`] = ` - - - - - -`; diff --git a/src/components/__tests__/Tooltip.test.tsx b/src/components/__tests__/Tooltip.test.tsx index d6b037e10c..6691bf1e89 100644 --- a/src/components/__tests__/Tooltip.test.tsx +++ b/src/components/__tests__/Tooltip.test.tsx @@ -159,6 +159,16 @@ describe('Tooltip', () => { }); }); + describe('ref', () => { + it('forwards the ref passed to the wrapped component', async () => { + const ref = React.createRef(); + + await setup({ children: }); + + expect(ref.current).not.toBeNull(); + }); + }); + describe('pressOut', () => { it('hides the tooltip when the user stop pressing the component', async () => { const { diff --git a/src/components/__tests__/__snapshots__/Avatar.test.tsx.snap b/src/components/__tests__/__snapshots__/Avatar.test.tsx.snap index 9e55666cef..8b2d11bc1d 100644 --- a/src/components/__tests__/__snapshots__/Avatar.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Avatar.test.tsx.snap @@ -14,7 +14,7 @@ exports[`renders avatar with icon 1`] = ` "alignItems": "center", "justifyContent": "center", }, - {}, + undefined, ] } > @@ -64,7 +64,7 @@ exports[`renders avatar with icon and custom background color 1`] = ` "alignItems": "center", "justifyContent": "center", }, - {}, + undefined, ] } > @@ -146,7 +146,7 @@ exports[`renders avatar with text 1`] = ` "alignItems": "center", "justifyContent": "center", }, - {}, + undefined, ] } > @@ -200,7 +200,7 @@ exports[`renders avatar with text and custom background color 1`] = ` "alignItems": "center", "justifyContent": "center", }, - {}, + undefined, ] } > @@ -254,7 +254,7 @@ exports[`renders avatar with text and custom colors 1`] = ` "alignItems": "center", "justifyContent": "center", }, - {}, + undefined, ] } > @@ -308,7 +308,7 @@ exports[`renders avatar with text and custom size 1`] = ` "alignItems": "center", "justifyContent": "center", }, - {}, + undefined, ] } > diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap index 22b83abaaa..625e3e15ee 100644 --- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap @@ -76,6 +76,7 @@ exports[`allows customizing Route's type via generics 1`] = ` onLayout={[Function]} style={ { + "backgroundColor": "rgba(243, 237, 247, 1)", "bottom": 0, "left": 0, "pointerEvents": "none", @@ -84,11 +85,9 @@ exports[`allows customizing Route's type via generics 1`] = ` } > diff --git a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap index 9084841e9b..de2cf95556 100644 --- a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap @@ -1949,15 +1949,13 @@ exports[`DataTable.Pagination renders data table pagination with options select [ { "alignItems": "center", - "flexDirection": "row", + "flexDirection": "row-reverse", "justifyContent": "center", }, { "opacity": 1, }, - { - "flexDirection": "row-reverse", - }, + undefined, ] } > diff --git a/src/utils/__tests__/splitStyles.test.ts b/src/utils/__tests__/splitStyles.test.ts deleted file mode 100644 index c27a53d940..0000000000 --- a/src/utils/__tests__/splitStyles.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { ViewStyle } from 'react-native'; - -import { describe, expect, it } from '@jest/globals'; - -import { splitStyles } from '../splitStyles'; - -describe('splitStyles', () => { - const styles: Readonly = Object.freeze({ - backgroundColor: 'red', - marginTop: 1, - marginBottom: 2, - marginLeft: 3, - padding: 4, - borderTopLeftRadius: 5, - borderTopRightRadius: 6, - right: 4, - }); - - it('splits margins, paddings, and border radiuses correctly', () => { - const marginPredicate = (style: string) => style.startsWith('margin'); - const paddingPredicate = (style: string) => style.startsWith('padding'); - const borderRadiusPredicate = (style: string) => - style.startsWith('border') && style.endsWith('Radius'); - const [filteredStyles, marginStyles, paddingStyles, borderRadiusStyles] = - splitStyles( - styles, - marginPredicate, - paddingPredicate, - borderRadiusPredicate - ); - - expect(keysLength(filteredStyles)).toBeGreaterThan(0); - expect(keysLength(filteredStyles)).toBeLessThan(keysLength(styles)); - for (const style in filteredStyles) { - expect(marginPredicate(style)).toBeFalsy(); - expect(paddingPredicate(style)).toBeFalsy(); - expect(borderRadiusPredicate(style)).toBeFalsy(); - } - - expect(keysLength(marginStyles)).toBeGreaterThan(0); - for (const style in marginStyles) { - expect(marginPredicate(style)).toBeTruthy(); - } - - expect(keysLength(paddingStyles)).toBeGreaterThan(0); - for (const style in paddingStyles) { - expect(paddingPredicate(style)).toBeTruthy(); - } - - expect(keysLength(borderRadiusStyles)).toBeGreaterThan(0); - for (const style in borderRadiusStyles) { - expect(borderRadiusPredicate(style)).toBeTruthy(); - } - }); - - it('filtered styles is an empty object if all styles matched some predicate', () => { - const styles = { - margin: 5, - padding: 6, - }; - const [filteredStyles] = splitStyles( - styles, - (style) => style.startsWith('margin'), - (style) => style.startsWith('padding') - ); - - expect(keysLength(filteredStyles)).toBe(0); - }); - - it('processes predicates in order', () => { - const [, marginStyles, marginStyles2, marginStyles3] = splitStyles( - styles, - (style) => style.startsWith('margin'), - (style) => style.startsWith('margin'), - (style) => style.startsWith('margin') - ); - - expect(keysLength(marginStyles)).toBeGreaterThan(0); - expect(keysLength(marginStyles2)).toBe(0); - expect(keysLength(marginStyles3)).toBe(0); - }); -}); - -function keysLength(object: object): number { - return Object.keys(object).length; -} diff --git a/src/utils/splitStyles.ts b/src/utils/splitStyles.ts deleted file mode 100644 index 910c62eb45..0000000000 --- a/src/utils/splitStyles.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { ViewStyle } from 'react-native'; - -type FiltersArray = readonly ((style: keyof ViewStyle) => boolean)[]; - -type MappedTuple = { - [Index in keyof Tuple]: ViewStyle; -} & { length: Tuple['length'] }; - -type Style = ViewStyle[keyof ViewStyle]; -type Entry = [keyof ViewStyle, Style]; - -/** - * Utility function to extract styles in separate objects - * - * @param styles The style object you want to filter - * @param filters The filters by which you want to split the styles - * @returns An array of filtered style objects: - * - The first style object contains the properties that didn't match any filter - * - After that there will be a style object for each filter you passed in the same order as the matching filters - * - A style property will exist in a single style object, the first filter it matched - */ -export function splitStyles( - styles: ViewStyle, - ...filters: Tuple -) { - if (process.env.NODE_ENV !== 'production' && filters.length === 0) { - console.error('No filters were passed when calling splitStyles'); - } - - // `Object.entries` will be used to iterate over the styles and `Object.fromEntries` will be called before returning - // Entries which match the given filters will be temporarily stored in `newStyles` - const newStyles: Entry[][] = filters.map(() => []); - - // Entries which match no filter - const rest: Entry[] = []; - - // Iterate every style property - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - outer: for (const item of Object.entries(styles) as Entry[]) { - // Check each filter - for (let i = 0; i < filters.length; i++) { - // Check if filter matches - if (filters[i](item[0])) { - newStyles[i].push(item); // Push to temporary filtered entries array - continue outer; // Skip to checking next style property - } - } - - // Adds to rest styles if not filtered - rest.push(item); - } - - // Put unmatched styles in the beginning - newStyles.unshift(rest); - - // Convert arrays of entries into objects - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - return newStyles.map((styles) => Object.fromEntries(styles)) as unknown as [ - ViewStyle, - ...MappedTuple, - ]; -}