GrChipGroup

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

Берут, когда фильтры списка строкой.

Когда брать

  • фильтры списка строкой — статусы, категории, метки: видно все варианты сразу, выбранных может быть несколько;
  • выбор из коротких вариантов вместо списка — период отчёта, приоритет: вариантов до десятка, и прятать их под кнопку незачем;
  • набор меток записи, который правят на местеclosable даёт крестик каждому чипу, а Delete снимает тот, что под фокусом;
  • поле формы, где значение — набор — группа читает контекст GrFormField и отдаёт значение в нативную форму по name.

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

НужноБерите
Один выбор, вариантов немного, вид переключателяGrSegmented
Один выбор с описаниями у вариантовGrRadioGroup
Множественный выбор списком с отметкамиGrCheckboxGroup
Вариантов десятки, их ищут вводомGrSelect / GrAutocomplete
Ряд снимаемых меток без выбораGrChip без группы

Роль зависит от множественности выбора

selection="multiple" (по умолчанию) объявляет группу listbox с aria-multiselectable, а чипы — option с aria-selected. selection="single"radiogroup и radio с aria-checked, как у GrSegmented.

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

Одна остановка `Tab`, внутрь — стрелками

Группа составная, поэтому занимает одну остановку в таб-порядке: активный чип держит tabindex="0", остальные -1. Стрелки работают по обеим осям — чипы переносятся на новую строку, и «вниз» означает следующий чип так же, как «вправо». Home/End уводят к краям.

Стрелка двигает только фокус, даже в одиночном режиме. У чипов есть второе действие — снятие по Delete, — и переносить выбор вместе с фокусом значило бы менять модель при попытке дойти до нужного чипа. В radiogroup формы принято обратное, но там у элементов одного действия.

Повторный выбор снимает отметку

В одиночном режиме клик по уже выбранному чипу сбрасывает значение в null. Набор фильтров без выбранного значения осмыслен («любой»), и отменить выбор иначе было бы нечем — в отличие от переключателей формы, где пустое значение обычно запрещено.

Состав рисует потребитель

Пропа options у группы нет: у каждого чипа своя подпись, иконка и тон, и генерировать их из плоского массива значило бы завести второй, более бедный способ описать то же самое. Отсюда же remove — группа его только ретранслирует, а убирает элемент из массива приложение.

Границы

  • своего порядка у группы нет — она рисует чипы в порядке слота;
  • группа не ограничивает число выбранных. Предел — правило приложения, и сообщение о нём тоже: группа не знает, что показать вместо отказа;
  • вложенных групп не бывает. Набор внутри набора даёт неразрешимый вопрос, чья стрелка обрабатывается.

Playground 10

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

Код
<GrChipGroup />

Установка

npm i @feugene/granularity

Импорт

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

API

Props

PropTypeпо умолчаниюОписание
tone"primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefinedundefined
closableboolean | undefinedfalseКрестик у всех чипов набора. Точечно перебивается пропом самого чипа.
modelValueGrChipValue | GrChipValue[] | null | undefinedundefinedОдиночный выбор — значение, множественный — массив.
disabledboolean | undefinedfalse
readonlyboolean | undefinedfalseВыбор видно, но он не меняется.
invalidboolean | undefinedfalse
requiredboolean | undefinedfalse
size"xs" | "sm" | "md" | "lg" | undefinedundefined
ariaLabelstring | undefinedundefined
namestring | undefinedundefinedИмя для нативной формы. Множественный выбор отдаёт по полю на значение.
darkboolean | undefinedundefined
radiusGrBadgeRadius | undefinedundefined
selectionGrChipSelection | undefined"multiple"

Slots

SlotTypeОписание
defaultanyЧипы набора.

Events

EventTypeОписание
update:modelValue[value: GrChipValue | GrChipValue[] | null]
change[value: GrChipValue | GrChipValue[] | null]
focus[event: FocusEvent]
blur[event: FocusEvent]
remove[value: GrChipValue]

Methods / Expose

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

Примеры 3

Фильтры списка

Множественный выбор: видно все варианты сразу, выбранных может быть несколько.

Показываем: Открытые, На ревью

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

import { GrCard, GrChip, GrChipGroup } from '@feugene/granularity'

const statuses = [
  { value: 'open', label: 'Открытые' },
  { value: 'review', label: 'На ревью' },
  { value: 'blocked', label: 'Заблокированные' },
  { value: 'done', label: 'Готовые' },
]

const selected = ref<string[]>(['open', 'review'])

const summary = computed(() => (selected.value.length
  ? statuses.filter(status => selected.value.includes(status.value)).map(s => s.label).join(', ')
  : 'любой статус'))
</script>

<template>
  <div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
    <GrChipGroup v-model="selected" aria-label="Статус задачи">
      <GrChip
        v-for="status in statuses"
        :key="status.value"
        :value="status.value"
        :label="status.label"
        tone="primary"
      />
    </GrChipGroup>

    <GrCard class="p-4 text-sm text-[var(--gr-muted-fg)]">
      Показываем: <span class="font-semibold text-[var(--gr-fg)]">{{ summary }}</span>
    </GrCard>
  </div>
</template>

Один выбор с отменой

Период отчёта. Повторное нажатие на выбранный чип возвращает «весь».

Период: week

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

import { GrCard, GrChip, GrChipGroup } from '@feugene/granularity'

const periods = [
  { value: 'day', label: 'День' },
  { value: 'week', label: 'Неделя' },
  { value: 'month', label: 'Месяц' },
  { value: 'quarter', label: 'Квартал' },
]

// Повторное нажатие на выбранный чип сбрасывает период в «любой».
const period = ref<string | null>('week')
</script>

<template>
  <div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
    <GrChipGroup v-model="period" selection="single" tone="info" aria-label="Период отчёта">
      <GrChip
        v-for="item in periods"
        :key="item.value"
        :value="item.value"
        :label="item.label"
      />
    </GrChipGroup>

    <GrCard class="p-4 text-sm text-[var(--gr-muted-fg)]">
      Период: <span class="font-semibold text-[var(--gr-fg)]">{{ period ?? 'весь' }}</span>
    </GrCard>
  </div>
</template>

Набор фильтров без выбранного значения осмыслен, поэтому одиночный выбор снимается.

Выбор и снятие вместе

Метки записи, которые можно и отметить, и убрать.

Стрелки водят фокус по набору, Delete снимает метку под фокусом.

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

import type { GrChipValue } from '@feugene/granularity'
import { GrChip, GrChipGroup } from '@feugene/granularity'

// Метки можно и выбирать, и снимать: выбор ведёт группа, состав — приложение.
const labels = ref(['срочно', 'бэкенд', 'регресс', 'релиз 0.25'])
const active = ref<string[]>(['срочно'])

function drop(value: GrChipValue): void {
  const label = String(value)
  labels.value = labels.value.filter(item => item !== label)
  active.value = active.value.filter(item => item !== label)
}
</script>

<template>
  <div class="grid gap-3">
    <GrChipGroup v-model="active" closable aria-label="Метки записи" @remove="drop">
      <GrChip
        v-for="label in labels"
        :key="label"
        :value="label"
        :label="label"
        tone="warning"
      />
    </GrChipGroup>

    <p class="text-sm text-[var(--gr-muted-fg)]">
      Стрелки водят фокус по набору, Delete снимает метку под фокусом.
    </p>
  </div>
</template>

Набор — одна остановка Tab: внутрь ведут стрелки, Delete снимает чип под фокусом.

Доступность

Паттерн APG
listbox / radiogroup (roving tabindex)
Клавиши
Набор — одна остановка Tab, внутрь ведут стрелки по обеим осям (чипы переносятся на новую строку), Home/End — к краям, Delete/Backspace — снять чип под фокусом. **Стрелка двигает только фокус, даже в одиночном режиме**: у чипа есть второе действие, и менять значение по дороге к нужному чипу нельзя

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

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