diff --git a/src/components/Avatar/AvatarIcon.tsx b/src/components/Avatar/AvatarIcon.tsx index 9c3b813e59..5e0c5d0c00 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 '../../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 51330dd70c..17d61354ff 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 '../../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 4f28cf0390..5c4a24623d 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, StyleSheet, View } from 'react-native'; +import { Animated, Easing, StyleSheet, View } from 'react-native'; import type { StyleProp, ViewStyle } from 'react-native'; import type { LayoutChangeEvent } from 'react-native'; @@ -14,6 +14,7 @@ import { useInternalTheme } from '../core/theming'; import type { $Omit, $RemoveChildren, Theme, ThemeProp } from '../types'; const DEFAULT_MAX_WIDTH = 960; +const ICON_SIZE = 40; export type Props = $Omit<$RemoveChildren, 'mode'> & { /** @@ -148,6 +149,7 @@ const Banner = ({ const showCallback = useLatestCallback(onShowAnimationFinished); const hideCallback = useLatestCallback(onHideAnimationFinished); + const { duration, easing } = theme.motion; const { scale } = theme.animation; const opacity = position.interpolate({ @@ -159,20 +161,22 @@ const Banner = ({ if (visible) { // show Animated.timing(position, { - duration: 250 * scale, + duration: duration.medium1 * scale, toValue: 1, useNativeDriver: false, + easing: Easing.bezier(...easing.standard), }).start(showCallback); } else { // hide Animated.timing(position, { - duration: 200 * scale, + duration: duration.short4 * scale, toValue: 0, useNativeDriver: false, + easing: Easing.bezier(...easing.standard), }).start(hideCallback); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [visible, position, scale]); + }, [visible, position, duration, easing, scale]); const handleLayout = ({ nativeEvent }: LayoutChangeEvent) => { const { height } = nativeEvent.layout; @@ -221,7 +225,7 @@ const Banner = ({ {icon ? ( - + ) : null} )} { Animated.timing(spinAnim, { toValue: sortDirection === 'ascending' ? 0 : 1, - duration: 150, + duration: duration.short3 * scale, + easing: Easing.bezier(...easing.standard), useNativeDriver: true, }).start(); - }, [sortDirection, spinAnim]); + }, [sortDirection, spinAnim, duration, easing, scale]); const textColor = theme.colors.onSurface; @@ -132,6 +142,7 @@ const DataTableTitle = ({ {icon} { hideCallback = undefined; }); + it('uses zero-duration animations when animation scale is disabled', async () => { + const timingSpy = jest.spyOn(Animated, 'timing'); + const view = await render( + + Text + + ); + + expect(timingSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ duration: 0 }) + ); + + await view.rerender( + + Text + + ); + + expect(timingSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ duration: 0 }) + ); + + timingSpy.mockRestore(); + }); + 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..de147e6520 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 { Animated } from 'react-native'; + +import { describe, expect, it, jest } from '@jest/globals'; import { render, screen } from '../../test-utils'; import Checkbox from '../Checkbox'; @@ -20,6 +22,26 @@ describe('DataTable.Header', () => { }); describe('DataTable.Title', () => { + it('uses zero-duration sort animation when animation scale is disabled', async () => { + const timingSpy = jest.spyOn(Animated, 'timing'); + + await render( + + Dessert + + ); + + expect(timingSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ duration: 0 }) + ); + + timingSpy.mockRestore(); + }); + 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 e9bc774f78..8685854cd8 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)", + }, + ], ], ] } @@ -689,21 +704,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)", + }, + ], ], ] } @@ -1307,21 +1325,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)", + }, + ], ], ] } @@ -1656,21 +1677,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)", + }, + ], ], ] } @@ -1893,21 +1917,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)", + }, + ], ], ] } @@ -2582,28 +2609,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, + "letterSpacing": 0.5, + "lineHeight": 16, }, - {}, - { - "marginLeft": 8, - }, - undefined, + [ + { + "alignItems": "center", + "lineHeight": 24, + }, + { + "maxHeight": 48, + }, + {}, + { + "marginLeft": 8, + }, + undefined, + ], ], ] } @@ -2710,28 +2739,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, + "letterSpacing": 0.5, + "lineHeight": 16, }, - { - "maxHeight": 48, - }, - {}, - { - "marginLeft": 8, - }, - undefined, + [ + { + "alignItems": "center", + "lineHeight": 24, + }, + { + "maxHeight": 48, + }, + {}, + { + "marginLeft": 8, + }, + undefined, + ], ], ] } @@ -2796,28 +2827,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, + ], ], ] }