diff --git a/src/components/Avatar/AvatarIcon.tsx b/src/components/Avatar/AvatarIcon.tsx index 2d2f08760b..f5f579b3e4 100644 --- a/src/components/Avatar/AvatarIcon.tsx +++ b/src/components/Avatar/AvatarIcon.tsx @@ -2,13 +2,11 @@ import { StyleSheet, View } from 'react-native'; import type { StyleProp, ViewProps, ViewStyle } from 'react-native'; import { useInternalTheme } from '../../core/theming'; -import { white } from '../../theme/colors'; +import { cornerFull } from '../../theme/tokens/sys/shape'; import type { ThemeProp } from '../../theme/types'; -import getContrastingColor from '../../utils/getContrastingColor'; import Icon from '../Icon'; import type { IconSource } from '../Icon'; - -const defaultSize = 64; +import { DEFAULT_SIZE, ICON_SIZE_RATIO, resolveAvatarColors } from './utils'; export type Props = ViewProps & { /** @@ -45,17 +43,18 @@ export type Props = ViewProps & { */ const Avatar = ({ icon, - size = defaultSize, + size = DEFAULT_SIZE, style, theme: themeOverrides, ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); - const { backgroundColor = theme.colors?.primary, ...restStyle } = - StyleSheet.flatten(style) || {}; - const textColor = - rest.color ?? - getContrastingColor(backgroundColor, white, 'rgba(0, 0, 0, .54)'); + const { backgroundColor, ...restStyle } = StyleSheet.flatten(style) || {}; + const { background, textColor } = resolveAvatarColors({ + theme, + backgroundColor, + color: rest.color, + }); return ( - + ); }; diff --git a/src/components/Avatar/AvatarImage.tsx b/src/components/Avatar/AvatarImage.tsx index e8b6e501b1..17d707630d 100644 --- a/src/components/Avatar/AvatarImage.tsx +++ b/src/components/Avatar/AvatarImage.tsx @@ -8,11 +8,11 @@ import type { ViewStyle, } from 'react-native'; +import { DEFAULT_SIZE, resolveAvatarColors } from './utils'; import { useInternalTheme } from '../../core/theming'; +import { cornerFull } from '../../theme/tokens/sys/shape'; import type { ThemeProp } from '../../theme/types'; -const defaultSize = 64; - export type AvatarImageSource = | ImageSourcePropType | ((props: { size: number }) => React.ReactNode); @@ -74,7 +74,7 @@ export type Props = ViewProps & { * ``` */ const AvatarImage = ({ - size = defaultSize, + size = DEFAULT_SIZE, source, style, onError, @@ -87,8 +87,9 @@ const AvatarImage = ({ testID, ...rest }: Props) => { - const { colors } = useInternalTheme(themeOverrides); - const { backgroundColor = colors?.primary } = StyleSheet.flatten(style) || {}; + const theme = useInternalTheme(themeOverrides); + const { backgroundColor } = StyleSheet.flatten(style) || {}; + const { background } = resolveAvatarColors({ theme, backgroundColor }); return ( { const theme = useInternalTheme(themeOverrides); - const { backgroundColor = theme.colors?.primary, ...restStyle } = - StyleSheet.flatten(style) || {}; - const textColor = - customColor ?? - getContrastingColor(backgroundColor, white, 'rgba(0, 0, 0, .54)'); + const { backgroundColor, ...restStyle } = StyleSheet.flatten(style) || {}; + const { background, textColor } = resolveAvatarColors({ + theme, + backgroundColor, + color: customColor, + }); const { fontScale } = useWindowDimensions(); return ( @@ -77,8 +76,8 @@ const AvatarText = ({ { width: size, height: size, - borderRadius: size / 2, - backgroundColor, + borderRadius: cornerFull, + backgroundColor: background, }, styles.container, restStyle, @@ -88,6 +87,7 @@ const AvatarText = ({ { + const usingDefault = backgroundColor == null; + const background = backgroundColor ?? theme.colors.primaryContainer; + const textColor = + color ?? + (usingDefault + ? theme.colors.onPrimaryContainer + : getContrastingColor(background, white, 'rgba(0, 0, 0, .54)')); + return { background, textColor }; +}; diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index 909f75bf3f..03332a55ed 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -29,6 +29,7 @@ import { useInternalTheme } from '../core/theming'; import type { Elevation, ThemeProp } from '../theme/types'; const DEFAULT_MAX_WIDTH = 960; +const ICON_SIZE = 40; type AnimationFinishedCallback = (result: { finished: boolean }) => void; @@ -165,8 +166,10 @@ const Banner = ({ const showCallback = useLatestCallback(onShowAnimationFinished); const hideCallback = useLatestCallback(onHideAnimationFinished); + const { duration, easing } = theme.motion; const { scale } = theme.animation; - const animationDuration = (visible ? 250 : 200) * scale; + const animationDuration = + (visible ? duration.medium1 : duration.short4) * scale; React.useEffect(() => { const callback = visible ? showCallback : hideCallback; @@ -175,12 +178,19 @@ const Banner = ({ visible ? 1 : 0, { duration: animationDuration, - easing: Easing.inOut(Easing.ease), + easing: Easing.bezier(...easing.standard), reduceMotion: ReduceMotion.Never, }, (finished) => scheduleOnRN(callback, { finished: finished ?? false }) ); - }, [animationDuration, hideCallback, position, showCallback, visible]); + }, [ + animationDuration, + easing, + hideCallback, + position, + showCallback, + visible, + ]); const surfaceStyle = useAnimatedStyle(() => ({ opacity: interpolate(position.value, [0, 0.1, 1], [0, 1, 1]), @@ -234,7 +244,7 @@ const Banner = ({ {icon ? ( - + ) : null} )} { spin.value = withTiming(sortDirection === 'ascending' ? 0 : 180, { - duration: 150, - easing: Easing.inOut(Easing.ease), + duration: duration.short3 * scale, + easing: Easing.bezier(...easing.standard), reduceMotion: reduceMotion ? ReduceMotion.Always : ReduceMotion.Never, }); - }, [reduceMotion, sortDirection, spin]); + }, [duration, easing, reduceMotion, scale, sortDirection, spin]); const animatedStyle = useAnimatedStyle(() => ({ transform: [{ rotate: `${spin.value}deg` }], @@ -141,6 +144,7 @@ const DataTableTitle = ({ {icon} { hideCallback = undefined; }); + it('uses zero-duration animations when animation scale is disabled', async () => { + const view = await render( + + Text + + ); + + await view.rerender( + + Text + + ); + // A disabled animation scale settles within a frame instead of taking the + // full `motion.duration.medium1`. + await act(() => { + jest.advanceTimersByTime(16); + }); + + expect(showCallback).toHaveBeenCalled(); + }); + describe('when component is rendered hidden', () => { // This behaviour is probably a bug. Needs triage before next version. it('will fire onHideAnimationFinished on mount', async () => { diff --git a/src/components/__tests__/DataTable.test.tsx b/src/components/__tests__/DataTable.test.tsx index 49f863720a..5d69106923 100644 --- a/src/components/__tests__/DataTable.test.tsx +++ b/src/components/__tests__/DataTable.test.tsx @@ -1,4 +1,6 @@ -import { describe, expect, it } from '@jest/globals'; +import { describe, expect, it, jest } from '@jest/globals'; +import { act } from '@testing-library/react-native'; +import { getAnimatedStyle } from 'react-native-reanimated'; import { render, screen } from '../../test-utils'; import Checkbox from '../Checkbox'; @@ -20,6 +22,46 @@ describe('DataTable.Header', () => { }); describe('DataTable.Title', () => { + const sortIcon = () => { + const node = screen.getByText('arrow-up', { + includeHiddenElements: true, + }).parent; + + if (!node) { + throw new Error('Sort icon not found'); + } + + return node; + }; + + it('uses zero-duration sort animation when animation scale is disabled', async () => { + const view = await render( + + Dessert + + ); + + await view.rerender( + + Dessert + + ); + await act(() => { + jest.advanceTimersByTime(16); + }); + + // A disabled animation scale lands on the final rotation within a frame. + expect(getAnimatedStyle(sortIcon())).toMatchObject({ + transform: [{ rotate: '180deg' }], + }); + }); + it('renders data table title with sort icon', async () => { const tree = ( await render( diff --git a/src/components/__tests__/__snapshots__/Avatar.test.tsx.snap b/src/components/__tests__/__snapshots__/Avatar.test.tsx.snap index 9e55666cef..074f5ba05d 100644 --- a/src/components/__tests__/__snapshots__/Avatar.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Avatar.test.tsx.snap @@ -5,8 +5,8 @@ exports[`renders avatar with icon 1`] = ` style={ [ { - "backgroundColor": "rgba(103, 80, 164, 1)", - "borderRadius": 32, + "backgroundColor": "rgba(234, 221, 255, 1)", + "borderRadius": 9999, "height": 64, "width": 64, }, @@ -26,7 +26,7 @@ exports[`renders avatar with icon 1`] = ` style={ [ { - "color": "#ffffff", + "color": "rgba(33, 0, 93, 1)", "fontSize": 38.4, }, [ @@ -56,7 +56,7 @@ exports[`renders avatar with icon and custom background color 1`] = ` [ { "backgroundColor": "#f44336", - "borderRadius": 32, + "borderRadius": 9999, "height": 64, "width": 64, }, @@ -105,8 +105,8 @@ exports[`renders avatar with image 1`] = ` style={ [ { - "backgroundColor": "rgba(103, 80, 164, 1)", - "borderRadius": 32, + "backgroundColor": "rgba(234, 221, 255, 1)", + "borderRadius": 9999, "height": 64, "width": 64, }, @@ -123,7 +123,7 @@ exports[`renders avatar with image 1`] = ` } style={ { - "borderRadius": 32, + "borderRadius": 9999, "height": 64, "width": 64, } @@ -137,8 +137,8 @@ exports[`renders avatar with text 1`] = ` style={ [ { - "backgroundColor": "rgba(103, 80, 164, 1)", - "borderRadius": 32, + "backgroundColor": "rgba(234, 221, 255, 1)", + "borderRadius": 9999, "height": 64, "width": 64, }, @@ -172,7 +172,14 @@ exports[`renders avatar with text 1`] = ` "textAlignVertical": "center", }, { - "color": "#ffffff", + "fontFamily": "System", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0.15, + "lineHeight": 24, + }, + { + "color": "rgba(33, 0, 93, 1)", "fontSize": 32, "lineHeight": 64, }, @@ -192,7 +199,7 @@ exports[`renders avatar with text and custom background color 1`] = ` [ { "backgroundColor": "#f44336", - "borderRadius": 32, + "borderRadius": 9999, "height": 64, "width": 64, }, @@ -225,6 +232,13 @@ exports[`renders avatar with text and custom background color 1`] = ` "textAlign": "center", "textAlignVertical": "center", }, + { + "fontFamily": "System", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0.15, + "lineHeight": 24, + }, { "color": "#ffffff", "fontSize": 32, @@ -245,8 +259,8 @@ exports[`renders avatar with text and custom colors 1`] = ` style={ [ { - "backgroundColor": "rgba(103, 80, 164, 1)", - "borderRadius": 32, + "backgroundColor": "rgba(234, 221, 255, 1)", + "borderRadius": 9999, "height": 64, "width": 64, }, @@ -279,6 +293,13 @@ exports[`renders avatar with text and custom colors 1`] = ` "textAlign": "center", "textAlignVertical": "center", }, + { + "fontFamily": "System", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0.15, + "lineHeight": 24, + }, { "color": "#FFFFFF", "fontSize": 32, @@ -299,8 +320,8 @@ exports[`renders avatar with text and custom size 1`] = ` style={ [ { - "backgroundColor": "rgba(103, 80, 164, 1)", - "borderRadius": 48, + "backgroundColor": "rgba(234, 221, 255, 1)", + "borderRadius": 9999, "height": 96, "width": 96, }, @@ -334,7 +355,14 @@ exports[`renders avatar with text and custom size 1`] = ` "textAlignVertical": "center", }, { - "color": "#ffffff", + "fontFamily": "System", + "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0.15, + "lineHeight": 24, + }, + { + "color": "rgba(33, 0, 93, 1)", "fontSize": 48, "lineHeight": 96, }, diff --git a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap index b1b80b7341..fb23016613 100644 --- a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap @@ -55,14 +55,18 @@ exports[`DataTable.Cell renders data table cell 1`] = ` }, { "color": "rgba(29, 27, 32, 1)", - "fontFamily": "System", - "fontWeight": "400", - "letterSpacing": 0, - }, - { "writingDirection": "ltr", }, - undefined, + [ + { + "fontFamily": "System", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0.25, + "lineHeight": 20, + }, + undefined, + ], ] } testID="undefined-text-container" @@ -129,14 +133,18 @@ exports[`DataTable.Cell renders right aligned data table cell 1`] = ` }, { "color": "rgba(29, 27, 32, 1)", - "fontFamily": "System", - "fontWeight": "400", - "letterSpacing": 0, - }, - { "writingDirection": "ltr", }, - undefined, + [ + { + "fontFamily": "System", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0.25, + "lineHeight": 20, + }, + undefined, + ], ] } testID="undefined-text-container" @@ -214,28 +222,30 @@ exports[`DataTable.Header renders data table header 1`] = ` }, { "color": "rgba(29, 27, 32, 1)", - "fontFamily": "System", - "fontWeight": "400", - "letterSpacing": 0, - }, - { "writingDirection": "ltr", }, [ { - "alignItems": "center", + "fontFamily": "System", "fontSize": 12, "fontWeight": "500", - "lineHeight": 24, - }, - { - "maxHeight": 48, - }, - {}, - { - "color": "rgba(73, 69, 79, 1)", + "letterSpacing": 0.5, + "lineHeight": 16, }, - undefined, + [ + { + "alignItems": "center", + "lineHeight": 24, + }, + { + "maxHeight": 48, + }, + {}, + { + "color": "rgba(73, 69, 79, 1)", + }, + undefined, + ], ], ] } @@ -295,28 +305,30 @@ exports[`DataTable.Header renders data table header 1`] = ` }, { "color": "rgba(29, 27, 32, 1)", - "fontFamily": "System", - "fontWeight": "400", - "letterSpacing": 0, - }, - { "writingDirection": "ltr", }, [ { - "alignItems": "center", + "fontFamily": "System", "fontSize": 12, "fontWeight": "500", - "lineHeight": 24, - }, - { - "maxHeight": 48, - }, - {}, - { - "color": "rgba(73, 69, 79, 1)", + "letterSpacing": 0.5, + "lineHeight": 16, }, - undefined, + [ + { + "alignItems": "center", + "lineHeight": 24, + }, + { + "maxHeight": 48, + }, + {}, + { + "color": "rgba(73, 69, 79, 1)", + }, + undefined, + ], ], ] } @@ -353,21 +365,24 @@ exports[`DataTable.Pagination renders data table pagination 1`] = ` }, { "color": "rgba(29, 27, 32, 1)", - "fontFamily": "System", - "fontWeight": "400", - "letterSpacing": 0, - }, - { "writingDirection": "ltr", }, [ { + "fontFamily": "System", "fontSize": 12, - "marginRight": 16, - }, - { - "color": "rgba(73, 69, 79, 1)", + "fontWeight": "400", + "letterSpacing": 0.4, + "lineHeight": 16, }, + [ + { + "marginRight": 16, + }, + { + "color": "rgba(73, 69, 79, 1)", + }, + ], ], ] } @@ -649,21 +664,24 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu }, { "color": "rgba(29, 27, 32, 1)", - "fontFamily": "System", - "fontWeight": "400", - "letterSpacing": 0, - }, - { "writingDirection": "ltr", }, [ { + "fontFamily": "System", "fontSize": 12, - "marginRight": 16, - }, - { - "color": "rgba(73, 69, 79, 1)", + "fontWeight": "400", + "letterSpacing": 0.4, + "lineHeight": 16, }, + [ + { + "marginRight": 16, + }, + { + "color": "rgba(73, 69, 79, 1)", + }, + ], ], ] } @@ -1187,21 +1205,24 @@ exports[`DataTable.Pagination renders data table pagination with label 1`] = ` }, { "color": "rgba(29, 27, 32, 1)", - "fontFamily": "System", - "fontWeight": "400", - "letterSpacing": 0, - }, - { "writingDirection": "ltr", }, [ { + "fontFamily": "System", "fontSize": 12, - "marginRight": 16, - }, - { - "color": "rgba(73, 69, 79, 1)", + "fontWeight": "400", + "letterSpacing": 0.4, + "lineHeight": 16, }, + [ + { + "marginRight": 16, + }, + { + "color": "rgba(73, 69, 79, 1)", + }, + ], ], ] } @@ -1496,21 +1517,24 @@ exports[`DataTable.Pagination renders data table pagination with options select }, { "color": "rgba(29, 27, 32, 1)", - "fontFamily": "System", - "fontWeight": "400", - "letterSpacing": 0, - }, - { "writingDirection": "ltr", }, [ { + "fontFamily": "System", "fontSize": 12, - "marginRight": 16, - }, - { - "color": "rgba(73, 69, 79, 1)", + "fontWeight": "400", + "letterSpacing": 0.4, + "lineHeight": 16, }, + [ + { + "marginRight": 16, + }, + { + "color": "rgba(73, 69, 79, 1)", + }, + ], ], ] } @@ -1789,21 +1813,24 @@ exports[`DataTable.Pagination renders data table pagination with options select }, { "color": "rgba(29, 27, 32, 1)", - "fontFamily": "System", - "fontWeight": "400", - "letterSpacing": 0, - }, - { "writingDirection": "ltr", }, [ { + "fontFamily": "System", "fontSize": 12, - "marginRight": 16, - }, - { - "color": "rgba(73, 69, 79, 1)", + "fontWeight": "400", + "letterSpacing": 0.4, + "lineHeight": 16, }, + [ + { + "marginRight": 16, + }, + { + "color": "rgba(73, 69, 79, 1)", + }, + ], ], ] } @@ -2402,28 +2429,30 @@ exports[`DataTable.Title renders data table title with press handler 1`] = ` }, { "color": "rgba(29, 27, 32, 1)", - "fontFamily": "System", - "fontWeight": "400", - "letterSpacing": 0, - }, - { "writingDirection": "ltr", }, [ { - "alignItems": "center", + "fontFamily": "System", "fontSize": 12, "fontWeight": "500", - "lineHeight": 24, - }, - { - "maxHeight": 48, - }, - {}, - { - "marginLeft": 8, + "letterSpacing": 0.5, + "lineHeight": 16, }, - undefined, + [ + { + "alignItems": "center", + "lineHeight": 24, + }, + { + "maxHeight": 48, + }, + {}, + { + "marginLeft": 8, + }, + undefined, + ], ], ] } @@ -2534,28 +2563,30 @@ exports[`DataTable.Title renders data table title with sort icon 1`] = ` }, { "color": "rgba(29, 27, 32, 1)", - "fontFamily": "System", - "fontWeight": "400", - "letterSpacing": 0, - }, - { "writingDirection": "ltr", }, [ { - "alignItems": "center", + "fontFamily": "System", "fontSize": 12, "fontWeight": "500", - "lineHeight": 24, - }, - { - "maxHeight": 48, + "letterSpacing": 0.5, + "lineHeight": 16, }, - {}, - { - "marginLeft": 8, - }, - undefined, + [ + { + "alignItems": "center", + "lineHeight": 24, + }, + { + "maxHeight": 48, + }, + {}, + { + "marginLeft": 8, + }, + undefined, + ], ], ] } @@ -2620,28 +2651,30 @@ exports[`DataTable.Title renders right aligned data table title 1`] = ` }, { "color": "rgba(29, 27, 32, 1)", - "fontFamily": "System", - "fontWeight": "400", - "letterSpacing": 0, - }, - { "writingDirection": "ltr", }, [ { - "alignItems": "center", + "fontFamily": "System", "fontSize": 12, "fontWeight": "500", - "lineHeight": 24, + "letterSpacing": 0.5, + "lineHeight": 16, }, - { - "maxHeight": 48, - }, - {}, - { - "color": "rgba(73, 69, 79, 1)", - }, - undefined, + [ + { + "alignItems": "center", + "lineHeight": 24, + }, + { + "maxHeight": 48, + }, + {}, + { + "color": "rgba(73, 69, 79, 1)", + }, + undefined, + ], ], ] }