Skip to content

Commit b9c9840

Browse files
authored
feat: implement freezeOnBlur (#302)
1 parent 024e92e commit b9c9840

File tree

15 files changed

+228
-12
lines changed

15 files changed

+228
-12
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'react-native-bottom-tabs': patch
3+
'@bottom-tabs/react-navigation': patch
4+
---
5+
6+
feat: implement freezeOnBlur

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;

0 commit comments

Comments
 (0)