GrChipGroup
A set of chips sharing one value: list filters, record labels, a quick pick.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the filters of a list in a line — statuses, categories, labels: every option is visible at once, and several may be selected;
- a choice among short options instead of a list — the period of a report, a priority: there are up to a dozen options, and there is no point hiding them under a button;
- a set of labels on a record that is edited in place —
closablegives every chip a cross, andDeleteremoves the one under focus; - a form field whose value is a set — the group reads the context of
GrFormFieldand gives its value to a native form byname.
When to take something else
| Need | Take |
|---|---|
| A single choice, few options, the look of a switch | GrSegmented |
| A single choice with descriptions on the options | GrRadioGroup |
| Multiple selection as a list with marks | GrCheckboxGroup |
| There are dozens of options, searched by typing | GrSelect / GrAutocomplete |
| A row of dismissible labels without selection | GrChip without a group |
The role depends on the multiplicity of the selection
selection="multiple" (the default) declares the group a listbox with
aria-multiselectable, and the chips option with aria-selected.
selection="single" gives a radiogroup and radio with aria-checked, as in
GrSegmented.
The chips get their role from the context and do not choose it themselves: otherwise a set of chips with different roles would be announced to a screen reader as anything but one widget.
One `Tab` stop, inside it the arrows
The group is composite, so it occupies one stop in the tab order: the active chip
holds tabindex="0", the rest -1. The arrows work along both axes — chips wrap
onto a new line, and “down” means the next chip just as “right” does. Home/End
take you to the edges.
An arrow moves the focus only, even in single mode. Chips have a second action —
dismissal with Delete — and moving the selection along with the focus would mean
changing the model while trying to reach the chip you want. In a form radiogroup
the opposite is customary, but there the elements have a single action.
Selecting again clears the mark
In single mode a click on an already selected chip resets the value to null. A set
of filters with no value selected makes sense (“any”), and there would otherwise be
no way to cancel the selection — unlike form switches, where an empty value is
usually forbidden.
The contents are drawn by the consumer
The group has no options prop: every chip has a label, an icon and a tone of its
own, and generating them from a flat array would mean introducing a second, poorer
way to describe the same thing. Hence remove as well — the group only relays it,
and the application removes the item from the array.
Limits
- the group has no order of its own — it draws the chips in the order of the slot;
- the group does not limit the number selected. The limit is a rule of the application, and so is the message about it: the group does not know what to show instead of a refusal;
- there are no nested groups. A set inside a set poses an unanswerable question about whose arrow is handled.
Playground 10
Loading…
<GrChipGroup />Install
npm i @feugene/granularityImport
import { GrChipGroup } from '@feugene/granularity/components/GrChipGroup'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
tone | "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined | undefined | — |
closable | boolean | undefined | false | A cross on every chip of the set. It is overridden pointwise by the prop of a chip. |
modelValue | GrChipValue | GrChipValue[] | null | undefined | undefined | A single selection is a value, a multiple one is an array. |
disabled | boolean | undefined | false | — |
readonly | boolean | undefined | false | The selection is visible but does not change. |
invalid | boolean | undefined | false | — |
required | boolean | undefined | false | — |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | — |
name | string | undefined | undefined | The name for a native form. A multiple selection gives one field per value. |
dark | boolean | undefined | undefined | — |
radius | GrBadgeRadius | undefined | undefined | — |
selection | GrChipSelection | undefined | "multiple" | — |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | The chips of the set. |
Events
| Event | Type | Description |
|---|---|---|
update:modelValue | [value: GrChipValue | GrChipValue[] | null] | — |
change | [value: GrChipValue | GrChipValue[] | null] | — |
focus | [event: FocusEvent] | — |
blur | [event: FocusEvent] | — |
remove | [value: GrChipValue] | — |
Methods / Expose
| Methods / Expose | Type | Description |
|---|---|---|
focus | () => void | undefined | — |
blur | () => void | undefined | — |
Examples 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>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>Removable
Стрелки водят фокус по набору, Delete снимает метку под фокусом.
<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>Accessibility
- APG pattern
listbox / radiogroup (roving tabindex)