Skip to content

Commit cec0ee8

Browse files
committed
feat: implement freezeOnBlur
1 parent 024e92e commit cec0ee8

File tree

11 files changed

+213
-10
lines changed

11 files changed

+213
-10
lines changed

apps/example/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import NativeBottomTabsSVGs from './Examples/NativeBottomTabsSVGs';
2929
import NativeBottomTabsRemoteIcons from './Examples/NativeBottomTabsRemoteIcons';
3030
import NativeBottomTabsUnmounting from './Examples/NativeBottomTabsUnmounting';
3131
import NativeBottomTabsCustomTabBar from './Examples/NativeBottomTabsCustomTabBar';
32+
import NativeBottomTabsFreezeOnBlur from './Examples/NativeBottomTabsFreezeOnBlur';
3233

3334
const FourTabsIgnoreSafeArea = () => {
3435
return <FourTabs ignoresTopSafeArea />;
@@ -130,6 +131,10 @@ const examples = [
130131
component: HiddenTab,
131132
name: 'Four Tabs - With Hidden Tab',
132133
},
134+
{
135+
component: NativeBottomTabsFreezeOnBlur,
136+
name: 'Native Bottom Tabs with freezeOnBlur',
137+
},
133138
{
134139
component: NativeBottomTabsSVGs,
135140
name: 'Native Bottom Tabs with SVG Icons',

apps/example/src/Examples/NativeBottomTabs.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ function NativeBottomTabs() {
2020
tabBarStyle={{
2121
backgroundColor: '#1E2D2F',
2222
}}
23+
screenOptions={{
24+
freezeOnBlur: true,
25+
}}
2326
rippleColor="#F7DBA7"
2427
tabLabelStyle={{
2528
fontFamily: 'Avenir',
@@ -42,7 +45,7 @@ function NativeBottomTabs() {
4245
console.log(
4346
`${Platform.OS}: Long press detected on tab with key ${data.target} at the screen level.`
4447
);
45-
setLabel('New Article')
48+
setLabel('New Article');
4649
},
4750
}}
4851
options={{
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import * as React from 'react';
2+
import { Platform, StyleSheet, Text, View } from 'react-native';
3+
import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation';
4+
5+
const store = new Set<Dispatch>();
6+
7+
type Dispatch = (value: number) => void;
8+
9+
function useValue() {
10+
const [value, setValue] = React.useState<number>(0);
11+
12+
React.useEffect(() => {
13+
const dispatch = (value: number) => {
14+
setValue(value);
15+
};
16+
store.add(dispatch);
17+
return () => {
18+
store.delete(dispatch);
19+
};
20+
}, [setValue]);
21+
22+
return value;
23+
}
24+
25+
function HomeScreen() {
26+
return (
27+
<View style={styles.screenContainer}>
28+
<Text>Home!</Text>
29+
</View>
30+
);
31+
}
32+
33+
function DetailsScreen(props: any) {
34+
const value = useValue();
35+
const screenName = props?.route?.params?.screenName;
36+
// only 1 'render' should appear at the time
37+
console.log(`${Platform.OS} Details Screen render ${value} ${screenName}`);
38+
return (
39+
<View style={styles.screenContainer}>
40+
<Text>Details!</Text>
41+
<Text style={{ alignSelf: 'center' }}>
42+
Details Screen {value} {screenName ? screenName : ''}{' '}
43+
</Text>
44+
</View>
45+
);
46+
}
47+
const Tab = createNativeBottomTabNavigator();
48+
49+
export default function NativeBottomTabsFreezeOnBlur() {
50+
React.useEffect(() => {
51+
let timer = 0;
52+
const interval = setInterval(() => {
53+
timer = timer + 1;
54+
store.forEach((dispatch) => dispatch(timer));
55+
}, 3000);
56+
return () => clearInterval(interval);
57+
}, []);
58+
59+
return (
60+
<Tab.Navigator
61+
screenOptions={{
62+
freezeOnBlur: true,
63+
}}
64+
>
65+
<Tab.Screen
66+
name="Article"
67+
component={HomeScreen}
68+
initialParams={{
69+
screenName: 'Article',
70+
}}
71+
options={{
72+
tabBarIcon: () => require('../../assets/icons/article_dark.png'),
73+
}}
74+
/>
75+
<Tab.Screen
76+
name="Albums"
77+
component={DetailsScreen}
78+
initialParams={{
79+
screenName: 'Albums',
80+
}}
81+
options={{
82+
tabBarIcon: () => require('../../assets/icons/grid_dark.png'),
83+
}}
84+
/>
85+
<Tab.Screen
86+
name="Contact"
87+
component={DetailsScreen}
88+
initialParams={{
89+
screenName: 'Contact',
90+
}}
91+
options={{
92+
tabBarIcon: () => require('../../assets/icons/person_dark.png'),
93+
}}
94+
/>
95+
<Tab.Screen
96+
name="Chat"
97+
component={DetailsScreen}
98+
initialParams={{
99+
screenName: 'Chat',
100+
}}
101+
options={{
102+
tabBarIcon: () => require('../../assets/icons/chat_dark.png'),
103+
}}
104+
/>
105+
</Tab.Navigator>
106+
);
107+
}
108+
109+
const styles = StyleSheet.create({
110+
screenContainer: {
111+
flex: 1,
112+
justifyContent: 'center',
113+
alignItems: 'center',
114+
},
115+
});

packages/react-native-bottom-tabs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"version": "0.41.2"
114114
},
115115
"dependencies": {
116+
"react-freeze": "^1.0.0",
116117
"sf-symbols-typescript": "^2.0.0",
117118
"use-latest-callback": "^0.2.1"
118119
},
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react';
2+
import { Freeze } from 'react-freeze';
3+
4+
interface FreezeWrapperProps {
5+
freeze: boolean;
6+
children: React.ReactNode;
7+
}
8+
9+
// Delay before applying freeze effect
10+
const ANIMATION_DELAY = 100;
11+
12+
// This component delays the freeze effect by animation delay.
13+
// So that the screen is not frozen immediately causing background flash.
14+
function DelayedFreeze({ freeze, children }: FreezeWrapperProps) {
15+
// flag used for determining whether freeze should be enabled
16+
const [freezeState, setFreezeState] = React.useState(false);
17+
18+
React.useEffect(() => {
19+
const id = setTimeout(() => {
20+
setFreezeState(freeze);
21+
}, ANIMATION_DELAY);
22+
23+
return () => {
24+
clearTimeout(id);
25+
};
26+
}, [freeze]);
27+
28+
return <Freeze freeze={freeze ? freezeState : false}>{children}</Freeze>;
29+
}
30+
31+
export default DelayedFreeze;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { View } from 'react-native';
2+
import type { ViewProps } from 'react-native';
3+
import DelayedFreeze from './DelayedFreeze';
4+
5+
interface ScreenProps extends ViewProps {
6+
children: React.ReactNode;
7+
freeze: boolean;
8+
focused?: boolean;
9+
}
10+
11+
function Screen({ children, freeze, focused, ...props }: ScreenProps) {
12+
return (
13+
<View
14+
collapsable={false}
15+
pointerEvents={focused ? 'auto' : 'none'}
16+
accessibilityElementsHidden={!focused}
17+
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
18+
{...props}
19+
>
20+
<DelayedFreeze freeze={freeze}>{children}</DelayedFreeze>
21+
</View>
22+
);
23+
}
24+
25+
export default Screen;

packages/react-native-bottom-tabs/src/TabView.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import type { TabViewItems } from './TabViewNativeComponent';
3+
import Screen from './Screen';
34
import {
45
type ColorValue,
56
Image,
@@ -116,8 +117,16 @@ interface Props<Route extends BaseRoute> {
116117
*/
117118
getTestID?: (props: { route: Route }) => string | undefined;
118119

120+
/**
121+
* Custom tab bar to render. Set to `null` to hide the tab bar completely.
122+
*/
119123
tabBar?: () => React.ReactNode;
120124

125+
/**
126+
* Get freezeOnBlur for the current screen. Uses false by default.
127+
*/
128+
getFreezeOnBlur?: (props: { route: Route }) => boolean | undefined;
129+
121130
tabBarStyle?: {
122131
/**
123132
* Background color of the tab bar.
@@ -177,6 +186,7 @@ const TabView = <Route extends BaseRoute>({
177186
hapticFeedbackEnabled = false,
178187
// Android's native behavior is to show labels when there are less than 4 tabs. We leave it as undefined to use the platform default behavior.
179188
labeled = Platform.OS !== 'android' ? true : undefined,
189+
getFreezeOnBlur = ({ route }: { route: Route }) => route.freezeOnBlur,
180190
tabBar: renderCustomTabBar,
181191
tabBarStyle,
182192
tabLabelStyle,
@@ -317,23 +327,20 @@ const TabView = <Route extends BaseRoute>({
317327
}
318328

319329
const focused = route.key === focusedKey;
330+
const freeze = !focused ? getFreezeOnBlur({ route }) : false;
320331

321332
return (
322-
<View
333+
<Screen
323334
key={route.key}
324-
collapsable={false}
325-
pointerEvents={focused ? 'auto' : 'none'}
326-
accessibilityElementsHidden={!focused}
327-
importantForAccessibility={
328-
focused ? 'auto' : 'no-hide-descendants'
329-
}
330-
style={[{ position: 'absolute' }, measuredDimensions]}
335+
freeze={!!freeze}
336+
focused={focused}
337+
style={[styles.screen, measuredDimensions]}
331338
>
332339
{renderScene({
333340
route,
334341
jumpTo,
335342
})}
336-
</View>
343+
</Screen>
337344
);
338345
})}
339346
</NativeTabView>
@@ -348,6 +355,9 @@ const styles = StyleSheet.create({
348355
height: '100%',
349356
flex: 1,
350357
},
358+
screen: {
359+
position: 'absolute',
360+
},
351361
});
352362

353363
export default TabView;

packages/react-native-bottom-tabs/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type BaseRoute = {
1515
activeTintColor?: string;
1616
hidden?: boolean;
1717
testID?: string;
18+
freezeOnBlur?: boolean;
1819
};
1920

2021
export type NavigationState<Route extends BaseRoute> = {

packages/react-navigation/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ export type NativeBottomTabNavigationOptions = {
9191
* TestID for the tab.
9292
*/
9393
tabBarButtonTestID?: string;
94+
95+
/**
96+
* Whether inactive screens should be suspended from re-rendering. Defaults to `false`.
97+
*
98+
* Only supported on iOS and Android.
99+
*/
100+
freezeOnBlur?: boolean;
94101
};
95102

96103
export type NativeBottomTabDescriptor = Descriptor<
@@ -127,6 +134,7 @@ export type NativeBottomTabNavigationConfig = Partial<
127134
| 'getActiveTintColor'
128135
| 'getTestID'
129136
| 'tabBar'
137+
| 'getFreezeOnBlur'
130138
>
131139
> & {
132140
tabBar?: (props: BottomTabBarProps) => React.ReactNode;

packages/react-navigation/src/views/NativeBottomTabView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ export default function NativeBottomTabView({
6363
return null;
6464
}}
6565
getLazy={({ route }) => descriptors[route.key]?.options.lazy ?? true}
66+
getFreezeOnBlur={({ route }) =>
67+
descriptors[route.key]?.options.freezeOnBlur
68+
}
6669
onTabLongPress={(index) => {
6770
const route = state.routes[index];
6871
if (!route) {

0 commit comments

Comments
 (0)