Skip to content

Drawer Navigation, Navigation Stack Refactor, and Nav Header Branding#1137

Open
kevinherdez wants to merge 1 commit intoNWACus:kevinherdez/mapProjectfrom
kevinherdez:kevinherdez/stackNavigationRefactor
Open

Drawer Navigation, Navigation Stack Refactor, and Nav Header Branding#1137
kevinherdez wants to merge 1 commit intoNWACus:kevinherdez/mapProjectfrom
kevinherdez:kevinherdez/stackNavigationRefactor

Conversation

@kevinherdez
Copy link
Collaborator

This PR address the following issues:

Overview

This pulls in @react-navigation/drawer to move the Menu in the drawer, and refactors the navigation stack to correctly hide the bottom tab in the views where it needs to be hidden according to the designs.

Where to Start

App.tsx

  • Just to see the starting point for the stack changes

RootStack.tsx

  • The new top level stack navigator. From here you'll be able to follow all of the new nested stack navigators

Refactor

The screen definitions that use to be in ObservationsScreen.tsx and WeatherScreen.tsx have been moved into MainStack.tsx

Navigation Stack Overview and Refactor

To achieve the behavior we want there are now 4 levels to the Navigation Stack

RootStackNavigation
|
DrawerNavigator
|
MainStackNavigator
|
BottomTabNavigator

RootStackNavigator

This contains the Drawer and any views that we want to present on top of the drawer. If you include these screens inside the DrawerNavigator itself, they replace the BottomTabs which isn't necessarily what we want

DrawerNavigator

This only contains the MainStackNavigator as its main screen and it contains the UI for the drawer itself.

MainStackNavigator

This contains the BottomTabs and any screen that is presented as a part of the "main" app experience. This moves a lot of views like the ForecastScreen, ObservationDetailScreen, etc out of the bottom tab navigator so that the bottom tabs are not shown in these views. This is the based on React Navigation docs as best way to hide the bottom tab in views: https://reactnavigation.org/docs/hiding-tabbar-in-screens/

BottomTabs

This is the old TabNavigator that was in App.tsx. However, the HomeTabScreen, ObservationsTabScreen, and the WeatherScreen have all been refactored to remove their own navigation stack. BottomTabs now only has 3 screens, none of which are their own stack navigators.

Design changes

Navigation Headers

BottomTabNavigationHeader was created for use in the 3 screens contained in BottomTabs. This is because the header definition in BottomTab.Navigator passes BottomTabHeaderProps instead of NativeStackHeaderProps. For simplicity, I broke out the different headers into 2 different components instead of trying to combine them into 1.

Branding

BottomTabNavigationHeader displays the branding for the center. Currently, that is just the logo with the acronym and no special colors

ForecastScreen

This PR removes the ability to change zones when viewing the full avalanche forecast as per designs

SafeArea

Many of the views that were a part of BottomTabs needed to be updated to have the correct bottom padding now that they no longer have a bottom tab

Recording with the changes in action

StackRework.mov

@yuliadub
Copy link
Collaborator

Navigation Stack Overview and Refactor

To achieve the behavior we want there are now 4 levels to the Navigation Stack

RootStackNavigation | DrawerNavigator | MainStackNavigator | BottomTabNavigator

what is the reason to have DrawerNavigator not be same level as the MainStackNavigator? reading about navigation depth and stuff, less levels means less complexity so just trying to understand the reason for the hierarchy that my monday 9 pm brian may very well be missing.

@kevinherdez
Copy link
Collaborator Author

what is the reason to have DrawerNavigator not be same level as the MainStackNavigator? reading about navigation depth and stuff, less levels means less complexity so just trying to understand the reason for the hierarchy that my monday 9 pm brian may very well be missing.

I could play around with that to be sure, but, from my understanding, the Drawer acts almost like a tab bar. When you navigate to those screens, it replaces the screens you were looking at instead of adding them to the stack.

Ex. I moved the Center Selector down into the drawer from the RootStack (not the best example it was just an easy lift to demonstrate). When you navigate to it as a part of the Drawer, it replaces the MainStack instead of being shown on top of the same stack.
Simulator Screen Recording - iPhone 17 Pro - 2026-03-17 at 10 18 17

@kevinherdez
Copy link
Collaborator Author

what is the reason to have DrawerNavigator not be same level as the MainStackNavigator? reading about navigation depth and stuff, less levels means less complexity so just trying to understand the reason for the hierarchy that my monday 9 pm brian may very well be missing.

I could play around with that to be sure, but, from my understanding, the Drawer acts almost like a tab bar. When you navigate to those screens, it replaces the screens you were looking at instead of adding them to the stack.

I reworked the stack to try to combine MainStack and Drawer and this was the default behavior. It doesn't seem like there's a way to change this behavior.

Simulator Screen Recording - iPhone 17 Pro - 2026-03-17 at 11 04 51

However, what I could do is remove the need for RootStack and move those views down into MainStack. Theres a difference in how the navigation works so I think it'll depend on we like best. In the gif below "Select Avalanche Center" is a part of MainStack and "About" is a part of RootStack

Pros:

  • It sounds like we want the drawer to close when the user selects the center. Moving down the stack does that
  • We remove 1 level of the stack navigation

Cons:

  • It feels a little jittery. This seems to be because navigating to a view in a nested navigator is more involved than the default behavior of navigating to a view in a parent navigator
  • Do we want to close the Drawer for all screens? It feels natural for "Select Avalanche Center" but less so in "About" (in my opinion at least)

Simulator Screen Recording - iPhone 17 Pro - 2026-03-17 at 11 26 48

@kevinherdez kevinherdez force-pushed the kevinherdez/stackNavigationRefactor branch from 86a09bc to 50deecf Compare March 17, 2026 18:39
initialUrl = event.url;
});

const linking = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you may already be aware of this (and since we never shipped this as far as I remember), this PR will break the deep linking and this will need to be updated

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup good call out. I was going to tackle that in a future PR because I need to add the UI changes around that too. I'll make an issue tracking this

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think this is high priority at this point, but want to make sure we dont forget about it! thank you!

requestedTime: RequestedTimeString;
};
export type MainStackParamList = {
bottomTabs: TabNavigationProps;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copilot tells me this should be to ensure type=safe nested navigation:

bottomTabs: NavigatorScreenParams<TabNavigatorParamList> | undefined;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its explanation: If nobody is doing nested navigate() calls across these boundaries (which seems likely given the architecture), the wrong types are mostly harmless at runtime — React Navigation doesn't actually type-check params at runtime. But it undermines the purpose of typing the param lists in the first place, and could cause confusing IDE suggestions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice I didn't know that. I'll make the changes for all of these.

id: string;
};
export type DrawerParamList = {
MainStack: MainStackNavigationProps;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly here for type-safe nexted navigation:

MainStack: NavigatorScreenParams<MainStackParamList> | undefined;

export type SideDrawerNavigationProps = DrawerNavigationProp<DrawerParamList>;

export type RootStackParamList = {
drawer: SideDrawerNavigationProps;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one more for type-safe nested navigation:

drawer: NavigatorScreenParams<DrawerParamList> | undefined;

// On phones without notches, the insets.top value will be 0 and we don't want the header to be flush with the top of the screen.
<View style={{width: '100%', backgroundColor: 'white', paddingTop: Math.max(8, insets.top)}}>
<HStack justifyContent="space-between" pb={8} style={options.headerStyle} space={8} pl={3} pr={16}>
// Setting the top padding to insets.top correclty aligns the view underneath the notches on iPhone. Trying to set the padding ourselves could lead to unexpected behavior
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"correctly" 🙂

}
}

forceAnimateMapRegion() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this force?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "force" is kind of a misnomer that Claude came up with. I was running into an issue where when I switched centers from the Obs or Weather tab, then map wouldn't center on the new center. Apparently the issue was that because the map view wasn't being shown, then the call to setCamera() was no-oping.

This gets called in AvalancheForecastZoneMap in a useFocusEffect so that when the view is shown again it'll recenter. I can change the name to something clearer

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh i remember when we wouldn't get the map to center on the default center back in the day 😄 it was a pain! I think a quick comment on why this is here would be helpful, and I think the name is fine!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! I'll add the comment

@yuliadub
Copy link
Collaborator

  • It feels a little jittery. This seems to be because navigating to a view in a nested navigator is more involved than the default behavior of navigating to a view in a parent navigator
  • Do we want to close the Drawer for all screens? It feels natural for "Select Avalanche Center" but less so in "About" (in my opinion at least)

You mean like if someone clicks About and then clicks back - should this close the drawer?

I dont see the jittery-ness but I must admit I have not loaded the app on my phone to play with so I trust you if you say it feels like a weird experience. This could be a question for design - maybe they can help chime in on what makes more sense.

A few thoughts:

  1. When it comes to our conversation about delevoper menu - maybe we remove that from the drawer and just make a button somewhere that only shows up on Preview that allows us to access it? wherever it would make the most sense and it be easiest 🤷‍♂️ this also makes it easy to take it out/change it as we wish back since it becomes its own thing. what do you think - would that make sense?

  2. Also, thinking about navigation more — do deep-linked or drawer-navigated screens need to sit above the drawer, or can they just land inside MainStack? Is the reason for RootStack having forecast/observation/nwacObservation to have deep-linked content appear as an overlay on top of the drawer rather than as a pushed screen within MainStack?

I started thinking about this because I noticed both MainStack and RootStack define the same screens (forecast, observation, nwacObservation) and got a bit confused about why we need two layers. Since zooming into a center on the map changes your default center, won't MainStack and RootStack always end up showing the same center anyway? If there's no need for the overlay behavior, could we consolidate to just the MainStack versions (and I guess thats another navigation tree simplification)

Let me know if I am making sense...I feel like I have overthought this and my brain is not computing 😆

@kevinherdez
Copy link
Collaborator Author

A few thoughts:

  1. When it comes to our conversation about delevoper menu - maybe we remove that from the drawer and just make a button somewhere that only shows up on Preview that allows us to access it? wherever it would make the most sense and it be easiest 🤷‍♂️ this also makes it easy to take it out/change it as we wish back since it becomes its own thing. what do you think - would that make sense?
  2. Also, thinking about navigation more — do deep-linked or drawer-navigated screens need to sit above the drawer, or can they just land inside MainStack? Is the reason for RootStack having forecast/observation/nwacObservation to have deep-linked content appear as an overlay on top of the drawer rather than as a pushed screen within MainStack?

I started thinking about this because I noticed both MainStack and RootStack define the same screens (forecast, observation, nwacObservation) and got a bit confused about why we need two layers. Since zooming into a center on the map changes your default center, won't MainStack and RootStack always end up showing the same center anyway? If there's no need for the overlay behavior, could we consolidate to just the MainStack versions (and I guess thats another navigation tree simplification)

Let me know if I am making sense...I feel like I have overthought this and my brain is not computing 😆

For 1: I think breaking the Dev menu out to its own view makes the most sense and maybe leaving the access point in the drawer would be good. At least for now because I can't really think of where to put that button.

For 2: That's a really great call out that I honestly didn't think about. I'll look into the deep linking setup to make sure we're not about to create more work by setting up the stack wrong, and I think that should dictate what we do here.

There reason that there are duplicate views defined in RootStack is because those views are being used in the Dev Menu and navigated to from the drawer menu. Before playing around with the experience more after one of the earlier comments, I thought that they needed to be defined in the drawer's parent stack for it to work well. I could move them down into MainStack and everything would be fine. The only difference is that the drawer would automatically close.

I think I'm being picky about the "jitteriness" I mentioned earlier. I think I just noticed how it was difference and probably not actually a problem

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants