Skip to content

Commit

Permalink
feat: simplify bottom tabs logic (#11175)
Browse files Browse the repository at this point in the history
* feat: simplify bottom tabs logic

* chore: better comment

* chore: remove old visual clue

* chore: make text non selectable
  • Loading branch information
MounirDhahri authored Nov 22, 2024
1 parent e91bac2 commit 8162eae
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 3 deletions.
26 changes: 23 additions & 3 deletions src/app/Navigation/AuthenticatedRoutes/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { tappedTabBar } from "@artsy/cohesion"
import { Text } from "@artsy/palette-mobile"
import { THEME } from "@artsy/palette-tokens"
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"
import { createNativeStackNavigator } from "@react-navigation/native-stack"
import { AppModule, modules } from "app/AppRegistry"
Expand All @@ -8,14 +10,16 @@ import { ProfileTab } from "app/Navigation/AuthenticatedRoutes/ProfileTab"
import { SearchTab } from "app/Navigation/AuthenticatedRoutes/SearchTab"
import { SellTab } from "app/Navigation/AuthenticatedRoutes/SellTab"
import { registerSharedModalRoutes } from "app/Navigation/AuthenticatedRoutes/SharedRoutes"
import { useBottomTabsBadges } from "app/Navigation/Utils/useBottomTabsBadges"
import { BottomTabOption, BottomTabType } from "app/Scenes/BottomTabs/BottomTabType"
import { BottomTabsButton } from "app/Scenes/BottomTabs/BottomTabsButton"
import { BottomTabsIcon } from "app/Scenes/BottomTabs/BottomTabsIcon"
import { bottomTabsConfig } from "app/Scenes/BottomTabs/bottomTabsConfig"
import { OnboardingQuiz } from "app/Scenes/Onboarding/OnboardingQuiz/OnboardingQuiz"
import { GlobalStore } from "app/store/GlobalStore"
import { internal_navigationRef } from "app/system/navigation/navigate"
import { postEventToProviders } from "app/utils/track/providers"
import { Platform } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"

if (Platform.OS === "ios") {
require("app/Navigation/AuthenticatedRoutes/NativeScreens")
Expand All @@ -39,7 +43,12 @@ type TabRoutesParams = {

const Tab = createBottomTabNavigator<TabRoutesParams>()

const BOTTOM_TABS_HEIGHT = 60

const AppTabs: React.FC = () => {
const { tabsBadges } = useBottomTabsBadges()
const insets = useSafeAreaInsets()

return (
<Tab.Navigator
screenOptions={({ route }) => {
Expand All @@ -49,15 +58,26 @@ const AppTabs: React.FC = () => {
tabBarStyle: {
animate: true,
position: "absolute",
height: BOTTOM_TABS_HEIGHT + insets.bottom,
display:
currentRoute && modules[currentRoute as AppModule]?.options.hidesBottomTabs
? "none"
: "flex",
},
tabBarHideOnKeyboard: true,
tabBarButton: (props) => {
return <BottomTabsButton tab={route.name} onPress={props.onPress} />
tabBarIcon: ({ focused }) => {
return <BottomTabsIcon tab={route.name} state={focused ? "active" : "inactive"} />
},
tabBarLabel: () => {
return (
<Text variant="xxs" style={{ top: -4 }} selectable={false}>
{bottomTabsConfig[route.name].name}
</Text>
)
},
tabBarActiveTintColor: THEME.colors["black100"],
tabBarInactiveTintColor: THEME.colors["black60"],
...tabsBadges[route.name],
}
}}
screenListeners={{
Expand Down
97 changes: 97 additions & 0 deletions src/app/Navigation/Utils/useBottomTabsBadges.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { useColor, useSpace } from "@artsy/palette-mobile"
import { renderHook } from "@testing-library/react-hooks"
import { useVisualClue } from "app/utils/hooks/useVisualClue"
import { useTabBarBadge } from "app/utils/useTabBarBadge"
import { useBottomTabsBadges } from "./useBottomTabsBadges"

// Mocking the necessary imports
jest.mock("@artsy/palette-mobile", () => ({
useColor: jest.fn(),
useSpace: jest.fn(),
}))
jest.mock("app/utils/hooks/useVisualClue", () => ({
useVisualClue: jest.fn(),
}))

jest.mock("app/utils/useTabBarBadge", () => ({
useTabBarBadge: jest.fn(),
}))

// Settings for the test
describe("useBottomTabsBadges", () => {
const mockUseColor = useColor as jest.Mock
const mockUseSpace = useSpace as jest.Mock
const mockUseVisualClue = useVisualClue as jest.Mock
const mockUseTabBarBadge = useTabBarBadge as jest.Mock

beforeEach(() => {
mockUseColor.mockReturnValue((color: string) => color)
mockUseSpace.mockReturnValue(() => 10)
})

it("returns default badge states when no clues or notifications are present", () => {
mockUseVisualClue.mockReturnValue({ showVisualClue: () => false })
mockUseTabBarBadge.mockReturnValue({
unreadConversationsCount: 0,
hasUnseenNotifications: false,
})

const { result } = renderHook(() => useBottomTabsBadges())

expect(result.current.tabsBadges).toMatchObject({
home: { badgeCount: undefined, badgeStyle: {} },
search: { badgeCount: undefined, badgeStyle: {} },
inbox: { badgeCount: undefined, badgeStyle: {} },
sell: { badgeCount: undefined, badgeStyle: {} },
profile: { badgeCount: undefined, badgeStyle: {} },
})
})

it('updates badge for "home" tab when unseen notifications are present', () => {
mockUseVisualClue.mockReturnValue({ showVisualClue: () => false })
mockUseTabBarBadge.mockReturnValue({
hasUnseenNotifications: true,
})

const { result } = renderHook(() => useBottomTabsBadges())

expect(result.current.tabsBadges.home).toMatchObject({
badgeCount: "",
badgeStyle: {
// Whatever style we have here
},
})
})

it('updates badge for "inbox" tab when unseen notifications are present', () => {
mockUseVisualClue.mockReturnValue({ showVisualClue: () => false })
mockUseTabBarBadge.mockReturnValue({
unreadConversationsCount: 5,
})

const { result } = renderHook(() => useBottomTabsBadges())

expect(result.current.tabsBadges.inbox).toMatchObject({
badgeCount: 5,
badgeStyle: {
// Whatever style we have here
},
})
})

it('prioritises conversations count over visual clues for "inbox" tab', () => {
mockUseVisualClue.mockReturnValue({ showVisualClue: () => true })
mockUseTabBarBadge.mockReturnValue({
unreadConversationsCount: 5,
})

const { result } = renderHook(() => useBottomTabsBadges())

expect(result.current.tabsBadges.inbox).toMatchObject({
badgeCount: 5,
badgeStyle: {
// Whatever style we have here
},
})
})
})
90 changes: 90 additions & 0 deletions src/app/Navigation/Utils/useBottomTabsBadges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useColor, useSpace } from "@artsy/palette-mobile"
import { BottomTabType } from "app/Scenes/BottomTabs/BottomTabType"
import { bottomTabsConfig } from "app/Scenes/BottomTabs/bottomTabsConfig"
import { useVisualClue } from "app/utils/hooks/useVisualClue"
import { useTabBarBadge } from "app/utils/useTabBarBadge"
import { StyleProp, TextStyle } from "react-native"

const VISUAL_CLUE_HEIGHT = 10

type BadgeProps = { badgeCount?: string | number; badgeStyle: StyleProp<TextStyle> }
/**
* This hook is used to get badge details for each bottom tab
* @returns an object with the badge count and style for each tab
* @example { home: { badgeCount: 5, badgeStyle: { backgroundColor: "red" } }, search: { badgeCount: undefined, badgeStyle: {} } }
*/
export const useBottomTabsBadges = () => {
const color = useColor()
const space = useSpace()

const { showVisualClue } = useVisualClue()

const { unreadConversationsCount, hasUnseenNotifications } = useTabBarBadge()

const tabsBadges: Record<string, BadgeProps> = {}

const visualClueStyles = {
backgroundColor: color("blue100"),
top: space(1),
minWidth: VISUAL_CLUE_HEIGHT,
maxHeight: VISUAL_CLUE_HEIGHT,
borderRadius: VISUAL_CLUE_HEIGHT / 2,
borderColor: color("white100"),
borderWidth: 1,
}

const hasVisualCluesForTab = (tab: BottomTabType) => {
const visualClues = bottomTabsConfig[tab].visualClues
if (!visualClues) {
return false
}

return visualClues?.find(showVisualClue)
}

Object.keys(bottomTabsConfig).forEach((tab) => {
const defaultBadgeProps: BadgeProps = {
badgeCount: undefined,
badgeStyle: {},
}

tabsBadges[tab] = defaultBadgeProps

if (hasVisualCluesForTab(tab as BottomTabType)) {
tabsBadges[tab] = {
badgeCount: "",
badgeStyle: {
...visualClueStyles,
},
}
}

switch (tab) {
case "home": {
if (hasUnseenNotifications) {
tabsBadges[tab] = {
badgeCount: "",
badgeStyle: {
...visualClueStyles,
},
}
}
return
}

case "inbox": {
if (unreadConversationsCount) {
tabsBadges[tab] = {
badgeCount: unreadConversationsCount,
badgeStyle: {
backgroundColor: color("red100"),
},
}
}
return
}
}
})

return { tabsBadges }
}
1 change: 1 addition & 0 deletions src/app/Scenes/BottomTabs/bottomTabsConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const bottomTabsConfig: {
route: BottomTabRoute
analyticsDescription: TappedTabBarArgs["tab"]
name: string
visualClues?: string[]
}
} = {
home: {
Expand Down

0 comments on commit 8162eae

Please sign in to comment.