Skip to content

Commit c9f13ad

Browse files
authored
feat: implement freezeOnBlur (#207)
1 parent f17fa06 commit c9f13ad

File tree

11 files changed

+196
-7
lines changed

11 files changed

+196
-7
lines changed

.changeset/purple-baboons-rhyme.md

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: add freezeOnBlur

apps/example/ios/Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,7 +1209,7 @@ PODS:
12091209
- ReactCommon/turbomodule/bridging
12101210
- ReactCommon/turbomodule/core
12111211
- Yoga
1212-
- react-native-bottom-tabs (0.7.3):
1212+
- react-native-bottom-tabs (0.7.6):
12131213
- DoubleConversion
12141214
- glog
12151215
- RCT-Folly (= 2024.01.01.00)
@@ -1222,7 +1222,7 @@ PODS:
12221222
- React-graphics
12231223
- React-ImageManager
12241224
- React-jsi
1225-
- react-native-bottom-tabs/common (= 0.7.3)
1225+
- react-native-bottom-tabs/common (= 0.7.6)
12261226
- React-NativeModulesApple
12271227
- React-RCTFabric
12281228
- React-rendererdebug
@@ -1234,7 +1234,7 @@ PODS:
12341234
- SDWebImageSVGCoder (>= 1.7.0)
12351235
- SwiftUIIntrospect (~> 1.0)
12361236
- Yoga
1237-
- react-native-bottom-tabs/common (0.7.3):
1237+
- react-native-bottom-tabs/common (0.7.6):
12381238
- DoubleConversion
12391239
- glog
12401240
- RCT-Folly (= 2024.01.01.00)
@@ -1943,7 +1943,7 @@ SPEC CHECKSUMS:
19431943
React-logger: d79b704bf215af194f5213a6b7deec50ba8e6a9b
19441944
React-Mapbuffer: b982d5bba94a8bc073bda48f0d27c9b28417fae3
19451945
React-microtasksnativemodule: 8fa285fed833a04a754bf575f8ded65fc240b88d
1946-
react-native-bottom-tabs: b6b3dc2e971c860a0a6d763701929d1899f666a0
1946+
react-native-bottom-tabs: 084cfd4d4b1e74c03f4196b3f62d39445882f45f
19471947
react-native-safe-area-context: 73505107f7c673cd550a561aeb6271f152c483b6
19481948
React-nativeconfig: 8c83d992b9cc7d75b5abe262069eaeea4349f794
19491949
React-NativeModulesApple: b8465afc883f5bf3fe8bac3767e394d581a5f123

apps/example/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import TintColorsExample from './Examples/TintColors';
2727
import NativeBottomTabsEmbeddedStacks from './Examples/NativeBottomTabsEmbeddedStacks';
2828
import NativeBottomTabsSVGs from './Examples/NativeBottomTabsSVGs';
2929
import NativeBottomTabsRemoteIcons from './Examples/NativeBottomTabsRemoteIcons';
30+
import NativeBottomTabsFreezeOnBlur from './Examples/NativeBottomTabsFreezeOnBlur';
3031

3132
const FourTabsIgnoreSafeArea = () => {
3233
return <FourTabs ignoresTopSafeArea />;
@@ -102,6 +103,10 @@ const examples = [
102103
name: 'Four Tabs - Transparent scroll edge appearance',
103104
platform: 'ios',
104105
},
106+
{
107+
component: NativeBottomTabsFreezeOnBlur,
108+
name: 'Native Bottom Tabs with freezeOnBlur',
109+
},
105110
{
106111
component: FourTabsOpaqueScrollEdgeAppearance,
107112
name: 'Four Tabs - Opaque scroll edge appearance',
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: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,18 @@
7777
"react": "18.3.1",
7878
"react-native": "0.75.4",
7979
"react-native-builder-bob": "^0.32.1",
80+
"react-native-screens": "4.3.0",
8081
"typescript": "^5.2.2"
8182
},
8283
"peerDependencies": {
8384
"react": "*",
84-
"react-native": "*"
85+
"react-native": "*",
86+
"react-native-screens": ">=3.29.0"
87+
},
88+
"peerDependenciesMeta": {
89+
"react-native-screens": {
90+
"optional": true
91+
}
8592
},
8693
"react-native-builder-bob": {
8794
"source": "src",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as React from 'react';
2+
import { View } from 'react-native';
3+
import type { StyleProp, ViewProps, ViewStyle } from 'react-native';
4+
5+
interface Props extends ViewProps {
6+
visible: boolean;
7+
children?: React.ReactNode;
8+
freezeOnBlur?: boolean;
9+
style?: StyleProp<ViewStyle>;
10+
collapsable?: boolean;
11+
}
12+
13+
let Screens: typeof import('react-native-screens') | undefined;
14+
15+
try {
16+
Screens = require('react-native-screens');
17+
} catch (e) {
18+
// Ignore
19+
}
20+
21+
export function Screen({ visible, ...rest }: Props) {
22+
if (Screens?.screensEnabled()) {
23+
return <Screens.Screen activityState={visible ? 2 : 0} {...rest} />;
24+
}
25+
return <View {...rest} />;
26+
}

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { ImageSource } from 'react-native/Libraries/Image/ImageSource';
1515
import TabViewAdapter from './TabViewAdapter';
1616
import useLatestCallback from 'use-latest-callback';
1717
import type { BaseRoute, NavigationState } from './types';
18+
import { Screen } from './Screen';
1819

1920
const isAppleSymbol = (icon: any): icon is { sfSymbol: string } =>
2021
icon?.sfSymbol;
@@ -116,6 +117,13 @@ interface Props<Route extends BaseRoute> {
116117
*/
117118
getTestID?: (props: { route: Route }) => string | undefined;
118119

120+
/**
121+
* Get freezeOnBlur for the current screen. Uses false by default.
122+
* Defaults to `true` when `enableFreeze()` is run at the top of the application.
123+
*
124+
*/
125+
getFreezeOnBlur?: (props: { route: Route }) => boolean | undefined;
126+
119127
/**
120128
* Background color of the tab bar.
121129
*/
@@ -160,6 +168,7 @@ const TabView = <Route extends BaseRoute>({
160168
tabBarInactiveTintColor: inactiveTintColor,
161169
getLazy = ({ route }: { route: Route }) => route.lazy,
162170
getLabelText = ({ route }: { route: Route }) => route.title,
171+
getFreezeOnBlur = ({ route }: { route: Route }) => route.freezeOnBlur,
163172
getIcon = ({ route, focused }: { route: Route; focused: boolean }) =>
164173
route.unfocusedIcon
165174
? focused
@@ -311,11 +320,14 @@ const TabView = <Route extends BaseRoute>({
311320
const focused = route.key === focusedKey;
312321
const opacity = focused ? 1 : 0;
313322
const zIndex = focused ? 0 : -1;
323+
const freezeOnBlur = getFreezeOnBlur({ route });
314324

315325
return (
316-
<View
326+
<Screen
317327
key={route.key}
318328
collapsable={false}
329+
visible={focused}
330+
freezeOnBlur={freezeOnBlur}
319331
pointerEvents={focused ? 'auto' : 'none'}
320332
accessibilityElementsHidden={!focused}
321333
importantForAccessibility={
@@ -331,7 +343,7 @@ const TabView = <Route extends BaseRoute>({
331343
route,
332344
jumpTo,
333345
})}
334-
</View>
346+
</Screen>
335347
);
336348
})}
337349
</TabViewAdapter>

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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ 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+
* Defaults to `true` when `enableFreeze()` is run at the top of the application.
98+
*
99+
* Only supported on iOS and Android.
100+
*/
101+
freezeOnBlur?: boolean;
94102
};
95103

96104
export type NativeBottomTabDescriptor = Descriptor<
@@ -117,5 +125,6 @@ export type NativeBottomTabNavigationConfig = Partial<
117125
| 'onTabLongPress'
118126
| 'getActiveTintColor'
119127
| 'getTestID'
128+
| 'getFreezeOnBlur'
120129
>
121130
>;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export default function NativeBottomTabView({
4545
const options = descriptors[route.key]?.options;
4646
return options?.tabBarItemHidden === true;
4747
}}
48+
getFreezeOnBlur={({ route }) =>
49+
descriptors[route.key]?.options.freezeOnBlur
50+
}
4851
getTestID={({ route }) =>
4952
descriptors[route.key]?.options.tabBarButtonTestID
5053
}

0 commit comments

Comments
 (0)