-
-
Notifications
You must be signed in to change notification settings - Fork 32.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
[base] Add useModal
hook
#38187
[base] Add useModal
hook
#38187
Conversation
Netlify deploy preview@material-ui/core: parsed: +0.15% , gzip: +0.21% Bundle size reportDetails of bundle changes (Toolpad) |
Signed-off-by: Marija Najdova <mnajdova@gmail.com>
// Modals don't open on the server so this won't conflict with concurrent requests. | ||
const manager = new ModalManager(); | ||
|
||
const useModal = (parameters: UseModalParameters) => { |
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.
To move in Base UI?
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.
In the future yes, we will. The idea was to add it in Joy UI first and validate it, and later move it to Base UI. For now, I need it in #38169
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.
Ok fair enough, I was thinking of exposing it as unstable from Base UI, but you own the Modal, so I think we are good, there is little risk that this gets duplicated with Material UI.
Things I find confusing:
- A Joy UI Modal is not equivalent to a Base UI Modal, it's equivalent to a Material UI Dialog. As far as know, this is not the right terminology from an a11y standpoint. See the 3 different notions:
- Dialog: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/
- Modal Dialog: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
- Modal: https://en.m.wikipedia.org/wiki/Modal_window
Overall, I feel that Joy UI Modal contradicts what we say in https://mui.com/base-ui/react-modal/
The term "modal" is sometimes used interchangeably with "dialog," but this is incorrect. A dialog may be modal or nonmodal (modeless).
A Joy UI Modal doesn't have to block interactions with the rest of the page. How about we rename Joy UI Modal to Dialog?
- The need for a hook. In Material UI component composition was enough. Is the objective to improve performance?
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.
The initial need for the hook was adding a Drawer
component in Joy UI. The Drawer
is basically a modal with different transitions. I started with implementing it using component composition, however, we discussed that probably extracting the logic in a hook would be better, so that we avoid some of the problems we have in Material UI with component composition (see #38169) - this is why I initially opened the pull request. However, it seems better to have this hook on Base UI level, so that long-term Material UI will benefit it too, both in terms of performance and avoiding some of the problems that naturally comes from component composition. We can base all Modal-like component to use the hook instead of using component composition.
I am moving the hook to Base UI as unstable.
A Joy UI Modal doesn't have to block interactions with the rest of the page. How about we rename Joy UI Modal to Dialog?
THey block, this is exactly the same behavior as Base UI - the Modal component was literally copy pasted. The Modals both in Base UI and Joy UI interrupt interactions.
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.
We discussed this with Olivier - here is the outcome:
-
✅ The Modal components work as expected both in Material UI, Base UI and Joy UI.
-
❓ The Joy UI's Modal page can likely be split into multiple pages, one talking about the modal and one talking about Dialogs (and alerts). It will be easier to distinguish what is modal vs dialog. cc @siriwatknp
-
❌ The biggest problem is the different behavior of the components that have some modals inside. For e.g. Material UI's Select dropdown is a modal - it blocks interactions with other elements, for e.g. [Menu] Clicking outside a menu blocks click from bubbling up #11243. This is the opposite behavior in Joy UI where the dropdown is non-modal. However, we should support both of these scenarios in both Material UI and Joy UI. What is most likely scenario is that on mobile the dropdowns need to be modal - so that clicking outside to close it does not unintentionally takes action. On desktop, it's likely most expected to have the non-modal scenario, as the screen is big enough to find a space to click without taking another action if that is the intention.
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.
A Joy UI Modal is not equivalent to a Base UI Modal, it's equivalent to a Material UI Dialog
@oliviertassinari Can you elaborate? From my view, Joy UI Modal is the same as Base UI Modal. You can pass any children to create blocking interaction to let users focus on the content of the Modal.
Now Joy UI also provides a ModalDialog
which follows https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
Meaning, if you want to create a modal dialog based on above pattern:
<Modal> // <-- the modal which blocks interaction
<ModalDialog> // <-- a container the building modal dialog
…
</ModalDialog>
</Modal>
A Joy UI Modal doesn't have to block interactions with the rest of the page. How about we rename Joy UI Modal to Dialog?
Joy UI Modal should block interactions. What we need to do is create a new component called Dialog
which will not block interaction by default and let developers integrate with their triggers.
The Joy UI's Modal page can likely be split into multiple pages, one talking about the modal and one talking about Dialogs (and alerts). It will be easier to distinguish what is modal vs dialog
Agree, we can consider splitting the page when we have a Dialog
component for Joy UI.
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.
@siriwatknp we discussed this offline with Olivier this comment #38187 (comment) summarized the conclusions. Everything is fine with the Joy UI's modal, we just likely need to split the page so that it's easier to consume :)
The other next thing would be supporting modal and non-modal version for all components that have some kind of drop downs, selects, autocomplete, menu etc.
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.
However, we should support both of these scenarios in both Material UI and Joy UI.
Oh, yes, this would be so great 👍
We can consider splitting the page
This would make for a higher level of clarity 💯
What we need to do is create a new component called Dialog which will not block interaction by default and let developers integrate with their triggers.
I'm not sure, what would a "non modal dialog" look like? Something like this https://opensource.ebay.com/mindpatterns/messaging/toast-dialog/index.html?
I think that https://mui.com/joy-ui/react-modal/#alert-dialog is wrong, it shouldn't close when clicking on the backdrop. https://www.notion.so/mui-org/Alert-Dialog-should-close-on-backdrop-click-443f21d45852469bb3c71d94be6a57e6
@michaldudak can you take a look at this? I will address #38187 (comment) separately - this PR just moved the logic for the modal into reusable hook that is reused in Base UI's and Joy UI's modal. Next steps:
|
// @ts-ignore internal logic - Base UI supports the manager as a prop too | ||
manager = defaultManager, | ||
closeAfterTransition = false, | ||
onTransitionEnter, |
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.
We can unify the transition APIs between components in a separate PR.
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.
Agree 👍
export type UseModalRootSlotProps<TOther = {}> = TOther & UseModalRootSlotOwnProps; | ||
|
||
export type UseModalParameters = { | ||
'aria-hidden'?: React.AriaAttributes['aria-hidden']; |
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.
It's unusual to have an ARIA attribute on a hook. It would be great if it was described or renamed (or, even better, both).
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.
Why would it be unusual to have it? If we don't have it, we would need to duplicate this logic three times and teach people how to process the prop before sending it to the hook under some other name. It's clear what it is. For e.g. react aria has this often, for e.g. https://react-spectrum.adobe.com/react-aria/useSelect.html.
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 meant it's not usual in our code base. Usually (if not always) we place ARIA attributes based on some other parameters. Having a verbatim aria-hidden
may suggest it's just forwarded as an element prop.
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.
Having a verbatim aria-hidden may suggest it's just forwarded as an element prop.
I understand but why is this bad thing? This is what I am trying to understand. The hooks are intended to be used in components, so why is it bad thing to just forward some prop?
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.
My point is that it may suggest it's just forwarded, but there is some logic associated with it, so it won't always end up being forwarded.
Plus, it's easier to specify hidden
in the hook's parameters than 'aria-hidden'
.
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 feel like it's straight forward, let's see if we will have complains about it. The hook is marked as unstable_
we can change if we see opportunities to improve.
if (open && isTopModal()) { | ||
handleMounted(); | ||
} else if (modalRef.current) { | ||
ariaHidden(modalRef.current, ariaHiddenProp); |
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.
Can't we return the aria-hidden
in getRootProps
instead of using DOM methods directly?
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 will cause another re-render. I wouldn't change the logic of it now. I am just extracting the logic in a hook. We can revisit and see if we can improve in a later iteration.
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.
Overall, looks good to me.
Co-authored-by: Michał Dudak <michal.dudak@gmail.com> Signed-off-by: Marija Najdova <mnajdova@gmail.com>
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.
All is good. We have different thoughts about 'aria-hidden` but it's just a matter of opinion, so I don't want this to block this PR. If you think the DX id good enough with your solution, feel free to merge it.
export type UseModalRootSlotProps<TOther = {}> = TOther & UseModalRootSlotOwnProps; | ||
|
||
export type UseModalParameters = { | ||
'aria-hidden'?: React.AriaAttributes['aria-hidden']; |
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.
My point is that it may suggest it's just forwarded, but there is some logic associated with it, so it won't always end up being forwarded.
Plus, it's easier to specify hidden
in the hook's parameters than 'aria-hidden'
.
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.
The code abstraction level looks great 👍
// demos - proptype generation | ||
{ | ||
files: ['docs/data/base/components/modal/UseModal.js'], |
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.
Maybe to generalize, if it's code generated, I guess this applies to everything.
Closes #38056
This PR extract the
Modal
's logic into a hook, that can be re-used in other components too, for e.g.Drawer
. All modal's logic is moved to the hook, the only bits left in the component are the slots creation and the rendered elements. In the hook I used Base UI's notion for creating event handlers, the rest is the logic that was previously leaving on Joy UI's Modal component.I didn't add tests for the
useModal
hook, as the Modal component was already covered for most of the logic.