diff --git a/example/src/DrawerItems.tsx b/example/src/DrawerItems.tsx
deleted file mode 100644
index 94afa3136c..0000000000
--- a/example/src/DrawerItems.tsx
+++ /dev/null
@@ -1,293 +0,0 @@
-import * as React from 'react';
-import { Platform, StyleSheet, View } from 'react-native';
-import type { ColorValue } from 'react-native';
-
-import { DrawerContentScrollView } from '@react-navigation/drawer';
-import Constants, { ExecutionEnvironment } from 'expo-constants';
-import {
- Badge,
- Button,
- Dialog,
- Drawer,
- Palette,
- Portal,
- Switch,
- Text,
- TouchableRipple,
- useTheme,
-} from 'react-native-paper';
-
-import { dynamicThemeSupported, isWeb } from '../utils';
-import { PreferencesContext } from './PreferencesContext';
-
-const DrawerItemsData = [
- {
- label: 'Inbox',
- icon: 'inbox',
- key: 0,
- right: () => 44,
- },
- {
- label: 'Starred',
- icon: 'star',
- key: 1,
- right: ({ color }: { color: ColorValue }) => (
-
- ),
- },
- { label: 'Sent mail', icon: 'send', key: 2 },
- { label: 'Colored label', icon: 'palette', key: 3 },
- {
- label: 'A very long title that will be truncated',
- icon: 'delete',
- key: 4,
- right: () => ,
- },
-];
-
-const DrawerCollapsedItemsData = [
- {
- label: 'Inbox',
- focusedIcon: 'inbox',
- unfocusedIcon: 'inbox-outline',
- key: 0,
- badge: 44,
- },
- {
- label: 'Starred',
- focusedIcon: 'star',
- unfocusedIcon: 'star-outline',
- key: 1,
- },
- {
- label: 'Sent mail',
- focusedIcon: 'send',
- unfocusedIcon: 'send-outline',
- key: 2,
- },
- {
- label: 'A very long title that will be truncated',
- focusedIcon: 'delete',
- unfocusedIcon: 'delete-outline',
- key: 3,
- },
- {
- label: 'Full width',
- focusedIcon: 'arrow-all',
- key: 4,
- },
- {
- focusedIcon: 'bell',
- unfocusedIcon: 'bell-outline',
- key: 5,
- badge: true,
- },
-];
-
-function DrawerItems() {
- const [drawerItemIndex, setDrawerItemIndex] = React.useState(0);
- const [showRTLDialog, setShowRTLDialog] = React.useState(false);
- const preferences = React.useContext(PreferencesContext);
-
- const _setDrawerItem = (index: number) => setDrawerItemIndex(index);
-
- const { colors } = useTheme();
- const isIOS = Platform.OS === 'ios';
- const expoGoExecution =
- Constants.executionEnvironment === ExecutionEnvironment.StoreClient;
-
- if (!preferences) throw new Error('PreferencesContext not provided');
-
- const {
- toggleShouldUseDynamicTheme,
- toggleTheme,
- toggleRtl: toggleRTL,
- toggleCollapsed,
- toggleCustomFont,
- toggleRippleEffect,
- customFontLoaded,
- rippleEffectEnabled,
- collapsed,
- rtl: isRTL,
- theme: { dark: isDarkTheme },
- shouldUseDynamicTheme,
- } = preferences;
-
- const _handleToggleRTL = () => {
- if (!isWeb && expoGoExecution) {
- setShowRTLDialog(true);
- return;
- }
-
- toggleRTL();
- };
-
- const _handleDismissRTLDialog = () => {
- setShowRTLDialog(false);
- };
-
- const coloredLabelTheme = {
- colors: {
- secondaryContainer: Palette.tertiary80,
- onSecondaryContainer: Palette.tertiary20,
- },
- };
-
- return (
-
- {collapsed && (
-
- {DrawerCollapsedItemsData.map((props, index) => (
- {
- _setDrawerItem(index);
- index === 4 && toggleCollapsed();
- }}
- />
- ))}
-
- )}
- {!collapsed && (
- <>
-
- {DrawerItemsData.map((props, index) => (
- _setDrawerItem(index)}
- />
- ))}
-
-
-
- {dynamicThemeSupported ? (
-
-
- Use Dynamic Theme
-
-
-
-
-
- ) : null}
-
-
- Dark Theme
-
-
-
-
-
-
-
-
- RTL
-
-
-
-
-
-
-
-
- Collapsed drawer *
-
-
-
-
-
-
-
-
- Custom font *
-
-
-
-
-
-
-
-
-
- {isIOS ? 'Highlight' : 'Ripple'} effect *
-
-
-
-
-
-
-
- {!collapsed && (
-
- * - optional example toggles
-
- )}
-
- React Native Paper Version{' '}
- {require('react-native-paper/package.json').version}
-
- >
- )}
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- drawerContent: {
- flex: 1,
- },
- preference: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingVertical: 12,
- paddingHorizontal: 16,
- },
- v3Preference: {
- height: 56,
- paddingHorizontal: 28,
- },
- badge: {
- alignSelf: 'center',
- },
- collapsedSection: {
- marginTop: 16,
- },
- annotation: {
- marginHorizontal: 24,
- marginVertical: 6,
- },
-});
-
-export default DrawerItems;
diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx
index 53132e299f..7891374962 100644
--- a/example/src/ExampleList.tsx
+++ b/example/src/ExampleList.tsx
@@ -1,9 +1,11 @@
-import { FlatList } from 'react-native';
+import { useMemo, useState } from 'react';
+import { FlatList, StyleSheet, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
-import { Divider, List, useTheme } from 'react-native-paper';
+import { Divider, List, Text, useTheme } from 'react-native-paper';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import ExampleListHeader from './ExampleListHeader';
import ActivityIndicatorExample from './Examples/ActivityIndicatorExample';
import AppbarExample from './Examples/AppbarExample';
import AvatarExample from './Examples/AvatarExample';
@@ -116,32 +118,71 @@ export default function ExampleList() {
const { colors } = useTheme();
const safeArea = useSafeAreaInsets();
+ const [query, setQuery] = useState('');
+
+ const filteredData = useMemo(() => {
+ const search = query.trim().toLowerCase();
+
+ if (!search) {
+ return data;
+ }
+
+ return data.filter(
+ ({ id, data: example }) =>
+ example.title.toLowerCase().includes(search) ||
+ id.toLowerCase().includes(search)
+ );
+ }, [query]);
+
return (
- (
- {
- // @ts-expect-error TypeScript can't call overloaded functions with union arguments.
- // https://github.com/microsoft/TypeScript/issues/40803
- navigation.navigate(item.id);
- }}
- />
- )}
- keyExtractor={({ id }) => id}
- data={data}
- />
+
+
+ (
+ {
+ // @ts-expect-error TypeScript can't call overloaded functions with union arguments.
+ // https://github.com/microsoft/TypeScript/issues/40803
+ navigation.navigate(item.id);
+ }}
+ />
+ )}
+ ListEmptyComponent={
+
+ {`No examples match "${query.trim()}"`}
+
+ }
+ keyExtractor={({ id }) => id}
+ data={filteredData}
+ />
+
);
}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ empty: {
+ padding: 16,
+ textAlign: 'center',
+ },
+});
diff --git a/example/src/ExampleListHeader.tsx b/example/src/ExampleListHeader.tsx
new file mode 100644
index 0000000000..e08deef858
--- /dev/null
+++ b/example/src/ExampleListHeader.tsx
@@ -0,0 +1,46 @@
+import { StyleSheet } from 'react-native';
+
+import { useNavigation } from '@react-navigation/native';
+import { Appbar, Searchbar } from 'react-native-paper';
+
+import { usePreferences } from './Preferences/usePreferences';
+
+type Props = {
+ query: string;
+ onQueryChange: (query: string) => void;
+};
+
+export default function ExampleListHeader({ query, onQueryChange }: Props) {
+ const navigation = useNavigation('ExampleList');
+ const { togglePreferences } = usePreferences();
+
+ const canGoBack = navigation.canGoBack();
+
+ return (
+
+ navigation.goBack() : undefined}
+ searchAccessibilityLabel={canGoBack ? 'go back' : 'search'}
+ traileringIcon="cog"
+ traileringIconAccessibilityLabel="preferences"
+ onTraileringIconPress={togglePreferences}
+ clearAccessibilityLabel="clear search"
+ autoCorrect={false}
+ autoCapitalize="none"
+ style={styles.searchbar}
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ searchbar: {
+ flex: 1,
+ },
+});
diff --git a/example/src/Examples/CardExample.tsx b/example/src/Examples/CardExample.tsx
index ad3451e8b0..ab451ca30e 100644
--- a/example/src/Examples/CardExample.tsx
+++ b/example/src/Examples/CardExample.tsx
@@ -12,7 +12,7 @@ import {
} from 'react-native-paper';
import { isWeb } from '../../utils';
-import { PreferencesContext } from '../PreferencesContext';
+import { usePreferences } from '../Preferences/usePreferences';
import ScreenWrapper from '../ScreenWrapper';
type Mode = 'elevated' | 'outlined' | 'contained';
@@ -21,7 +21,7 @@ const CardExample = () => {
const { colors } = useTheme();
const [selectedMode, setSelectedMode] = React.useState('elevated' as Mode);
const [isSelected, setIsSelected] = React.useState(false);
- const preferences = React.useContext(PreferencesContext);
+ const { toggleTheme } = usePreferences();
const modes: Mode[] = ['elevated', 'outlined', 'contained'];
@@ -179,13 +179,7 @@ const CardExample = () => {
- {
- preferences?.toggleTheme();
- }}
- mode={selectedMode}
- >
+
}
diff --git a/example/src/Examples/SearchbarExample.tsx b/example/src/Examples/SearchbarExample.tsx
index 739002156b..cb8c0cc931 100644
--- a/example/src/Examples/SearchbarExample.tsx
+++ b/example/src/Examples/SearchbarExample.tsx
@@ -11,10 +11,12 @@ import {
useTheme,
} from 'react-native-paper';
+import { usePreferences } from '../Preferences/usePreferences';
import ScreenWrapper from '../ScreenWrapper';
const SearchExample = () => {
const navigation = useNavigation('Searchbar');
+ const { togglePreferences } = usePreferences();
const [isVisible, setIsVisible] = React.useState(false);
const [searchQueries, setSearchQuery] = React.useState({
@@ -27,7 +29,7 @@ const SearchExample = () => {
searchWithoutBottomLine: '',
loadingViewMode: '',
clickableBack: '',
- clickableDrawer: '',
+ clickablePreferences: '',
clickableLoading: '',
});
@@ -185,15 +187,15 @@ const SearchExample = () => {
onChangeText={(query) =>
setSearchQuery({
...searchQueries,
- clickableDrawer: query,
+ clickablePreferences: query,
})
}
- value={searchQueries.clickableDrawer}
+ value={searchQueries.clickablePreferences}
onIconPress={() => {
Keyboard.dismiss();
- navigation.openDrawer();
+ togglePreferences();
}}
- icon="menu"
+ icon="cog"
style={styles.searchbar}
/>
{
- const preferences = React.useContext(PreferencesContext);
+ const { toggleTheme } = usePreferences();
const [options, setOptions] = React.useState({
showSnackbar: false,
@@ -31,9 +31,7 @@ const SnackbarExample = () => {
const action = {
label: showLongerAction ? 'Toggle Theme' : 'Action',
- onPress: () => {
- preferences?.toggleTheme();
- },
+ onPress: toggleTheme,
};
return (
diff --git a/example/src/PreferencesContext.tsx b/example/src/Preferences/PreferencesContext.tsx
similarity index 64%
rename from example/src/PreferencesContext.tsx
rename to example/src/Preferences/PreferencesContext.tsx
index b5e381ae05..2e1c9b97e1 100644
--- a/example/src/PreferencesContext.tsx
+++ b/example/src/Preferences/PreferencesContext.tsx
@@ -2,17 +2,20 @@ import * as React from 'react';
import type { Theme } from 'react-native-paper';
-export const PreferencesContext = React.createContext<{
+export type Preferences = {
toggleTheme: () => void;
toggleRtl: () => void;
- toggleCollapsed: () => void;
toggleCustomFont: () => void;
+ togglePreferences: () => void;
toggleRippleEffect: () => void;
toggleShouldUseDynamicTheme?: () => void;
+ resetPreferences: () => void;
theme: Theme;
rtl: boolean;
- collapsed: boolean;
customFontLoaded: boolean;
+ preferencesVisible: boolean;
rippleEffectEnabled: boolean;
shouldUseDynamicTheme?: boolean;
-} | null>(null);
+};
+
+export const PreferencesContext = React.createContext(null);
diff --git a/example/src/Preferences/PreferencesModal.tsx b/example/src/Preferences/PreferencesModal.tsx
new file mode 100644
index 0000000000..fa2eb731a8
--- /dev/null
+++ b/example/src/Preferences/PreferencesModal.tsx
@@ -0,0 +1,199 @@
+import { useState } from 'react';
+import {
+ Platform,
+ ScrollView,
+ StyleSheet,
+ useWindowDimensions,
+ View,
+} from 'react-native';
+
+import Constants, { ExecutionEnvironment } from 'expo-constants';
+import {
+ Button,
+ Dialog,
+ Portal,
+ Switch,
+ Text,
+ TouchableRipple,
+ useTheme,
+} from 'react-native-paper';
+import { Modal } from 'react-native-paper';
+
+import { usePreferences } from './usePreferences';
+import { dynamicThemeSupported, isWeb } from '../../utils';
+
+export default function PreferencesModal() {
+ const [showRTLDialog, setShowRTLDialog] = useState(false);
+ const theme = useTheme();
+ const { height: windowHeight } = useWindowDimensions();
+
+ const isIOS = Platform.OS === 'ios';
+ const expoGoExecution =
+ Constants.executionEnvironment === ExecutionEnvironment.StoreClient;
+
+ const {
+ toggleShouldUseDynamicTheme,
+ toggleTheme,
+ toggleRtl: toggleRTL,
+ toggleCustomFont,
+ toggleRippleEffect,
+ togglePreferences,
+ resetPreferences,
+ preferencesVisible,
+ customFontLoaded,
+ rippleEffectEnabled,
+ rtl: isRTL,
+ theme: { dark: isDarkTheme },
+ shouldUseDynamicTheme,
+ } = usePreferences();
+
+ const _handleToggleRTL = () => {
+ if (!isWeb && expoGoExecution) {
+ setShowRTLDialog(true);
+ return;
+ }
+
+ toggleRTL();
+ };
+
+ const _handleDismissRTLDialog = () => {
+ setShowRTLDialog(false);
+ };
+
+ return (
+ <>
+
+
+
+ {dynamicThemeSupported ? (
+
+
+ Use Dynamic Theme
+
+
+
+
+
+ ) : null}
+
+
+ Dark Theme
+
+
+
+
+
+
+
+
+ RTL
+
+
+
+
+
+
+
+
+ Custom font *
+
+
+
+
+
+
+
+
+
+ {isIOS ? 'Highlight' : 'Ripple'} effect *
+
+
+
+
+
+
+
+
+
+
+ * - optional example toggles
+
+
+ React Native Paper Version{' '}
+ {require('react-native-paper/package.json').version}
+
+
+
+
+
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ modalContent: {
+ marginHorizontal: 24,
+ borderRadius: 28,
+ },
+ scrollView: {
+ flexGrow: 0,
+ flexShrink: 1,
+ },
+ scrollViewContent: {
+ paddingVertical: 12,
+ },
+ preference: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ },
+ v3Preference: {
+ height: 56,
+ paddingHorizontal: 28,
+ },
+ resetButton: {
+ marginHorizontal: 28,
+ marginVertical: 12,
+ },
+ annotation: {
+ marginHorizontal: 24,
+ marginVertical: 6,
+ },
+});
diff --git a/example/src/Preferences/usePreferences.tsx b/example/src/Preferences/usePreferences.tsx
new file mode 100644
index 0000000000..85555f0223
--- /dev/null
+++ b/example/src/Preferences/usePreferences.tsx
@@ -0,0 +1,11 @@
+import { useContext } from 'react';
+
+import { PreferencesContext } from './PreferencesContext';
+
+export function usePreferences() {
+ const preferences = useContext(PreferencesContext);
+
+ if (!preferences) throw new Error('PreferencesContext not provided');
+
+ return preferences;
+}
diff --git a/example/src/Preferences/useSetupPreferences.ts b/example/src/Preferences/useSetupPreferences.ts
new file mode 100644
index 0000000000..5313f9853d
--- /dev/null
+++ b/example/src/Preferences/useSetupPreferences.ts
@@ -0,0 +1,172 @@
+import * as React from 'react';
+import { I18nManager, Platform } from 'react-native';
+
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import * as Updates from 'expo-updates';
+import {
+ DarkTheme,
+ LightTheme,
+ DynamicLightTheme,
+ DynamicDarkTheme,
+} from 'react-native-paper';
+
+import type { Preferences } from './PreferencesContext';
+import { dynamicThemeSupported } from '../../utils';
+
+const PERSISTENCE_KEY = 'NAVIGATION_STATE';
+const PREFERENCES_KEY = 'APP_PREFERENCES';
+
+const getInitialRtl = () => {
+ if (Platform.OS === 'web' && typeof document !== 'undefined') {
+ return document.documentElement.dir === 'rtl';
+ }
+
+ return I18nManager.getConstants().isRTL;
+};
+
+export const navigationPersistor = {
+ async persist(state: unknown) {
+ await AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state));
+ },
+ async restore() {
+ const state = await AsyncStorage.getItem(PERSISTENCE_KEY);
+
+ return state ? JSON.parse(state) : undefined;
+ },
+};
+
+export function useSetupPreferences() {
+ const [isReady, setIsReady] = React.useState(false);
+
+ const [initialRtl] = React.useState(getInitialRtl);
+
+ const [shouldUseDynamicTheme, setShouldUseDynamicTheme] =
+ React.useState(true);
+ const [isDarkMode, setIsDarkMode] = React.useState(false);
+ const [rtl, setRtl] = React.useState(initialRtl);
+ const [customFontLoaded, setCustomFont] = React.useState(false);
+ const [rippleEffectEnabled, setRippleEffectEnabled] = React.useState(true);
+ const [preferencesVisible, setPreferencesVisible] = React.useState(false);
+
+ const [navigationKey, setNavigationKey] = React.useState(0);
+
+ const theme =
+ dynamicThemeSupported && shouldUseDynamicTheme
+ ? isDarkMode
+ ? DynamicDarkTheme
+ : DynamicLightTheme
+ : isDarkMode
+ ? DarkTheme
+ : LightTheme;
+
+ const direction: 'rtl' | 'ltr' = rtl ? 'rtl' : 'ltr';
+
+ React.useEffect(() => {
+ const restorePrefs = async () => {
+ try {
+ const prefString = await AsyncStorage.getItem(PREFERENCES_KEY);
+ const preferences = JSON.parse(prefString || '');
+
+ if (preferences) {
+ setIsDarkMode(preferences.theme === 'dark');
+
+ if (typeof preferences.rtl === 'boolean') {
+ setRtl(preferences.rtl);
+ }
+ }
+ } catch (e) {
+ // ignore error
+ } finally {
+ setIsReady(true);
+ }
+ };
+
+ void restorePrefs();
+ }, []);
+
+ React.useEffect(() => {
+ const savePrefs = async () => {
+ if (!isReady) {
+ return;
+ }
+
+ try {
+ await AsyncStorage.setItem(
+ PREFERENCES_KEY,
+ JSON.stringify({
+ theme: isDarkMode ? 'dark' : 'light',
+ rtl,
+ })
+ );
+ } catch (e) {
+ // ignore error
+ }
+
+ if (Platform.OS === 'web' && typeof document !== 'undefined') {
+ document.documentElement.dir = direction;
+ }
+
+ if (I18nManager.getConstants().isRTL !== rtl) {
+ I18nManager.forceRTL(rtl);
+
+ if (Platform.OS !== 'web') {
+ await Updates.reloadAsync();
+ }
+ }
+ };
+
+ void savePrefs();
+ }, [direction, isDarkMode, isReady, rtl]);
+
+ const resetPreferences = React.useCallback(async () => {
+ setShouldUseDynamicTheme(true);
+ setIsDarkMode(false);
+ setCustomFont(false);
+ setRippleEffectEnabled(true);
+ setPreferencesVisible(false);
+
+ try {
+ await AsyncStorage.multiRemove([PREFERENCES_KEY, PERSISTENCE_KEY]);
+ } catch (e) {
+ // ignore error
+ }
+
+ setNavigationKey((oldValue) => oldValue + 1);
+ }, []);
+
+ const preferences: Preferences = React.useMemo(
+ () => ({
+ toggleShouldUseDynamicTheme: () =>
+ setShouldUseDynamicTheme((oldValue) => !oldValue),
+ toggleTheme: () => setIsDarkMode((oldValue) => !oldValue),
+ toggleRtl: () => setRtl((oldValue) => !oldValue),
+ toggleCustomFont: () => setCustomFont((oldValue) => !oldValue),
+ toggleRippleEffect: () => setRippleEffectEnabled((oldValue) => !oldValue),
+ togglePreferences: () => setPreferencesVisible((oldValue) => !oldValue),
+ resetPreferences,
+ preferencesVisible,
+ customFontLoaded,
+ rippleEffectEnabled,
+ shouldUseDynamicTheme,
+ theme,
+ rtl,
+ }),
+ [
+ rtl,
+ theme,
+ customFontLoaded,
+ preferencesVisible,
+ shouldUseDynamicTheme,
+ rippleEffectEnabled,
+ resetPreferences,
+ ]
+ );
+
+ return {
+ preferences,
+ isReady,
+ isDarkMode,
+ direction,
+ navigationKey,
+ };
+}
diff --git a/example/src/RootNavigator.tsx b/example/src/RootNavigator.tsx
index b64fd1d802..69df2388e1 100644
--- a/example/src/RootNavigator.tsx
+++ b/example/src/RootNavigator.tsx
@@ -1,6 +1,5 @@
import { Platform, StyleSheet, View } from 'react-native';
-import { useNavigation } from '@react-navigation/native';
import {
createNativeStackNavigator,
createNativeStackScreen,
@@ -9,46 +8,84 @@ import {
import { Appbar } from 'react-native-paper';
import ExampleList, { examples } from './ExampleList';
+import PreferencesModal from './Preferences/PreferencesModal';
+import { usePreferences } from './Preferences/usePreferences';
+import SamplesList, { samples } from './SamplesList';
const { TeamDetails, ...examplesWithoutParams } = examples;
type ExampleRouteName = keyof typeof examplesWithoutParams;
+type SampleRouteName = keyof typeof samples;
const fromEntries = (
entries: Array<[Key, Value]>
) => Object.fromEntries(entries) as Record;
function Header({ navigation, route, options, back }: NativeStackHeaderProps) {
- const drawerNavigation = useNavigation('Home');
+ const { togglePreferences } = usePreferences();
+
+ const isIOS = Platform.OS === 'ios';
+
+ const backAction = navigation.goBack()} />;
+ const searchAction = (
+ navigation.navigate('ExampleList')}
+ />
+ );
return (
-
- {back ? (
- navigation.goBack()} />
- ) : (
- drawerNavigation.openDrawer()}
- />
- )}
+
+ {back ? backAction : isIOS ? searchAction : null}
+ {!isIOS && !back && searchAction}
+
);
}
const Root = createNativeStackNavigator({
- layout: ({ children }) => {children},
+ initialRouteName: 'SamplesList',
+ layout: ({ children }) => (
+ <>
+ {children}
+
+ >
+ ),
screenOptions: {
header: (props) => ,
},
screens: {
+ SamplesList: createNativeStackScreen({
+ screen: SamplesList,
+ options: {
+ title: 'Samples',
+ },
+ linking: '',
+ }),
+ ...fromEntries(
+ (
+ Object.entries(samples) as [
+ SampleRouteName,
+ (typeof samples)[SampleRouteName],
+ ][]
+ ).map(([id, sample]) => [
+ id,
+ createNativeStackScreen({
+ screen: sample.screen,
+ options: {
+ title: sample.title,
+ },
+ }),
+ ])
+ ),
ExampleList: createNativeStackScreen({
screen: ExampleList,
options: {
title: 'Examples',
+ headerShown: false,
},
- linking: '',
+ linking: 'examples',
}),
...fromEntries(
(
diff --git a/example/src/Samples/ArticleSample.tsx b/example/src/Samples/ArticleSample.tsx
new file mode 100644
index 0000000000..8f70ed69af
--- /dev/null
+++ b/example/src/Samples/ArticleSample.tsx
@@ -0,0 +1,121 @@
+import * as React from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import {
+ Button,
+ Card,
+ Chip,
+ FAB,
+ IconButton,
+ Text,
+ Tooltip,
+} from 'react-native-paper';
+
+import type { SampleConfig } from './types';
+import ScreenWrapper from '../ScreenWrapper';
+
+export const ArticleSampleConfig: SampleConfig = {
+ title: 'Article',
+ icon: 'newspaper-variant-outline',
+ components: [
+ 'Button',
+ 'Card',
+ 'Chip',
+ 'FAB',
+ 'IconButton',
+ 'Text',
+ 'Tooltip',
+ ],
+};
+
+const TOPICS = ['Material 3', 'Design systems', 'React Native'];
+
+const ArticleSample = () => {
+ const [liked, setLiked] = React.useState(false);
+ const [bookmarked, setBookmarked] = React.useState(false);
+ const [menuExpanded, setMenuExpanded] = React.useState(false);
+
+ return (
+ <>
+
+
+
+
+
+
+ Material Design 3 leans on tonal color, larger corner radii and
+ motion to express hierarchy. Paper ships those decisions as
+ components, so a screen built from the defaults already follows
+ the spec.
+
+
+ {TOPICS.map((topic) => (
+
+ {topic}
+
+ ))}
+
+
+
+
+
+ setLiked(!liked)}
+ />
+
+
+ setBookmarked(!bookmarked)}
+ />
+
+
+
+
+
+
+
+ setMenuExpanded(false)}
+ trigger={{
+ icon: 'share-variant',
+ onPress: () => setMenuExpanded(true),
+ }}
+ items={[
+ { icon: 'link', label: 'Copy link', onPress: () => {} },
+ { icon: 'email', label: 'Send by email', onPress: () => {} },
+ { icon: 'comment-outline', label: 'Comment', onPress: () => {} },
+ ]}
+ />
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ content: {
+ padding: 16,
+ },
+ cardContent: {
+ gap: 16,
+ },
+ cardIcons: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ topics: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 8,
+ },
+});
+
+export default ArticleSample;
diff --git a/example/src/Samples/ContactsSample.tsx b/example/src/Samples/ContactsSample.tsx
new file mode 100644
index 0000000000..641fef07ab
--- /dev/null
+++ b/example/src/Samples/ContactsSample.tsx
@@ -0,0 +1,108 @@
+import * as React from 'react';
+import { FlatList, StyleSheet } from 'react-native';
+
+import {
+ Avatar,
+ Badge,
+ Divider,
+ FAB,
+ List,
+ Searchbar,
+} from 'react-native-paper';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import type { SampleConfig } from './types';
+import ScreenWrapper from '../ScreenWrapper';
+
+export const ContactsSampleConfig: SampleConfig = {
+ title: 'Contacts',
+ icon: 'account-group-outline',
+ components: ['Avatar', 'Badge', 'Divider', 'FAB', 'List', 'Searchbar'],
+};
+
+const CONTACTS = [
+ { id: '1', name: 'Ada Lovelace', role: 'Engineering', unread: 3 },
+ { id: '2', name: 'Grace Hopper', role: 'Engineering', unread: 0 },
+ { id: '3', name: 'Katherine Johnson', role: 'Research', unread: 12 },
+ { id: '4', name: 'Mary Jackson', role: 'Research', unread: 0 },
+ { id: '5', name: 'Radia Perlman', role: 'Networking', unread: 1 },
+ { id: '6', name: 'Barbara Liskov', role: 'Architecture', unread: 0 },
+];
+
+const getInitials = (name: string) =>
+ name
+ .split(' ')
+ .map((part) => part[0])
+ .join('');
+
+const ContactsSample = () => {
+ const insets = useSafeAreaInsets();
+ const [query, setQuery] = React.useState('');
+
+ const search = query.trim().toLowerCase();
+ const contacts = search
+ ? CONTACTS.filter((contact) => contact.name.toLowerCase().includes(search))
+ : CONTACTS;
+
+ return (
+ <>
+
+
+ contact.id}
+ keyboardShouldPersistTaps="handled"
+ ItemSeparatorComponent={Divider}
+ contentContainerStyle={{ paddingBottom: insets.bottom + 96 }}
+ renderItem={({ item }) => (
+ {}}
+ left={({ style }) => (
+
+ )}
+ right={({ style }) =>
+ item.unread ? (
+ {item.unread}
+ ) : null
+ }
+ />
+ )}
+ />
+
+
+ {}}
+ style={[styles.fab, { bottom: insets.bottom + 16 }]}
+ />
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ searchbar: {
+ margin: 16,
+ },
+ badge: {
+ alignSelf: 'center',
+ },
+ fab: {
+ position: 'absolute',
+ right: 16,
+ },
+});
+
+export default ContactsSample;
diff --git a/example/src/Samples/HelpCenterSample.tsx b/example/src/Samples/HelpCenterSample.tsx
new file mode 100644
index 0000000000..17293d2366
--- /dev/null
+++ b/example/src/Samples/HelpCenterSample.tsx
@@ -0,0 +1,111 @@
+import * as React from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import {
+ Icon,
+ List,
+ Modal,
+ Portal,
+ Text,
+ TouchableRipple,
+ useTheme,
+} from 'react-native-paper';
+
+import type { SampleConfig } from './types';
+import ScreenWrapper from '../ScreenWrapper';
+
+export const HelpCenterSampleConfig: SampleConfig = {
+ title: 'Help center',
+ icon: 'help-circle-outline',
+ components: ['Icon', 'List', 'Modal', 'Portal', 'Text', 'TouchableRipple'],
+};
+
+const TOPICS = [
+ {
+ id: 'getting-started',
+ title: 'Getting started',
+ icon: 'rocket-launch-outline',
+ answers: ['Install the app', 'Create your first workspace'],
+ },
+ {
+ id: 'billing',
+ title: 'Billing',
+ icon: 'credit-card-outline',
+ answers: ['Update your payment method', 'Download past invoices'],
+ },
+ {
+ id: 'privacy',
+ title: 'Privacy',
+ icon: 'shield-lock-outline',
+ answers: ['Manage data sharing', 'Delete your account'],
+ },
+];
+
+const HelpCenterSample = () => {
+ const { colors } = useTheme();
+ const [contactVisible, setContactVisible] = React.useState(false);
+
+ return (
+ <>
+
+
+
+ {TOPICS.map((topic) => (
+ }
+ >
+ {topic.answers.map((answer) => (
+ {}} />
+ ))}
+
+ ))}
+
+
+
+ setContactVisible(true)}>
+
+
+ Still stuck? Contact support
+
+
+
+
+
+ setContactVisible(false)}
+ contentContainerStyle={[
+ styles.modal,
+ { backgroundColor: colors.surface },
+ ]}
+ >
+ Contact support
+
+ Write to support@example.com and we will get back to you within one
+ business day.
+
+
+
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ contact: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 16,
+ padding: 16,
+ },
+ modal: {
+ margin: 24,
+ padding: 24,
+ borderRadius: 28,
+ gap: 8,
+ },
+});
+
+export default HelpCenterSample;
diff --git a/example/src/Samples/OrdersSample.tsx b/example/src/Samples/OrdersSample.tsx
new file mode 100644
index 0000000000..5212715192
--- /dev/null
+++ b/example/src/Samples/OrdersSample.tsx
@@ -0,0 +1,157 @@
+import * as React from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import {
+ Banner,
+ Button,
+ Checkbox,
+ DataTable,
+ Menu,
+ Text,
+} from 'react-native-paper';
+
+import type { SampleConfig } from './types';
+import ScreenWrapper from '../ScreenWrapper';
+
+export const OrdersSampleConfig: SampleConfig = {
+ title: 'Orders',
+ icon: 'table-large',
+ components: ['Banner', 'Button', 'Checkbox', 'DataTable', 'Menu', 'Text'],
+};
+
+const ORDERS = [
+ { id: 'A-1041', customer: 'Ada Lovelace', total: 128 },
+ { id: 'A-1042', customer: 'Grace Hopper', total: 64 },
+ { id: 'A-1043', customer: 'Alan Turing', total: 512 },
+ { id: 'A-1044', customer: 'Barbara Liskov', total: 96 },
+ { id: 'A-1045', customer: 'Radia Perlman', total: 240 },
+ { id: 'A-1046', customer: 'Mary Jackson', total: 32 },
+];
+
+const ITEMS_PER_PAGE = 3;
+
+type SortKey = 'id' | 'customer' | 'total';
+
+const SORT_LABELS: Record = {
+ id: 'Order number',
+ customer: 'Customer',
+ total: 'Total',
+};
+
+const OrdersSample = () => {
+ const [bannerVisible, setBannerVisible] = React.useState(true);
+ const [menuVisible, setMenuVisible] = React.useState(false);
+ const [sortBy, setSortBy] = React.useState('id');
+ const [selected, setSelected] = React.useState([]);
+ const [page, setPage] = React.useState(0);
+
+ const sorted = ORDERS.slice().sort((a, b) =>
+ sortBy === 'total'
+ ? a.total - b.total
+ : String(a[sortBy]).localeCompare(String(b[sortBy]))
+ );
+
+ const from = page * ITEMS_PER_PAGE;
+ const to = Math.min(from + ITEMS_PER_PAGE, sorted.length);
+
+ const toggleSelected = (id: string) =>
+ setSelected((current) =>
+ current.includes(id)
+ ? current.filter((item) => item !== id)
+ : [...current, id]
+ );
+
+ const selectSort = (key: SortKey) => {
+ setSortBy(key);
+ setMenuVisible(false);
+ setPage(0);
+ };
+
+ return (
+
+ setBannerVisible(false) }]}
+ >
+ Two orders are waiting for a payment confirmation.
+
+
+
+ {selected.length} selected
+
+
+
+
+
+
+ Order
+
+ Customer
+
+ Total
+
+
+ {sorted.slice(from, to).map((order) => (
+
+
+ toggleSelected(order.id)}
+ />
+
+ {order.id}
+
+ {order.customer}
+
+ {`$${order.total}`}
+
+ ))}
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ toolbar: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: 16,
+ },
+ selectColumn: {
+ flex: 0.5,
+ },
+ customerColumn: {
+ flex: 2,
+ },
+});
+
+export default OrdersSample;
diff --git a/example/src/Samples/PlayerSample.tsx b/example/src/Samples/PlayerSample.tsx
new file mode 100644
index 0000000000..d8f72c2638
--- /dev/null
+++ b/example/src/Samples/PlayerSample.tsx
@@ -0,0 +1,82 @@
+import * as React from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import {
+ Icon,
+ ProgressBar,
+ SegmentedButtons,
+ Surface,
+ Text,
+ ToggleButton,
+} from 'react-native-paper';
+
+import type { SampleConfig } from './types';
+import ScreenWrapper from '../ScreenWrapper';
+
+export const PlayerSampleConfig: SampleConfig = {
+ title: 'Now playing',
+ icon: 'play-circle-outline',
+ components: [
+ 'Icon',
+ 'ProgressBar',
+ 'SegmentedButtons',
+ 'Surface',
+ 'Text',
+ 'ToggleButton',
+ ],
+};
+
+const PlayerSample = () => {
+ const [repeat, setRepeat] = React.useState('off');
+ const [speed, setSpeed] = React.useState('1');
+
+ return (
+
+
+
+
+
+
+ Nightfall
+ Aurora Skies · Long Way Home
+
+
+
+
+ value && setRepeat(value)}
+ >
+
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ content: {
+ padding: 16,
+ gap: 24,
+ },
+ cover: {
+ height: 200,
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 16,
+ },
+});
+
+export default PlayerSample;
diff --git a/example/src/Samples/SettingsSample.tsx b/example/src/Samples/SettingsSample.tsx
new file mode 100644
index 0000000000..ea83866c99
--- /dev/null
+++ b/example/src/Samples/SettingsSample.tsx
@@ -0,0 +1,104 @@
+import * as React from 'react';
+import { StyleSheet } from 'react-native';
+
+import {
+ Button,
+ Dialog,
+ Divider,
+ List,
+ Portal,
+ RadioButton,
+ Switch,
+ Text,
+} from 'react-native-paper';
+
+import type { SampleConfig } from './types';
+import ScreenWrapper from '../ScreenWrapper';
+
+export const SettingsSampleConfig: SampleConfig = {
+ title: 'Settings',
+ icon: 'cog-outline',
+ components: [
+ 'Button',
+ 'Dialog',
+ 'Divider',
+ 'List',
+ 'Portal',
+ 'RadioButton',
+ 'Switch',
+ 'Text',
+ ],
+};
+
+const SettingsSample = () => {
+ const [notifications, setNotifications] = React.useState(true);
+ const [backgroundSync, setBackgroundSync] = React.useState(false);
+ const [density, setDensity] = React.useState('comfortable');
+ const [dialogVisible, setDialogVisible] = React.useState(false);
+
+ const hideDialog = () => setDialogVisible(false);
+
+ return (
+
+
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ onPress={() => setDialogVisible(true)}
+ />
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ dialogTitle: {
+ textAlign: 'center',
+ },
+});
+
+export default SettingsSample;
diff --git a/example/src/Samples/SignUpSample.tsx b/example/src/Samples/SignUpSample.tsx
new file mode 100644
index 0000000000..ee85c84a61
--- /dev/null
+++ b/example/src/Samples/SignUpSample.tsx
@@ -0,0 +1,109 @@
+import * as React from 'react';
+import { StyleSheet } from 'react-native';
+
+import {
+ Button,
+ Checkbox,
+ Divider,
+ Snackbar,
+ Text,
+ TextInput,
+} from 'react-native-paper';
+import type { TextInputAccessoryProps } from 'react-native-paper';
+
+import type { SampleConfig } from './types';
+import ScreenWrapper from '../ScreenWrapper';
+
+export const SignUpSampleConfig: SampleConfig = {
+ title: 'Sign up',
+ icon: 'account-plus-outline',
+ components: [
+ 'Button',
+ 'Checkbox',
+ 'Divider',
+ 'Snackbar',
+ 'Text',
+ 'TextInput',
+ ],
+};
+
+const SignUpSample = () => {
+ const [email, setEmail] = React.useState('');
+ const [password, setPassword] = React.useState('');
+ const [secure, setSecure] = React.useState(true);
+ const [accepted, setAccepted] = React.useState(false);
+ const [submitted, setSubmitted] = React.useState(false);
+
+ const emailIcon = (props: TextInputAccessoryProps) => (
+
+ );
+
+ const passwordIcon = (props: TextInputAccessoryProps) => (
+ setSecure(!secure)}
+ />
+ );
+
+ return (
+ <>
+
+ Create your account
+
+ Sign up to sync your projects across every device.
+
+
+
+
+
+ setAccepted(!accepted)}
+ />
+
+
+
+
+
+
+
+ setSubmitted(false)}>
+ Account created
+
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ content: {
+ padding: 16,
+ gap: 16,
+ },
+});
+
+export default SignUpSample;
diff --git a/example/src/Samples/WorkspaceSample.tsx b/example/src/Samples/WorkspaceSample.tsx
new file mode 100644
index 0000000000..199e252bde
--- /dev/null
+++ b/example/src/Samples/WorkspaceSample.tsx
@@ -0,0 +1,140 @@
+import * as React from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import { useNavigation } from '@react-navigation/native';
+import {
+ Appbar,
+ Avatar,
+ BottomNavigation,
+ Drawer,
+ Text,
+} from 'react-native-paper';
+import type { BottomNavigationRoute } from 'react-native-paper';
+
+import type { SampleConfig } from './types';
+import ScreenWrapper from '../ScreenWrapper';
+
+export const WorkspaceSampleConfig: SampleConfig = {
+ title: 'Workspace',
+ icon: 'view-dashboard-outline',
+ components: ['Appbar', 'Avatar', 'BottomNavigation', 'Drawer', 'Text'],
+};
+
+const FOLDERS = [
+ { key: 'primary', label: 'Primary', icon: 'inbox' },
+ { key: 'starred', label: 'Starred', icon: 'star-outline' },
+ { key: 'archive', label: 'Archive', icon: 'archive-outline' },
+];
+
+const TEAM = ['Ada Lovelace', 'Grace Hopper', 'Alan Turing'];
+
+const InboxRoute = () => {
+ const [folder, setFolder] = React.useState('primary');
+
+ return (
+
+
+ {FOLDERS.map((item) => (
+ setFolder(item.key)}
+ />
+ ))}
+
+
+ );
+};
+
+const TasksRoute = () => (
+
+ Today
+ Review the release checklist
+ Prepare the design handoff
+
+);
+
+const TeamRoute = () => (
+
+ {TEAM.map((member) => (
+
+
+ {member}
+
+ ))}
+
+);
+
+const renderScene = BottomNavigation.SceneMap({
+ inbox: InboxRoute,
+ tasks: TasksRoute,
+ team: TeamRoute,
+});
+
+const routes: BottomNavigationRoute[] = [
+ {
+ key: 'inbox',
+ title: 'Inbox',
+ focusedIcon: 'inbox',
+ unfocusedIcon: 'inbox-outline',
+ badge: 4,
+ },
+ {
+ key: 'tasks',
+ title: 'Tasks',
+ focusedIcon: 'check-circle',
+ unfocusedIcon: 'check-circle-outline',
+ },
+ {
+ key: 'team',
+ title: 'Team',
+ focusedIcon: 'account-group',
+ unfocusedIcon: 'account-group-outline',
+ },
+];
+
+const WorkspaceSample = () => {
+ const navigation = useNavigation('WorkspaceSample');
+ const [index, setIndex] = React.useState(0);
+
+ React.useLayoutEffect(() => {
+ navigation.setOptions({ headerShown: false });
+ }, [navigation]);
+
+ return (
+
+
+ navigation.goBack()} />
+
+ {}} />
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ screen: {
+ flex: 1,
+ },
+ content: {
+ padding: 16,
+ gap: 16,
+ },
+ member: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 16,
+ },
+});
+
+export default WorkspaceSample;
diff --git a/example/src/Samples/types.ts b/example/src/Samples/types.ts
new file mode 100644
index 0000000000..7efa77a30d
--- /dev/null
+++ b/example/src/Samples/types.ts
@@ -0,0 +1,5 @@
+export type SampleConfig = {
+ title: string;
+ icon: string;
+ components: string[];
+};
diff --git a/example/src/SamplesList.tsx b/example/src/SamplesList.tsx
new file mode 100644
index 0000000000..21ee3a19d3
--- /dev/null
+++ b/example/src/SamplesList.tsx
@@ -0,0 +1,86 @@
+import { FlatList, StyleSheet, View } from 'react-native';
+
+import { useNavigation } from '@react-navigation/native';
+import { Avatar, Card, Chip } from 'react-native-paper';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import ArticleSample, { ArticleSampleConfig } from './Samples/ArticleSample';
+import ContactsSample, { ContactsSampleConfig } from './Samples/ContactsSample';
+import HelpCenterSample, {
+ HelpCenterSampleConfig,
+} from './Samples/HelpCenterSample';
+import OrdersSample, { OrdersSampleConfig } from './Samples/OrdersSample';
+import PlayerSample, { PlayerSampleConfig } from './Samples/PlayerSample';
+import SettingsSample, { SettingsSampleConfig } from './Samples/SettingsSample';
+import SignUpSample, { SignUpSampleConfig } from './Samples/SignUpSample';
+import WorkspaceSample, {
+ WorkspaceSampleConfig,
+} from './Samples/WorkspaceSample';
+
+export const samples = {
+ SignUpSample: { ...SignUpSampleConfig, screen: SignUpSample },
+ ContactsSample: { ...ContactsSampleConfig, screen: ContactsSample },
+ ArticleSample: { ...ArticleSampleConfig, screen: ArticleSample },
+ SettingsSample: { ...SettingsSampleConfig, screen: SettingsSample },
+ PlayerSample: { ...PlayerSampleConfig, screen: PlayerSample },
+ OrdersSample: { ...OrdersSampleConfig, screen: OrdersSample },
+ HelpCenterSample: { ...HelpCenterSampleConfig, screen: HelpCenterSample },
+ WorkspaceSample: { ...WorkspaceSampleConfig, screen: WorkspaceSample },
+};
+
+type SampleId = keyof typeof samples;
+
+const data = (Object.keys(samples) as SampleId[]).map((id) => ({
+ id,
+ ...samples[id],
+}));
+
+export default function SamplesList() {
+ const navigation = useNavigation('SamplesList');
+ const safeArea = useSafeAreaInsets();
+
+ return (
+ id}
+ contentContainerStyle={[
+ styles.content,
+ {
+ paddingBottom: safeArea.bottom + 16,
+ paddingLeft: safeArea.left,
+ paddingRight: safeArea.right,
+ },
+ ]}
+ showsVerticalScrollIndicator={false}
+ renderItem={({ item }) => (
+ navigation.navigate(item.id)}>
+ }
+ />
+
+
+ {item.components.map((component) => (
+
+ {component}
+
+ ))}
+
+
+
+ )}
+ />
+ );
+}
+
+const styles = StyleSheet.create({
+ content: {
+ padding: 16,
+ gap: 16,
+ },
+ tags: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 8,
+ },
+});
diff --git a/example/src/index.tsx b/example/src/index.tsx
index afa3941044..bfaf5e4c56 100644
--- a/example/src/index.tsx
+++ b/example/src/index.tsx
@@ -1,30 +1,18 @@
-import * as React from 'react';
-import { I18nManager, Platform } from 'react-native';
+import { Platform } from 'react-native';
-import AsyncStorage from '@react-native-async-storage/async-storage';
-import {
- createDrawerNavigator,
- createDrawerScreen,
-} from '@react-navigation/drawer';
import { createStaticNavigation } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { useKeepAwake } from 'expo-keep-awake';
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,
-} from 'react-native-paper';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { PaperProvider } from 'react-native-paper';
-import DrawerItems from './DrawerItems';
-import { PreferencesContext } from './PreferencesContext';
+import { PreferencesContext } from './Preferences/PreferencesContext';
+import {
+ navigationPersistor,
+ useSetupPreferences,
+} from './Preferences/useSetupPreferences';
import App from './RootNavigator';
-import { dynamicThemeSupported } from '../utils';
import {
CombinedDarkTheme,
CombinedDefaultTheme,
@@ -32,54 +20,12 @@ import {
createConfiguredFontTheme,
} from '../utils/themes';
-const PERSISTENCE_KEY = 'NAVIGATION_STATE';
-const PREFERENCES_KEY = 'APP_PREFERENCES';
-
-const getInitialRtl = () => {
- if (Platform.OS === 'web' && typeof document !== 'undefined') {
- return document.documentElement.dir === 'rtl';
- }
-
- return I18nManager.getConstants().isRTL;
-};
+const Navigation = createStaticNavigation(App);
-const Drawer = createDrawerNavigator({
- screens: {
- Home: createDrawerScreen({
- screen: App,
- options: {
- headerShown: false,
- },
- linking: {
- path: '',
- initialRouteName: 'ExampleList',
- },
- }),
- },
-}).with(({ Navigator }) => {
- const preferences = React.useContext(PreferencesContext);
- const insets = useSafeAreaInsets();
- const { left, right } = insets || { left: 0, right: 0 };
- const collapsedDrawerWidth = 100 + Math.max(left, right);
-
- return (
- }
- />
- );
-});
-
-const Navigation = createStaticNavigation(Drawer);
-
-type RootDrawerType = typeof Drawer;
+type RootNavigationType = typeof App;
declare module '@react-navigation/core' {
- interface RootNavigator extends RootDrawerType {}
+ interface RootNavigator extends RootNavigationType {}
}
export default function PaperExample() {
@@ -89,114 +35,15 @@ export default function PaperExample() {
Abel: require('../assets/fonts/Abel-Regular.ttf'),
});
- const [isReady, setIsReady] = React.useState(false);
-
- const [shouldUseDynamicTheme, setShouldUseDynamicTheme] =
- React.useState(true);
- const [isDarkMode, setIsDarkMode] = React.useState(false);
- const [rtl, setRtl] = React.useState(getInitialRtl);
- const [collapsed, setCollapsed] = React.useState(false);
- const [customFontLoaded, setCustomFont] = React.useState(false);
- const [rippleEffectEnabled, setRippleEffectEnabled] = React.useState(true);
-
- const theme =
- dynamicThemeSupported && shouldUseDynamicTheme
- ? isDarkMode
- ? DynamicDarkTheme
- : DynamicLightTheme
- : isDarkMode
- ? DarkTheme
- : LightTheme;
-
- const direction = rtl ? 'rtl' : 'ltr';
-
- React.useEffect(() => {
- const restorePrefs = async () => {
- try {
- const prefString = await AsyncStorage.getItem(PREFERENCES_KEY);
- const preferences = JSON.parse(prefString || '');
-
- if (preferences) {
- setIsDarkMode(preferences.theme === 'dark');
-
- if (typeof preferences.rtl === 'boolean') {
- setRtl(preferences.rtl);
- }
- }
- } catch (e) {
- // ignore error
- } finally {
- setIsReady(true);
- }
- };
-
- void restorePrefs();
- }, []);
-
- React.useEffect(() => {
- const savePrefs = async () => {
- if (!isReady) {
- return;
- }
-
- try {
- await AsyncStorage.setItem(
- PREFERENCES_KEY,
- JSON.stringify({
- theme: isDarkMode ? 'dark' : 'light',
- rtl,
- })
- );
- } catch (e) {
- // ignore error
- }
-
- if (Platform.OS === 'web' && typeof document !== 'undefined') {
- document.documentElement.dir = direction;
- }
-
- if (I18nManager.getConstants().isRTL !== rtl) {
- I18nManager.forceRTL(rtl);
-
- if (Platform.OS !== 'web') {
- await Updates.reloadAsync();
- }
- }
- };
-
- void savePrefs();
- }, [direction, isDarkMode, isReady, rtl]);
-
- const preferences = React.useMemo(
- () => ({
- toggleShouldUseDynamicTheme: () =>
- setShouldUseDynamicTheme((oldValue) => !oldValue),
- toggleTheme: () => setIsDarkMode((oldValue) => !oldValue),
- toggleRtl: () => setRtl((oldValue) => !oldValue),
- toggleCollapsed: () => setCollapsed((oldValue) => !oldValue),
- toggleCustomFont: () => setCustomFont((oldValue) => !oldValue),
- toggleRippleEffect: () => setRippleEffectEnabled((oldValue) => !oldValue),
- customFontLoaded,
- rippleEffectEnabled,
- shouldUseDynamicTheme,
- theme,
- collapsed,
- rtl,
- }),
- [
- rtl,
- theme,
- collapsed,
- customFontLoaded,
- shouldUseDynamicTheme,
- rippleEffectEnabled,
- ]
- );
+ const { preferences, isReady, isDarkMode, direction, navigationKey } =
+ useSetupPreferences();
if (!isReady || !fontsLoaded) {
return null;
}
+ const { theme, customFontLoaded, rippleEffectEnabled } = preferences;
+
const combinedTheme = isDarkMode ? CombinedDarkTheme : CombinedDefaultTheme;
const configuredFontTheme = createConfiguredFontTheme(combinedTheme);
const configuredFontNavigationTheme =
@@ -209,26 +56,16 @@ export default function PaperExample() {
return (
{
void SplashScreen.hideAsync();
}}