GrTabPanels

Package: @feugene/granularitycoreGroup: navigation

Renders tab panels linked to `GrTabs` by ARIA for accessible tabs.

Machine-translated from the Russian original, not yet reviewed. Read the original

When to take it

  • the tabs need panels — the component links them to GrTabs by ARIA, with no manual ids;
  • a panel is heavy — lazy mounting does not build the content until the first showing;
  • the state of a panel has to be kept — a hidden panel stays mounted, and what was typed into it is not lost;
  • the layout of the tabs is your own — the panels stand separately from the list of tabs rather than under it.

When to take something else

NeedTake
The list of tabs itself is neededGrTabs
The sections expand rather than switchGrCollapse
The view of one piece of content is switchedGrSegmented

The pairing is held by a shared `idBase`

<GrTabs v-model="tab" :tabs="tabs" id-base="settings" />

<GrTabPanels v-model="tab" id-base="settings">
  <GrTabPanel value="profile">…</GrTabPanel>
  <GrTabPanel value="billing">…</GrTabPanel>
</GrTabPanels>

idBase is reactive: a change of it reaches the panels. It used to be computed once at setup — the panels were left with the old ids, GrTabs moved to the new ones, and aria-controlsaria-labelledby diverged silently.

Without idBase it is generated by itself, but then GrTabs will not see it: a panel will refer to a tab that does not exist. In a dev build that is reported with a warning — the same device with which GrFormField catches a forgotten control.

`keepAlive` and `lazy`

By default an inactive panel is not in the DOM. keepAlive leaves it hidden (hidden) — the state of the forms is not lost. lazy adds “do not mount until it has been shown for the first time” to that: together they give “mount once on demand and never destroy again”.

keepAlivelazyThe behaviour
the panel is mounted when shown and unmounted when left
all of the panels are in the DOM at once, the inactive ones hidden
the panel appears in the DOM on the first showing and stays

Playground 2

Loading…

Code
<GrTabPanels />

Install

npm i @feugene/granularity

Import

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

API

Props

PropTypedefaultDescription
idBasestring | undefinedundefined
modelValuerequiredstring

Slots

SlotTypeDescription
defaultanyThe panels (`GrTabPanel`).

Examples 2

Keep Alive

draft — состояние поля не теряется
Смонтированы: draft. Панель появляется в DOM при первом показе и дальше не разрушается.

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

import { GrInput, GrTabPanel, GrTabPanels, GrTabs } from '@feugene/granularity'

const tab = ref('draft')
const draft = ref('Черновик переживает переключение вкладок')
const mounted = ref<string[]>([])

const tabs = [
  { value: 'draft', label: 'Черновик' },
  { value: 'preview', label: 'Предпросмотр' },
  { value: 'history', label: 'История' },
]

function markMounted(value: string): string {
  if (!mounted.value.includes(value))
    mounted.value = [...mounted.value, value]

  return value
}
</script>

<template>
  <div class="grid gap-4">
    <GrTabs v-model="tab" :tabs="tabs" id-base="keep-alive-demo" />

    <!-- `keepAlive` + `lazy`: панель монтируется при первом показе и дальше живёт. -->
    <GrTabPanels v-model="tab" id-base="keep-alive-demo">
      <GrTabPanel value="draft" keep-alive lazy>
        <div class="grid gap-2 p-3">
          <span class="text-sm text-[var(--gr-muted-fg)]">{{ markMounted('draft') }} — состояние поля не теряется</span>
          <GrInput v-model="draft" size="sm" aria-label="Черновик" />
        </div>
      </GrTabPanel>

      <GrTabPanel value="preview" keep-alive lazy>
        <div class="p-3 text-sm">
          {{ markMounted('preview') }}: {{ draft || '—' }}
        </div>
      </GrTabPanel>

      <GrTabPanel value="history" keep-alive lazy>
        <div class="p-3 text-sm text-[var(--gr-muted-fg)]">
          {{ markMounted('history') }} — панель смонтировалась только сейчас
        </div>
      </GrTabPanel>
    </GrTabPanels>

    <div class="rounded-2xl border border-dashed border-[var(--gr-brd)] p-3 text-sm text-[var(--gr-muted-fg)]">
      Смонтированы: <span class="font-semibold text-[var(--gr-fg)]">{{ mounted.join(', ') || '—' }}</span>.
      Панель появляется в DOM при первом показе и дальше не разрушается.
    </div>
  </div>
</template>

Accessible tabs with linked panels

Overview: a summary of the workspace.

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

import { GrTabPanel, GrTabPanels, GrTabs, type GrTab } from '@feugene/granularity'

const active = ref('overview')
const tabs: GrTab[] = [
  { value: 'overview', label: 'Overview' },
  { value: 'activity', label: 'Activity', badge: '3' },
  { value: 'settings', label: 'Settings' },
]
</script>

<template>
  <!-- Одинаковый `id-base` связывает вкладки и панели по ARIA (aria-controls ↔ aria-labelledby). -->
  <div class="grid max-w-lg gap-3">
    <GrTabs v-model="active" :tabs="tabs" id-base="demo-tabs" />

    <GrTabPanels
      v-model="active"
      id-base="demo-tabs"
      class="rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4 text-sm text-[var(--gr-fg)]"
    >
      <GrTabPanel value="overview">Overview: a summary of the workspace.</GrTabPanel>
      <GrTabPanel value="activity">Activity: 3 new items since your last visit.</GrTabPanel>
      <GrTabPanel value="settings">Settings: manage members and preferences.</GrTabPanel>
    </GrTabPanels>
  </div>
</template>

Component documentationAll components