Skip to content

Tablet Navbar issues fixed#492

Merged
mahigangal merged 4 commits into
mainfrom
418-better-tablet-navbar
Mar 28, 2026
Merged

Tablet Navbar issues fixed#492
mahigangal merged 4 commits into
mainfrom
418-better-tablet-navbar

Conversation

@mahigangal
Copy link
Copy Markdown
Collaborator

@mahigangal mahigangal commented Mar 3, 2026

Description

This PR fixes the responsive course header behavior for issue #418. Previously, the header switched to the drawer menu only at a fixed breakpoint (768px). On tablet/smaller desktop widths, role/course-dependent nav items (especially professor links like Course Settings and Insights) could overflow, causing a horizontal scrollbar and dropdown placement issues.

The header now uses a 3-mode system:

  • desktop
  • compact (reduced top-level nav padding)
  • drawer (hamburger)

Mode selection is based on measured rendered width (not fixed nav-item-count estimates), so it helps with student vs professor nav differences, enabled course features, zoom/larger font size scenarios.

Additionally, NavigationMenu now supports an optional showViewport prop so hidden measurement navbars can skip rendering an extra viewport element.

No dependencies, env changes, DB changes, or install steps are required.

Closes #418

Screenshots

  1. Professor course at ~1024px
Screenshot 2026-03-05 at 3 56 56 PM
  1. Professor course at ~1280px
Screenshot 2026-03-05 at 3 57 53 PM
  1. Student course at ~1024px
Screenshot 2026-03-05 at 3 59 41 PM
  1. Narrow tablet width ~820px (student or professor)
Screenshot 2026-03-05 at 4 01 07 PM
  1. Queues dropdown near a threshold width (~820px)
Screenshot 2026-03-05 at 4 01 49 PM
  1. Profile dropdown near a threshold width (~1024px)
Screenshot 2026-03-05 at 4 02 45 PM

All screenshots show no horizontal scrollbar and correct nav mode for width/role.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • This requires a run of yarn install
  • This change requires an addition/change to the production .env variables. These changes are below:
  • This change requires developers to add new .env variables. The file and variables needed are below:
  • This change requires a database query to update old data on production. This query is below:

How Has This Been Tested?

Manually tested on course pages with different roles and feature sets, focusing on widths where overflow previously occurred.

Repro steps:

  1. Open a student course home page and a professor course home page.
  2. Resize viewport across tablet/smaller desktop widths (for example around 820px, 1024px, 1280px).
  3. Verify the header switches between desktop, compact, and drawer based on fit.
  4. Confirm there is no horizontal scrollbar.
  5. Open the profile dropdown and queue dropdown near threshold widths and verify positioning/interaction.
  6. Repeat checks at browser zoom levels (125%, 150%) and confirm nav switches mode instead of overflowing.

Zoom testing note: I could only test zoom reliably at desktop width. At 100% zoom, all nav elements are shown in desktop mode; after zooming in enough, the navbar shifts to drawer mode without introducing a horizontal scrollbar. I couldn’t reliably test zoom at other widths in DevTools device mode.

Checklist:

  • I have performed a code review of my own code (under the "Files Changed" tab on github) to ensure nothing is committed that shouldn't be (e.g. leftover console.logs, leftover unused logic, or anything else that was accidentally committed)
  • I have commented my code where needed
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • I have checked that new and existing tests pass locally with my changes
  • Any work that this PR is dependent on has been merged into the main branch
  • Any UI changes have been checked to work on desktop, tablet, and mobile

@mahigangal mahigangal linked an issue Mar 3, 2026 that may be closed by this pull request
@mahigangal mahigangal changed the title Better Tablet Navbar issues fixed Tablet Navbar issues fixed Mar 5, 2026
@mahigangal mahigangal marked this pull request as ready for review March 6, 2026 00:41
@mahigangal mahigangal requested review from AdamFipke and bhunt02 March 6, 2026 00:41
@mahigangal mahigangal self-assigned this Mar 6, 2026
@AdamFipke
Copy link
Copy Markdown
Collaborator

AdamFipke commented Mar 9, 2026

image In regards to this, do you think you can shorten the width of the navbar so there isn't a whole lot of empty space? Probably just by adding a max-width.

Also, the nav elements have the blue underline in that screenshot and are aligned to the right, would it be possible to try to match the styling of mobile?
image
For this, you might just need to adjust some styles to use lg: instead of md: but just make sure to manually test everything please

Copy link
Copy Markdown
Collaborator

@AdamFipke AdamFipke left a comment

Choose a reason for hiding this comment

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

See comments.

Rendering 3 navbars and using 2 of them just for measuring the width of the desktop navbar and condensed navbar to determine what mode the true navbar should be in is a creative approach, but I'm unsure if it'll be worth the performance cost. I will leave this to you to test more. Maybe one of us can come up with another way that's better for performance if it turns out to be noticeably hurting performance.

Check out React Dev Tools and see if there's lots of unnecessary re-renders. Also try using chrome dev tools to throttle performance and see if it stills feels responsive.

Image

Comment on lines +183 to +186
const showDesktopPresentation =
orientation === 'horizontal' && !forceDrawerPresentation
const showDrawerPresentation =
orientation === 'vertical' || forceDrawerPresentation
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Small thing. As much as this logic is fine in that if one is false the other is true, I would rather just have 1 variable for maintainability reasons (if in the future these are changed, it might accidentally create a state where both showDesktopPresentation and showDrawerPresentation are both true or both false).

const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const isDesktop = useMediaQuery('(min-width: 768px)')
const [navMode, setNavMode] = useState<NavMode>('drawer')
const isPhone = useMediaQuery('(max-width: 640px)')
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

you might want to leave this as 768px instead of 640px since basically the entire site uses md: as the breakpoint between mobile and desktop for CSS, so in the case where the browser width is at 700px it might show the desktop navbar but with mobile styles.

return (
<div ref={availableWidthRef} className="relative w-full">
<div className="pointer-events-none invisible absolute left-0 top-0 -z-10 h-0 overflow-hidden">
<div ref={regularMeasureRef} className="w-max">
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Okay I get this now.

We're rendering 3 navbar components all the time.

2 of these navbar components are invisible and literally only exist so their width can be measured. 1 navbar has the condensed classnames, and the other the standard desktop navbar.

The final navbar is the real one.

It's kinda creative, but I think rendering 3 navbars might have some performance cost considerations that might not be worth itt so I might wait on merging this in case someone thinks of a better solution.

Also the 2 invisible navbars wouldn't technically be the true width since the CSS styles may adjust the width of the navbar (and its items) once it hits below the mobile breakpoint. But I guess as long as you have that isPhone like you have that matches the breakpoint of the CSS, that should be fine since it'll always ensure the mode is in drawer below that breakpoint regardless of if the CSS styles shrink the invisible navbars. I would really put this in a comment though


useLayoutEffect(() => {
updateNavMode()
}, [updateNavMode, pathname, course, userInfo])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For someone fresh looking at this, I would definitely put a comment saying what this code is doing.

"This is to update the navmode when the navbar would get different elements. For example, there's different navbar elements from organization view vs course view, professor view vs TA view vs student view, based on whether the course has queues enabled, etc. useLayoutEffect is used instead of useEffect so that setNavMode calculations are done before the browser repaints the screen (so users don't see an incorrect navbar for a frame)"

I will note:

  • you will want to use useCourseFeatures since that's where stuff like isQueuesEnabled are.
  • Debounce, debounce, debounce. If an object is changing a lot, each one will trigger a re-render for this. For the use effect below, you REALLY want to debounce it since if the browser width is changing a lot, it will spam a ton of re-renders.
  • Instead of having a useEffect/useLayoutEffect with some seemingly arbitrary dependency array variables (which will throw a linter error btw if you have the react extension), you would instead want to take advantage of the fact that there are invisible navbars being rendered and use their widths. I notice that the useEffect hook below does use the width of the invisible navbars. So, you should use that logic inside of a useLayoutEffect and just remove the arbitrary array dependencies. I also want to be clear that I'm still not totally on board with the idea of rendering 3 navbars, but if we decide to go that way, we should remove the redundant code that would be causing extra re-renders as that's kinda overkill and causing unnecessary performance loss. I would also double check to make sure that it still works after making these changes. If it doesn't, then there is a flaw in the approach of using invisible navbars and measuring their widths and another approach should be used.
  • Get the React Developer Tools extension for chrome and turn on "Highlight updates when components render" (under browser inspect -> Components -> Settings Cog -> General). Try resizing the window and see how many times the navbar re-renders. Also try navigating around a course and see how many times it re-renders.

Copy link
Copy Markdown
Collaborator

@AdamFipke AdamFipke Mar 10, 2026

Choose a reason for hiding this comment

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

Huh, so I went and manually tested it while going to try something else and maybe this is less performance intensive than I thought? Like it really does only trigger the re-render at the breakpoints. Moving the window width a lot doesn't seem to cause any re-renders. So maybe debouncing isn't really that necessary.
image

Although I guess this only shows the visible navbar. The invisible ones would also be re-rendering a bunch.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Okay I was correct about the dependency arrays though (and that there was a useEffect and a useLayoutEffect when a useLayoutEffect was needed).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Okay I did find one kinda funny bug with it that causes an infinite loop, but it needs super specific circumstances:

  1. Shrink to drawer view. Open the drawer (but don't close it)
  2. Expand the viewport slowly to the point where it becomes the desktop view.

This will close the drawer. When the drawer closes, the viewport width shrinks a tiny bit (it's a feature from that drawer component), this causes a switch from 'desktop' view to 'drawer' view since it's just below the breakpoint width again. This re-opens the drawer (since it never fully closed), expanding the viewport width a tiny bit, switching to 'desktop' mode again, which then closes the drawer, causing the width to shrink, which re-opens the drawer again, and so on. It's an infinite loop of the drawer just opening and closing.

It's not really worth fixing, but just kinda funny.

image

Also, yeah I've thought about this implementation more, I think the idea of using invisible navbars to measure width should be fine I think

@AdamFipke
Copy link
Copy Markdown
Collaborator

(image)
In regards to this, do you think you can shorten the width of the navbar so there isn't a whole lot of empty space? Probably just by adding a max-width.
Also, the nav elements have the blue underline in that screenshot and are aligned to the right, would it be possible to try to match the styling of mobile?
(image)
For this, you might just need to adjust some styles to use lg: instead of md: but just make sure to manually test everything please

In regards to this comment, the solution I suggested to use lg: wouldn't really fix it since whether the drawer or navbar is shown is dependent on the width of the navbar elements -> which we use JS to measure.

So the goal here is to only apply the md: styles to the navbar elements when the navbar is in drawer mode rather than at the 768px breakpoint. There's different approaches you can take here.

The way I would suggest is go through the components of navigation-menu.tsx and change it so any md: classes are instead applied if the orientation is 'horizontal'. Something like:

className={cn(
        // ... all the previous classes
        orientation === 'horizontal' && "hover:text-helpmeblue", // was previously "md:hover:text-helpmeblue"
        className,
      )}

Just make sure that these classes are added near the end of the cn function.

The reason why this works:

  • Normally, any classes with md: have a media query saying "if the browser width is greater than 768px, apply this style. This style takes priority over styles without any media query"
  • orientation === 'horizontal' is basically the "desktop" styles
  • the cn function uses tailwind-merge as part of it. This function will overwrite earlier classnames with ones added later in the function. This will allow us to overwrite mobile class names (which come first) with the desktop ones. I should note that a lot of this are inside the navigationMenuTriggerStyle function (that's cva for some reason, it doesn't need to be it could be cn), so that will be just a small refactor.

@mahigangal
Copy link
Copy Markdown
Collaborator Author

Thank you for the comments and suggestions! I have implemented the requested navbar review changes:

  • switched to a single showDrawerPresentation source of truth
  • aligned the forced drawer breakpoint to 768px
  • replaced the redundant useEffect/dependency-array approach with a single useLayoutEffect + ResizeObserver
  • added comments explaining the hidden measurement navbars and nav-mode recalculation
  • refactored shared nav styling to follow orientation instead of md: breakpoints
  • updated drawer/tablet styling to match mobile more closely: left alignment, gray selected state, no blue underline
    fixed drawer-mode queue dropdown styling at tablet widths

I manually tested desktop/compact/drawer transitions, drawer layout, and queue dropdown behavior and did not find issues.
Screenshot 2026-03-16 at 1 09 03 PM
Screenshot 2026-03-16 at 1 09 35 PM

@mahigangal mahigangal requested a review from AdamFipke March 16, 2026 20:14
'inset-x-0 bottom-0 mt-24 rounded-t-[10px]',
direction === 'left' &&
'left-0 top-0 h-full w-[60vw] max-w-80 rounded-r-[10px]',
'left-0 top-0 max-h-[100dvh] w-[60vw] max-w-80 overflow-y-auto overflow-x-hidden rounded-r-[10px]',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Image

Why is the sidebar now... short? I mentioned there should be a shorter max width, not height. probably just changing it from max-w-80 to max-w-72 or something.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Oh sorry, I misread it as reducing the height and getting rid of the blank white space between the elements and the Profile element. I spent a lot of time on this, my bad. I'll fix it right away.

Copy link
Copy Markdown
Collaborator Author

@mahigangal mahigangal Mar 17, 2026

Choose a reason for hiding this comment

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

Screenshot 2026-03-16 at 11 29 18 PM

I have made the height full window and reduced the width (w-[46vw] max-w-60).

@mahigangal mahigangal requested a review from AdamFipke March 17, 2026 06:36
Copy link
Copy Markdown
Collaborator

@AdamFipke AdamFipke left a comment

Choose a reason for hiding this comment

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

just one small comment, should be good to merge after

Comment on lines +200 to +205
const queueDropdownListClass = showDrawerPresentation
? `grid w-full gap-1 p-4 ${sortedQueues.length > 6 ? 'grid-cols-2' : 'grid-cols-1'}`
: `grid gap-1 p-4 md:grid-cols-2 lg:w-[600px] lg:gap-2 ${sortedQueues.length > 6 ? 'w-[95vw] grid-cols-2' : 'w-[60vw]'}`
const emptyQueueStateClass = showDrawerPresentation
? 'w-full p-4 text-center text-sm text-gray-500'
: `w-[60vw] p-4 text-center text-sm text-gray-500 ${role === Role.PROFESSOR ? 'lg:w-[600px]' : 'lg:w-[400px]'}`
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

wait i'm confused. Why are these here if they're only used in one area? I feel like it'd be a lot more maintainable to keep the classname on the elements themselves generally. You can also use the cn helper function for formatting conditional classnames nicely. Same goes for horizontalInsetImportantClass.

The rest are probably fine to leave as they are since they're used in multiple areas but in general you could go either way

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thank you for pointing this out, used cn(...) to inline the single-use classnames in HeaderBar.tsx and kept the reused shared class constants extracted.

@mahigangal mahigangal requested a review from AdamFipke March 28, 2026 01:05
@mahigangal mahigangal merged commit b6a8ae7 into main Mar 28, 2026
2 checks passed
className={
isACourseSettingsPage
? // the hover:border-none is because the inner link has a hover effect that adds another border
'md:border-helpmeblue bg-zinc-300/80 md:border-b-2 md:bg-white md:hover:border-none md:focus:border-none'
Copy link
Copy Markdown
Collaborator

@AdamFipke AdamFipke May 13, 2026

Choose a reason for hiding this comment

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

I tried ctrl+fing "border-helpmeblue" and I see that the original desktop styles have actually been completely removed (presumably got misinterpreted for some reason after my first round of feedback). I was expecting that the changes were at least tested. I'll look into fixing it, but I might just revert the changes done by this PR

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah, the more I look at it now, the more random broken changes I see. It's not worth my time to try to salvage this.

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.

Better Tablet Navbar

2 participants