GrLoading

Пакет: @feugene/granularityядроГруппа: Обратная связь

Берут, когда контент уже есть и обновляется.

Когда брать

  • контент уже есть и обновляется — таблица перезапрашивается, форма отправляется: старое видно, но недоступно;
  • ожидание короткоеdelay не показывает спиннер вовсе, если ответ пришёл быстро;
  • блокировать надо весь экранfullscreen на время операции, которую нельзя прерывать;
  • оверлей нужен императивно — директива v-loading вместо компонента в разметке.

Когда взять другое

НужноБерите
Контента ещё нет вовсеGrSkeleton
Известна доля выполненногоGrProgressBar / GrProgressCircle
Ожидание внутри кнопкиGrButton с loading
Загрузка файлаGrFileUpload

Два режима

По умолчанию оверлей ложится на ближайшего позиционированного предка — это загрузка куска страницы. fullscreen накрывает весь экран: это загрузка уровня приложения.

Директива режим выбирает сама: target = document.body → fullscreen, иначе инлайновый. Контейнеру со position: static она временно ставит relative, а контейнеру со скруглением — overflow: hidden, чтобы размытая подложка не вылезала за скруглённую форму.

Задержка

delay (мс) откладывает показ. Запрос, ответивший быстрее задержки, не показывает оверлей вовсе — мигание раздражает сильнее, чем отсутствие индикации.

<GrLoading v-if="pending" :delay="200" />

Отсчёт начинается с монтирования, то есть с момента старта загрузки. Пока он идёт, контент не блокируется: иначе быстрый запрос молча замораживал бы форму.

Доступность

Оверлей — role="status" с aria-live="polite": подпись читается диктором в момент появления, не перебивая пользователя.

Визуального перекрытия недостаточно: без блокировки таб уходит в форму, которую уже не видно, а диктор читает её как обычную. Директива поэтому в момент показа объявляет контейнер aria-busy="true", а его остальным детям ставит inert — поддерево целиком выпадает из таб-порядка, из событий указателя и из дерева доступности. При закрытии inert снимается только с того, что поставила она сама, и фокус возвращается туда, где был, если пользователь не увёл его сам.

Декларативный <GrLoading> соседями не распоряжается — он их не знает. Нужна блокировка на этом же контейнере — берите директиву.

Фокус-ловушки внутри оверлея нет намеренно: ловить в нём нечего, а inert соседей закрывает задачу целиком.

Слой

Полноэкранный режим сидит на токене --gr-z-loading (1150) — выше модалок, потому что блокирует приложение целиком, и ниже тостов, чтобы не прятать уведомление о фоновой ошибке. Инлайновый режим к шкале отношения не имеет: z-10 — порядок внутри своего контейнера.

zIndexVar подменяет переменную слоя своей — escape-hatch того же вида, что у useFloating. Сырого числа компонент не принимает: см. docs/z-index.md.

Скрим

Затемнение под панелью — роль темы --gr-overlay-bg, та же, что у GrModal и GrDrawer: оверлей загрузки обязан выглядеть как остальные модальные слои и менять плотность вместе с темой.

Проп background задаёт свой background-color и отменяет скрим целиком — нужен, когда оверлей ложится на уже затемнённую поверхность.

Спиннер и содержимое

spinnerSize и spinnerTone идут через GrIcon — шкала иконок и токены текста, а не пиксели в разметке. spinner подменяет саму иконку, animated выключает вращение.

Слот заменяет содержимое панели целиком — прогресс с процентами, кнопку отмены долгой операции:

<GrLoading>
  <GrProgressBar :value="percent" class="w-48" />
  <GrButton size="xs" variant="outline" @click="abort">Отменить</GrButton>
</GrLoading>

Подпись по умолчанию берётся из локали (gr.loading.defaultText); text задаёт свою, пустая строка убирает совсем.

Директива

const controller = createLoading({ target: '#report', text: 'Считаем отчёт', delay: 200 })
controller.setText('Почти готово')
controller.close()

v-loading принимает либо булево, либо объект опций (те же, что у компонента, плюс target и fullscreen). Смена target или режима пересоздаёт оверлей, всё остальное обновляется на месте.

Playground 8

Загружается…

Код
<GrLoading />

Установка

npm i @feugene/granularity

Импорт

import { GrLoading } from '@feugene/granularity/components/GrLoading'

API

Props

PropTypeпо умолчаниюОписание
textstring | undefinedundefinedПодпись под спиннером. Пустая строка убирает её совсем. По умолчанию — из локали.
zIndexVarstring | undefinedundefinedИмя CSS-переменной слоя — escape-hatch мимо `--gr-z-loading`.
spinnerComponent | undefinedundefinedСвой компонент спиннера вместо иконки по умолчанию.
spinnerClassstring | undefinedundefinedДополнительные классы обёртки спиннера.
spinnerSizenumber | "xs" | "sm" | "md" | "lg" | undefined28Размер спиннера: шкала пакета либо произвольный в пикселях.
spinnerToneGrIconTone | undefined"neutral"Тон спиннера из палитры.
animatedboolean | undefinedtrueВращение спиннера. По умолчанию включено.
backgroundstring | undefinedundefinedСвой `background-color`. Задан — дефолтный скрим `--gr-overlay-bg` снимается.
fullscreenboolean | undefinedfalseНакрыть весь экран (`position: fixed`) вместо ближайшего позиционированного предка.
delaynumber | undefined0Задержка показа в миллисекундах: короткая загрузка не мигает оверлеем.
customClassstring | undefinedundefinedДополнительные классы корня оверлея.

Slots

SlotTypeОписание
defaultanyСодержимое панели целиком вместо спиннера с подписью.

Events

EventTypeОписание
show[]

Примеры 5

Оверлей поверх куска страницы

Базовый сценарий: оверлей поверх карточки, пока обновляются данные. Подпись читается диктором — у корня role="status".

Invoice list
Use `GrLoading` as an overlay above an existing card or section while async data is refreshing.
No `text` prop here: the caption comes from the active locale — switch RU/EN to see it change.

Inline
<script setup lang="ts">
import { ref } from 'vue'

import { GrButton, GrLoading } from '@feugene/granularity'

const loading = ref(false)
</script>

<template>
  <div class="grid gap-3">
    <GrButton class="justify-self-start" @click="loading = !loading">
      {{ loading ? 'Hide' : 'Show' }} inline loading
    </GrButton>

    <div class="relative min-h-[180px] rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
      <div class="grid gap-2 text-sm text-[var(--gr-muted-fg)]">
        <div class="font-medium text-[var(--gr-fg)]">Invoice list</div>
        <div>Use `GrLoading` as an overlay above an existing card or section while async data is refreshing.</div>
        <div>No `text` prop here: the caption comes from the active locale — switch RU/EN to see it change.</div>
      </div>

      <GrLoading v-if="loading" />
    </div>
  </div>
</template>

Задержка и своя панель

delay не даёт оверлею мигнуть на быстром ответе, а слот заменяет содержимое панели — прогресс и кнопка отмены.

Quarterly report
The fast request finishes before the delay elapses, so the overlay never appears.

Delay Slot
<script setup lang="ts">
import { ref } from 'vue'

import { GrButton, GrLoading, GrProgressBar } from '@feugene/granularity'

const fastLoading = ref(false)
const exportLoading = ref(false)
const percent = ref(0)

let exportTimer: number | undefined

// Быстрый ответ: задержка 300 мс не даёт оверлею мигнуть.
function runFast() {
  fastLoading.value = true
  window.setTimeout(() => {
    fastLoading.value = false
  }, 200)
}

function runExport() {
  exportLoading.value = true
  percent.value = 0

  exportTimer = window.setInterval(() => {
    percent.value = Math.min(100, percent.value + 8)
    if (percent.value === 100)
      abortExport()
  }, 220)
}

function abortExport() {
  window.clearInterval(exportTimer)
  exportLoading.value = false
}
</script>

<template>
  <div class="grid gap-3">
    <div class="flex flex-wrap gap-3">
      <GrButton variant="outline" @click="runFast">
        Fast request (200 ms)
      </GrButton>
      <GrButton @click="runExport">
        Export report
      </GrButton>
    </div>

    <div class="relative min-h-[200px] rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
      <div class="grid gap-2 text-sm text-[var(--gr-muted-fg)]">
        <div class="font-medium text-[var(--gr-fg)]">Quarterly report</div>
        <div>The fast request finishes before the delay elapses, so the overlay never appears.</div>
      </div>

      <GrLoading v-if="fastLoading" :delay="300" text="Refreshing..." />

      <GrLoading v-if="exportLoading" custom-class="rounded-xl">
        <div class="text-sm font-medium text-[var(--gr-fg)]">Building the export</div>
        <GrProgressBar :value="percent" class="w-52" />
        <GrButton size="xs" variant="outline" @click="abortExport">
          Cancel
        </GrButton>
      </GrLoading>
    </div>
  </div>
</template>

Директива с блокировкой содержимого

Директива v-loading объявляет контейнер aria-busy и помечает его содержимое inert: под оверлеем не остаётся ни таб-порядка, ни доступного дерева.

While the overlay is up, the form below is `inert`: Tab skips it and screen readers ignore it. The container itself reports `aria-busy`.

Directive
<script setup lang="ts">
import { ref } from 'vue'

import { GrButton, GrInput, vLoading } from '@feugene/granularity'

const loading = ref(false)
const name = ref('Alan Turing')

function save() {
  loading.value = true
  window.setTimeout(() => {
    loading.value = false
  }, 2000)
}
</script>

<template>
  <div class="grid gap-3">
    <GrButton class="justify-self-start" :disabled="loading" @click="save">
      Save profile
    </GrButton>

    <div class="text-xs text-[var(--gr-muted-fg)]">
      While the overlay is up, the form below is `inert`: Tab skips it and screen readers ignore it.
      The container itself reports `aria-busy`.
    </div>

    <div
      v-loading="{ loading, text: 'Saving profile...', delay: 150 }"
      class="rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4"
    >
      <div class="grid gap-3">
        <GrInput v-model="name" aria-label="Full name" />
        <GrButton variant="outline" class="justify-self-start">
          Reset
        </GrButton>
      </div>
    </div>
  </div>
</template>

Свой вид

Настройка background, spinnerTone и spinnerSize под плотные дашборды; animated выключает вращение.

Brand migration
Custom background and a static, tinted spinner adapt the overlay to dense dashboards.

Custom Appearance
<script setup lang="ts">
import { ref } from 'vue'

import { GrButton, GrLoading } from '@feugene/granularity'

const loading = ref(false)
</script>

<template>
  <div class="grid gap-3">
    <GrButton variant="outline" class="justify-self-start" @click="loading = !loading">
      Toggle custom overlay
    </GrButton>

    <div class="relative min-h-[180px] rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
      <div class="grid gap-2 text-sm text-[var(--gr-muted-fg)]">
        <div class="font-medium text-[var(--gr-fg)]">Brand migration</div>
        <div>Custom background and a static, tinted spinner adapt the overlay to dense dashboards.</div>
      </div>

      <GrLoading
        v-if="loading"
        text="Preparing migration plan..."
        background="color-mix(in srgb, var(--gr-fg) 78%, transparent)"
        custom-class="rounded-xl"
        spinner-tone="primary"
        :spinner-size="36"
        :animated="false"
      />
    </div>
  </div>
</template>

Полноэкранный цикл ожидания

Полноэкранный режим на токене --gr-z-loading: он выше модалок, потому что блокирует приложение целиком.

Fullscreen overlay closes automatically after a short async cycle.

Fullscreen
<script setup lang="ts">
import { ref } from 'vue'

import { GrButton, GrLoading } from '@feugene/granularity'

const loading = ref(false)

function runFullscreenSync() {
  loading.value = true

  window.setTimeout(() => {
    loading.value = false
  }, 1400)
}
</script>

<template>
  <div class="grid gap-3">
    <GrButton class="justify-self-start" @click="runFullscreenSync">
      Simulate global sync
    </GrButton>

    <div class="text-xs text-[var(--gr-muted-fg)]">
      Fullscreen overlay closes automatically after a short async cycle.
    </div>

    <GrLoading v-if="loading" fullscreen text="Syncing workspace data..." style="--gr-muted-fg: white;" />
  </div>
</template>

Документация компонентаВсе компоненты