GrRadioGroup

Пакет: @feugene/granularityядроГруппа: Формы

Берут, когда вариантов до семи и все видны.

Когда брать

  • вариантов до семи и все видны — способ доставки, тип оплаты, роль: выбор без лишнего клика по панели;
  • у вариантов есть описание — строка пояснения под подписью, чего не даёт селект;
  • группа — часть формы — общее name, disabled и readonly раздаются через контекст;
  • вариант выглядит кнопкойvariant="button" без потери семантики radiogroup.

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

НужноБерите
Вариантов больше семиGrSelect
Вариантов 2–5 и они переключают вид, а не значениеGrSegmented
Можно выбрать несколькоGrCheckboxGroup
Переключатель один и он про «включено/выключено»GrSwitch
Один переключатель вне группыGrRadio

Два режима

<!-- Пропом: короткий путь для плоского списка. -->
<GrRadioGroup v-model="status" :options="options" />

<!-- Слотом: когда вариантам нужна своя разметка. -->
<GrRadioGroup v-model="status">
  <GrRadio value="draft">Черновик</GrRadio>
  <GrRadio value="review">На проверке</GrRadio>
</GrRadioGroup>

Оба режима получают одну и ту же клавиатуру и один и тот же контекст: состав группы собирается регистрацией детей, а не обходом DOM, поэтому работает и на сервере.

Опции

interface GrRadioGroupOption {
  value: string | number | boolean
  label: string
  disabled?: boolean
  description?: string
}

disabled отключает один вариант, не переводя всю группу на слот — раньше это был единственный способ. description рисуется под подписью и только в варианте radiobox: у кнопочного чипа описанию негде жить.

Раскладка

orientationvertical (по умолчанию) или horizontal. Кнопочный вариант всегда горизонтальный: его собирает GrButtonGroup, и orientation там ни на что не влияет.

Состояния

disabled гасит всю группу, readonly оставляет выбор видимым, но неизменяемым: aria-readonly объявляет сама группа (у роли radio такого атрибута нет), а переключатели перестают обещать клик курсором.

invalid и required доходят и до группы, и до вида переключателей. Внутри GrFormField то же самое приходит из контекста поля: имя через aria-labelledby, подсказка и текст ошибки — через aria-describedby, ошибка — через aria-invalid. Группа не labelable-элемент, поэтому <label for> для неё неприменим.

Клавиатура

Вся раскладка описана в карточке GrRadio: группа — одна остановка Tab, внутри /// по кругу и Home/End на края. Отключённые варианты пропускаются везде — и в roving tabindex, и при обходе стрелками.

Playground 7

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

Код
<GrRadioGroup />

Установка

npm i @feugene/granularity

Импорт

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

API

Props

PropTypeпо умолчаниюОписание
variantGrRadioGroupVariant | undefined"radiobox"
optionsGrRadioGroupOption[] | undefinedundefined
disabledboolean | undefinedfalse
readonlyboolean | undefinedfalseТолько для чтения: выбор видно, но он не меняется.
invalidboolean | undefinedfalseВизуальное и ARIA-состояние ошибки.
requiredboolean | undefinedfalseОбязательное поле (`aria-required`).
size"xs" | "sm" | "md" | "lg" | undefinedundefined
ariaLabelstring | undefinedundefined
namestring | undefinedundefined
orientationGrRadioGroupOrientation | undefined"vertical"Раскладка варианта `radiobox`. Кнопочный вариант всегда горизонтальный — его собирает `GrButtonGroup`.
modelValueобязательныйGrRadioValue

Slots

SlotTypeОписание
defaultanyСобственная разметка переключателей вместо генерации из `options`.

Events

EventTypeОписание
update:modelValue[value: GrRadioValue]
change[value: GrRadioValue]
focus[event: FocusEvent]
blur[event: FocusEvent]

Methods / Expose

Methods / ExposeTypeОписание
focus() => void
blur() => void

Примеры 4

Группа, порождённая из списка опций

Быстрый старт-сценарий для options: одна декларация массива сразу даёт полную radio-группу без ручного рендера каждого элемента.

Selected state:
In review
Отключённый вариант пропускается и стрелками, и `Tab`.

Options
<script setup lang="ts">
import { computed, ref } from 'vue'

import type { GrRadioGroupOrientation } from '@feugene/granularity'
import { GrRadioGroup, GrSegmented } from '@feugene/granularity'

const status = ref('review')
const orientation = ref<GrRadioGroupOrientation>('vertical')
const readonly = ref(false)

// Опция умеет быть отключённой и нести пояснение — без перехода на слот.
const options = [
  { value: 'draft', label: 'Draft', description: 'Виден только автору' },
  { value: 'review', label: 'In review', description: 'Ждёт решения редактора' },
  { value: 'published', label: 'Published', description: 'Опубликовано на сайте' },
  { value: 'archived', label: 'Archived', description: 'Доступно после снятия блокировки', disabled: true },
]

const selectedOption = computed(() => options.find(option => option.value === status.value)?.label ?? status.value)
</script>

<template>
  <div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
    <div class="grid gap-4">
      <div class="flex flex-wrap items-center gap-4">
        <GrSegmented
          v-model="orientation"
          size="sm"
          :options="[
            { value: 'vertical', label: 'vertical' },
            { value: 'horizontal', label: 'horizontal' },
          ]"
        />
        <label class="flex items-center gap-2 text-sm text-[var(--gr-muted-fg)]">
          <input v-model="readonly" type="checkbox">
          readonly
        </label>
      </div>

      <GrRadioGroup
        v-model="status"
        :options="options"
        :orientation="orientation"
        :readonly="readonly"
      />
    </div>

    <div class="rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4 text-sm text-[var(--gr-muted-fg)]">
      Selected state:
      <div class="mt-2 text-base font-semibold text-[var(--gr-fg)]">
        {{ selectedOption }}
      </div>
      <div class="mt-3">
        Отключённый вариант пропускается и стрелками, и `Tab`.
      </div>
    </div>
  </div>
</template>

Полная линейка размеров

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

variant="button" — ступень в ступень с GrButton
xs
sm
md
lg
variant="radiobox" — коробка, точка и подпись тоже по ступеням
xs
sm
md
lg

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

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

const sizes = ['xs', 'sm', 'md', 'lg'] as const

const viewOptions = [
  { value: 'board', label: 'Board' },
  { value: 'calendar', label: 'Calendar' },
  { value: 'table', label: 'Table' },
]

const planOptions = [
  { value: 'free', label: 'Free' },
  { value: 'team', label: 'Team' },
]

// По значению на ступень: одна модель на все четыре сделала бы выбор общим, и
// разница между ступенями читалась бы хуже.
const buttonView = ref<Record<string, string>>({ xs: 'board', sm: 'calendar', md: 'board', lg: 'table' })
const radioboxPlan = ref<Record<string, string>>({ xs: 'free', sm: 'team', md: 'free', lg: 'team' })
</script>

<template>
  <div class="grid gap-6">
    <div class="grid gap-3">
      <div class="text-xs font-semibold text-[var(--gr-muted-fg)]">
        variant="button" — ступень в ступень с GrButton
      </div>

      <div
        v-for="size in sizes"
        :key="`button-${size}`"
        class="flex flex-wrap items-center gap-3"
      >
        <code class="w-8 text-xs text-[var(--gr-muted-fg)]">{{ size }}</code>
        <GrRadioGroup
          v-model="buttonView[size]"
          :options="viewOptions"
          variant="button"
          :size="size"
        />
        <!-- Кнопка рядом той же ступени: у кнопочного варианта карта размеров общая
             с `GrButton`, и высоты обязаны совпадать. -->
        <GrButton :size="size" variant="outline">
          GrButton {{ size }}
        </GrButton>
      </div>
    </div>

    <div class="grid gap-3">
      <div class="text-xs font-semibold text-[var(--gr-muted-fg)]">
        variant="radiobox" — коробка, точка и подпись тоже по ступеням
      </div>

      <div
        v-for="size in sizes"
        :key="`radiobox-${size}`"
        class="flex flex-wrap items-center gap-3"
      >
        <code class="w-8 text-xs text-[var(--gr-muted-fg)]">{{ size }}</code>
        <GrRadioGroup
          v-model="radioboxPlan[size]"
          :options="planOptions"
          :size="size"
          orientation="horizontal"
        />
      </div>
    </div>
  </div>
</template>

Свои слоты для пояснений к вариантам

Когда у опций есть secondary badges и статусы, удобнее перейти от options к slot-based composition поверх GrRadioGroup + GrRadio.

Routed through: slack

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

import { GrBadge, GrRadio, GrRadioGroup } from '@feugene/granularity'

const channel = ref('slack')
</script>

<template>
  <div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
    <GrRadioGroup v-model="channel" name="incident-channel">
      <GrRadio value="slack">
        <span class="inline-flex items-center gap-2">
          Slack
          <GrBadge tone="success" size="sm">Primary</GrBadge>
        </span>
      </GrRadio>
      <GrRadio value="email">
        <span class="inline-flex items-center gap-2">
          Email
          <GrBadge tone="warning" size="sm">Fallback</GrBadge>
        </span>
      </GrRadio>
      <GrRadio value="pagerduty">
        <span class="inline-flex items-center gap-2">
          PagerDuty
          <GrBadge tone="danger" size="sm">Escalation</GrBadge>
        </span>
      </GrRadio>
    </GrRadioGroup>

    <div class="rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4 text-sm text-[var(--gr-muted-fg)]">
      Routed through: <span class="font-semibold text-[var(--gr-fg)]">{{ channel }}</span>
    </div>
  </div>
</template>

Inheritance

Active target: staging

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

import { GrRadio, GrRadioGroup, GrSwitch } from '@feugene/granularity'

const environment = ref('staging')
const disabled = ref(false)
</script>

<template>
  <div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
    <GrRadioGroup v-model="environment" name="target-environment" :disabled="disabled">
      <GrRadio value="local">Local preview</GrRadio>
      <GrRadio value="staging">Staging</GrRadio>
      <GrRadio value="production">Production</GrRadio>
    </GrRadioGroup>

    <div class="grid gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
      <GrSwitch v-model="disabled" size="sm">
        Disable full group
      </GrSwitch>
      <div class="text-sm text-[var(--gr-muted-fg)]">
        Active target: <span class="font-semibold text-[var(--gr-fg)]">{{ environment }}</span>
      </div>
    </div>
  </div>
</template>

Доступность

Паттерн APG
radio
Клавиши
Space, Enter — выбрать; / — следующий, / — предыдущий (выбор едет вместе с фокусом, по кругу); Home/End — первый/последний доступный. Группа — одна остановка Tab, отключённые варианты пропускаются

Полный клавиатурный контракт пакета

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