GrCheckboxGroup

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

Берут, когда значений несколько из короткого набора.

Когда брать

  • значений несколько из короткого набора — права, теги, дни недели, каналы уведомлений;
  • все варианты должны быть видны — выбор виден целиком, без раскрытия панели;
  • группа — часть формы — общая модель string[], общие disabled, readonly и invalid;
  • вариантов до десятка — дальше список становится длиннее экрана.

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

НужноБерите
Вариантов многоGrSelect с multiple
Вариантов много и их ищут вводомGrAutocomplete
Можно выбрать только одинGrRadioGroup
Чекбокс одинGrCheckbox
Варианты вложеныGrTreeSelect

Модель и контекст

Группа раздаёт вложенным чекбоксам выбранные значения, name, size и состояния disabled/readonly/invalid, поэтому собственный v-model им не нужен — достаточно value:

<GrCheckboxGroup v-model="channels" name="channels" :options="options" />

<GrCheckboxGroup v-model="channels" direction="horizontal">
  <GrCheckbox value="sms">SMS</GrCheckbox>
  <GrCheckbox value="email">Email</GrCheckbox>
</GrCheckboxGroup>

name уходит на каждый отмеченный чекбокс, поэтому нативная форма получает повторяющееся поле: new FormData(form).getAll('channels').

disabled отдельной опции сильнее доступной группы. Значение в модели, которого нет среди options, группа не теряет: снятие соседнего чекбокса его сохраняет.

Роль и ARIA

role="group", а не radiogroup: у чекбоксов нет roving tabindex и переезда выбора стрелками — каждый остаётся собственной остановкой Tab, ровно как набор нативных <input type="checkbox">.

role="group" не поддерживает aria-required и aria-readonly (axe роняет это как critical aria-allowed-attr), поэтому оба состояния объявляют сами чекбоксы. aria-invalid, наоборот, висит только на группе: продублированный на каждом пункте, он заставил бы диктор повторить «неверное значение» столько раз, сколько в группе чекбоксов, — пункты при ошибке только перекрашиваются.

Обязательность

Как и у одиночного чекбокса, required — объявление; проверяет правило формы. Для группы работает встроенное required: пустой массив считается пустым значением.

const rules: GrFormRules = {
  channels: [{ required: true, message: 'Выберите хотя бы один канал' }],
}

Playground 7

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

Код
<GrCheckboxGroup />

Установка

npm i @feugene/granularity

Импорт

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

API

Props

PropTypeпо умолчаниюОписание
optionsGrCheckboxGroupOption[] | undefinedundefined
disabledboolean | undefinedfalse
readonlyboolean | undefinedfalseТолько для чтения: выбор видно, но он не меняется.
invalidboolean | undefinedfalseВизуальное и ARIA-состояние ошибки для всей группы.
requiredboolean | undefinedfalseОбязательная группа: `aria-required` объявляют сами чекбоксы.
size"xs" | "sm" | "md" | "lg" | undefinedundefined
ariaLabelstring | undefinedundefined
namestring | undefinedundefinedОбщее имя для нативной формы: значения уйдут как повторяющиеся поля.
directionGrCheckboxGroupDirection | undefined"vertical"
modelValueобязательныйstring[]

Slots

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

Events

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

Methods / Expose

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

Примеры 2

Множественный выбор из списка опций

Одна модель string[] на всю группу: чекбоксам не нужен собственный v-model, а раскладка переключается пропом direction.

Email
SMS
Push
Webhook
Selected: email, push

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

import { GrCheckboxGroup, GrSegmented } from '@feugene/granularity'

const options = [
  { value: 'email', label: 'Email' },
  { value: 'sms', label: 'SMS' },
  { value: 'push', label: 'Push' },
  { value: 'webhook', label: 'Webhook', disabled: true },
]

const channels = ref(['email', 'push'])
const direction = ref<'vertical' | 'horizontal'>('vertical')
</script>

<template>
  <div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_240px]">
    <div class="grid gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
      <GrCheckboxGroup
        v-model="channels"
        name="channels"
        :options="options"
        :direction="direction"
        aria-label="Notification channels"
      />
    </div>

    <div class="grid gap-3 rounded-2xl border border-dashed border-[var(--gr-brd)] p-4">
      <GrSegmented
        v-model="direction"
        size="sm"
        :options="[
          { value: 'vertical', label: 'Vertical' },
          { value: 'horizontal', label: 'Horizontal' },
        ]"
      />

      <div class="text-sm text-[var(--gr-muted-fg)]">
        Selected: <span class="font-semibold text-[var(--gr-fg)]">{{ channels.join(', ') || 'none' }}</span>
      </div>
    </div>
  </div>
</template>

Проверка внутри GrForm

Обязательность объявляется через aria-required, а проверяет её правило формы — нативный required не блокирует отправку молча.

Incidents
Releases
Weekly digest
I accept the notification policy

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

import { GrButton, GrCheckbox, GrCheckboxGroup, GrForm, GrFormField, type GrFormRules } from '@feugene/granularity'

const options = [
  { value: 'incidents', label: 'Incidents' },
  { value: 'releases', label: 'Releases' },
  { value: 'digest', label: 'Weekly digest' },
]

const model = reactive({ scopes: [] as string[], terms: false })

// `required` пустым считает `null`/`''`/`[]`, но не `false`: снятый чекбокс —
// это законное значение поля. «Согласие обязательно» — это `validator`.
const rules: GrFormRules = {
  scopes: [{ required: true, message: 'Pick at least one subscription' }],
  terms: [{ validator: value => value === true || 'Accept the policy to continue' }],
}

const submitted = ref(false)
</script>

<template>
  <GrForm
    :model="model"
    :rules="rules"
    class="grid max-w-md gap-4"
    @submit="submitted = true"
  >
    <GrFormField name="scopes" label="Subscriptions">
      <GrCheckboxGroup v-model="model.scopes" name="scopes" :options="options" required />
    </GrFormField>

    <GrFormField name="terms" label="Policy">
      <GrCheckbox v-model="model.terms" required>
        I accept the notification policy
      </GrCheckbox>
    </GrFormField>

    <div class="flex gap-2">
      <GrButton type="submit" size="sm">
        Save preferences
      </GrButton>
    </div>

    <p v-if="submitted" class="text-sm text-[var(--gr-success-text)]">
      Saved — the form is valid.
    </p>
  </GrForm>
</template>

Доступность

Паттерн APG
group
Клавиши
стрелок нет: каждый чекбокс — своя остановка Tab, как у набора нативных <input type="checkbox">

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

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