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

Transitions from the HeadlessUI's library don't work #23

Closed
erip2 opened this issue Dec 4, 2022 · 18 comments
Closed

Transitions from the HeadlessUI's library don't work #23

erip2 opened this issue Dec 4, 2022 · 18 comments
Labels
bug Something isn't working problem-solving solving problems that arise in using the package

Comments

@erip2
Copy link

erip2 commented Dec 4, 2022

Hey there!

I am using your package and it's very good. It really makes it easy and reusable this kind of component throughout the application. I am also new to Vue, so I am sorry if the title doesn't make sense.

The only problem that I have with the package is the component. The thing is that I use HeadlessUI (https://headlessui.com/) and their modal has transitions before and after the modal is shown. I want these transitions to be consistent in all the app modals.
But because your package wraps the confirm dialogs inside the DialogsWrapper the transition doesn't happen, because the component is mounted/unmounted with the wrapper's logic.

Is it possible for me to extend this component or maybe you can make some changes in the package if you think that this issue is more universal?

Thanks!

@harmyderoman
Copy link
Owner

Hi, Eri!

Thanks for the kind words! I'm glad that my package is useful.

I checked Headless UI and it works well with my package. You just need to give it some time to execute the leave transition.

  function setIsOpen(value) {
    isOpen.value = value

    setTimeout(() => {
      emit('confirm')
    }, 300)
    
  }

@harmyderoman
Copy link
Owner

Full example (I got the code from Headless UI docs):

// src/App.vue
<script setup>
import { createConfirmDialog, DialogsWrapper } from 'vuejs-confirm-dialog'
import HeadlessDialog from './components/HeadlessDialog.vue'

const dialog = createConfirmDialog(HeadlessDialog)

</script>

<template>
  <div>
    <h1 class="text-3xl font-bold underline">
    Hello world!
  </h1>
    <button @click="dialog.reveal">Open Dialog</button>
  </div>
  <DialogsWrapper />

</template>

The Component:

// src/components/HeadlessDialog.vue
<script setup>
  import { ref } from 'vue'
  import {
    Dialog,
    DialogPanel,
    DialogTitle,
    DialogDescription,
    TransitionRoot,
    TransitionChild
  } from '@headlessui/vue'

  defineProps({
    open: Boolean
  })

  const emit = defineEmits(['confrim', 'cancel'])

  const isOpen = ref(true)

  function setIsOpen(value) {
    isOpen.value = value

    setTimeout(() => {
      emit('confirm')
    }, 300)
    
  }
</script>

<template>
    <TransitionRoot
    :show="isOpen"
    as="template"
  >
    <Dialog @close="setIsOpen" class="relative z-50">
    <!-- The backdrop, rendered as a fixed sibling to the panel container -->
    <TransitionChild
        enter="duration-300 ease-out"
        enter-from="opacity-0"
        enter-to="opacity-100"
        leave="duration-200 ease-in"
        leave-from="opacity-100"
        leave-to="opacity-0"
      >
    <div class="fixed inset-0 bg-black/30" aria-hidden="true" />
    </TransitionChild>

    <!-- Full-screen container to center the panel -->
    <div class="fixed inset-0 flex items-center justify-center p-4">
      <!-- The actual dialog panel -->
      <TransitionChild
        enter="duration-300 ease-out"
        enter-from="opacity-0 scale-95"
        enter-to="opacity-100 scale-100"
        leave="duration-200 ease-in"
        leave-from="opacity-100 scale-100"
        leave-to="opacity-0 scale-95"
      >
      <DialogPanel class="w-full max-w-sm rounded bg-white border-1">
        <DialogTitle>Complete your order</DialogTitle>
      <DialogDescription>
        This will permanently deactivate your account
      </DialogDescription>

      <p>
        Are you sure you want to deactivate your account? All of your data will be
        permanently removed. This action cannot be undone.
      </p>

      <button @click="setIsOpen(false)">Deactivate</button>
      <button @click="setIsOpen(false)">Cancel</button>
    </DialogPanel>
      </TransitionChild>
    </div>
  </Dialog>
    </TransitionRoot>
</template>

@harmyderoman harmyderoman added the problem-solving solving problems that arise in using the package label Dec 4, 2022
@harmyderoman
Copy link
Owner

harmyderoman commented Dec 4, 2022

Another option is to use this function useConfirmDialog. It's pretty much the same, but without DialogsWrapper. It's even more suitable for use with HeadlessUI, but it's a little harder to scale and reuse.

@erip2
Copy link
Author

erip2 commented Dec 5, 2022

Hey there, thanks for the quick answers and explanations!

I tried your two first answers here, and although they fix the transition when the modal is leaving, still there is no transition when the modal is showing.

I also tried to do it with setTimeout like this:

onMounted(() => {
    setTimeout(() => {
        isOpen.value = true;
    }, 300)
});

but with no effect. I also tried to add a wrapper on so I could show it firstly and then the TransitionRoot with a setTimouet, but again, that wasn't a solution.

As for the last option you provided, I'm not very positive in adding a library just for this, although it looks like a neat package and maybe we can add it in the future.

@harmyderoman
Copy link
Owner

@erip2 can you provide your code? Because in mine both transitions work.

@harmyderoman harmyderoman changed the title Is there any way to extend the DialogsWrapper component? Transitions from the HeadlessUI's library don't work Dec 5, 2022
@erip2
Copy link
Author

erip2 commented Dec 5, 2022

Sure, my main component is this:

<script setup>
import {
    Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot,
} from '@headlessui/vue';
import { ExclamationTriangleIcon, XMarkIcon } from '@heroicons/vue/24/outline'
import { ref } from 'vue';

defineProps({
    open: Boolean,
    question: String,
})

const emit = defineEmits(['confirm', 'cancel'])

const isOpen = ref(false)

function closeModal(value) {
    isOpen.value = false;

    setTimeout(() => {
        emit(value)
    }, 300)
}
</script>

<template>
  <TransitionRoot :show="isOpen" as="template">
    <Dialog as="div" class="relative z-10" @close="closeModal('cancel')">
      <TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100" leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
        <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
      </TransitionChild>
      <div class="fixed inset-0 z-10 overflow-y-auto">
        <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
          <TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200" leave-from="opacity-100 translate-y-0 sm:scale-100" leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
            <DialogPanel class="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
              <div class="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
                <button type="button" class="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" @click="emit('cancel')">
                  <span class="sr-only">Close</span>
                  <XMarkIcon class="h-6 w-6" aria-hidden="true" />
                </button>
              </div>
              <div class="sm:flex sm:items-center">
                <div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
                  <ExclamationTriangleIcon class="h-6 w-6 text-red-600" aria-hidden="true" />
                </div>
                <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
                  <DialogTitle as="h3" class="text-lg font-medium leading-6 text-gray-900">{{question}}</DialogTitle>
                </div>
              </div>
              <div class="mt-5 sm:mt-7 sm:flex sm:flex-row-reverse">
                <button type="button" @click="closeModal('confirm')" class="inline-flex w-full justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm">Confirm</button>
                <button type="button" @click="closeModal('cancel')" class="mt-3 inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:mt-0 sm:w-auto sm:text-sm">Cancel</button>
              </div>
            </DialogPanel>
          </TransitionChild>
        </div>
      </div>
    </Dialog>
  </TransitionRoot>
</template>

I was inspired by this article: https://dev.to/garmideroman/the-simplest-way-to-deal-with-modal-dialogs-in-vue-3-59hl, especially the "Reusing" section. In this way I have this file:

useConfirmBeforeAction.vue

import { createConfirmDialog } from 'vuejs-confirm-dialog';
import ConfirmModal from '../Components/Shared/ConfirmModal.vue';

const useConfirmBeforeAction = (action, props) => {
    const { reveal, onConfirm } = createConfirmDialog(ConfirmModal, props);

    onConfirm(action);

    reveal();
};

export default useConfirmBeforeAction;

and finally, on the component I want this modal, I have a button that when it's clicked, it calls this function:

const onDeleteStatus = (id, index) => {
    useConfirmBeforeAction(() => {
        deleteStatus(id, index)
    }, {
        question: 'Are you sure you want to delete the status?',
    })
}

@harmyderoman
Copy link
Owner

  • useConfirmBeforeAction.vue must have a .js or .ts extension, not .vue.
  • const isOpen = ref(true)
    And this code also works fine

@harmyderoman
Copy link
Owner

modal

@erip2
Copy link
Author

erip2 commented Dec 5, 2022

useConfirmBeforeAction.vue must have a .js or .ts extension, not .vue.

A typo, it's a .js file.

const isOpen = ref(true)
And this code also works fine

I tried it like this, but still no success.

Anyway, it's obvious that there's something wrong on my end. I'll investigate it further.

Thanks for the answers!

@harmyderoman
Copy link
Owner

@erip2 you're welcome!

@harmyderoman harmyderoman pinned this issue Dec 5, 2022
@harmyderoman
Copy link
Owner

@erip2 did you find out what the problem was?

@harmyderoman harmyderoman added bug Something isn't working enhancement New feature or request labels Dec 17, 2022
@harmyderoman harmyderoman reopened this Dec 17, 2022
@erip2
Copy link
Author

erip2 commented Dec 18, 2022

Yes, I did. The problem encountered because I am showing the confirm modal inside another modal.

So, the code is like this:

      </div>
      <DialogsWrapper />
    </template>
  </Modal>
</template>

Anyway, there's still something strange. In one case the opening transition doesn't work, but in another modal it does, although the code is the same, meaning the <DialogsWrapper /> is placed in the same position in both the modals.

This is the case where it works normally:
case1

And this is the case where it doesn't:
case2

Haven't figured it out at the moment where is the difference.

If I try to put the DialogsWrapper outside the modal, the transitions work, but it is not good, because the ESC button functionality doesn't work properly.

@harmyderoman
Copy link
Owner

@erip2 Oh no, you should always put DialogsWrapper outside any other wrappers.
The ESC button works well in my code.

@erip2
Copy link
Author

erip2 commented Dec 18, 2022

Mm, I based the logic from here tailwindlabs/headlessui#426 (comment)

Also, because when I tried to place it outside (which would be perfect in my case since almost every page should have a confirm dialog), the esc functionality is broken; meaning the outer dialogs get closed firstly.

@harmyderoman
Copy link
Owner

@erip2 you should place it only once in App.vue, not on every page in your app.

@harmyderoman
Copy link
Owner

Mm, I based the logic from here tailwindlabs/headlessui#426 (comment)

My package works differently. In the case of nested modals, you just need to create a new dialog with createConfirmDialog in your parent component. My package will take care of the rendering.

The Esc functionality should work. But if not, than provide your code I will try to fix it.

@harmyderoman harmyderoman removed the enhancement New feature or request label Dec 19, 2022
@erip2
Copy link
Author

erip2 commented Feb 10, 2023

Hello there again @harmyderoman !
Sorry for my late response, but I had a lot of work with the project and couldn't reply here in time.

Anyway, I managed to create a sandbox for you to see: https://codesandbox.io/s/vuejs-confirm-dialog-esc-example-luxigt
I had to use the Options API since sandbox threw errors when using Composition API; anyway, it's the same thing.

As you can see, when <DialogsWrapper /> is only in the end of App.vue the ESC functionality does not work properly. The first modal is closed before the confirm modal.

Thanks!

@harmyderoman
Copy link
Owner

Hi, @erip2! Please open new issue for this topic. I'll try to help you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working problem-solving solving problems that arise in using the package
Projects
None yet
Development

No branches or pull requests

2 participants