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
56 changes: 56 additions & 0 deletions docs/6.x/docs/guides/theming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,62 @@ export default function Main() {
}
```

## Contrast levels

Material Design 3 defines three contrast levels, `standard`, `medium` and `high`. The higher levels increase the contrast between text and background colors, which helps users with low vision and makes the app easier to read in bright light.

Each level is an exported theme that you pass to `PaperProvider`, the same way as the default themes:

```js
import * as React from 'react';
import { PaperProvider, HighContrastLightTheme } from 'react-native-paper';

export default function Main() {
return (
<PaperProvider theme={HighContrastLightTheme}>
<App />
</PaperProvider>
);
}
```

The available themes are:

- `LightTheme` and `DarkTheme`
- `MediumContrastLightTheme` and `MediumContrastDarkTheme`
- `HighContrastLightTheme` and `HighContrastDarkTheme`

The `medium` and `high` schemes meet the WCAG contrast ratios of 4.5:1 and 7:1 respectively for every text and background role pair.

Note that passing a `theme` turns off automatic system dark mode, so pick the theme yourself when you follow the system setting:

```js
import { useColorScheme } from 'react-native';
import {
PaperProvider,
HighContrastDarkTheme,
HighContrastLightTheme,
} from 'react-native-paper';

export default function Main() {
const isDarkMode = useColorScheme() === 'dark';

return (
<PaperProvider
theme={isDarkMode ? HighContrastDarkTheme : HighContrastLightTheme}
>
<App />
</PaperProvider>
);
}
```

### Contrast and dynamic colors

There are matching dynamic themes: `MediumContrastDynamicLightTheme`, `HighContrastDynamicLightTheme`, `MediumContrastDynamicDarkTheme` and `HighContrastDynamicDarkTheme`.

Android exposes no contrast adjusted version of its system palette, so these fall back to the static schemes. Using the standard contrast system colors at a raised level would quietly lower the contrast you asked for.

## Adapting React Navigation theme

The `adaptNavigationTheme` function takes an existing React Navigation theme and returns a React Navigation theme using the colors from Material Design 3. This theme can be passed to `NavigationContainer` so that React Navigation's UI elements have the same color scheme as Paper.
Expand Down
23 changes: 23 additions & 0 deletions example/src/DrawerItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Drawer,
Palette,
Portal,
SegmentedButtons,
Switch,
Text,
TouchableRipple,
Expand Down Expand Up @@ -104,9 +105,11 @@ function DrawerItems() {
toggleCollapsed,
toggleCustomFont,
toggleRippleEffect,
setContrast,
customFontLoaded,
rippleEffectEnabled,
collapsed,
contrast,
rtl: isRTL,
theme: { dark: isDarkTheme },
shouldUseDynamicTheme,
Expand Down Expand Up @@ -191,6 +194,20 @@ function DrawerItems() {
</View>
</TouchableRipple>

<View style={[styles.preference, styles.contrastPreference]}>
<Text variant="labelLarge">Contrast</Text>
<SegmentedButtons
value={contrast}
onValueChange={(value) => setContrast(value)}
density="small"
buttons={[
{ value: 'standard', label: 'Standard' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
]}
/>
</View>

<TouchableRipple onPress={_handleToggleRTL}>
<View style={[styles.preference, styles.v3Preference]}>
<Text variant="labelLarge">RTL</Text>
Expand Down Expand Up @@ -277,6 +294,12 @@ const styles = StyleSheet.create({
height: 56,
paddingHorizontal: 28,
},
contrastPreference: {
flexDirection: 'column',
alignItems: 'stretch',
gap: 12,
paddingHorizontal: 28,
},
badge: {
alignSelf: 'center',
},
Expand Down
4 changes: 4 additions & 0 deletions example/src/PreferencesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import * as React from 'react';

import type { Theme } from 'react-native-paper';

type ContrastLevel = 'standard' | 'medium' | 'high';

export const PreferencesContext = React.createContext<{
toggleTheme: () => void;
toggleRtl: () => void;
toggleCollapsed: () => void;
toggleCustomFont: () => void;
toggleRippleEffect: () => void;
toggleShouldUseDynamicTheme?: () => void;
setContrast: (contrast: ContrastLevel) => void;
theme: Theme;
contrast: ContrastLevel;
rtl: boolean;
collapsed: boolean;
customFontLoaded: boolean;
Expand Down
72 changes: 57 additions & 15 deletions example/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import * as Updates from 'expo-updates';
import {
PaperProvider,
DarkTheme,
LightTheme,
DynamicLightTheme,
DynamicDarkTheme,
DynamicLightTheme,
HighContrastDarkTheme,
HighContrastDynamicDarkTheme,
HighContrastDynamicLightTheme,
HighContrastLightTheme,
LightTheme,
MediumContrastDarkTheme,
MediumContrastDynamicDarkTheme,
MediumContrastDynamicLightTheme,
MediumContrastLightTheme,
PaperProvider,
} from 'react-native-paper';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

Expand All @@ -26,12 +34,39 @@ import { PreferencesContext } from './PreferencesContext';
import App from './RootNavigator';
import { dynamicThemeSupported } from '../utils';
import {
CombinedDarkTheme,
CombinedDefaultTheme,
createCombinedTheme,
createConfiguredFontNavigationTheme,
createConfiguredFontTheme,
} from '../utils/themes';

type ContrastLevel = 'standard' | 'medium' | 'high';

const THEMES = {
light: {
standard: LightTheme,
medium: MediumContrastLightTheme,
high: HighContrastLightTheme,
},
dark: {
standard: DarkTheme,
medium: MediumContrastDarkTheme,
high: HighContrastDarkTheme,
},
};

const DYNAMIC_THEMES = {
light: {
standard: DynamicLightTheme,
medium: MediumContrastDynamicLightTheme,
high: HighContrastDynamicLightTheme,
},
dark: {
standard: DynamicDarkTheme,
medium: MediumContrastDynamicDarkTheme,
high: HighContrastDynamicDarkTheme,
},
};

const PERSISTENCE_KEY = 'NAVIGATION_STATE';
const PREFERENCES_KEY = 'APP_PREFERENCES';

Expand Down Expand Up @@ -98,15 +133,11 @@ export default function PaperExample() {
const [collapsed, setCollapsed] = React.useState(false);
const [customFontLoaded, setCustomFont] = React.useState(false);
const [rippleEffectEnabled, setRippleEffectEnabled] = React.useState(true);
const [contrast, setContrast] = React.useState<ContrastLevel>('standard');

const theme =
dynamicThemeSupported && shouldUseDynamicTheme
? isDarkMode
? DynamicDarkTheme
: DynamicLightTheme
: isDarkMode
? DarkTheme
: LightTheme;
const themes =
dynamicThemeSupported && shouldUseDynamicTheme ? DYNAMIC_THEMES : THEMES;
const theme = themes[isDarkMode ? 'dark' : 'light'][contrast];

const direction = rtl ? 'rtl' : 'ltr';

Expand All @@ -122,6 +153,13 @@ export default function PaperExample() {
if (typeof preferences.rtl === 'boolean') {
setRtl(preferences.rtl);
}

if (
preferences.contrast === 'medium' ||
preferences.contrast === 'high'
) {
setContrast(preferences.contrast);
}
}
} catch (e) {
// ignore error
Expand All @@ -145,6 +183,7 @@ export default function PaperExample() {
JSON.stringify({
theme: isDarkMode ? 'dark' : 'light',
rtl,
contrast,
})
);
} catch (e) {
Expand All @@ -165,7 +204,7 @@ export default function PaperExample() {
};

void savePrefs();
}, [direction, isDarkMode, isReady, rtl]);
}, [contrast, direction, isDarkMode, isReady, rtl]);

const preferences = React.useMemo(
() => ({
Expand All @@ -176,9 +215,11 @@ export default function PaperExample() {
toggleCollapsed: () => setCollapsed((oldValue) => !oldValue),
toggleCustomFont: () => setCustomFont((oldValue) => !oldValue),
toggleRippleEffect: () => setRippleEffectEnabled((oldValue) => !oldValue),
setContrast,
customFontLoaded,
rippleEffectEnabled,
shouldUseDynamicTheme,
contrast,
theme,
collapsed,
rtl,
Expand All @@ -187,6 +228,7 @@ export default function PaperExample() {
rtl,
theme,
collapsed,
contrast,
customFontLoaded,
shouldUseDynamicTheme,
rippleEffectEnabled,
Expand All @@ -197,7 +239,7 @@ export default function PaperExample() {
return null;
}

const combinedTheme = isDarkMode ? CombinedDarkTheme : CombinedDefaultTheme;
const combinedTheme = createCombinedTheme(theme, isDarkMode);
const configuredFontTheme = createConfiguredFontTheme(combinedTheme);
const configuredFontNavigationTheme =
createConfiguredFontNavigationTheme(combinedTheme);
Expand Down
62 changes: 28 additions & 34 deletions example/utils/themes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,38 @@ import {
DefaultTheme as NavigationDefaultTheme,
} from '@react-navigation/native';
import type { Theme as ReactNavigationTheme } from '@react-navigation/native';
import {
adaptNavigationTheme,
DarkTheme,
LightTheme,
configureFonts,
} from 'react-native-paper';
import { adaptNavigationTheme, configureFonts } from 'react-native-paper';
import type { Theme } from 'react-native-paper';

const { LightTheme: NavLightTheme, DarkTheme: NavDarkTheme } =
adaptNavigationTheme({
reactNavigationLight: NavigationDefaultTheme,
reactNavigationDark: NavigationDarkTheme,
});
/**
* Merges the React Navigation theme into a Paper theme.
*
* The Paper theme is passed in, and also given to `adaptNavigationTheme`, so
* that the selected contrast level is kept.
*/
export const createCombinedTheme = (paperTheme: Theme, isDark: boolean) => {
const { LightTheme: NavLightTheme, DarkTheme: NavDarkTheme } =
adaptNavigationTheme({
reactNavigationLight: NavigationDefaultTheme,
reactNavigationDark: NavigationDarkTheme,
materialLight: isDark ? undefined : paperTheme,
materialDark: isDark ? paperTheme : undefined,
});

export const CombinedDefaultTheme = {
...LightTheme,
...NavLightTheme,
colors: {
...LightTheme.colors,
...NavLightTheme.colors,
},
fonts: {
...LightTheme.fonts,
...NavLightTheme.fonts,
},
};
const navTheme = isDark ? NavDarkTheme : NavLightTheme;

export const CombinedDarkTheme = {
...DarkTheme,
...NavDarkTheme,
colors: {
...DarkTheme.colors,
...NavDarkTheme.colors,
},
fonts: {
...DarkTheme.fonts,
...NavDarkTheme.fonts,
},
return {
...paperTheme,
...navTheme,
colors: {
...paperTheme.colors,
...navTheme.colors,
},
fonts: {
...paperTheme.fonts,
...navTheme.fonts,
},
};
};

export const createConfiguredFontTheme = (
Expand Down
28 changes: 27 additions & 1 deletion src/core/__tests__/theming.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { describe, expect, it } from '@jest/globals';

import { DarkTheme, LightTheme } from '../../theme/schemes';
import {
DarkTheme,
HighContrastDarkTheme,
HighContrastLightTheme,
LightTheme,
} from '../../theme/schemes';
import { adaptNavigationTheme } from '../theming';

const NavigationLightTheme = {
Expand Down Expand Up @@ -273,4 +278,25 @@ describe('adaptNavigationTheme', () => {
expect(navLight).not.toHaveProperty('fonts');
expect(navDark).not.toHaveProperty('fonts');
});

it('adapts the colors of a raised-contrast material theme', () => {
const materialLight = HighContrastLightTheme;
const materialDark = HighContrastDarkTheme;

const { LightTheme: navLight, DarkTheme: navDark } = adaptNavigationTheme({
reactNavigationLight: NavigationLightTheme,
reactNavigationDark: NavigationDarkTheme,
materialLight,
materialDark,
});

// Apps spread the navigation colors over the Paper theme, so these
// must match the contrast level that was asked for.
expect(navLight.colors.primary).toBe(materialLight.colors.primary);
expect(navLight.colors.text).toBe(materialLight.colors.onSurface);
expect(navDark.colors.primary).toBe(materialDark.colors.primary);

expect(navLight.colors.primary).not.toBe(LightTheme.colors.primary);
expect(navDark.colors.primary).not.toBe(DarkTheme.colors.primary);
});
});
Loading