[material-ui][Modal] Apply aria-hidden to the correct elements when Modals are mounted to other places than document.body
Fixes https://github.com/mui/material-ui/issues/19450
Lets do https://github.com/mui/material-ui/pull/34165 again. Before if someone used disablePortal and opened a modal the whole page became inaccessible to Screen Readers because aria-hidden only got applied to siblings of document.body, in this case all of them. This PR fixes that.
Changes:
- Make sure we pass in the correct container if
disablePortalis used. - If the modal container is deep in the tree we need to make sure every sibling in every ancestor level is aria-hidden and every aria-hidden we added gets cleaned up properly on unmount.
- In addition because we don't differentiate between aria-hidden tags that got added by modals and added by devs, we need to pierce through (remove) every aria-hidden attribute in every ancestor of the last modal container; this is to prevent no accessible elements on a page when multiple modals are present.
Known problems:
- You will have a bad time if you put your dialog in a container next to 1000s of siblings - bad idea before, bad idea now.
- Previous bug still stands when you mess with aria-hidden states in the tree outside your modal, when the modal gets removed your changes won't be preserved (because the modal will restore the state when it was added)
I'll port this to next too when I have time.
- [x] I have followed (at least) the PR section of the contributing guide.
Netlify deploy preview
https://deploy-preview-43318--material-ui.netlify.app/
Bundle size report
@mui/materialβparsed: πΊ+320B(+0.06%) gzip: πΊ+160B(+0.10%)
@mui/labβparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/systemβparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/utilsβparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/material/Dialogβparsed: πΊ+326B(+0.34%) gzip: πΊ+135B(+0.40%) @mui/material/Modalβparsed: πΊ+326B(+0.37%) gzip: πΊ+131B(+0.41%) @mui/material/Menuβparsed: πΊ+323B(+0.31%) gzip: πΊ+153B(+0.42%) @mui/material/Popoverβparsed: πΊ+323B(+0.33%) gzip: πΊ+143B(+0.41%) @mui/material/Selectβparsed: πΊ+323B(+0.23%) gzip: πΊ+143B(+0.30%) @mui/material/SwipeableDrawerβparsed: πΊ+322B(+0.31%) gzip: πΊ+136B(+0.37%) @mui/material/Drawerβparsed: πΊ+321B(+0.33%) gzip: πΊ+122B(+0.35%) @mui/material/TextFieldβparsed: πΊ+321B(+0.21%) gzip: πΊ+132B(+0.26%) @mui/material/TablePaginationβparsed: πΊ+320B(+0.18%) gzip: πΊ+140B(+0.25%) @mui/lab/AdapterDateFnsβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/AdapterDayjsβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/AdapterLuxonβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/AdapterMomentβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/CalendarPickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/CalendarPickerSkeletonβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/ClockPickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/DatePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/DateRangePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/DateRangePickerDayβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/DateTimePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/DesktopDatePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/DesktopDateRangePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/DesktopDateTimePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/DesktopTimePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/LoadingButtonβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/LocalizationProviderβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/Masonryβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/MobileDatePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/MobileDateRangePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/MobileDateTimePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/MobileTimePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/MonthPickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/PickersDayβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/StaticDatePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/StaticDateRangePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/StaticDateTimePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/StaticTimePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/TabContextβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/TabListβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/TabPanelβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/Timelineβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/TimelineConnectorβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/TimelineContentβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/TimelineDotβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/TimelineItemβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/TimelineOppositeContentβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/TimelineSeparatorβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/TimePickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/TreeItemβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/TreeViewβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/useAutocompleteβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/lab/YearPickerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Accordionβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/AccordionActionsβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/AccordionDetailsβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/AccordionSummaryβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Alertβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/AlertTitleβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/AppBarβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Autocompleteβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Avatarβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/AvatarGroupβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Backdropβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Badgeβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/BottomNavigationβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/BottomNavigationActionβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Boxβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Breadcrumbsβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Buttonβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/ButtonBaseβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/ButtonGroupβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Cardβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/CardActionAreaβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/CardActionsβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/CardContentβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/CardHeaderβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/CardMediaβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Checkboxβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Chipβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/CircularProgressβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/ClickAwayListenerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Collapseβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Containerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/CssBaselineβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/DefaultPropsProviderβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/DialogActionsβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/DialogContentβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/DialogContentTextβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/DialogTitleβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Dividerβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Fabβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Fadeβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/FilledInputβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/FormControlβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/FormControlLabelβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/FormGroupβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/FormHelperTextβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/FormLabelβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/GlobalStylesβparsed: 0B(0.00%) gzip: 0B(0.00%) @mui/material/Gridβparsed: 0B(0.00%) gzip: 0B(0.00%)
Generated by :no_entry_sign: dangerJS against 069631770fec95a40adfe7bcfe76eacc4102f5a1
Your argos check seems to be borked especially I made no changed that would affect visuals.
Should I not touch the unstable folder in the future? Could you explain why does it exist?
Should I not touch the unstable folder in the future? Could you explain why does it exist?
I reverted the changes in Base UI (mui-base directory). It included the unstable folder because Base UI was still in beta. We no longer maintain Base UI in this repositoryβit's now legacy and will be removed in the future (timeline unknown). Material UI used to depend on Base UI, but we've since separated the code, causing duplication. We don't plan to make any further changes to Base UI here. The issue is related to Material UI's Modal. Base UI with headless components is now being developed in a separate repo: mui/base-ui, which will be integrated into Material UI once stable.
In addition because we don't differentiate between aria-hidden tags that got added by modals and added by devs, we need to pierce through (remove) every aria-hidden attribute in every ancestor of the last modal container; this is to prevent no accessible elements on a page when multiple modals are present.
I didn't understand this. Can you please explain more clearly with an example?
I think I explained the technicality of it here but basically someone can put a Modal with disablePortal inside an element that is already aria-hidden and should be aria-hidden, then opens said Modal. In that case we can't afford not to remove that aria-hidden tag from the ancestor because we cannot allow to be the whole UI inaccessible, that's the absolute worst case.
If you have multiple modals that function that gets aria-hidden elements currently cannot differentiate between if it's actually a dev mistake (and warn) or it was ModalManager that added that prop.
Previous bug still stands when you mess with aria-hidden states in the tree outside your modal, when the modal gets removed your changes won't be preserved (because the modal will restore the state when it was added)
What bug is this? Can you give a bug reproduction in the form of StackBlitz or CodeSandbox?
We get the original elements aria-hidden states to restore (were they originally hidden or not) at modal mount time. When we come back and restore them on modal unmount, someone might've changed one of those elements while the modal was mounted. In the end we restore them to a state that the dev changed since then. This I think this can only be partially fixed and it would be some effort to implement.
We also need to add a test case for the Modal component with the disablePortal prop to verify that aria-hidden is handled correctly
I'll add some more tests now.
test added
@Gr3q, it wouldβve been great to use aria-modal instead of managing aria-hidden ourselves. That would have allowed us to remove all this logic. Unfortunately, I guess it's not widely supported yet, and also ariaModal API isn't available on Firefox versions below 119, while we still support Firefox from version 115. Although we don't use ariaModal property, do you think we can use aria-modal attribute now?
even though screenreader support is not as good it can still be added. But the docs https://w3c.github.io/aria/#aria-modal implies that there should be only 1 aria-modal at a time, so some logic would need to be written to remove it from all modals except the toplevel one.
Plus according the docs we can't escape implementing this same logic because all other elements bust be marked as inert (instead of aria-hidden). Technically it's not a MUST, but by their spec a modal doesn't enforce focus either.
Quote from the docs
When a modal element is displayed, authors SHOULD mark all other contents as inert (such as "inert subtrees" in HTML) if the ability to do so exists in the host language.
That's the problem of the developer mounting the Modal on the wrong place. We shouldn't handle this and remove aria-hidden for them. They should be able to find out that they are mounting the Modal in the wrong container or remove aria-hidden themselves. Could you remove this part of the logic and the relevant test for it (if any).
I was a little too verbose on the dev errors. Taking away dev errors, ModalManager adds (and removes) aria-hidden tags itself.
Just using that as baseline in this example scenario:
- User is interacting with a dialog with
disablePortalenabled - The UI has a dialog that opens when it loses connection to the backend without
disablePortal(so it mounts next to root). The frontend loses connection and the dialog opens while the first dialog was open.
In this case the whole page becomes inaccessible because the dialog the user was using hid the parent element of the lost connection dialog beforehand (only if we remove that part of the code as you suggested).
This is a common enough scenario to handle and even if it isn't I can't guarantee there isn't another edge-case caused by ModalManager manipulating aria-hidden tags unless you remove disablePortal and container support from it altogether. So that code must stay.
I noticed that in the Menu component (which uses Modal), the scroll isnβt locked when the disablePortal prop is used. Here's a video showing the behavior in this PR vs production
I see. I'm not sure what is the optimal logic when selecting the scrollContainer in ModalManager.ts#handleContainer.
This https://github.com/mui/material-ui/pull/33168 changed it so if the modal is in shadow dom it always selects body. Maybe we need to do that in all cases?
Edit: I probably need to ~recursively~ go up the tree to select the correct scrollable ancestor.
Edit: I probably need to ~recursively~ go up the tree to select the correct scrollable ancestor.
Yes, that seems like the right approach. You can use the isOverflowing method to check for a vertical scrollbar, and if there isn't, move up using parentElement.
I fixed it, the original code was not the best either because it had isOverflowing and containerWindow.getComputedStyle(parent).overflowY === 'scroll' that more or less do the same thing, but one is used when applies padding and the other one is used to hide the scrollbar (and they are applied to the same elements).
I could probably refactor this further to do both in 1 loop (aside from the shadow dom fix).
I don't know how to write the tests for this, the expected outcome is that the page and the modal cannot be scrolled.
- when
disabledPortalis used - when there are multiple scrollable ancestors
- when there are multiple scrollable ancestors and each of those elements have a fixed element
- when there are multiple scrollable ancestors and each of those elements have a fixed element, and the second (nested) scrollable has
position:relative
The fixed elements should still look good and be cleaned up properly.
I know that the current tests are not passing.
I know that the current tests are not passing.
Those tests are verifying the wrong things, here is a snipped from one, see my comments:
// This is wrong, this indicates nothing, the page might or might not be scrollable
expect(document.body.parentElement.style).to.have.property('overflow', '');
setProps({ open: true });
// it wasn't scrollable so we didn't need to apply overflow: hidden to it, this fails because of it.
expect(document.body.parentElement.style).to.have.property('overflow', 'hidden');
setProps({ open: false });
expect(document.body.parentElement.style).to.have.property('overflow', '');
I could try to fix them but I don't know what I should expect instead.
I changed the tests for Modal, as I said before they were testing the wrong things, now they are actually checking if their child is mounted or not on open/close.
The remaining tests that are failing in ModalManager because of this test (assumption) "should disable the scroll even when not overflowing". I admit I don't understand why is that necessary when the page is not scrollable. Maybe because the Modal content itself can cause an overflow? I can add some code so we always apply overflow: hidden to the container as well and those tests will most likely pass.
I fixed the behaviour now completely, you can manually add another overflow:scroll around the menu button in the docs to test nested scrollable containers. The top bar is also not moving permanently to the left anymore.
The only thing left are the new tests.
Edit: and the existing tests are breaking because for some reason the window.document.documentElement.clientWidth reports 0 in the tests for some reason.
I can do that, the question is will it work properly if we have a Menu with disablePortal inside a Dialog that has scrollable content, which is not a good pattern, but still?
I reverted back the scrollLock change and amended the tests for it. It seems to work fine for both disabledPortal and regular modals even with nested scrollbars.
Tests are passing on my end, I can commit pnpm dedupe changes but I much prefer you would do that - the tooling for this project gives me trouble on every turn for some reason.
@ZeeshanTamboli any progress on this?
@Gr3q aria-modal property was added to Dialog in the recent release version 6.1.5 in #44118.
@Gr3q aria-modal property was added to Dialog in the recent release version 6.1.5 in https://github.com/mui/material-ui/pull/44118.
Yes...and? What do you want me to do about it?
Can I assume you tested screen-reader (including Android, iOS and Windows screen-readers) compatibility, tested multiple open modals at the same time including nested modals? It all works and usable, not just compliant?
You can take this PR or leave it, but I definitely don't have time to do any of that for you with aria-modals, that would be just starting from scratch for me with all that testing. Plus I wouldn't be able to remove this patch from our own codebase anyway when I'm done (without good reason), when this implementation passed 3 accessibility audits already.
If you take it, you can build upon it to add/remove inert props when they become widely supported (instead of aria-hidden), plus this and https://github.com/mui/material-ui/pull/44118 can exist side-by-side.
You can also leave this PR or take it over completely, I don't mind. In that case I also don't take responsibility for any of the functionality either.
@Gr3q aria-modal property was added to Dialog in the recent release version 6.1.5 in #44118.
Yes...and? What do you want me to do about it?
Can I assume you tested screen-reader (including Android, iOS and Windows screen-readers) compatibility, tested multiple open modals at the same time including nested modals? It all works and usable, not just compliant?
You can take this PR or leave it, but I definitely don't have time to do any of that for you with
aria-modals, that would be just starting from scratch for me with all that testing. Plus I wouldn't be able to remove this patch from our own codebase anyway when I'm done (without good reason), when this implementation passed 3 accessibility audits already.If you take it, you can build upon it to add/remove
inertprops when they become widely supported (instead of aria-hidden), plus this and #44118 can exist side-by-side.You can also leave this PR or take it over completely, I don't mind. In that case I also don't take responsibility for any of the functionality either.
@Gr3q, I just wanted to inform you and see if that could simplify things. No need to start from scratch.
Could you take a look at this comment: https://github.com/mui/material-ui/pull/43318#discussion_r1816582932?
@Gr3q, I just wanted to inform you and see if that could simplify things. No need to start from scratch.
If you want to keep the previous behaviour - make only the toplevel modal accessible for screenreaders and to be able to trap focus - then I can't.
But I can't say for sure without some real screenreader testing with aria-modals set, like how they handle focus, tabbing, multiple aria-modals, nested aria-modals etc. I could only simplify the code here if they are capable of doing the work for us.
This page is not scrollable: https://deploy-preview-43318--material-ui.netlify.app/material-ui/react-modal/. Maybe some logic is wrong.
I narrowed it down to the server-side modal on the page. I'll check where it's going wrong.
Edit: The server-side modal in the docs is always set to open and disableScrollLock is not set on it, so it's technically working as intended. It didn't work as intended before, so technically this is a breaking change?
The server-side modal in the docs is always set to open and
disableScrollLockis not set on it, so it's technically working as intended. It didn't work as intended before, so technically this is a breaking change?
The issue seems to be that the server-side modal has a custom container (container={() => rootRef.current!}). Previously, handleContainer considered this custom container, so overflow: hidden wasnβt applied to it (because it didn't overflow), and scrolling wasnβt locked. Now, it's always the body element, so overflow: hidden is applied, locking scrolling. This is the breaking change I was concerned about in this discussion.
The issue seems to be that the server-side modal has a custom container (container={() => rootRef.current!}). Previously, handleContainer considered this custom container, so overflow: hidden wasnβt applied to it (because it didn't overflow), and scrolling wasnβt locked. Now, it's always the body element, so overflow: hidden is applied, locking scrolling. This is the breaking change I was concerned about in https://github.com/mui/material-ui/pull/43318#discussion_r1786644290.
@ZeeshanTamboli I'm not sure I have a good answer to that.
My assumption is that disableScrollLock is made to prevent scrolling on the page when a modal is open, not on the immediate (parent) container, because that doesn't really have the desired effect (doesn't actually prevent scrolling). Maybe my assumption is wrong.
We made that change because it was not behaving as desired for disablePortal. I treat disablePortal and container the same, because what you can do with disablePortal={true} you can do it directly with container. For example in the server-side example if you remove setting container it will display exactly the same as with container.
If they are meant to be different I'm not sure what is the best course of action, container prop has nothing in it's docstring indicating that it will affect scroll locking behaviour in any way.
In short I can't come up with have any use-cases where the behaviour you are describing is useful by myself. If can provide me one or two I can start implementing something that matches those use cases too
@ZeeshanTamboli I'm not sure I have a good answer to that.
My assumption is that
disableScrollLockis made to prevent scrolling on the page when a modal is open, not on the immediate (parent) container, because that doesn't really have the desired effect (doesn't actually prevent scrolling). Maybe my assumption is wrong.We made that change because it was not behaving as desired for
disablePortal. I treatdisablePortalandcontainerthe same, because what you can do withdisablePortal={true}you can do it directly withcontainer. For example in the server-side example if you remove settingcontainerit will display exactly the same as withcontainer.If they are meant to be different I'm not sure what is the best course of action,
containerprop has nothing in it's docstring indicating that it will affect scroll locking behaviour in any way.In short I can't come up with have any use-cases where the behaviour you are describing is useful by myself. If can provide me one or two I can start implementing something that matches those use cases too
As per the current production code, the prevention of scrolling is on the container, where the scrollContainer in the code is the resolvedContainer (either the one provided by the developer or the default body). If you inspect in https://mui.com/material-ui/react-modal/#server-side-modal, you will see overflow: hidden is applied on Box container.
You are right that the disablePortal={true} is the same as container. The container prop only has an effect when disablePortal={false}. When disablePortal={true}, the container prop is ignored since no portal is created. But I think it's about the scroll lock style getting applied on the container. What if a developer has a custom container and no disablePortal?
I think we will have to revert a few commits here till this comment.
@ZeeshanTamboli I'm not sure I have a good answer to that. My assumption is that
disableScrollLockis made to prevent scrolling on the page when a modal is open, not on the immediate (parent) container, because that doesn't really have the desired effect (doesn't actually prevent scrolling). Maybe my assumption is wrong. We made that change because it was not behaving as desired fordisablePortal. I treatdisablePortalandcontainerthe same, because what you can do withdisablePortal={true}you can do it directly withcontainer. For example in the server-side example if you remove settingcontainerit will display exactly the same as withcontainer. If they are meant to be different I'm not sure what is the best course of action,containerprop has nothing in it's docstring indicating that it will affect scroll locking behaviour in any way. In short I can't come up with have any use-cases where the behaviour you are describing is useful by myself. If can provide me one or two I can start implementing something that matches those use cases tooAs per the current production code, the prevention of scrolling is on the
container, where thescrollContainerin the code is the resolvedContainer (either the one provided by the developer or the defaultbody). If you inspect in https://mui.com/material-ui/react-modal/#server-side-modal, you will seeoverflow: hiddenis applied on Box container.You are right that the
disablePortal={true}is the same ascontainer. Thecontainerprop only has an effect whendisablePortal={false}. WhendisablePortal={true}, thecontainerprop is ignored since no portal is created. But I think it's about the scroll lock style getting applied on thecontainer. What if a developer has a custom container and no disablePortal? I think we will have to revert a few commits here till this comment.
If you want to preserve that behaviour I agree, as long as disablePortal will work the same as now in this PR.
You did most of the changes there, would you like me to change it now or would you like to do it?
You did most of the changes there, would you like me to change it now or would you like to do it?
Please look into it. I don't have the bandwidth for it.
As per the current production code, the prevention of scrolling is on the container, where the scrollContainer in the code is the resolvedContainer (either the one provided by the developer or the default body). If you inspect in https://mui.com/material-ui/react-modal/#server-side-modal, you will see overflow: hidden is applied on Box container.
You are right that the disablePortal={true} is the same as container. The container prop only has an effect when disablePortal={false}. When disablePortal={true}, the container prop is ignored since no portal is created. But I think it's about the scroll lock style getting applied on the container. What if a developer has a custom container and no disablePortal? I think we will have to revert a few commits here till https://github.com/mui/material-ui/pull/43318#issuecomment-2382701323.
I'm changing the code, but I want to summarize the bugs this PR had, then what you want.
Bugs:
- scrollLock doesn't work for
disablePortalon the docs page - I found out the correct behaviour is to lock the body element (just like before) because that's the only way scroll locking works. When container is provided locking was applied to the container (and that didn't work, this is the bevaiour on the main branch) - Because I changed (fixed) the scroll locking behaviour to always apply to body, the whole modal doc page got locked. This was due to the server-side modal always
openwith nodisableScrollLock.disablePortalapplied or not doesn't make a difference here.
So let me clarify what you want:
You want me to change it that scroll lock works peropely when:
-
disablePortalis set -
disablePortalis not set
and it shouldn't work properly (aka scroll lock applies to the container that effectively does nothing to prevent page scrolling):
-
containeris set, regardless ifdisablePortalis set or not. This means even thoughcontaineris not used whendisablePortalis true and the modal shouldn't be mounted to the container at all, you still want the scroll lock to apply to that container.
I want you to confirm this because this is the only way bugs 1 and 2 can be resolved on the Modal docs page at the same time. (Edit: I'm asking because I will need to implement some workarounds to handle that)
@Gr3q It's been a while, but from what I remember you're rightβthat's what's needed.
@ZeeshanTamboli I fixed the doc page, it does what you want (so no breaking changes).
Added a comment that technically the behaviour around the scrolllock+container is still a bug, hopefully someone will remove it when the next major version comes.
Fixed the existing tests and added new tests for this too.
So now the PR should be ready. :crossed_fingers: