Adding Confirmation modal service to the root of our React app

Hi all,

I just opened a PR which I’d like to share with you: Fixes #32880 - Add confirmation modal service by laviro · Pull Request #8619 · theforeman/foreman · GitHub

Many pages need to show a toast message when the page loads,
especially after a full page reload with pages that were written in .erb.
in modern React-router pages, we can raise a toast message easily as the result of the API call,
but when moving between pages, sometimes you need to get the toast message when the page just loaded. You can find the code for it in foreman/layout_helper.rb at develop · theforeman/foreman · GitHub

currently, there is no common confirmation modal component, and most components or jQuery code,
trigger a window.confirm() function which isn’t the best UX in my opinion.

I am suggesting adding the confirmation modal to the root of our app,
and by using Redux, to enable anyone to call it by dispatching an action like:

openConfirmModal({ label, message, onConfirm })

as well as closeConfirmModal()

The modal will follow the guidelines in PatternFly 4 • Modal

and for a removal operation will follow this design:
unnamed

would glad to get your feedback,
Thanks, Ron

2 Likes

I’d really love if we could be using the same components for same use-case, doing it through Redux action works for me, because we can dispatch that even from legacy JS and that’s the only thing that matters to me and your solution does that very well.

I’ve already mentioned in the PR, it would be great to choose between the two designs (destructive, confirmative). If it’s not in the first PR, please implement the destructive desing.

1 Like

Thanks @Ron_Lavi !

I like the clean PF4 design, much better than the native console.confirm, but on the other hand like the simplicity of console.confirm:

if (confirm("are you sure?") {
  doSomething();
}

I have some mixed feeling with redux here, IMHO we overuse our store for storing temporary states.
I think redux is great, and re-rendering by states gives a lot of benefits, but I wonder what kind of data is needed to be store and what is just redundant. Adding redux adds an extra layer and complexity that in some cases just aren’t needed.

i.e does it make sense to manage dropdowns state (isOpen) within redux? In my view, dropdowns state is the same as the confirmation dialog state - a very short-term state.

I’ve encountered with a different approach confirm functionally in react, which doesn’t involve redux nor inner states - react-confirm
It’s not a Dialog component, it just keeps the same functionally of console.confirm with no need of state management at all.

i.e

const handleDelete = async () => {
  if (await foremanConfrim('Are your sure?')) {
    console.log('yes');
  } else {
    console.log('no');
  }
}

Looks clean and simple to me, just as console.confirm is

1 Like

As we just discussed in the UX Interest Group Meeting #13
in the PR mentioned above, I will do just the presentational work and then we can decide which approach will trigger it, Redux action or the React confirmable wrapper,

After thinking about it a little bit, I think the wrapper will fail exactly where I got stuck and had to do a workaround in Fixes #32819 - Add Index page template by laviro · Pull Request #8596 · theforeman/foreman · GitHub

Basically, there is a table dropdown and by default when clicking outside of it, it’s being hidden.

In the table scenario, I would like to click on a Delete action, which opens the modal,
but when clicking anywhere on the screen, including on the modal, the dropdown that had open it will do unmount and therefore the confirm modal will unmount too.
This is exactly what will happen if we use GitHub - haradakunihiko/react-confirm: Small library which makes your Dialog component callable. because the logic is happening inside the unmounting component.
I had to handle a complex state to avoid that from happening: Fixes #32819 - Add Index page template by laviro · Pull Request #8596 · theforeman/foreman · GitHub
e.g:

  const [open, setOpen] = React.useState(false);
  useEffect(() => {
    preventDropdownToggle(open);
  }, [open, preventDropdownToggle]);

with the Redux approach that won’t happen, because the logic is placed outside of the fragile dropdown component, in the root of the app, and even if it unmounts, the confirm modal will still work with no workaround needed.

Maybe it’s beyond my knowledge, but when you use a confirmation modal or any other modal, you should use a backdrop (grey background) https://www.patternfly.org/v4/components/backdrop/design-guidelines that prevents a user from interacting with a page (also un mounting dropdown) unless the modal is closed.

1 Like

correct, the modal itself will prevent close on outer click,
but the dropdown which in this case kinda wraps the modal does, leading to unexpected behavior, though with the Redux implementation the modal will remain open without any special workaround.

For this kind of issues there is a click away listener approach, i.e Material-UI implemented it for controlling click events outside the element - Detect click outside React component - Material-UI

I haven’t found any docs about this apprcoch in PF, though we can add it simply to our components, by 3rd party lib (foreman uses already react-onclickoutside) or a custom hook - Little Neat trick to capture click outside with React Hook | by Pitipat Srichairat | Medium

PF4 are using their own click outside listeners, I would prefer not to wrap 3rd libraries components or do sort of workarounds, but rather create a simpler generic solution, in this case, it can be using Redux or another tool, although when the logic lives in components that can unmount for some reason, I would prefer to move the logic into a more stable place and trigger it from outside, for me Redux sounds like a great solution for it,

waiting for your responses so I know whether to implement a different JSX component which is based on passing props and state setters to it, or to implement it with Redux or some other tool, because the implementation would be different in each case

I don’t think the click-away listener approach is a workaround, it’s a public interface in Material-UI and other UI libraries, we can reuse this behavior easily across components.

But this is another subject, which deserved a different discussion. Regarding the confirmation dialog, how can we consume this service over erb pages? i.e -

 "link_to "Dangerous zone", dangerous_zone_path, data: { confirm: 'Are you sure?' }"

I think this is a very nice approach: https://derk-jan.com/2020/10/rails-ujs-custom-confirm/
basically, if we override the Rails.confirm implementation
as explained here: rails/confirm.coffee at e9aa7ecdee0aa7bb4dcfa5046881bde2f1fe21cc · rails/rails · GitHub

we could do something like:

import Rails from '@rails/ujs';
// This is the native confirm, showing a browser alert/confirm dialog
const nativeConfirm = Rails.confirm;

let __SkipConfirmation = false;

Rails.confirm = function (message: string, element: HTMLElement) {
  ...
  ...
  // Here a custom dialog can be shown. use whatever method you like. This
  // hypothetical function shows a dialog.
  //
  dispatch(openConfirmModal({message, onConfirm}))

  // This ensures that the original event that caused this confirmation is
  // swallowed and the action is NOT executed.
  return false;
};
1 Like

btw, I see that Katello already did it in katello/katello.js at ea9570ae2e2529b54f20f113abdab86b83371040 · Katello/katello · GitHub :slight_smile: