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! |
There was a problem hiding this comment.
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.
Okey, I now see that it is an independent component. Skip my previous comment in this context :)
edit-post/components/layout/index.js
Outdated
There was a problem hiding this comment.
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.
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
MobileScrollLockto be enabled if present. - Make users of
MobileScrollLockresponsible 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
There was a problem hiding this comment.
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.
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. |
There was a problem hiding this comment.
<Popover.MobileScrollLock /> wasn't updated to reflect the latest changes.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I think that it can be renamed to ScrollLock as it is no longer dependent on any mobile related logic :)
There was a problem hiding this comment.
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 |
732f150 to
444461a
Compare
|
Hi @gziolo, I believe I've address all your review comments. I like your idea of a |
components/scroll-lock/test/index.js
Outdated
There was a problem hiding this comment.
There is no risk, all test suites are isolated when you use Jest :)
There was a problem hiding this comment.
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.
Interesting, I will check it once in master, out of curiosity :)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Nit: can we extract inline variable for those 3 flags? We use it twice.
There was a problem hiding this comment.
Thanks for catching that. I pushed a fix.
gziolo
left a comment
There was a problem hiding this comment.
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
Popoverto prevent body scrolling while a modal popover is open. It also adds aPopover.MobileScrollLockcomponent so other modal-on-mobile UI like the default and publish options sidebars can declare scroll lock as well.Popover.MobileScrollLockis used in the editor layout to declare scroll lock when sidebars are displayed on mobile.Scroll locking is accomplished by adding a
lockscrollclass 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
touchmoveevents, but I'd like to learn more first to be sure we don't break other mobile interactions involvingtouchmove.Additional comments
Popover.MobileScrollLockbut did not because that knowledge is currently duplicated between editor modules andcomponents/popover.overflow: hiddenis applied to the body at a min-width of 600px.Checklist: