Как создать всплывающее окно в Svelte без каких-либо других библиотек или зависимостей.

Я реализовал UI-компоненты своего последнего проекта Papyrs без сторонних библиотек дизайн-системы — т.е. создал все компоненты с нуля. Я сделал это, чтобы получить полный контроль и гибкость над разными блоками моего самоуверенного макета.

В этом сообщении в блоге я рассказываю, как вы можете разработать компонент всплывающего окна в Svelte.

Скелет

Всплывающее окно — это плавающий контейнер, отображаемый поверх содержимого рядом с якорем — обычно кнопкой, — которая инициирует его отображение. Чтобы улучшить визуальную фокусировку наложения, фон обычно используется для частичного затемнения вида за ним.

Мы можем начать реализацию с репликации приведенного выше скелета в компоненте с именем Popover.svelte​, который содержит button​ и div​.

<button>Open</button>

<div
    role="dialog"
    aria-labelledby="Title"
    aria-describedby="Description"
    aria-orientation="vertical"
>
    <div>Backdrop</div>
    <div>Content</div>
</div>

Чтобы улучшить доступность, мы можем установить роль dialog​ и предоставить некоторую информацию aria​ (см. Документацию MDN для более подробной информации).

Анимация

Мы создаем состояние booleanvisible — для отображения или закрытия всплывающего окна. При нажатии на button состояние устанавливается на true и выполняется рендеринг наложения. Наоборот, при нажатии на фон он превращается в false​ и закрывается.

Кроме того, мы добавляем прослушиватель кликов во всплывающем окне, который ничего не делает, кроме как останавливает распространение события. Это полезно, чтобы избежать закрытия оверлея, когда пользователь взаимодействует с его содержимым.

Мы также можем заставить оверлей появляться и исчезать изящно благодаря директиве перехода, также известной как черная магия Svelte 😁.

<script lang="ts">
  import { fade, scale } from 'svelte/transition';
  import { quintOut } from 'svelte/easing';

  let visible = false;
</script>

<button on:click={() => (visible = true)}>Open</button>

{#if visible}
  <div
    role="dialog"
    aria-labelledby="Title"
    aria-describedby="Description"
    aria-orientation="vertical"
    transition:fade
    on:click|stopPropagation
  >
    <div
      on:click|stopPropagation={() => (visible = false)}
      transition:scale={{ delay: 25, duration: 150, easing: quintOut }}
    >
      Backdrop
    </div>
    <div>Content</div>
  </div>
{/if}

Позиция над контентом

Всплывающее окно должно отображаться над всем содержимым независимо от того, прокручивалась страница или нет. Поэтому мы можем использовать позицию fixed в качестве отправной точки. Его содержимое и фон имеют позиционирование absolute​. Фон также должен закрывать экран, но является дочерним элементом оверлея — следовательно, «абсолютным» — и контент должен располагаться рядом с якорем.

Остальной код CSS, который мы добавляем в наше решение, представляет собой минимальные настройки стиля для ширины, высоты или цвета.

<script lang="ts">
  import { fade, scale } from 'svelte/transition';
  import { quintOut } from 'svelte/easing';

  let visible = false;
</script>

<button on:click={() => (visible = true)}>Open</button>

{#if visible}
  <div
    role="dialog"
    aria-labelledby="Title"
    aria-describedby="Description"
    aria-orientation="vertical"
    transition:fade
    class="popover"
    on:click|stopPropagation
  >
    <div
      on:click|stopPropagation={() => (visible = false)}
      transition:scale={{ delay: 25, duration: 150, easing: quintOut }}
      class="backdrop"
    />
    <div class="wrapper">Content</div>
  </div>
{/if}

<style>
  .popover {
    position: fixed;
    inset: 0;

    z-index: 997;
  }

  .backdrop {
    position: absolute;
    inset: 0;

    background: rgba(0, 0, 0, 0.3);
  }

  .wrapper {
    position: absolute;

    min-width: 200px;
    max-width: 200px;

    min-height: 100px;

    width: fit-content;
    height: auto;

    overflow: hidden;

    display: flex;
    flex-direction: column;
    align-items: flex-start;

    background: white;
    color: black;
  }
</style>

Позиция рядом с якорем

Чтобы установить наложение рядом с кнопкой, мы должны получить ссылку на этот элемент, чтобы найти его положение в окне просмотра. Для этой цели мы можем bind​ якорь.

Когда ссылка готова или размер окна изменен (позиция может измениться, если пользователь изменит размер браузера), мы используем метод getBoundingClientRect() для запроса информации о позиции.

В конечном итоге мы переводим эту информацию JavaScript в переменные CSS, чтобы отобразить содержимое всплывающего окна именно в той позиции, которую мы хотели бы установить.

<script lang="ts">
  // ...
  
  let anchor: HTMLButtonElement | undefined = undefined;

  let bottom: number;
  let left: number;

  const initPosition = () =>
    ({ bottom, left } = anchor?.getBoundingClientRect() ?? { bottom: 0, left: 0 });

  $: anchor, initPosition();
</script>

<svelte:window on:resize={initPosition} />

<button on:click={() => (visible = true)} bind:this={anchor}>Open</button>

{#if visible}
  <div
    role="dialog"
    aria-labelledby="Title"
    aria-describedby="Description"
    aria-orientation="vertical"
    transition:fade
    class="popover"
    on:click|stopPropagation
    style="--popover-top: {`${bottom}px`}; --popover-left: {`${left}px`}"
  >
    <!-- ... -->
  </div>
{/if}

<style>
  /** ... */

  .wrapper {
    position: absolute;

    top: calc(var(--popover-top) + 10px);
    left: var(--popover-left);

    /** ... */
  }
</style>

Фрагмент кода выше был обрезан, чтобы показать только то, что относится к этой главе. В целом код компонента выглядит следующим образом:

<script lang="ts">
  import { fade, scale } from 'svelte/transition';
  import { quintOut } from 'svelte/easing';

  let visible = false;
  let anchor: HTMLButtonElement | undefined = undefined;

  let bottom: number;
  let left: number;

  const initPosition = () =>
    ({ bottom, left } = anchor?.getBoundingClientRect() ?? { bottom: 0, left: 0 });

  $: anchor, initPosition();
</script>

<svelte:window on:resize={initPosition} />

<button on:click={() => (visible = true)} bind:this={anchor}>Open</button>

{#if visible}
  <div
    role="dialog"
    aria-labelledby="Title"
    aria-describedby="Description"
    aria-orientation="vertical"
    transition:fade
    class="popover"
    on:click|stopPropagation
    style="--popover-top: {`${bottom}px`}; --popover-left: {`${left}px`}"
  >
    <div
      on:click|stopPropagation={() => (visible = false)}
      transition:scale={{ delay: 25, duration: 150, easing: quintOut }}
      class="backdrop"
    />
    <div class="wrapper">Content</div>
  </div>
{/if}

<style>
  .popover {
    position: fixed;
    inset: 0;

    z-index: 997;
  }

  .backdrop {
    position: absolute;
    inset: 0;

    background: rgba(0, 0, 0, 0.3);
  }

  .wrapper {
    position: absolute;

    top: calc(var(--popover-top) + 10px);
    left: var(--popover-left);

    min-width: 200px;
    max-width: 200px;

    min-height: 100px;

    width: fit-content;
    height: auto;

    overflow: hidden;

    display: flex;
    flex-direction: column;
    align-items: flex-start;

    background: white;
    color: black;
  }
</style>

Вот и все! Мы реализовали минимальное пользовательское всплывающее окно, которое можно использовать в любых приложениях Svelte без какой-либо зависимости.

Заключение

Мы не украшали решение, поэтому результат остается визуально грубым по краям, но всплывающее окно работает, как и ожидалось.

Чтобы продолжить, внедрить больше опций или сделать его блестящим, вы можете, например, взглянуть на открытый исходный код Papyrs на GitHub 🤗.

​В бесконечность и дальше
Дэвид​

Want to Connect?
For more adventures, follow me on Twitter