Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/friendly-items-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'react-native-bottom-tabs': patch
'@bottom-tabs/react-navigation': patch
---

feat: implement freezeOnBlur
5 changes: 5 additions & 0 deletions apps/example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import NativeBottomTabsSVGs from './Examples/NativeBottomTabsSVGs';
import NativeBottomTabsRemoteIcons from './Examples/NativeBottomTabsRemoteIcons';
import NativeBottomTabsUnmounting from './Examples/NativeBottomTabsUnmounting';
import NativeBottomTabsCustomTabBar from './Examples/NativeBottomTabsCustomTabBar';
import NativeBottomTabsFreezeOnBlur from './Examples/NativeBottomTabsFreezeOnBlur';

const FourTabsIgnoreSafeArea = () => {
return <FourTabs ignoresTopSafeArea />;
Expand Down Expand Up @@ -130,6 +131,10 @@ const examples = [
component: HiddenTab,
name: 'Four Tabs - With Hidden Tab',
},
{
component: NativeBottomTabsFreezeOnBlur,
name: 'Native Bottom Tabs with freezeOnBlur',
},
{
component: NativeBottomTabsSVGs,
name: 'Native Bottom Tabs with SVG Icons',
Expand Down
2 changes: 1 addition & 1 deletion apps/example/src/Examples/NativeBottomTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function NativeBottomTabs() {
console.log(
`${Platform.OS}: Long press detected on tab with key ${data.target} at the screen level.`
);
setLabel('New Article')
setLabel('New Article');
},
}}
options={{
Expand Down
115 changes: 115 additions & 0 deletions apps/example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as React from 'react';
import { Platform, StyleSheet, Text, View } from 'react-native';
import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation';

const store = new Set<Dispatch>();

type Dispatch = (value: number) => void;

function useValue() {
const [value, setValue] = React.useState<number>(0);

React.useEffect(() => {
const dispatch = (value: number) => {
setValue(value);
};
store.add(dispatch);
return () => {
store.delete(dispatch);
};
}, [setValue]);

return value;
}

function HomeScreen() {
return (
<View style={styles.screenContainer}>
<Text>Home!</Text>
</View>
);
}

function DetailsScreen(props: any) {
const value = useValue();
const screenName = props?.route?.params?.screenName;
// only 1 'render' should appear at the time
console.log(`${Platform.OS} Details Screen render ${value} ${screenName}`);
return (
<View style={styles.screenContainer}>
<Text>Details!</Text>
<Text style={{ alignSelf: 'center' }}>
Details Screen {value} {screenName ? screenName : ''}{' '}
</Text>
</View>
);
}
const Tab = createNativeBottomTabNavigator();

export default function NativeBottomTabsFreezeOnBlur() {
React.useEffect(() => {
let timer = 0;
const interval = setInterval(() => {
timer = timer + 1;
store.forEach((dispatch) => dispatch(timer));
}, 3000);
return () => clearInterval(interval);
}, []);

return (
<Tab.Navigator
screenOptions={{
freezeOnBlur: true,
}}
>
<Tab.Screen
name="Article"
component={HomeScreen}
initialParams={{
screenName: 'Article',
}}
options={{
tabBarIcon: () => require('../../assets/icons/article_dark.png'),
}}
/>
<Tab.Screen
name="Albums"
component={DetailsScreen}
initialParams={{
screenName: 'Albums',
}}
options={{
tabBarIcon: () => require('../../assets/icons/grid_dark.png'),
}}
/>
<Tab.Screen
name="Contact"
component={DetailsScreen}
initialParams={{
screenName: 'Contact',
}}
options={{
tabBarIcon: () => require('../../assets/icons/person_dark.png'),
}}
/>
<Tab.Screen
name="Chat"
component={DetailsScreen}
initialParams={{
screenName: 'Chat',
}}
options={{
tabBarIcon: () => require('../../assets/icons/chat_dark.png'),
}}
/>
</Tab.Navigator>
);
}

const styles = StyleSheet.create({
screenContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
7 changes: 6 additions & 1 deletion apps/example/src/Examples/NativeBottomTabsSVGs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ const Tab = createNativeBottomTabNavigator();

function NativeBottomTabsSVGs() {
return (
<Tab.Navigator sidebarAdaptable>
<Tab.Navigator
sidebarAdaptable
screenOptions={{
freezeOnBlur: true,
}}
>
<Tab.Screen
name="Article"
component={Article}
Expand Down
1 change: 1 addition & 0 deletions docs/docs/docs/guides/standalone-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ Each route in the `routes` array can have the following properties:
- `badge`: Badge text to display on the tab
- `activeTintColor`: Custom active tint color for this specific tab
- `lazy`: Whether to lazy load this tab's content
- `freezeOnBlur`: Whether to freeze the tab's content when it's not visible

### Helper Props

Expand Down
8 changes: 7 additions & 1 deletion docs/docs/docs/guides/usage-with-react-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Whether to show labels in tabs.

Changes ripple color on tab press.

#### `disablePageAnimations` <Badge text="iOS" type="info" />
#### `disablePageAnimations`

Whether to disable page animations between tabs.

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

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.

#### `freezeOnBlur`
Boolean indicating whether to prevent inactive screens from re-rendering. Defaults to false.

It's working separately from `enableFreeze()` in `react-native-screens`. So settings won't be shared between them.


#### `tabBarButtonTestID`

Test ID for the tab item. This can be used to find the tab item in the native view hierarchy.
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-bottom-tabs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"version": "0.41.2"
},
"dependencies": {
"react-freeze": "^1.0.0",
"sf-symbols-typescript": "^2.0.0",
"use-latest-callback": "^0.2.1"
},
Expand Down
31 changes: 31 additions & 0 deletions packages/react-native-bottom-tabs/src/DelayedFreeze.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { Freeze } from 'react-freeze';

interface FreezeWrapperProps {
freeze: boolean;
children: React.ReactNode;
}

// Animation delay for freeze effect.
const ANIMATION_DELAY = 200;

// This component delays the freeze effect by animation delay.
// So that the screen is not frozen immediately causing background flash.
function DelayedFreeze({ freeze, children }: FreezeWrapperProps) {
// flag used for determining whether freeze should be enabled
const [freezeState, setFreezeState] = React.useState(false);

React.useEffect(() => {
const id = setTimeout(() => {
setFreezeState(freeze);
}, ANIMATION_DELAY);

return () => {
clearTimeout(id);
};
}, [freeze]);

return <Freeze freeze={freeze ? freezeState : false}>{children}</Freeze>;
}

export default DelayedFreeze;
25 changes: 25 additions & 0 deletions packages/react-native-bottom-tabs/src/Screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { View } from 'react-native';
import type { ViewProps } from 'react-native';
import DelayedFreeze from './DelayedFreeze';

interface ScreenProps extends ViewProps {
children: React.ReactNode;
freeze: boolean;
focused?: boolean;
}

function Screen({ children, freeze, focused, ...props }: ScreenProps) {
return (
<View
collapsable={false}
pointerEvents={focused ? 'auto' : 'none'}
accessibilityElementsHidden={!focused}
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
{...props}
>
<DelayedFreeze freeze={freeze}>{children}</DelayedFreeze>
</View>
);
}

export default Screen;
28 changes: 19 additions & 9 deletions packages/react-native-bottom-tabs/src/TabView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import type { TabViewItems } from './TabViewNativeComponent';
import Screen from './Screen';
import {
type ColorValue,
Image,
Expand Down Expand Up @@ -116,8 +117,16 @@
*/
getTestID?: (props: { route: Route }) => string | undefined;

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

/**
* Get freezeOnBlur for the current screen. Uses false by default.
*/
getFreezeOnBlur?: (props: { route: Route }) => boolean | undefined;

tabBarStyle?: {
/**
* Background color of the tab bar.
Expand Down Expand Up @@ -177,6 +186,7 @@
hapticFeedbackEnabled = false,
// 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.
labeled = Platform.OS !== 'android' ? true : undefined,
getFreezeOnBlur = ({ route }: { route: Route }) => route.freezeOnBlur,
tabBar: renderCustomTabBar,
tabBarStyle,
tabLabelStyle,
Expand Down Expand Up @@ -209,7 +219,7 @@

if (!loaded.includes(focusedKey)) {
// Set the current tab to be loaded if it was not loaded before
setLoaded((loaded) => [...loaded, focusedKey]);

Check warning on line 222 in packages/react-native-bottom-tabs/src/TabView.tsx

View workflow job for this annotation

GitHub Actions / lint

'loaded' is already declared in the upper scope on line 218 column 10
}

const icons = React.useMemo(
Expand Down Expand Up @@ -317,23 +327,20 @@
}

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

return (
<View
<Screen
key={route.key}
collapsable={false}
pointerEvents={focused ? 'auto' : 'none'}
accessibilityElementsHidden={!focused}
importantForAccessibility={
focused ? 'auto' : 'no-hide-descendants'
}
style={[{ position: 'absolute' }, measuredDimensions]}
freeze={!!freeze}
focused={focused}
style={[styles.screen, measuredDimensions]}
>
{renderScene({
route,
jumpTo,
})}
</View>
</Screen>
);
})}
</NativeTabView>
Expand All @@ -348,6 +355,9 @@
height: '100%',
flex: 1,
},
screen: {
position: 'absolute',
},
});

export default TabView;
1 change: 1 addition & 0 deletions packages/react-native-bottom-tabs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type BaseRoute = {
activeTintColor?: string;
hidden?: boolean;
testID?: string;
freezeOnBlur?: boolean;
};

export type NavigationState<Route extends BaseRoute> = {
Expand Down
6 changes: 6 additions & 0 deletions packages/react-navigation/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ export type NativeBottomTabNavigationOptions = {
* TestID for the tab.
*/
tabBarButtonTestID?: string;

/**
* Whether inactive screens should be suspended from re-rendering. Defaults to `false`.
*/
freezeOnBlur?: boolean;
};

export type NativeBottomTabDescriptor = Descriptor<
Expand Down Expand Up @@ -127,6 +132,7 @@ export type NativeBottomTabNavigationConfig = Partial<
| 'getActiveTintColor'
| 'getTestID'
| 'tabBar'
| 'getFreezeOnBlur'
>
> & {
tabBar?: (props: BottomTabBarProps) => React.ReactNode;
Expand Down
3 changes: 3 additions & 0 deletions packages/react-navigation/src/views/NativeBottomTabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export default function NativeBottomTabView({
return null;
}}
getLazy={({ route }) => descriptors[route.key]?.options.lazy ?? true}
getFreezeOnBlur={({ route }) =>
descriptors[route.key]?.options.freezeOnBlur
}
onTabLongPress={(index) => {
const route = state.routes[index];
if (!route) {
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17105,6 +17105,7 @@ __metadata:
"@types/jest": "npm:^29.5.5"
"@types/react": "npm:^18.2.44"
react: "npm:18.3.1"
react-freeze: "npm:^1.0.0"
react-native: "npm:0.75.4"
react-native-builder-bob: "npm:^0.32.1"
sf-symbols-typescript: "npm:^2.0.0"
Expand Down
Loading