Skip to content

Conversation

amyleadem
Copy link
Contributor

@amyleadem amyleadem commented Aug 16, 2023

Summary

Fixed a Safari-only bug that caused the mobile menu to unexpectedly close at a narrow range of window widths.

Breaking change

This is not a breaking change.

Related issue

Closes #5371

Related pull requests

Changelog PR

Preview link

Preview link: Documentation page

Problem statement

In Safari, the mobile header panel will unexpectedly close when opened in a narrow range of window widths. This issue happens when the mobile menu is opened in windows widths within the 15px preceding the $theme-header-max-width breakpoint. With the default "desktop" value of ` 1024px, this issue happens at window widths of 1009px - 1023px.

The issue is demonstrated in this GIF when testing the range right before the "desktop" breakpoint:

safari-bug (2)

Known Safari bug

This issue happens in Safari because it does not include the width of the vertical scrollbar when it calculates the width of the viewport. (See this open webkit bug report). This means that in Safari, the registered viewport width will change depending on the presence or absence of a vertical scrollbar.

In USWDS, opening the mobile menu removes the vertical scrollbar on <body> because its overflow is set to hidden. When this scrollbar disappears, Safari updates the value of the viewport size to be 15px (the width of the scrollbar) wider than the original value. If this new viewport width registers as a "desktop" width, the CSS will show the desktop view of the header.

For example, if you trigger the mobile menu at an original viewport width of 1020px, Safari will reset value of the viewport width to 1035px once the menu is opened. Because this new width is greater than the default $theme-header-max-width value of 1024px, the CSS turns on desktop menu display.

Solution

To resolve this, this PR forces Safari to keep the the body's vertical scrollbar when the mobile menu is open. This fix is applied only to Safari and only to the affected 15px of screen width (by default, 1009-1024px).

Forcing the scrollbar at these window widths prevents Safari from changing its width definition when the mobile menu is open, which removes confusion about which version of the header navigation to show.

Note
This script checks specifically for "not Chrome" because the Chrome browser reports having both Chrome and Safari userAgent. More details in this MDN article.

Testing and review

Review:

  • Consider if allowing vertical scroll for these 15px is a reasonable cost for this solution
  • Confirm that this change only affects Safari and only at the affected screen widths (by default, 1009px-1024px)
  • Confirm that the is-safari class is added to Safari only
  • Confirm the code meets standards
  • Confirm the changelog looks good

Use Storybook to test that the mobile menu opens in Safari:

  1. Open the Documentation page in Safari
  2. Resize the window to somewhere between 1009px-1024px
  3. Click the "Menu" button to open the mobile navigation panel
  4. Confirm that the mobile header successfully opens as expected

Use Storybook to test that this does not affect other browsers:

  1. Open the Documentation page in other browsers
  2. Resize the window to somewhere between 1009px-1024px
  3. Click the "Menu" button to open the mobile navigation panel
  4. Confirm that the main page content is not scrollable

- This forces the browser scrollbar to prevent media query confusion
@amyleadem amyleadem marked this pull request as ready for review August 22, 2023 15:27
@amyleadem amyleadem requested review from mahoneycm and mejiaj August 22, 2023 15:41
Copy link
Contributor

@mahoneycm mahoneycm left a comment

Choose a reason for hiding this comment

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

This is working well for me! While the scrolling isn't ideal, I think it's a rare case someone would find themselves at that viewport size and try to scroll.

A couple potential recommendations to try as well as a documentation edit then I think we'll be golden 🥇

Comment on lines 85 to 92
// Detect Safari and add a body class for a Safari-only CSS bug fix.
// More details in https://github.com/uswds/uswds/pull/5443
const isSafari = navigator.userAgent.indexOf("Safari") !== -1;
const isNotChrome = navigator.userAgent.indexOf("Chrome") === -1;
if (isSafari && isNotChrome) {
body.classList.add("is-safari");
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I was wondering why we check for Chrome but not other browsers. I've found articles that explain that Chromium browsers will provide the Safari user agent as well as the chomium one.

Could we add a note about this and potentially a source so if we forget and question this down the road it's well documented?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call, @mahoneycm. I've added a comment in the code and a note in 5a41838.

I also included a note in the Solution section of the PR description pointing to this MDN article that explains userAgent detection with Safari and Chrome.

Comment on lines 485 to 489
@media (min-width: $safari-header-bug-min-width) {
.usa-js-mobile-nav--active.is-safari {
overflow: scroll;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

In your JS solution, were you capturing the current scroll position then using that to lock the view to that position?

I was curious if we could set position: fixed with the current top offset to lock vertical scrolling at these widths but it seems like your previous solution was doing something similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting idea!
In 625c7f4, I added position: fixed to the body CSS when the overflow: scroll rule is enacted in Safari, and it does prevent scroll while maintaining the scrollbar. (Woohoo!)

A couple of things I noticed: When the menu is opened in this state, the page content does shift down a bit so that the content aligns with the top of the viewport. In the current state, the content also shifts, but not as much and only on page close.

I will work on thinking through if there are any possible downsides to this approach. I will also experiment with iOS tomorrow.

Copy link
Contributor

Choose a reason for hiding this comment

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

This stack overflow solution uses JavaScript to calculate the position of the viewport after the scroll and set the document body top to match it to prevent the content shift.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like this idea. Thanks! Implemented in 0600cf0.

Now when the mobile menu is open in the 15px window range in Safari, the body element will have fixed positioning and receive the current JS scrollTop value to set the body'stop position.

Trying to think of any potential unintended side effects and I think we are good to go. Curious if anyone else has thoughts on this.

Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed that while there isn't a vertical shift when the menu opens, there is a shift to the top when the menu is then closed.

If we can get that sorted I think we'll be in a good standing! I like that this solution prevents scrolling on the affected widths.

Copy link
Contributor Author

@amyleadem amyleadem Sep 12, 2023

Choose a reason for hiding this comment

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

@mahoneycm This is pre-existing behavior, so I am inclined to leave it as is in this PR just to keep things scoped. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

@amyleadem Good point! Since the header isn't sticky and remains at the top, I think that we can leave it 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mahoneycm Great! Ready to smash that "approve" button? Are there any outstanding items?

Copy link
Contributor

Choose a reason for hiding this comment

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

Lgtm!

Copy link
Contributor

@mejiaj mejiaj left a comment

Choose a reason for hiding this comment

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

Nice fix. Added some suggestions. I was able to test this in Safari 16.6 without any issues.

Consider moving the Safari check to its own function and calling that in toggleNav().

Copy link
Contributor Author

@amyleadem amyleadem left a comment

Choose a reason for hiding this comment

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

Thanks for the suggestions @mejiaj. Definitely feels more readable now. You and @mahoneycm are making me better!

@amyleadem amyleadem requested a review from mejiaj September 20, 2023 16:49
Copy link
Contributor

@mejiaj mejiaj left a comment

Choose a reason for hiding this comment

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

Very minor suggestion to separate browser check from setting padding. Otherwise LGTM.

I tested functionality on:

  • Chromium v116
  • Firefox v118
  • Safari v16.6
  • Safari iPad mini (2022)

The class only shows in Safari and the scrollbar is gone.

Comment on lines 87 to 95
const setSafariScrollPosition = (body) => {
const isSafari = navigator.userAgent.includes("Safari");
const isNotChrome = !navigator.userAgent.includes("Chrome");
const currentScrollPosition = `-${window.scrollY}px`;
if (isSafari && isNotChrome) {
body.style.setProperty("--scrolltop", currentScrollPosition);
body.classList.add("is-safari");
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

On initial load, the safari class is missing. It's only added on initial toggle.

It might be better to split this into two functions, one that simply checks for safari (and possibly adds class) and another that handles the scroll.

Example
image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mejiaj Good idea. I broke out addSafariClass() from setSafariScrollPosition() in 75ed3a3. Let me know what you think.

@amyleadem amyleadem requested a review from mejiaj September 22, 2023 16:19
Copy link
Contributor

@mejiaj mejiaj left a comment

Choose a reason for hiding this comment

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

Nice job, I was able to confirm:

  • Safari class doesn't show on other browsers
  • Scroll error is gone in Safari
  • Only padding right is modified when setting/removing styles

@mejiaj mejiaj requested a review from thisisdano September 25, 2023 14:07
Copy link
Contributor

@thisisdano thisisdano left a comment

Choose a reason for hiding this comment

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

Well-scoped solution

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.

USWDS - Bug: Header navigation layout broken in Safari desktop just below desktop breakpoint
4 participants