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`] = ` } >