-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Avoid scroll bleed when displaying modal UI on mobile #4398
Avoid scroll bleed when displaying modal UI on mobile #4398
Conversation
00da4b7
to
de1fca4
Compare
Nice work, thanks for working on this!
Is this actually the case, though? I know the following is just testing in the inspector, but for me it works fine to set https://cloudup.com/cYbErRWbcOC In my experience, the only thing that resets the scroll position is if for whatever reason the main scrolling canvas changes height. Is it when it lands on a real device that it stops working? |
Hi @jasmussen, I'm sorry for the delay in response. Here is what I'm seeing when I the The scroll unfortunately resets. I am thinking saving and restoring the body's One idea is to lock and unlock scroll in What do you think? |
It seems I was mistaken indeed. On Android and in the Chrome simulator, applying It does seem like, perhaps, your approach is what we'll have to do — store the scroll position and then restore it on modal-close. This one seemed promising: https://benfrain.com/preventing-body-scroll-for-modals-in-ios/ |
Regarding your link, I love the idea of preventing the default action for I've wondered whether it would be too clever though. I am not yet very familiar with touch events. Here are a couple of questions I need to answer:
In the meantime, I'm thinking it probably makes sense to save and restore body scroll position. |
Thanks for keeping on this Brandon. Just to clarify — you should decide, based on your knowledge of this, what is the best programmatic approach to solving this problem. While I might sometimes suggest something to explore, it's merely a casual suggestion, and being mostly a designer I'd always defer to you and others on the code implementation. So go forth with what you feel will work best 🏅 |
d8fae1d
to
2bda1c6
Compare
@jasmussen I think this is ready for consideration. I'm not 100% comfortable with saving and restoring scroll position because I'm afraid it will interfere with other components that scroll-into-view, but in my testing, scrolling after inserting a block is working as I'd expect. But if we land #5316, I believe I can update |
@jasmussen I take that back. I had previously tested on an Android device but am seeing issues restoring scroll position on the latest. It seems to restore but not actually update the display until I interact with something. I'll reply again once I know more. |
I committed a fix. I thought this was working previously but don't see how. The scroll position needed to be restored on I noticed I don't have a test for restoring scroll position, so I'm adding one. |
I am not sure how to reasonably test restoring scroll position, but I may be able to add an e2e test that resizes the window to a mobile viewport width to trigger scroll locking. It might be worth landing this as-is though. |
In my testing, this is absolutely working, and working really really well. Because it's so good, I strongly agree it would be good to get in asap. Adding a quick code review but experience wise this seems great. Thank you! |
@@ -0,0 +1,129 @@ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a folder with all Higher-Order Components: https://github.com/WordPress/gutenberg/tree/master/components/higher-order. We also try to come up with a pattern for naming such helpers. It seems like in this case withScrollLock
would be a good pick. In general, we prefix all HOCs with with
in the majority of cases. The only exception is when a component might not be rendered when a given condition occurs. In such case if
is the preferred prefix. @aduth might even update our docs somewhere, but I'm sure he commented about it at least twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okey, I now see that it is an independent component. Skip my previous comment in this context :)
edit-post/components/layout/index.js
Outdated
@@ -112,26 +115,31 @@ function Layout( { | |||
openedGeneralSidebar !== null && <GeneralSidebar | |||
openedGeneralSidebar={ openedGeneralSidebar } /> | |||
} | |||
<Popover.MobileScrollLock enabled={ layoutHasOpenSidebar && isMobileViewport } /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is also ifViewportMatch
HOC which might be used to wrap this component. In that case, it would never mount when the viewport is larger than small (mobile). It leaves layoutHasOpenSidebar
part in here. My question is if we can convert this component into HOC that would be applied to the popover instead? In that case we would naturally combine two related concepts. This MobileScrollLock
would be a sibling component that renders only on mobile and when any popover is actually rendered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would love to use ifViewportMatch
here, but my understanding is that it depends on resolving a circular dependency between @wordpress/components
and @wordpress/viewport
(see #5316). We can probably get most of the way there though:
- Update
MobileScrollLock
to be enabled if present. - Make users of
MobileScrollLock
responsible for only adding for mobile viewports. I believe we only use it in two places right now.
Once we can use ifViewportMatch
, we can push that requirement into MobileScrollLock
.
components/popover/index.js
Outdated
@@ -331,4 +335,7 @@ Popover.contextTypes = { | |||
|
|||
Popover.Slot = () => <Slot bubblesVirtually name={ SLOT_NAME } />; | |||
|
|||
// Make this available for use by other modal UI that is not based on Popover | |||
Popover.MobileScrollLock = MobileScrollLock; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to make it reusable for other use cases it should be exposed as its own component to avoid confusion. I was sure it is specific to the Popover component.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're right and will make the change. The original thinking was that it was ideally only for popover but could be used for Popover-like components.
Thanks for testing, @jasmussen, and for the review, @gziolo! |
This has been updated in response to review comments. |
return ( | ||
<div> | ||
Sidebar Content! | ||
{ isMobile && <Popover.MobileScrollLock /> } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<Popover.MobileScrollLock />
wasn't updated to reflect the latest changes.
```jsx | ||
import { MobileScrollLock } from '@wordpress/components'; | ||
|
||
function Sidebar( { isMobile } ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think that using HOC would be the cleanest solution because the scroll lock behavior would be applied on top of the component that needs it.
function Sidebar() {
return (
<div>
Sidebar Content!
</div>
);
}
export default withMobileScrollLock( Sidebar );
components/popover/index.js
Outdated
return <span ref={ this.bindNode( 'anchor' ) }>{ content }</span>; | ||
return <span ref={ this.bindNode( 'anchor' ) }> | ||
{ content } | ||
{ this.state.isMobile && expandOnMobile && <MobileScrollLock /> } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This component would benefit from using this new HOC withViewportMatch
, too.
It's still tricky because it depends on another prop. I think we could add additional checks to HOC:
export default withMobileScrollLock(
( props ) => Boolean( props.expandOnMobile ),
)( Popover );
This assume isMobile
is always handled by the internal check based on withViewportMatch
.
edit-post/components/layout/index.js
Outdated
@@ -112,26 +115,31 @@ function Layout( { | |||
openedGeneralSidebar !== null && <GeneralSidebar | |||
openedGeneralSidebar={ openedGeneralSidebar } /> | |||
} | |||
{ layoutHasOpenSidebar && isMobileViewport && <MobileScrollLock /> } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When using HOC withMobileScrollLock
we wouldn't have to put this logic in here at all, which is quite disconnected from the sidebar component that needs them.
@@ -0,0 +1,21 @@ | |||
MobileScrollLock |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it can be renamed to ScrollLock
as it is no longer dependent on any mobile related logic :)
--lockCounter; | ||
} | ||
|
||
return class MobileScrollLock extends Component { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's now a general purpose scroll lock component :)
Thanks adding your changes to the PR 👍
Yes, this needs to be resolved separately first. Then we would be able to refactor what we have in this PR and introduce const withMobileScrollLock = ( predicate) => ( WrappedComponent ) => {
const MobileScrollLock = compose(
ifViewportMatches( '< small' ),
ifCondition( predicate ),
)( ScrollLock );
const EnhancedComponent = ( props ) => (
<Fragment>
<MobileScrollLock { ...props } />
<WrappedComponent { ...props } />
</Fragment>
);
EnhancedComponent.displayName = getWrapperDisplayName( WrappedComponent, 'withMobileScrollLock' );
return EnhancedComponent;
}; I don't want to block this PR until we find a viable solution for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above.
732f150
to
444461a
Compare
Hi @gziolo, I believe I've address all your review comments. I like your idea of a |
describe( 'scroll-lock', () => { | ||
const lockingClassName = 'test-lock-scroll'; | ||
|
||
// Use a separate document to reduce the risk of test side-effects. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no risk, all test suites are isolated when you use Jest :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was very excited by this, but in my testing, it seems like the global document
persists between tests and suites.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, I will check it once in master
, out of curiosity :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a class to the body in one test, checked for its presence in beforeEach
, and found the class on the body in the second test. Are you up for letting me know if you find different behavior? I'd love to take advantage of isolated environment.
edit-post/components/layout/index.js
Outdated
@@ -82,6 +84,11 @@ function Layout( { | |||
) } | |||
{ editorSidebarOpened && <Sidebar /> } | |||
{ pluginSidebarOpened && <PluginsPanel /> } | |||
{ | |||
isMobileViewport && | |||
( publishSidebarOpened || editorSidebarOpened || pluginSidebarOpened ) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can we extract inline variable for those 3 flags? We use it twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching that. I pushed a fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great after a few iterations. Thanks for addressing all feedback 👍
I noticed that @jasmussen has tested it already, so I'll assume it work as advertised :)
Let's get this for 2.5 to make sure there are no unintended issues. |
Just to note that core uses a CSS class |
Thanks, that's good to know, @afercia. In my testing, it was necessary to also add the class to the document element effectively prevent scroll bleed on iOS devices. It's possible there is something I missed. Any thoughts here? |
I need to rebase this and relocate the rules to component CSS. I'm planning to do that this morning. Then this should be ready to go. |
5c3fe3e
to
044a556
Compare
Description
This is a PR for avoiding scroll bleed when displaying modal UI on mobile devices. This is meant to address the first part of #4082.
How Has This Been Tested?
I successfully ran the unit tests. I also tried running the e2e tests but had an inconsistent experience with different failures with each run.
I manually tested using an iPhone 6s simulator, an iPhone X simulator, and a Galaxy S5 with Chrome:
Types of changes
This PR updates
Popover
to prevent body scrolling while a modal popover is open. It also adds aPopover.MobileScrollLock
component so other modal-on-mobile UI like the default and publish options sidebars can declare scroll lock as well.Popover.MobileScrollLock
is used in the editor layout to declare scroll lock when sidebars are displayed on mobile.Scroll locking is accomplished by adding a
lockscroll
class to the<html>
and<body>
elements. Unlocking restores the pre-lock scroll position.This solution is body-specific, so scroll bleed to other ancestor elements is still possible. We can prevent scroll bleed on mobile by preventing the default action on
touchmove
events, but I'd like to learn more first to be sure we don't break other mobile interactions involvingtouchmove
.Additional comments
Popover.MobileScrollLock
but did not because that knowledge is currently duplicated between editor modules andcomponents/popover
.overflow: hidden
is applied to the body at a min-width of 600px.Checklist: