Skip to content

Modals

Dialogs can be displayed as modals with the Modal component. Modal renders a Backdrop and a dialog on top of all other content on the page in order to limit user interaction to the dialog.

Basic usage

To display a modal, you can use one of the methods exposed by the useModals() composable.

vue
<script setup lang="ts">
  import useModals from '@leaflink/stash/useModals';
  import BasicModal from '../components/Modal/BasicModal.vue';

  const modals = useModals();

  function openBasicModal() {
    modals.open({ component: BasicModal });
  }
</script>

<template>
  <Button @click="openBasicModal">Open Basic Modal</Button>
</template>

Use this component to create custom modals in your app. See the Modal docs for full usage.

vue
<script setup lang="ts">
  import useModals from '@leaflink/stash/useModals';
  import MyModal from './MyModal.vue';

  const modals = useModals();

  function openModal() {
    modals.open({ component: MyModal });
  }
</script>
vue
<script setup lang="ts">
  import Modal from '@leaflink/stash/Modal.vue';
  import Button from '@leaflink/stash/Button.vue';

  const emit = defineEmits(['close', 'dismiss', 'cancel']);
</script>

<template>
  <Modal open size="narrow" title="Test modal" @dismiss="emit('dismiss')">
    <h1>Hello World!</h1>
    <Button @click="emit('close')">Close</Button>
    <Button @click="emit('cancel')">Cancel</Button>
    <slot></slot>
  </Modal>
</template>

Installation

You don't have to do anything to start using modals. Modals are actually enabled as a separate plugin within Stash named ModalsPlugin. The plugin is installed automatically when using @leaflink/stash.

You should be able to call useModals() right away after installing and running app.use('@leaflink/stash').

If you instead want to set modal options:

ts
import stash from '@leaflink/stash';

app.use(stash, {
  modals: {
    mountNodeId: 'some-id',
    mountNodeClass: 'some-class',
  },
});

If you don't need or want it to be loaded you can opt out:

ts
import stash from '@leaflink/stash';

app.use(stash, { modals: false });

If you opt out from loading it automatically from Stash, you can also load it manually:

ts
import { createApp } from 'vue';
import ModalsPlugin from '@leaflink/stash/ModalsPlugin';

const app = createApp(App);

app.use(ModalsPlugin); 

The ModalsPlugin can receive options as the second argument to app.use().

ts
import { createApp } from 'vue';
import ModalsPlugin from '@leaflink/stash/ModalsPlugin';

const app = createApp(App);

app.use(ModalsPlugin, { mountNodeClass: 'my-class' }); 

Modals component

The Modals component is responsible for rendering all modals opened with the useModals() composable. You won't typically need to use this component directly because it's injected by the ModalsPlugin within Stash automatically, but it's available if you need to.

Modals are opened in the order they're called. With two or more modals opened at a time, the modal called first takes priority, and any additional modals will be queued up in sequence.

ts
const modals = useModals();

modals.open({ component: GlobalModal });
modals.open({ component: LocalModal });

Note

In most cases, the presentation of modals should be intentionally managed by developers. The modal management inside Modals.vue was primarily designed to prevent collisions between unrelated modals on page load; for example, if a global modal were to be presented on a page that also loads a modal.

Default event listeners

The Modals component sets 3 common event listeners that LeafLink uses.

  1. dismiss
  2. close
  3. cancel

The listeners simply call the close method from useModals.

Devs can override these default listeners.

For example, if you need to open a confirm prompt when closing a modal, set the disableDefaultListeners open to true inside the modal's options.

ts
const modals = useModals();

function onModalClose() {
  confirm('Are you sure?')
  modals.close();
}

modals.open({
  component: MyModal,
  attributes: {
    onDismiss: onModalClose, 
  },
  options: {
    disableDefaultListeners: true, 
  },
});

Testing

In order to ensure that modals are being displayed during component tests, you need to use the stash plugin.

ts
import { render } from '@testing-library/vue';
import stash from '@leaflink/stash';
import userEvent from '@testing-library/user-event';

it('shows a modal when button is clicked', async () => {
  render(Component, {
    global: {
      plugins: [stash],
    },
  });

  const user = userEvent.setup();
  await user.click(screen.getByRole('button'));

  expect(await screen.findByText('Modal Title')).toBeInTheDocument();
});

Cleaning up

To ensure Modals aren't left open after every test, you can add a global afterEach hook to ensure that modals are being closed after every test. If you're using @leaflink/dom-testing-utils this is extremely easy.

In setup-env.ts, just add the following line:

ts
import '@leaflink/dom-testing-utils/setup-env';

If you're not using that library, you can also add this explicitly.

ts
import { config } from '@vue/test-utils';
import stash from '@leaflink/stash';
import useModals from '@leaflink/stash/useModals';

config.global.plugins = [stash];

afterEach(() => {
  useModals().closeAll();
});

If you only need to do this for a single test for some reason, you can also do it in the test itself:

ts
import { render } from '@testing-library/vue';
import stash from '@leaflink/stash';
import useModals from '@leaflink/stash/useModals';
import userEvent from '@testing-library/user-event';

afterEach(() => {
  useModals().closeAll();
});

it('shows a modal when button is clicked', async () => {
  render(Component, {
    global: {
      plugins: [stash],
    },
  });

  const user = userEvent.setup();
  await user.click(screen.getByRole('button'));

  expect(await screen.findByText('Modal Title')).toBeInTheDocument();
});

API

See the documentation below for a complete reference to all the props and classes available to the components mentioned here.