Skip to content

Commit da6c285

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

File tree

14 files changed

+222
-12
lines changed

14 files changed

+222
-12
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function NativeBottomTabs() {
4242
console.log(
4343
`${Platform.OS}: Long press detected on tab with key ${data.target} at the screen level.`
4444
);
45-
setLabel('New Article')
45+
setLabel('New Article');
4646
},
4747
}}
4848
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+
});

apps/example/src/Examples/NativeBottomTabsSVGs.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ const Tab = createNativeBottomTabNavigator();
88

99
function NativeBottomTabsSVGs() {
1010
return (
11-
<Tab.Navigator sidebarAdaptable>
11+
<Tab.Navigator
12+
sidebarAdaptable
13+
screenOptions={{
14+
freezeOnBlur: true,
15+
}}
16+
>
1217
<Tab.Screen
1318
name="Article"
1419
component={Article}

docs/docs/docs/guides/standalone-usage.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ Each route in the `routes` array can have the following properties:
188188
- `badge`: Badge text to display on the tab
189189
- `activeTintColor`: Custom active tint color for this specific tab
190190
- `lazy`: Whether to lazy load this tab's content
191+
- `freezeOnBlur`: Whether to freeze the tab's content when it's not visible
191192

192193
### Helper Props
193194

docs/docs/docs/guides/usage-with-react-navigation.mdx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ Whether to show labels in tabs.
105105

106106
Changes ripple color on tab press.
107107

108-
#### `disablePageAnimations` <Badge text="iOS" type="info" />
108+
#### `disablePageAnimations`
109109

110110
Whether to disable page animations between tabs.
111111

@@ -266,6 +266,12 @@ Due to native limitations on iOS, this option doesn't hide the tab item **when h
266266

267267
Whether this screens should render the first time it's accessed. Defaults to true. Set it to false if you want to render the screen on initial render.
268268

269+
#### `freezeOnBlur`
270+
Boolean indicating whether to prevent inactive screens from re-rendering. Defaults to false.
271+
272+
It's working separately from `enableFreeze()` in `react-native-screens`. So settings won't be shared between them.
273+
274+
269275
#### `tabBarButtonTestID`
270276

271277
Test ID for the tab item. This can be used to find the tab item in the native view hierarchy.

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+
// Animation delay for freeze effect.
10+
const ANIMATION_DELAY = 200;
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;

0 commit comments

Comments
 (0)