Skip to content
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

[Modal] Disable background scrolling in iOS #5750

Open
tao-qian opened this issue Dec 9, 2016 · 68 comments
Open

[Modal] Disable background scrolling in iOS #5750

tao-qian opened this issue Dec 9, 2016 · 68 comments
Labels
bug 🐛 Something doesn't work component: modal This is the name of the generic UI component, not the React module! external dependency Blocked by external dependency, we can’t do anything about it

Comments

@tao-qian
Copy link

tao-qian commented Dec 9, 2016

Tested dialogs in http://www.material-ui.com/#/components/dialog.
On desktop Chrome, background scrolling is disabled when dialogs are shown.
However, it is not disabled in iOS Safari or Chrome.

@oliviertassinari oliviertassinari added the component: dialog This is the name of the generic UI component, not the React module! label Dec 18, 2016
@oliviertassinari oliviertassinari changed the title Dialog Should Disable Background Scrolling on iOS [Dialog] Should Disable Background Scrolling on iOS Dec 18, 2016
@sedletsky
Copy link

And with Popover - when it's open you can scroll screen behind to negative position on iOS - very annoying...

Anybody did research or some related links?

@Nopik
Copy link

Nopik commented Apr 23, 2017

@oliviertassinari I'm being bitten by this bug, too. I can try to fix it, though at the moment I'm not even sure where to start. Do you have any idea what can be causing this?

@mbrookes
Copy link
Member

@oliviertassinari Same problem on next, including full-screen dialogs.

@oliviertassinari
Copy link
Member

As raised by someone on the bootstrap thread, that seems to be a Safari browser bug. We can't do much about it here. I'm closing the issue. It's unfortunate.

@daniel-rabe
Copy link

daniel-rabe commented Dec 15, 2017

i had the issue with the popover component, so I added a custom BackdropComponent that cancels the touchmove event

import * as React from 'react';
import Backdrop, { BackdropProps } from 'material-ui/Modal/Backdrop';

/**
 * Prevents scrolling of content behind the backdrop.
 */
export class BackDropIOSWorkaround extends React.PureComponent<BackdropProps> {
    protected onTouchMove(event: React.TouchEvent<HTMLDivElement>): void {
        event.preventDefault();
    }

    public render(): JSX.Element {
        return (
            <Backdrop {...this.props} onTouchMove={this.onTouchMove}/>
        );
    }
}
<Popover
    BackdropInvisible={false}
    BackdropComponent={BackDropIOSWorkaround}
    anchorEl={this.clickElement}
    onRequestClose={this.unexpandChoices}
    anchorOrigin={{vertical: 'top', horizontal: 'left'}}
    transformOrigin={{vertical: 'top', horizontal: 'left'}}
>
    <List disablePadding={true}>
        {this.choices()}
    </List>
</Popover>

@abcd-ca
Copy link

abcd-ca commented Dec 20, 2017

@daniel-rabe 's solution looks good but would be for Material UI v1, not previous versions

@jpmoyn
Copy link

jpmoyn commented Feb 21, 2018

@oliviertassinari I see that the Dialog sets the style overflow-y: hidden; to the body. Would it be possible to add a Dialog prop that additionally sets the style position: fixed; on the body?

This would save a lot of people time in having to manually add position: fixed to the body when working with the Dialog component for mobile safari.

@oliviertassinari
Copy link
Member

oliviertassinari commented Feb 22, 2018

Would it be possible to add a Dialog prop that additionally sets the style position: fixed; on the body?

@jpmoyn No, you would reset the scroll position to the top of the page by doing so. Users will no longer be at the right scroll position once the dialog is closed.

@jacobweber
Copy link

Would it be possible to implement the onTouchMove workaround by default? I'm not sure it's possible to specify a custom BackdropComponent everywhere it's needed. This bug affects Dialog, Select, SwipeableDrawer, etc.

@daniel-rabe
Copy link

daniel-rabe commented Apr 5, 2018

you can override the default-props of material-ui's BackDrop before creating your App:

import BackDrop from 'material-ui/Modal/Backdrop';
BackDrop.defaultProps = {...BackDrop.defaultProps, onTouchMove: preventBackdropScroll};

export function preventBackdropScroll(event: React.TouchEvent<HTMLElement>): void {
    let target: HTMLElement | null = (event.target as HTMLDivElement);
    while (target != null && target !== document.body) {
        const scrollHeight: number = target.scrollHeight;
        const clientHeight: number = target.clientHeight;
        if (scrollHeight > clientHeight) {
            return;
        }
        target = target.parentElement;
    }
    event.preventDefault();
}

@jacobweber
Copy link

@daniel-rabe Sneaky! I like it. Unfortunately it didn't fix the issue for me.

@oliviertassinari
Copy link
Member

oliviertassinari commented Apr 9, 2018

@jacobweber Are you saying #5750 (comment) workaround doesn't work? If it comes with no side effect, we could add it to the core of the library.

@jacobweber
Copy link

It didn't work for me (although I could see the function being invoked). Although maybe I'm doing something differently; I didn't get a chance to investigate this too deeply.

I also noticed that using -webkit-overflow-scrolling: touch in my scrollable views seems to trigger this behavior, for what it's worth.

@daniel-rabe
Copy link

the workaround does not work for current iOS, i dont know since when version exactly

@daniel-rabe
Copy link

daniel-rabe commented Apr 17, 2018

this solution works for me atm

source: https://stackoverflow.com/questions/41594997/ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-posi

import Fade from 'material-ui/transitions/Fade';

function fadeOnEnter(node: HTMLElement, isAppearing: boolean): void {
    let clientY: number | null = null; // remember Y position on touch start
    const touchStart: (event: Event) => void = (event: Event) => {
        if ((event as TouchEvent).targetTouches.length === 1) {
            clientY = (event as TouchEvent).targetTouches[0].clientY;
        }
    };
    const touchMove: (event: Event) => void = (event: Event) => {
        if ((event as TouchEvent).targetTouches.length === 1) {
            disableRubberBand(event as TouchEvent);
        }
    };
    const disableRubberBand: (event: TouchEvent) => void = (event: TouchEvent) => {
        const tmpClientY: number = event.targetTouches[0].clientY - (clientY || 0);

        if (node.scrollTop === 0 && tmpClientY > 0) {
            // element is at the top of its scroll
            event.preventDefault();
        }

        if (isOverlayTotallyScrolled() && tmpClientY < 0) {
            // element is at the top of its scroll
            event.preventDefault();
        }
    };
    const isOverlayTotallyScrolled: () => boolean = () => {
        // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
        return node.scrollHeight - node.scrollTop <= node.clientHeight;
    };
    node.addEventListener('touchstart', touchStart, false);
    node.addEventListener('touchmove', touchMove, false);
}

Fade.defaultProps = {...Fade.defaultProps, onEnter: fadeOnEnter};

@oliviertassinari oliviertassinari added the external dependency Blocked by external dependency, we can’t do anything about it label May 2, 2018
@mui mui deleted a comment from g-forgues May 2, 2018
@mui mui deleted a comment from gottfired May 2, 2018
@oliviertassinari oliviertassinari changed the title [Dialog] Should Disable Background Scrolling on iOS [Dialog] Disable Background Scrolling on iOS May 2, 2018
@icodeforlove
Copy link

Hopefully this will be fixed soon.

For our use-case we we were having issues with fullscreen dialogs so we just capture the window.scrollY on load, and then do a window.scrollTo to restore the original scroll. The only issue we noticed is that you may still see the background moving but at least the user does not end up on a different location.

@alansouzati
Copy link
Contributor

How about setting overflow: hidden for everything that is aria-hidden: true while the dialog is open? this fixes the issue on my end, and I think it would be nice to solve this in the meantime in MUI. It looks like MUI is already putting aria-hidden in these elements.

In the meantime here is what Ive done:

 const onUpdateOverflow = useCallback(
    () =>
      (document.getElementById("root").style.overflow =
        document.body.style.overflow),
    []
  );
  useEffect(() => {
    var observer = new MutationObserver(onUpdateOverflow);

    const observerInstance = observer.observe(document.body, {
      attributes: true,
      attributeFilter: ["style"]
    });

    return () => observerInstance && observerInstance.disconnect();
  }, [onUpdateOverflow]);

@alansouzati
Copy link
Contributor

This PR adds the workaround that fixes the Safari bug 🙏

@alansouzati
Copy link
Contributor

I stand corrected. this fixes the problem but losses scroll position. im trying another work around.

@theKashey
Copy link

There one more fix which can help for the majority of cases - inert which is coming to a full support (only Firefox is one little behind)

@alansouzati
Copy link
Contributor

alansouzati commented Jun 23, 2022

ive implemented this hook, and if you place this in your index.js it should fix the safari bug...the basic idea is to set overflow hidden on the HTML tag, and reset the scroll position back when the modal closes.

import { useCallback, useEffect, useRef } from "react";
import { useMediaQuery, useTheme } from "@mui/material";

export const useAppScrollLock = () => {
  const theme = useTheme();
  const inMobile = useMediaQuery(theme.breakpoints.down("sm"));
  const scrollTop = useRef(null);
  const onUpdateOverflow = useCallback(() => {
    const scrollContainer = document.documentElement;
    if (
      document.body.style.overflow === "hidden" &&
      scrollContainer.style.overflow !== "hidden"
    ) {
      scrollTop.current = scrollContainer.scrollTop;
      scrollContainer.style.overflow = "hidden";
    } else if (document.body.style.overflow !== "hidden" && scrollTop.current) {
      scrollContainer.style.overflow = "";
      scrollContainer.scrollTop = scrollTop.current;
      scrollTop.current = null;
    }
  }, []);
  useEffect(() => {
    let observerInstance;
    if (inMobile) {
      const observer = new MutationObserver(onUpdateOverflow);
      observerInstance = observer.observe(document.body, {
        attributes: true,
        attributeFilter: ["style"]
      });
    }
    return () => observerInstance && observerInstance.disconnect();
  }, [onUpdateOverflow, inMobile]);
};

@VinceCYLiao
Copy link
Contributor

VinceCYLiao commented Nov 18, 2022

I propose that in ModalManage's handleContainer function, we can also set scroll container's touch action style to "none" when open, and restore it when close. Although this will only work on IOS version 13 and later, I think this is OK as a temporary workaround until the iOS safari bug is fixed.

You can try it here on your iOS device.

@oliviertassinari If you think this workaround is OK, I will create a pull request, thanks!

@stasbarannik
Copy link

The issue definitely should be fixed on MUI level or on Safari level, but as temporary solution you can use just styling.
One of cause of different behavior on Safari is thing that overflow:hidden for body doesn't work on Safari. To fix it you can use this solution

// Media for Safari only
@media not all and (min-resolution:.001dpcm) {
@supports (-webkit-appearance:none) {
&[style*="overflow:hidden"],
&[style*="overflow: hidden"] {
touch-action: none; // Fix for Safari bug with overflow hidden property
-ms-touch-action: none;
}
}
}

@rekloot
Copy link

rekloot commented Jan 12, 2023

I was banging my head for this one, so I might share a solution that worked like a charm using Dialog Mui. Check out this npm package: https://www.npmjs.com/package/inobounce You can add it without a dependency also.

Just call it in your index.js just after your .render: iNoBounce.disable()

And when opening your Dialog do iNoBounce.enable() and onClose iNoBounce.disable(). Works like a charm with Mui on IoS!

Make sure your body has -webkit-overflow-scrolling: touch;

@jesusvallez
Copy link

jesusvallez commented Feb 17, 2023

Fix to resolve this problem. Working on my production web page with dropdowns, popovers, dialogs...

  useEffect(() => {
    if (globalThis?.document) {
      const body = globalThis.document.body

      const observer = new MutationObserver(() => {
        body.style.touchAction = body.style.overflow === 'hidden' ? 'none' : ''
      })

      observer.observe(body, {
        attributes: true,
        attributeFilter: ['style'],
      })
    }
  }, [])

@theKashey
Copy link

Woah, CSS is saving the world once again. touch-action sounds exactly like the solution

@silwalprabin
Copy link

The above solution (CSS: #5750 (comment) or JS: #5750 (comment)) will work until we have any input field in modal. If we have input field in modal and it's focused then we see scrolling of background again :(

@silwalprabin
Copy link

Along with touch-action: none; , also added position: fixed; so that scrolling with input selected will not make the background move.

@arbazdogar20
Copy link

arbazdogar20 commented Aug 10, 2023

You can Simply use this lines of code in the Select Component.

onOpen={() => (document.body.style.touchAction = 'none')}
onClose={() => (document.body.style.touchAction = 'auto')}

@nipkai
Copy link

nipkai commented Feb 27, 2024

Along with touch-action: none; , also added position: fixed; so that scrolling with input selected will not make the background move.

Unfortunately if you do that and use the MuiDrawer, you lose your initial scroll..

So I have an infinite scroll, when I scroll down quite a bit, open my drawer, to check my filters and close it again, I will start at the top of the page. Which is really annoying for the user

@martin-linden
Copy link

martin-linden commented Apr 8, 2024

Along with touch-action: none; , also added position: fixed; so that scrolling with input selected will not make the background move.

Unfortunately if you do that and use the MuiDrawer, you lose your initial scroll..

So I have an infinite scroll, when I scroll down quite a bit, open my drawer, to check my filters and close it again, I will start at the top of the page. Which is really annoying for the user

Not only that. If you add position fixed on the body, all sorts of weird UI changes will happen since it will mess with any other styling on the page. Even if you apply this in full screen dialogs you will have time to see the changes once you close the modal. The biggest issue is when you have to use some input field inside the modal which makes this even harder to solve.

I'm honestly a bit shocked that this doesn't seem to be a bigger issue and that it has not been resolved yet. I mean, this issue has been opened since 2016 and we're still trying to make some strange hack to maybe get it to work. Modals are pretty important in websites and web apps. It's no secret that Apple does not like any sort of site/web app that could compare with a native app in any way so i'm not really surprised they don't want to solve this, but I still feel like there should be a better solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐛 Something doesn't work component: modal This is the name of the generic UI component, not the React module! external dependency Blocked by external dependency, we can’t do anything about it
Projects
None yet
Development

No branches or pull requests