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

[base] Add useModal hook #38187

Merged
merged 28 commits into from
Aug 11, 2023
Merged

[base] Add useModal hook #38187

merged 28 commits into from
Aug 11, 2023

Conversation

mnajdova
Copy link
Member

@mnajdova mnajdova commented Jul 27, 2023

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.

@mnajdova mnajdova added component: modal This is the name of the generic UI component, not the React module! package: joy-ui Specific to @mui/joy labels Jul 27, 2023
@mui-bot
Copy link

mui-bot commented Jul 27, 2023

Netlify deploy preview

@material-ui/core: parsed: +0.15% , gzip: +0.21%
@material-ui/unstyled: parsed: +0.61% , gzip: +0.41%
@mui/joy: parsed: +0.20% , gzip: +0.25%

Bundle size report

Details of bundle changes (Toolpad)
Details of bundle changes

Generated by 🚫 dangerJS against 5ce713a

Signed-off-by: Marija Najdova <mnajdova@gmail.com>
@mnajdova mnajdova marked this pull request as ready for review July 27, 2023 15:25
packages/mui-joy/src/Modal/useModal.ts Outdated Show resolved Hide resolved
// Modals don't open on the server so this won't conflict with concurrent requests.
const manager = new ModalManager();

const useModal = (parameters: UseModalParameters) => {
Copy link
Member

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?

Copy link
Member Author

@mnajdova mnajdova Jul 31, 2023

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

Copy link
Member

@oliviertassinari oliviertassinari Jul 31, 2023

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:

  1. 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:

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?

  1. The need for a hook. In Material UI component composition was enough. Is the objective to improve performance?

Copy link
Member Author

@mnajdova mnajdova Aug 1, 2023

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.

Copy link
Member Author

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:

  1. ✅ The Modal components work as expected both in Material UI, Base UI and Joy UI.

  2. ❓ 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

  3. ❌ 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.

Copy link
Member

@siriwatknp siriwatknp Aug 9, 2023

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.

Copy link
Member Author

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.

Copy link
Member

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

@mnajdova mnajdova marked this pull request as draft August 1, 2023 07:07
@mnajdova mnajdova changed the title [joy] Add useModal internal hook [base] Add useModal hook Aug 1, 2023
@mnajdova mnajdova added package: base-ui Specific to @mui/base and removed package: joy-ui Specific to @mui/joy labels Aug 2, 2023
@mnajdova
Copy link
Member Author

mnajdova commented Aug 2, 2023

@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:

@mnajdova mnajdova requested review from michaldudak and siriwatknp and removed request for michaldudak and siriwatknp August 3, 2023 07:09
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Aug 3, 2023
@github-actions github-actions bot added PR: out-of-date The pull request has merge conflicts and can't be merged and removed PR: out-of-date The pull request has merge conflicts and can't be merged labels Aug 3, 2023
packages/mui-base/src/unstable_useModal/useModal.ts Outdated Show resolved Hide resolved
packages/mui-base/src/unstable_useModal/useModal.ts Outdated Show resolved Hide resolved
// @ts-ignore internal logic - Base UI supports the manager as a prop too
manager = defaultManager,
closeAfterTransition = false,
onTransitionEnter,
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree 👍

packages/mui-base/src/unstable_useModal/useModal.types.ts Outdated Show resolved Hide resolved
packages/mui-base/src/unstable_useModal/useModal.types.ts Outdated Show resolved Hide resolved
packages/mui-base/src/unstable_useModal/useModal.types.ts Outdated Show resolved Hide resolved
packages/mui-base/src/unstable_useModal/useModal.types.ts Outdated Show resolved Hide resolved
export type UseModalRootSlotProps<TOther = {}> = TOther & UseModalRootSlotOwnProps;

export type UseModalParameters = {
'aria-hidden'?: React.AriaAttributes['aria-hidden'];
Copy link
Member

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).

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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'.

Copy link
Member Author

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.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Aug 4, 2023
if (open && isTopModal()) {
handleMounted();
} else if (modalRef.current) {
ariaHidden(modalRef.current, ariaHiddenProp);
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

@siriwatknp siriwatknp left a 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.

mnajdova and others added 2 commits August 9, 2023 11:34
Co-authored-by: Michał Dudak <michal.dudak@gmail.com>
Signed-off-by: Marija Najdova <mnajdova@gmail.com>
Copy link
Member

@michaldudak michaldudak left a 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'];
Copy link
Member

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'.

@mnajdova mnajdova merged commit d410b47 into mui:master Aug 11, 2023
8 checks passed
Copy link
Member

@oliviertassinari oliviertassinari left a 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 👍

Comment on lines +302 to +304
// demos - proptype generation
{
files: ['docs/data/base/components/modal/UseModal.js'],
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: modal This is the name of the generic UI component, not the React module! package: base-ui Specific to @mui/base
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Modal][base-ui] Create the hook
5 participants