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(
-
`;
-
-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,
- ];
-}