Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/6.x/docs/guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ The `style` props for `Appbar` and `Appbar.Header` no longer accept `Animated.Va

The `style.elevation` property is no longer supported. Use the `elevated` prop to control Appbar elevation.

### Banner

The banner background is now always `theme.colors.surfaceContainerLow` and no longer depends on the `elevation` prop, which only controls the shadow. At the default `elevation` of `1` the rendered color is unchanged.

### Menu

The menu background is now always `theme.colors.surfaceContainer`, following the Material Design 3 spec, and no longer depends on the `elevation` prop, which only controls the shadow. At the default `elevation` of `2` the rendered color is unchanged.

### Surface

- The `elevation` prop no longer accepts a React Native `Animated.Value`. Any `elevation` changes are animated automatically.
Expand All @@ -138,6 +146,7 @@ The `style.elevation` property is no longer supported. Use the `elevated` prop t
- The `pointerEvents` prop is no longer supported as it's deprecated in React Native Web. You can specify `pointerEvents` in the `style` prop instead.
- The `overflow: 'hidden'` style is no longer supported in `style` as it can clip shadows. You can nest a `View` inside the `Surface` and apply `overflow: 'hidden'` to that instead.
- The default `testID` for `Surface` was removed. You can specify a `testID` explicitly if you need it.
- A new `container` prop sets the background to a semantic surface-family color role from the theme (e.g. `container="surfaceContainerLow"`). When `container` is set, `elevation` only controls the shadow. Precedence: `backgroundColor` > `container` > the color derived from `elevation`.

e.g.:

Expand Down
6 changes: 5 additions & 1 deletion docs/src/data/themeColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const themeColors = {
},
Banner: {
'-': {
backgroundColor: 'theme.colors.surfaceContainerLow',
textColor: 'theme.colors.onSurface',
'action textColor': 'theme.colors.primary',
},
Expand Down Expand Up @@ -246,7 +247,7 @@ export const themeColors = {
},
Menu: {
'-': {
backgroundColor: 'theme.colors.elevation.level2',
backgroundColor: 'theme.colors.surfaceContainer',
},
},
'Menu.Item': {
Expand Down Expand Up @@ -314,6 +315,9 @@ export const themeColors = {
elevated: {
backgroundColor: 'theme.colors.elevation[elevation]',
},
'with container': {
backgroundColor: 'theme.colors[container]',
},
},
Text: {
'-': {
Expand Down
61 changes: 59 additions & 2 deletions example/src/Examples/SurfaceExample.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import * as React from 'react';
import { ScrollView, StyleSheet, View } from 'react-native';

import { Surface, Text, Palette, List, IconButton } from 'react-native-paper';
import type { Elevation } from 'react-native-paper';
import {
Surface,
Text,
Palette,
List,
IconButton,
useTheme,
} from 'react-native-paper';
import type { Elevation, SurfaceRole } from 'react-native-paper';

import ScreenWrapper from '../ScreenWrapper';

const elevationLevels: Elevation[] = [0, 1, 2, 3, 4, 5];

const containerRoles: SurfaceRole[] = [
'surface',
'surfaceDim',
'surfaceBright',
'surfaceVariant',
'surfaceContainerLowest',
'surfaceContainerLow',
'surfaceContainer',
'surfaceContainerHigh',
'surfaceContainerHighest',
'inverseSurface',
];

const AnimatedSurface = () => {
const [index, setIndex] = React.useState(3);

Expand Down Expand Up @@ -37,8 +57,20 @@ const AnimatedSurface = () => {
};

const SurfaceExample = () => {
const theme = useTheme();

const elevationValues: Elevation[] = [0, 1, 2, 3, 4, 5];

const onColorFor = (role: SurfaceRole) => {
if (role === 'inverseSurface') {
return theme.colors.inverseOnSurface;
}
if (role === 'surfaceVariant') {
return theme.colors.onSurfaceVariant;
}
return theme.colors.onSurface;
};

const renderSurface = (index: Elevation, mode: 'flat' | 'elevated') => (
<Surface
key={index}
Expand Down Expand Up @@ -75,6 +107,31 @@ const SurfaceExample = () => {
</ScrollView>
</List.Section>

<List.Section title="Semantic container colors">
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.scroll}
>
{containerRoles.map((role) => (
<Surface
key={role}
style={styles.surface}
borderRadius={8}
mode="flat"
container={role}
>
<Text
variant="bodySmall"
style={[styles.centerText, { color: onColorFor(role) }]}
>
{role}
</Text>
</Surface>
))}
</ScrollView>
</List.Section>

<List.Section title="Animated elevation">
<AnimatedSurface />
</List.Section>
Expand Down
4 changes: 3 additions & 1 deletion src/components/Banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ export type Props = Omit<ViewProps, 'style'> & {
contentStyle?: StyleProp<ViewStyle>;
/**
* @supported Available in v5.x with theme version 3
* Changes Banner shadow and background on iOS and Android.
* Changes Banner shadow on iOS and Android. The banner background is
* `theme.colors.surfaceContainerLow` and is not affected by `elevation`.
*/
elevation?: Elevation;
/**
Expand Down Expand Up @@ -211,6 +212,7 @@ const Banner = ({
{...rest}
style={[surfaceStyle, style]}
theme={theme}
container="surfaceContainerLow"
elevation={elevation}
>
<View style={[styles.wrapper, contentStyle]}>
Expand Down
7 changes: 4 additions & 3 deletions src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,14 @@ export type Props = {
contentStyle?: StyleProp<SurfaceStyle>;
style?: StyleProp<ViewStyle>;
/**
* Elevation level of the menu's content. Shadow styles are calculated based on this value. Default `backgroundColor` is taken from the corresponding `theme.colors.elevation` property. By default equals `2`.
* Elevation level of the menu's content. Shadow styles are calculated based on this value. The menu background is `theme.colors.surfaceContainer` per the MD3 spec and is not affected by `elevation`. By default equals `2`.
* @supported Available in v5.x with theme version 3
*/
elevation?: Elevation;
/**
* Mode of the menu's content.
* - `elevated` - Surface with a shadow and background color corresponding to set `elevation` value.
* - `flat` - Surface without a shadow, with the background color corresponding to set `elevation` value.
* - `elevated` - Surface with a shadow corresponding to set `elevation` value.
* - `flat` - Surface without a shadow.
*
* @supported Available in v5.x with theme version 3
*/
Expand Down Expand Up @@ -704,6 +704,7 @@ const Menu = ({
>
<Surface
mode={mode}
container="surfaceContainer"
borderRadius={theme.shapes.corner.extraSmall}
style={[
styles.shadowMenuContainer,
Expand Down
52 changes: 43 additions & 9 deletions src/components/Surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Animated, {

import { useInternalTheme } from '../core/theming';
import { androidElevationLevels, shadow } from '../theme/tokens/sys/elevation';
import type { Elevation, ThemeProp } from '../theme/types';
import type { Elevation, SurfaceRole, ThemeProp } from '../theme/types';

type AnimatedStyleProp<Key extends keyof ViewStyle> = Extract<
AnimatedStyle<Required<Pick<ViewStyle, Key>>>,
Expand All @@ -22,8 +22,8 @@ type BorderRadius = AnimatedStyleProp<'borderRadius'>;

type SurfaceVisualProps = {
/**
* Background color of the Surface. Overrides the color derived from
* `elevation`.
* Background color of the Surface. Overrides both `container` and the
* color derived from `elevation`.
*/
backgroundColor?: ColorValue;
/**
Expand Down Expand Up @@ -104,22 +104,43 @@ export type Props = Omit<ViewProps, 'pointerEvents' | 'style'> &
* To achieve the same effect, wrap the content in a child View with the overflow style.
*/
style?: StyleProp<SurfaceStyle>;
/**
* Semantic color role of the Surface background, resolved from the
* theme, e.g. `container="surfaceContainerLow"` renders with
* `theme.colors.surfaceContainerLow`.
*
* When `container` is set, `elevation` only controls the shadow (and
* z-order on Android) and no longer affects the background color.
*
* Precedence: `backgroundColor` > `container` >
* `theme.colors.elevation[levelN]` derived from `elevation`.
*
* Prefer `container` when the fill is an MD3 surface-family role (as
* component specs define it, e.g. menus use `surfaceContainer`). Use
* `backgroundColor` only for raw, non-semantic colors. Components with
* variant-driven fills outside the surface family should keep resolving
* their own tokens and pass the result via `backgroundColor`.
*/
container?: SurfaceRole;
/**
* @supported Available in v5.x with theme version 3
* Changes shadows and background on iOS and Android.
* Used to create UI hierarchy between components.
*
* Note: If `mode` is set to `flat`, Surface doesn't have a shadow.
*
* Note: If `backgroundColor` or `container` is set, `elevation` only
* affects the shadow.
*
* Note: In version 2 the `elevation` prop was accepted via `style` prop i.e. `style={{ elevation: 4 }}`.
* It's no longer supported with theme version 3 and you should use `elevation` property instead.
*/
elevation?: Elevation;
/**
* @supported Available in v5.x with theme version 3
* Mode of the Surface.
* - `elevated` - Surface with a shadow and background color corresponding to set `elevation` value.
* - `flat` - Surface without a shadow, with the background color corresponding to set `elevation` value.
* - `elevated` - Surface with a shadow and background color corresponding to set `elevation` value (unless `backgroundColor` or `container` is set).
* - `flat` - Surface without a shadow, with the background color corresponding to set `elevation` value (unless `backgroundColor` or `container` is set).
*/
mode?: 'flat' | 'elevated';
/**
Expand Down Expand Up @@ -150,9 +171,19 @@ export type Props = Omit<ViewProps, 'pointerEvents' | 'style'> &
* import { StyleSheet } from 'react-native';
*
* const MyComponent = () => (
* <Surface style={styles.surface} elevation={4} borderRadius={8}>
* <Text>Surface</Text>
* </Surface>
* <>
* <Surface style={styles.surface} elevation={4} borderRadius={8}>
* <Text>Surface</Text>
* </Surface>
* <Surface
* style={styles.surface}
* container="surfaceContainerLow"
* elevation={1}
* borderRadius={8}
* >
* <Text>Semantic container color</Text>
* </Surface>
* </>
* );
*
* export default MyComponent;
Expand All @@ -170,6 +201,7 @@ export type Props = Omit<ViewProps, 'pointerEvents' | 'style'> &
*/
const Surface = ({
elevation = 1,
container,
children,
theme: overriddenTheme,
style,
Expand Down Expand Up @@ -199,7 +231,9 @@ const Surface = ({
const { colors } = theme;

const backgroundColor =
customBackgroundColor ?? colors.elevation?.[`level${elevation}`];
customBackgroundColor ??
(container != null ? colors[container] : undefined) ??
colors.elevation?.[`level${elevation}`];

const backgroundStyle = { backgroundColor };

Expand Down
15 changes: 14 additions & 1 deletion src/components/__tests__/Banner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
} from '@jest/globals';
import { act } from '@testing-library/react-native';

import { render } from '../../test-utils';
import { render, screen } from '../../test-utils';
import { LightTheme } from '../../theme/schemes';
import Banner from '../Banner';

it('renders hidden banner, without action buttons and without image', async () => {
Expand Down Expand Up @@ -104,6 +105,18 @@ it('renders visible banner, with action buttons and with image', async () => {
expect(tree).toMatchSnapshot();
});

it('renders banner with surfaceContainerLow background', async () => {
await render(
<Banner visible testID="banner">
Text
</Banner>
);

expect(screen.getByTestId('banner')).toHaveStyle({
backgroundColor: LightTheme.colors.surfaceContainerLow,
});
});

it('render visible banner, with custom theme', async () => {
const tree = (
await render(
Expand Down
4 changes: 2 additions & 2 deletions src/components/__tests__/Menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ it('renders not visible menu', async () => {
const elevations: Elevation[] = [0, 1, 2, 3, 4, 5];

elevations.forEach((elevation) =>
it(`renders menu with background color based on elevation value = ${elevation}`, async () => {
it(`renders menu with surfaceContainer background regardless of elevation value = ${elevation}`, async () => {
const testID = 'menu-with-elevation';

await render(
Expand All @@ -71,7 +71,7 @@ elevations.forEach((elevation) =>
);

expect(screen.getByTestId(testID)).toHaveStyle({
backgroundColor: LightTheme.colors.elevation[`level${elevation}`],
backgroundColor: LightTheme.colors.surfaceContainer,
});
})
);
Expand Down
Loading
Loading