lindesvard / pushmodal

Handle shadcn dialog, sheet and drawer with ease
MIT License
170 stars 9 forks source link

Prevent dialog from closing #5

Open alexambrinos opened 5 months ago

alexambrinos commented 5 months ago

Hi, thanks for this amazing library! I was trying to convert my app to using this library for handling the modal state but inevitable I have hit a barrier.

I was trying to stack a dialog with an alert dialog from radix ui. I wanted to prevent closing the dialog when trying to close it with a dirty form inside it and show the alert dialog instead. But I can't do that since there's no access to the controlled state of the modal (specifically the onOpenChange of the Root)

If someone can find a way to implement this kind of flow without modifications to the library I would love to know. The only thing that comes to my mind would be to manage the "closable" state in the StateItem like it's being done with the closedAt property. And than pass a "setClosable" it in the Component so it can be changed within the modal.

lindesvard commented 5 months ago

Hey! Thanks for trying it out 🥳 Hmm this should be possible with todays solution.

Just use onPointerDownOutside inside your modal and preventDefault + push a confirm modal when ever the form is dirty.

const ModalExample: () => {
const [isDirty, setDirty] = useState(false);
return (
  <DialogContent
    onPointerDownOutside={(e) => {
      if (isDirty) {
        e.preventDefault();
        pushModal('Dynamic');
      } else {
        // Do something else?
      }
    }}
  >
    <button className="bg-black text-white p-2" onClick={() => setDirty((p) => !p)}>
      {isDirty ? 'Dirty' : 'Not Dirty'}
    </button>
  </DialogContent>
);

https://github.com/lindesvard/pushmodal/assets/1987198/54690366-1190-4c42-bc27-27c8ec4ead01

alexambrinos commented 5 months ago

Oh great, thanks for your input! So implementing a prevent close function in onPointerDownOutside, along with onEscapeKeyDown and passing it to the DialogPrimitive.Close button should cover all the possible ways to exit of the dialog/sheet, so it's being managed outside of the library. I guess I'll implement it this way for now. The only bad thing I can see is that developers need to pay attention to covering all the possible ways of exiting the dialog.

Edit: Also I was trying to popAllModals() within the "exit" button in alert dialog so it closes every dialog opened, but the library doesn't notify the emitter with 'change' like in the popModal(). I think the emitter should notify all the dialog closed so the callback can be called for each. So then the useOnPushModal that I have defined in the dialog is not being called.

emitter.emit('change', {
              name: match.name,
              open: false,
              props: match.props,
});
lindesvard commented 5 months ago

I will fix the event thingy.

Regarding disable close. There's a lot of different ways to do this. I will play around and see what the best solution would be.

digitdustin commented 5 months ago

Also could use this 🙌

digitdustin commented 5 months ago

@alexambrinos I managed to get around this by using the AlertDialog as a wrapping component.

import { AlertDialog } from '@/components/ui/alert-dialog';

export const {
  pushModal,
  popModal,
  ...
} = createPushModal({
  modals: {
    ...
    AlertModal: {
      //@ts-ignore
      Wrapper: AlertDialog,
      Component: AlertModal,
    },
  },
});

The modal doesn't close on interaction and you can still remove it by using popModal. TS isn't happy but oh well

alexambrinos commented 5 months ago

@digitdustin Great that you found something that works for you! If you always want to have the dialog not closable it's a great ideea. The setup with the AlertDialog as a wrapper is literally the opposite of the Dialog Wrapper, meaning if I wanted to close the modal with "esc" key for example, I would need to write specific code to pop the modal. I needed something to manage the closable state based on something in the dialog (my specific need was a dirty form) and switch between closable and not closable. I have a add customer form which is defined inside a Sheet (chose the sheet because I want it to be accessible in multiple parts of the app without interrupting the main activity). I wanted to alert the user if it closes the sheet by mistake. Maybe I have a very specific edge case so I hope more people would benefit from this change than just me. 😅

lindesvard commented 5 months ago

Hey, have not had time to look into this. Will try to find time this week!

JJozef commented 4 months ago

[!IMPORTANT]

  • I'm using Next.js and shadcn
  • This when using the dynamic Dialog and Drawer.

export default function CreateDialog() {
  const [isLoading, startTransition] = useTransition()

  const onSubmit = (values) => {
    startTransition(async () => {
     // server action
    })
  }

  return (
    <Dynamic.Content
      // to avoid this
      onPointerDownOutside={(e) => {}}
      onEscapeKeyDown={(e) => {}} 

      // new prop e.g:
      closseable={isLoading} // and this disable the option to close the dialog and drawer, until the server action is finished.
    >
      {/* form */}
   </Dynamic.Content>
  )
}
// per default

<Dialog open={} onOpenChange={}>
      {/* content */}
</Dialog>

[!WARNING] You need to add this prop in both, otherwise you will get an error.

// dialog.{jsx,tsx,js}

const DialogContent = forwardRef(
  ({ className, children, hiddenClose = false, ...props }, ref) => (
    // ...
        {!hiddenClose && (
          <DialogPrimitive.Close className='absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none'>
            <Cross2Icon className='size-4' />
            <span className='sr-only'>Close</span>
          </DialogPrimitive.Close>
        )}
    // ...
  )
)
// drawer.{jsx,tsx,js}

const DrawerContent = forwardRef(
  ({ className, children, hiddenClose = false, ...props }, ref) => (
    // ...
        {!hiddenClose && (
          <div className='mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted' />
        )}
    // ...
  )
)

// component.{jsx,tsx,js}

export default function CreateDialog() {
  const [isLoading, startTransition] = useTransition()

  const onSubmit = (values) => {
    startTransition(async () => {
     // server action
    })
  }

  return (
    <Dynamic.Content
      hiddenClose={isLoading}  // automatically hides when the form is submitted
    >
      {/* form */}
    </Dynamic.Content>
  )
}