diff --git a/docs/pages/10.migration-guide-to-5.0.md b/docs/pages/10.migration-guide-to-5.0.md index bb19c0fd7a..84817916bc 100644 --- a/docs/pages/10.migration-guide-to-5.0.md +++ b/docs/pages/10.migration-guide-to-5.0.md @@ -75,6 +75,7 @@ theme: { inverseOnSurface, inverseSurface, inversePrimary, + backdrop, elevation: { level0, level1, diff --git a/docs/pages/2.theming.md b/docs/pages/2.theming.md index 777e86dd07..c3afb57b0d 100644 --- a/docs/pages/2.theming.md +++ b/docs/pages/2.theming.md @@ -128,6 +128,7 @@ A theme usually contains the following properties: - `inverseOnSurface` - `inverseSurface` - `inversePrimary` + - `backdrop` - `typescale` (`object`): various fonts styling properties under the text variant key used in component. - [`variant` e.g. `labelMedium`] (`object`): diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx index 758e77ae16..6c18bdbaf1 100644 --- a/src/components/Dialog/Dialog.tsx +++ b/src/components/Dialog/Dialog.tsx @@ -114,6 +114,7 @@ const Dialog = ({ styles.container, style, ]} + theme={theme} > {React.Children.toArray(children) .filter((child) => child != null && typeof child !== 'boolean') diff --git a/src/components/FAB/FABGroup.tsx b/src/components/FAB/FABGroup.tsx index 564a3cc036..1c8115f8c9 100644 --- a/src/components/FAB/FABGroup.tsx +++ b/src/components/FAB/FABGroup.tsx @@ -22,13 +22,14 @@ type Props = { * An action item should contain the following properties: * - `icon`: icon to display (required) * - `label`: optional label text - * - `accessibilityLabel`: accessibility label for the action, uses label by default if specified * - `color`: custom icon color of the action item * - `labelTextColor`: custom label text color of the action item + * - `accessibilityLabel`: accessibility label for the action, uses label by default if specified * - `style`: pass additional styles for the fab item, for example, `backgroundColor` * - `labelStyle`: pass additional styles for the fab item label, for example, `backgroundColor` - * - `size`: size of action item. Defaults to `small`. @supported Available in v5.x * - `onPress`: callback that is called when `FAB` is pressed (required) + * - `size`: size of action item. Defaults to `small`. @supported Available in v5.x + * - `testID`: testID to be used on tests */ actions: Array<{ icon: IconSource; @@ -55,6 +56,10 @@ type Props = { * Custom color for the `FAB`. */ color?: string; + /** + * Custom backdrop color for opened speed dial background. + */ + backdropColor?: string; /** * Function to execute on pressing the `FAB`. */ @@ -170,6 +175,7 @@ const FABGroup = ({ onStateChange, color: colorProp, variant = 'primary', + backdropColor: customBackdropColor, }: Props) => { const { current: backdrop } = React.useRef( new Animated.Value(0) @@ -238,7 +244,7 @@ const FABGroup = ({ const toggle = () => onStateChange({ open: !open }); const { labelColor, backdropColor, stackedFABBackgroundColor } = - getFABGroupColors({ theme }); + getFABGroupColors({ theme, customBackdropColor }); const backdropOpacity = open ? backdrop.interpolate({ diff --git a/src/components/FAB/utils.ts b/src/components/FAB/utils.ts index 0ffddee29c..fb99e4c1db 100644 --- a/src/components/FAB/utils.ts +++ b/src/components/FAB/utils.ts @@ -305,7 +305,16 @@ const getLabelColor = ({ theme }: { theme: Theme }) => { return color(theme.colors.text).fade(0.54).rgb().string(); }; -const getBackdropColor = ({ theme }: { theme: Theme }) => { +const getBackdropColor = ({ + theme, + customBackdropColor, +}: { + theme: Theme; + customBackdropColor?: string; +}) => { + if (customBackdropColor) { + return customBackdropColor; + } if (theme.isV3) { return color(theme.colors.background).alpha(0.95).rgb().string(); } @@ -319,10 +328,16 @@ const getStackedFABBackgroundColor = ({ theme }: { theme: Theme }) => { return theme.colors.surface; }; -export const getFABGroupColors = ({ theme }: { theme: Theme }) => { +export const getFABGroupColors = ({ + theme, + customBackdropColor, +}: { + theme: Theme; + customBackdropColor?: string; +}) => { return { labelColor: getLabelColor({ theme }), - backdropColor: getBackdropColor({ theme }), + backdropColor: getBackdropColor({ theme, customBackdropColor }), stackedFABBackgroundColor: getStackedFABBackgroundColor({ theme }), }; }; diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 1ac15279d5..b9a2b9b982 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -10,16 +10,14 @@ import { View, NativeEventSubscription, } from 'react-native'; -import color from 'color'; import { getStatusBarHeight, getBottomSpace, } from 'react-native-iphone-x-helper'; import Surface from './Surface'; -import { useTheme } from '../core/theming'; +import { withTheme } from '../core/theming'; import useAnimatedValue from '../utils/useAnimatedValue'; import { addEventListener } from '../utils/addEventListener'; -import { MD3Colors } from '../styles/themes/v3/tokens'; type Props = { /** @@ -51,6 +49,14 @@ type Props = { * Use this prop to change the default wrapper style or to override safe area insets with marginTop and marginBottom. */ style?: StyleProp; + /** + * @optional + */ + theme: ReactNativePaper.Theme; + /** + * testID to be used on tests. + */ + testID?: string; }; const DEFAULT_DURATION = 220; @@ -96,7 +102,7 @@ const BOTTOM_INSET = getBottomSpace(); * export default MyComponent; * ``` */ -export default function Modal({ +function Modal({ dismissable = true, visible = false, overlayAccessibilityLabel = 'Close modal', @@ -104,6 +110,8 @@ export default function Modal({ children, contentContainerStyle, style, + theme, + testID = 'modal', }: Props) { const visibleRef = React.useRef(visible); @@ -111,8 +119,6 @@ export default function Modal({ visibleRef.current = visible; }); - const theme = useTheme(); - const { scale } = theme.animation; const opacity = useAnimatedValue(visible ? 1 : 0); @@ -209,6 +215,7 @@ export default function Modal({ accessibilityLiveRegion="polite" style={StyleSheet.absoluteFill} onAccessibilityEscape={hideModal} + testID={testID} > { + it('should return custom color', () => { + expect( + getFABGroupColors({ + theme: getTheme(), + customBackdropColor: 'transparent', + }) + ).toMatchObject({ + backdropColor: 'transparent', + }); + }); + + it('should return correct backdrop color, for theme version 3', () => { + expect( + getFABGroupColors({ + theme: getTheme(), + }) + ).toMatchObject({ + backdropColor: color(getTheme().colors.background) + .alpha(0.95) + .rgb() + .string(), + }); + }); + + it('should return correct backdrop color, for theme version 2', () => { + expect( + getFABGroupColors({ + theme: getTheme(false, false), + }) + ).toMatchObject({ + backdropColor: getTheme(false, false).colors.backdrop, + }); + }); +}); + +describe('getFABGroupColors - label color', () => { + it('should return correct theme color, for theme version 3', () => { + expect( + getFABGroupColors({ + theme: getTheme(), + }) + ).toMatchObject({ + labelColor: getTheme().colors.onSurface, + }); + }); + + it('should return correct theme color, dark mode, for theme version 2', () => { + expect( + getFABGroupColors({ + theme: getTheme(true, false), + }) + ).toMatchObject({ + labelColor: getTheme(true, false).colors.text, + }); + }); + + it('should return correct theme color, light mode, for theme version 2', () => { + expect( + getFABGroupColors({ + theme: getTheme(false, false), + }) + ).toMatchObject({ + labelColor: color(getTheme(false, false).colors.text) + .fade(0.54) + .rgb() + .string(), + }); + }); +}); + +describe('getFABGroupColors - stacked FAB background color', () => { + it('should return correct theme color, for theme version 3', () => { + expect( + getFABGroupColors({ + theme: getTheme(), + }) + ).toMatchObject({ + stackedFABBackgroundColor: getTheme().colors.elevation.level3, + }); + }); + + it('should return correct theme color, dark mode, for theme version 2', () => { + expect( + getFABGroupColors({ + theme: getTheme(false, false), + }) + ).toMatchObject({ + stackedFABBackgroundColor: getTheme(false, false).colors.surface, + }); + }); +}); diff --git a/src/components/__tests__/Modal.test.js b/src/components/__tests__/Modal.test.js new file mode 100644 index 0000000000..120bfd1962 --- /dev/null +++ b/src/components/__tests__/Modal.test.js @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { render } from 'react-native-testing-library'; +import Modal from '../Modal'; + +describe('Modal', () => { + it('should render custom backdrop color', () => { + const { getByTestId } = render( + + ); + + expect(getByTestId('modal-backdrop').props.style).toEqual( + expect.arrayContaining([ + expect.objectContaining({ backgroundColor: 'transparent' }), + ]) + ); + }); +}); diff --git a/src/components/__tests__/__snapshots__/ListSection.test.js.snap b/src/components/__tests__/__snapshots__/ListSection.test.js.snap index ca5713b6ed..e3124d6ced 100644 --- a/src/components/__tests__/__snapshots__/ListSection.test.js.snap +++ b/src/components/__tests__/__snapshots__/ListSection.test.js.snap @@ -16,6 +16,7 @@ exports[`renders list section with custom title style 1`] = ` "scale": 1, }, "colors": Object { + "backdrop": "rgba(50, 47, 55, 0.4)", "background": "rgba(255, 251, 254, 1)", "elevation": Object { "level0": "transparent", @@ -47,7 +48,6 @@ exports[`renders list section with custom title style 1`] = ` "primaryContainer": "rgba(234, 221, 255, 1)", "secondary": "rgba(98, 91, 113, 1)", "secondaryContainer": "rgba(232, 222, 248, 1)", - "shadow": "rgba(0, 0, 0, 1)", "surface": "rgba(255, 251, 254, 1)", "surfaceDisabled": "rgba(28, 27, 31, 0.12)", "surfaceVariant": "rgba(231, 224, 236, 1)", @@ -456,6 +456,7 @@ exports[`renders list section with subheader 1`] = ` "scale": 1, }, "colors": Object { + "backdrop": "rgba(50, 47, 55, 0.4)", "background": "rgba(255, 251, 254, 1)", "elevation": Object { "level0": "transparent", @@ -487,7 +488,6 @@ exports[`renders list section with subheader 1`] = ` "primaryContainer": "rgba(234, 221, 255, 1)", "secondary": "rgba(98, 91, 113, 1)", "secondaryContainer": "rgba(232, 222, 248, 1)", - "shadow": "rgba(0, 0, 0, 1)", "surface": "rgba(255, 251, 254, 1)", "surfaceDisabled": "rgba(28, 27, 31, 0.12)", "surfaceVariant": "rgba(231, 224, 236, 1)", @@ -894,6 +894,7 @@ exports[`renders list section without subheader 1`] = ` "scale": 1, }, "colors": Object { + "backdrop": "rgba(50, 47, 55, 0.4)", "background": "rgba(255, 251, 254, 1)", "elevation": Object { "level0": "transparent", @@ -925,7 +926,6 @@ exports[`renders list section without subheader 1`] = ` "primaryContainer": "rgba(234, 221, 255, 1)", "secondary": "rgba(98, 91, 113, 1)", "secondaryContainer": "rgba(232, 222, 248, 1)", - "shadow": "rgba(0, 0, 0, 1)", "surface": "rgba(255, 251, 254, 1)", "surfaceDisabled": "rgba(28, 27, 31, 0.12)", "surfaceVariant": "rgba(231, 224, 236, 1)", diff --git a/src/styles/themes/v3/DarkTheme.tsx b/src/styles/themes/v3/DarkTheme.tsx index 672ff972d4..4fc29685e3 100644 --- a/src/styles/themes/v3/DarkTheme.tsx +++ b/src/styles/themes/v3/DarkTheme.tsx @@ -1,7 +1,7 @@ import color from 'color'; import type { MD3Theme } from '../../../types'; import { MD3LightTheme } from './LightTheme'; -import { tokens } from './tokens'; +import { MD3Colors, tokens } from './tokens'; const { palette, opacity } = tokens.md.ref; @@ -43,10 +43,10 @@ export const MD3DarkTheme: MD3Theme = { onErrorContainer: palette.error80, onBackground: palette.neutral90, outline: palette.neutralVariant60, - shadow: palette.neutral0, inverseSurface: palette.neutral90, inverseOnSurface: palette.neutral20, inversePrimary: palette.primary40, + backdrop: color(MD3Colors.neutralVariant20).alpha(0.4).rgb().string(), elevation: { level0: 'transparent', // Note: Color values with transparency cause RN to transfer shadows to children nodes diff --git a/src/styles/themes/v3/LightTheme.tsx b/src/styles/themes/v3/LightTheme.tsx index 75796c0f78..9496c93999 100644 --- a/src/styles/themes/v3/LightTheme.tsx +++ b/src/styles/themes/v3/LightTheme.tsx @@ -1,6 +1,6 @@ import color from 'color'; import type { MD3Theme } from '../../../types'; -import { tokens, typescale } from './tokens'; +import { MD3Colors, tokens, typescale } from './tokens'; const { palette, opacity } = tokens.md.ref; @@ -41,10 +41,10 @@ export const MD3LightTheme: MD3Theme = { onErrorContainer: palette.error10, onBackground: palette.neutral10, outline: palette.neutralVariant50, - shadow: palette.neutral0, inverseSurface: palette.neutral20, inverseOnSurface: palette.neutral95, inversePrimary: palette.primary80, + backdrop: color(MD3Colors.neutralVariant20).alpha(0.4).rgb().string(), elevation: { level0: 'transparent', // Note: Color values with transparency cause RN to transfer shadows to children nodes diff --git a/src/types.tsx b/src/types.tsx index fb9dfc19a9..4a4acb5990 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -66,10 +66,10 @@ export type MD3Colors = { onErrorContainer: string; onBackground: string; outline: string; - shadow: string; inverseSurface: string; inverseOnSurface: string; inversePrimary: string; + backdrop: string; elevation: MD3ElevationColors; };