diff --git a/example/src/DrawerItems.tsx b/example/src/DrawerItems.tsx
index c23e850adf..94afa3136c 100644
--- a/example/src/DrawerItems.tsx
+++ b/example/src/DrawerItems.tsx
@@ -32,11 +32,7 @@ const DrawerItemsData = [
icon: 'star',
key: 1,
right: ({ color }: { color: ColorValue }) => (
-
+
),
},
{ label: 'Sent mail', icon: 'send', key: 2 },
@@ -45,7 +41,7 @@ const DrawerItemsData = [
label: 'A very long title that will be truncated',
icon: 'delete',
key: 4,
- right: () => ,
+ right: () => ,
},
];
diff --git a/example/src/Examples/BadgeExample.tsx b/example/src/Examples/BadgeExample.tsx
index c3df954659..7ca1a43f9c 100644
--- a/example/src/Examples/BadgeExample.tsx
+++ b/example/src/Examples/BadgeExample.tsx
@@ -30,7 +30,7 @@ const BadgeExample = () => {
-
+
12
@@ -39,7 +39,7 @@ const BadgeExample = () => {
{
-
+
-
+
@@ -79,10 +79,15 @@ const styles = StyleSheet.create({
button: {
opacity: 0.6,
},
- badge: {
+ textBadge: {
position: 'absolute',
- top: 4,
- right: 0,
+ top: 12,
+ left: 38,
+ },
+ dotBadge: {
+ position: 'absolute',
+ top: 14,
+ right: 14,
},
label: {
flex: 1,
diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx
index 0fe09c6558..869adf6820 100644
--- a/src/components/Badge.tsx
+++ b/src/components/Badge.tsx
@@ -1,13 +1,18 @@
-import * as React from 'react';
-import { Animated, StyleSheet, useWindowDimensions } from 'react-native';
-import type { StyleProp, TextStyle } from 'react-native';
+import type { StyleProp, TextProps, TextStyle } from 'react-native';
+import { StyleSheet } from 'react-native';
+
+import Animated from 'react-native-reanimated';
import { useInternalTheme } from '../core/theming';
+import { cornerFull } from '../theme/tokens/sys/shape';
import type { ThemeProp } from '../types';
-const defaultSize = 20;
+const SMALL_SIZE = 6;
+const LARGE_SIZE = 16;
+const MAX_LARGE_WIDTH = 36;
+const LARGE_PADDING = 4;
-export type Props = React.ComponentProps & {
+export type Props = TextProps & {
/**
* Whether the badge is visible
*/
@@ -16,12 +21,7 @@ export type Props = React.ComponentProps & {
* Content of the `Badge`.
*/
children?: string | number;
- /**
- * Size of the `Badge`.
- */
- size?: number;
style?: StyleProp;
- ref?: React.RefObject;
/**
* @optional
*/
@@ -32,6 +32,10 @@ export type Props = React.ComponentProps & {
* Badges are small status descriptors for UI elements.
* A badge consists of a small circle, typically containing a number or other short set of characters, that appears in proximity to another object.
*
+ * The badge is styled differently based on whether `children` is passed:
+ * - Small dot when it doesn't have `children`
+ * - Larger pill when it has `children`
+ *
* ## Usage
* ```js
* import * as React from 'react';
@@ -46,64 +50,52 @@ export type Props = React.ComponentProps & {
*/
const Badge = ({
children,
- size = defaultSize,
style,
theme: themeOverrides,
visible = true,
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
- const { current: opacity } = React.useRef(
- new Animated.Value(visible ? 1 : 0)
- );
- const { fontScale } = useWindowDimensions();
-
- const isFirstRendering = React.useRef(true);
const {
animation: { scale },
} = theme;
- React.useEffect(() => {
- // Do not run animation on very first rendering
- if (isFirstRendering.current) {
- isFirstRendering.current = false;
- return;
- }
-
- Animated.timing(opacity, {
- toValue: visible ? 1 : 0,
- duration: 150 * scale,
- useNativeDriver: true,
- }).start();
- }, [visible, opacity, scale]);
-
- const { backgroundColor = theme.colors.error, ...restStyle } =
- (StyleSheet.flatten(style) || {}) as TextStyle;
-
const textColor = theme.colors.onError;
- const borderRadius = size / 2;
+ const isLarge = children != null;
+ const badgeSize = isLarge ? LARGE_SIZE : SMALL_SIZE;
+ const labelFont = theme.fonts.labelSmall;
- const paddingHorizontal = 3;
+ const transitionStyle = {
+ opacity: visible ? 1 : 0,
+ transitionDuration: 150 * scale,
+ transitionProperty: 'opacity',
+ };
return (
diff --git a/src/components/BottomNavigation/BottomNavigationBar.tsx b/src/components/BottomNavigation/BottomNavigationBar.tsx
index b254be6d83..f25bcf0170 100644
--- a/src/components/BottomNavigation/BottomNavigationBar.tsx
+++ b/src/components/BottomNavigation/BottomNavigationBar.tsx
@@ -681,11 +681,9 @@ const BottomNavigationBar = ({
{typeof badge === 'boolean' ? (
-
+
) : (
-
- {badge}
-
+ {badge}
)}
diff --git a/src/components/Drawer/DrawerCollapsedItem.tsx b/src/components/Drawer/DrawerCollapsedItem.tsx
index 1542eafe36..6e4fdebaa6 100644
--- a/src/components/Drawer/DrawerCollapsedItem.tsx
+++ b/src/components/Drawer/DrawerCollapsedItem.tsx
@@ -67,7 +67,6 @@ export type Props = ViewProps & {
testID?: string;
};
-const badgeSize = 8;
const iconSize = 24;
const itemSize = 56;
const outlineHeight = 32;
@@ -200,11 +199,9 @@ const DrawerCollapsedItem = ({
{badge !== false && (
{typeof badge === 'boolean' ? (
-
+
) : (
-
- {badge}
-
+ {badge}
)}
)}
diff --git a/src/components/__tests__/Badge.test.tsx b/src/components/__tests__/Badge.test.tsx
index 325d7a00bf..2b2ff26ca2 100644
--- a/src/components/__tests__/Badge.test.tsx
+++ b/src/components/__tests__/Badge.test.tsx
@@ -1,6 +1,6 @@
import { expect, it } from '@jest/globals';
-import { render } from '../../test-utils';
+import { render, screen } from '../../test-utils';
import { red500 } from '../../theme/colors';
import Badge from '../Badge';
@@ -16,20 +16,8 @@ it('renders badge with content', async () => {
expect(tree).toMatchSnapshot();
});
-it('renders badge in different size', async () => {
- const tree = (await render(3)).toJSON();
-
- expect(tree).toMatchSnapshot();
-});
-
it('renders badge as hidden', async () => {
- const tree = (
- await render(
-
- 3
-
- )
- ).toJSON();
+ const tree = (await render(3)).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -41,3 +29,39 @@ it('renders badge in different color', async () => {
expect(tree).toMatchSnapshot();
});
+
+it('applies small dot dimensions when no children', async () => {
+ await render();
+
+ expect(screen.getByTestId('badge')).toHaveStyle({
+ height: 6,
+ minWidth: 6,
+ borderRadius: 9999,
+ });
+});
+
+it('applies large pill dimensions when children are present', async () => {
+ await render(3);
+
+ expect(screen.getByTestId('badge')).toHaveStyle({
+ height: 16,
+ minWidth: 16,
+ paddingHorizontal: 4,
+ fontSize: 11,
+ lineHeight: 16,
+ borderRadius: 9999,
+ });
+});
+
+it('clips oversized label via maxWidth', async () => {
+ await render(9999999);
+
+ expect(screen.getByTestId('badge')).toHaveStyle({ maxWidth: 36 });
+});
+
+it('does not apply typography or padding to dot badge', async () => {
+ await render();
+
+ expect(screen.getByTestId('badge')).not.toHaveStyle({ paddingHorizontal: 4 });
+ expect(screen.getByTestId('badge')).not.toHaveStyle({ fontSize: 11 });
+});
diff --git a/src/components/__tests__/__snapshots__/Badge.test.tsx.snap b/src/components/__tests__/__snapshots__/Badge.test.tsx.snap
index 571b6ac583..0090bbfbc0 100644
--- a/src/components/__tests__/__snapshots__/Badge.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Badge.test.tsx.snap
@@ -2,48 +2,75 @@
exports[`renders badge 1`] = `
`;
exports[`renders badge as hidden 1`] = `
3
@@ -52,50 +79,46 @@ exports[`renders badge as hidden 1`] = `
exports[`renders badge in different color 1`] = `
- 3
-
-`;
-
-exports[`renders badge in different size 1`] = `
-
3
@@ -104,24 +127,44 @@ exports[`renders badge in different size 1`] = `
exports[`renders badge with content 1`] = `
3
diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap
index 342f5369bf..33855bb467 100644
--- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap
@@ -263,24 +263,31 @@ exports[`allows customizing Route's type via generics 1`] = `
}
>
@@ -517,24 +524,31 @@ exports[`allows customizing Route's type via generics 1`] = `
}
>
@@ -993,24 +1007,31 @@ exports[`hides labels in non-shifting bottom navigation 1`] = `
}
>
@@ -1187,24 +1208,31 @@ exports[`hides labels in non-shifting bottom navigation 1`] = `
}
>
@@ -1381,24 +1409,31 @@ exports[`hides labels in non-shifting bottom navigation 1`] = `
}
>
@@ -1736,24 +1771,31 @@ exports[`hides labels in shifting bottom navigation 1`] = `
}
>
@@ -1930,24 +1972,31 @@ exports[`hides labels in shifting bottom navigation 1`] = `
}
>
@@ -2124,24 +2173,31 @@ exports[`hides labels in shifting bottom navigation 1`] = `
}
>
@@ -2616,24 +2672,31 @@ exports[`renders bottom navigation with getLazy 1`] = `
}
>
@@ -2930,24 +2993,31 @@ exports[`renders bottom navigation with getLazy 1`] = `
}
>
@@ -3244,24 +3314,31 @@ exports[`renders bottom navigation with getLazy 1`] = `
}
>
@@ -3558,24 +3635,31 @@ exports[`renders bottom navigation with getLazy 1`] = `
}
>
@@ -3872,24 +3956,31 @@ exports[`renders bottom navigation with getLazy 1`] = `
}
>
@@ -4352,24 +4443,31 @@ exports[`renders bottom navigation with scene animation 1`] = `
}
>
@@ -4615,24 +4713,31 @@ exports[`renders bottom navigation with scene animation 1`] = `
}
>
@@ -4878,24 +4983,31 @@ exports[`renders bottom navigation with scene animation 1`] = `
}
>
@@ -5141,24 +5253,31 @@ exports[`renders bottom navigation with scene animation 1`] = `
}
>
@@ -5404,24 +5523,31 @@ exports[`renders bottom navigation with scene animation 1`] = `
}
>
@@ -5763,24 +5889,31 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = `
}
>
@@ -5952,24 +6085,31 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = `
}
>
@@ -6141,24 +6281,31 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = `
}
>
@@ -6496,24 +6643,31 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = `
}
>
@@ -6667,24 +6821,31 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = `
}
>
@@ -6838,24 +6999,31 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = `
}
>
@@ -7009,24 +7177,31 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = `
}
>
@@ -7180,24 +7355,31 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = `
}
>
@@ -7567,24 +7749,31 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom
}
>
@@ -7881,24 +8070,31 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom
}
>
@@ -8195,24 +8391,31 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom
}
>
@@ -8675,24 +8878,31 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav
}
>
@@ -8938,24 +9148,31 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav
}
>
@@ -9201,24 +9418,31 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav
}
>
@@ -9620,24 +9844,31 @@ exports[`renders non-shifting bottom navigation 1`] = `
}
>
@@ -9934,24 +10165,31 @@ exports[`renders non-shifting bottom navigation 1`] = `
}
>
@@ -10248,24 +10486,31 @@ exports[`renders non-shifting bottom navigation 1`] = `
}
>
@@ -10728,24 +10973,31 @@ exports[`renders shifting bottom navigation 1`] = `
}
>
@@ -10991,24 +11243,31 @@ exports[`renders shifting bottom navigation 1`] = `
}
>
@@ -11254,24 +11513,31 @@ exports[`renders shifting bottom navigation 1`] = `
}
>
@@ -11517,24 +11783,31 @@ exports[`renders shifting bottom navigation 1`] = `
}
>
@@ -11780,24 +12053,31 @@ exports[`renders shifting bottom navigation 1`] = `
}
>