vuetify icon indicating copy to clipboard operation
vuetify copied to clipboard

[Bug Report][3.3.19] Clicking close button of `VSnackbar` opened after `VDialog` closes the dialog too

Open MetRonnie opened this issue 2 years ago • 6 comments

Environment

Vuetify Version: 3.5.1 Last working version: 3.1.8 Vue Version: 3.4.15 Browsers: Edge 117.0.2045.47 OS: Windows 10

Steps to reproduce

  1. Have a v-snackbar that opens after a v-dialog (e.g. showing an error message after submitting a form in the dialog).
  2. Click the close button in the snackbar

Expected Behavior

The snackbar closes.

Actual Behavior

Both the snackbar and the dialog close.

Reproduction Link

https://play.vuetifyjs.com/#...

Other comments

Probably related to the fix for https://github.com/vuetifyjs/vuetify/issues/16893.

I tried using @click.stop in the snackbar's close button but that didn't do anything.

This is a recreation of https://github.com/vuetifyjs/vuetify/issues/17013 after that was closed as a duplicate of https://github.com/vuetifyjs/vuetify/issues/7310 but then the latter was closed as only applying to v2

MetRonnie avatar Oct 02 '23 16:10 MetRonnie

The dialog will close on every click outside of it, you can use the persistent prop to alter that behavior. Demo

3aluw avatar Oct 06 '23 18:10 3aluw

See https://github.com/vuetifyjs/vuetify/issues/17398 for why I am not currently using persistent

MetRonnie avatar Oct 09 '23 09:10 MetRonnie

The problem has been resolved ? have any new update ? pls help me

loihp avatar Nov 13 '23 02:11 loihp

This also applies to datepickers.

ThomasWestrelin avatar Jan 25 '24 15:01 ThomasWestrelin

I ended up having to use persistent and then manually adding back in the close-on-click-outside and close-on-escape functionality

  1. Using v-click-outside directive with a handler:

    onClickOutside = (e) => {
      // Only close on click outside if it's the "scrim", i.e. we are
      // not clicking on an error snackbar for example
      if (e.target?.classList.contains('v-overlay__scrim')) {
        close()
      }
    }
    
  2. onKeydown = (e) => {
      if (e.key === 'Escape') {
        close()
      }
    }
    
    onMounted(() => {
      document.addEventListener('keydown', onKeydown)
    })
    
    onBeforeUnmount(() => {
      document.removeEventListener('keydown', onKeydown)
    })
    

However I haven't figured out how to re-enable the browser back button closing the dialog

MetRonnie avatar Jan 25 '24 16:01 MetRonnie

I ran into this issue too and the only solution I found is to use the attach property to teleport the snackbar inside the dialog if one exists. I created a small helper composable to have a reactive attach variable, which will either be the dialogs overlay content or true, based on whether or not a dialog exists when the snackbar text changes.

I don't like this solution. There should be a way to make the dialog aware of the snackbar and setting an exception for the outside click close.

Composable:

type Teleport = boolean | string | HTMLElement;
const useDialogAwareTeleport = (
  teleportOverride?: MaybeRefOrGetter<Teleport | undefined>,
  elementRef?: MaybeRefOrGetter<undefined | { $el: Node | null }>,
  resetOnChange?: MaybeRefOrGetter,
  dialogSelector: string = ".v-dialog > .v-overlay__content"
) => {
  const dialogAwareTeleport = ref<Teleport>();

  watchEffect(() => {
    toValue(resetOnChange); // This makes sure the watcher is run on changes of resetOnChange refs
    if (toValue(teleportOverride) !== undefined) {
      dialogAwareTeleport.value = toValue(teleportOverride)
      return;
    }

    const containingDialog = findContainingDialog();
    dialogAwareTeleport.value = containingDialog ? containingDialog : true;
  });

  function findContainingDialog() {
    const dialogs = document.querySelectorAll<HTMLElement>(dialogSelector);
    if (dialogs.length === 0) return null;

    const elementContainer = toValue(elementRef);
    if (!elementContainer) return dialogs[0];

    let containingDialog = null;
    for (let i = 0; i < dialogs.length && containingDialog === null; i++) {
      const dialog = dialogs[i];
      if (dialog.contains(elementContainer.$el)) {
        containingDialog = dialog;
      }
    }
    return containingDialog;
  }

  return { dialogAwareTeleport };
};

export { useDialogAwareTeleport };

Usage (some business logic code left out):

<template>
  <v-snackbar
    v-model="isOpen"
    :timeout="-1"
    multi-line
    :attach="dialogAwareTeleport"
  >
    <span>{{snackbarText}}</span>

    <template #actions>
      <e-button
        ref="closeButtonRef"
        variant="text"
        icon="mdi-close"
        @click="closeSnackbar"
      />
    </template>
  </v-snackbar>
</template>
<script setup lang="ts">

const isOpen = ref(false);
const snackbarText = ref("");
const closeButtonRef = ref();


const snackbarIsVisible = useElementVisibility(closeButtonRef);
watch(snackbarIsVisible, (isVisible) => {
  if (!isVisible && isOpen.value) closeSnackbar();
});


const { dialogAwareTeleport } = useDialogAwareTeleport(
  undefined,
  undefined,
  snackbarText,
  ".v-dialog > .v-overlay__content"
);
</script>

AntonioDell avatar Feb 16 '24 11:02 AntonioDell

I think I've got a fix for this, will open a PR soon

MetRonnie avatar Mar 13 '24 18:03 MetRonnie