GrChip
Берут, когда снимаемая метка у сущности.
Когда брать
- снимаемая метка у сущности — теги задачи, получатели письма, выбранные файлы: крестик у каждой, список ведёт приложение;
- фильтр, который включают и выключают —
selectableдаёт переключатель сaria-pressed, набор таких фильтров собираетGrChipGroup; - быстрый выбор из коротких вариантов — период, статус, приоритет: чипы занимают строку, а не выпадающий список;
- свой ряд тегов рядом с полем — когда ввод и хранение уже написаны, а нужна только плитка с крестиком.
Когда взять другое
| Нужно | Берите |
|---|---|
| Статус или ярлык без интерактива | GrBadge |
| Поле, которое само набирает теги из ввода | GrInputTag |
| Счётчик поверх кнопки или иконки | GrBadgeWrap |
| Один выбор из вариантов одной строкой | GrSegmented |
| Много вариантов, которые ищут вводом | GrSelect / GrAutocomplete |
Крестик — не всегда кнопка
Разметка чипа зависит от того, кликают ли по нему самому.
Чип не виджет (просто снимаемая метка) — корень <span>, а крестик
настоящая <button> со своим именем и своей остановкой Tab. Так устроены
чипы внутри GrInputTag.
Чип виджет (selectable или внутри группы) — корень <button>, и крестик
внутри него кнопкой быть не может: роль объявляет потомков презентационными
(axe ловит это как nested-interactive), а <button> внутри <button>
невалиден по контент-модели HTML. Поэтому крестик становится aria-hidden-
<span>, снятие уходит на Delete/Backspace, и чип объявляет это через
aria-keyshortcuts. Тем же приёмом живут закрываемые вкладки
GrTabs.
Практическое следствие: внутрь выбираемого чипа нельзя класть ссылку. Нужна ссылка — чип не выбираемый.
Снятие чип не выполняет
remove — это просьба, а не факт: состав набора лежит у потребителя, и убрать
элемент из массива может только он. Чип не прячет себя сам и ничего не
объявляет в живой регион — объявлять «убрано» до того, как это случилось, было
бы враньём.
Подпись нужна отдельно от слота
Проп label дублирует содержимое слота не по недосмотру: из него собирается имя
кнопки снятия. Двадцать кнопок с именем «Убрать» подряд не дают выбрать нужную —
имя обязано называть сам тег, поэтому по умолчанию оно «Убрать {label}».
Содержимое сложнее строки — задавайте removeLabel руками.
Выбор виден заливкой, а не контуром
Выбранный чип берёт плотный вариант своего тона — тот же, что даёт dark.
Оттенок при этом не меняется: набор фильтров, где выбранное перекрашивается в
соседний цвет, читается как радуга, а не как состояние.
Контур и лёгкое утолщение шрифта эту работу не тянут: в ряду из пяти фильтров выбранный по ним не находится с одного взгляда. Перепад светлоты находится — и переживает монохром и дальтонизм, в отличие от «стал чуть темнее».
Нецветовой канал при этом остаётся: у выбранного чипа font-weight: 600. Он же
вытягивает случай, когда всему набору задан dark и заливка у выбранного и
невыбранного совпадает.
Тона общие с бейджем, размеры свои
Палитра из восьми тонов и три радиуса взяты у GrBadge без
изменений: чип и бейдж часто стоят в одном ряду и обязаны совпадать по цвету.
А вот шкала размеров у чипа другая. У бейджа кегль на ступень мельче
(--gr-control-text-2xs…md), потому что метка не цель нажатия; по чипу кликают,
и кегль у него совпадает с соседним контролом (--gr-control-text-xs…lg).
Отсюда же явная высота, которой у бейджа нет вовсе.
Высота при этом ниже контрольной — 24 / 28 / 32 / 36 px против 28 / 32 / 40 /
44 у поля ввода и кнопки: чип плотнее, потому что ходит наборами и по несколько
штук в строке. Практическое следствие стоит знать заранее: потолок шкалы — 36px,
то есть до 44×44, которых требуют WCAG 2.5.5 и Apple HIG для цели под палец, чип
не дотягивается ни на какой ступени. Нужна именно такая цель — берите
GrButton с size="lg", а не чип.
Границы
- виртуализации нет. Тысяча чипов в ряду — это не ряд чипов, а список:
режьте набор сами или берите
GrSelectсvirtual; - перетаскиванием чипы не переставляются. Порядок задаёт массив
потребителя; нужен ручной порядок —
GrSortableList; - своего ввода у чипа нет. Он показывает готовое значение;
набор из ввода — это
GrInputTag.
Playground 11
Загружается…
<GrChip />Установка
npm i @feugene/granularityИмпорт
import { GrChip } from '@feugene/granularity/components/GrChip'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
tone | "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined | undefined | — |
closable | boolean | undefined | false | Крестик. Внутри виджета он не кнопка — см. блок про роли ниже. |
disabled | boolean | undefined | false | — |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | Доступное имя, когда подпись не объясняет назначения. |
dark | boolean | undefined | undefined | — |
radius | GrBadgeRadius | undefined | undefined | — |
value | GrChipValue | undefined | undefined | Значение чипа в группе. Вне `GrChipGroup` не используется. |
label | string | undefined | undefined | Подпись. Нужна отдельно от слота: из неё собирается имя кнопки снятия — «Убрать» на двадцати кнопках подряд не даёт выбрать нужную. |
selectable | boolean | undefined | false | Чип становится переключателем: `aria-pressed`, `Enter`/`Space`. |
selected | boolean | undefined | false | Состояние переключателя вне группы. Внутри группы значение ведёт группа. |
removeLabel | string | undefined | undefined | Имя кнопки снятия целиком. Перебивает собранное из `label`. |
removeTabindex | number | undefined | undefined | `tabindex` кнопки снятия — для чипа внутри поля ввода. Там таб-стоп принадлежит `<input>`, а ряд чипов ходит стрелками: у `GrInputTag` кольцо оставляет ровно один `0`, у `GrAutocomplete` их нет вовсе. Проп нужен потому, что кольцом управляет родитель — он знает и порядок чипов, и что стоит за краем ряда. |
Slots
| Slot | Type | Описание |
|---|---|---|
default | any | Подпись чипа. Внутри выбираемого чипа — только фразовое содержимое. |
icon | any | Иконка перед подписью. |
Events
| Event | Type | Описание |
|---|---|---|
remove | [] | — |
update:selected | [value: boolean] | — |
Methods / Expose
| Methods / Expose | Type | Описание |
|---|---|---|
focus | () => void | undefined | — |
blur | () => void | undefined | — |
removeEl | HTMLButtonElement | null | Кнопка снятия — цель roving-фокуса у родителя. Отдаётся элементом, а не методом `focusRemove`: кольцу нужен сам узел, оно само решает, когда и куда переносить фокус. |
Примеры 3
Метки записи со снятием
Крестик у каждой метки, состав ведёт приложение.
<script setup lang="ts">
import { ref } from 'vue'
import { GrCard, GrChip } from '@feugene/granularity'
// Метки задачи: состав ведёт приложение, чип только просит его убрать.
const tags = ref(['design', 'frontend', 'нужен ревью', 'v0.25'])
function drop(tag: string): void {
tags.value = tags.value.filter(item => item !== tag)
}
</script>
<template>
<GrCard class="p-4">
<div class="flex flex-wrap items-center gap-2">
<GrChip
v-for="tag in tags"
:key="tag"
:label="tag"
tone="slate"
closable
@remove="drop(tag)"
/>
<span v-if="!tags.length" class="text-sm text-[var(--gr-muted-fg)]">
Меток не осталось
</span>
</div>
</GrCard>
</template>Событие remove — это просьба: чип не прячет себя сам, потому что массив лежит у вас.
Иконка, переключатель, выключенный чип
Три состояния подряд: метка с иконкой, чип-переключатель и недоступный чип.
<script setup lang="ts">
import { ref } from 'vue'
import IconCheck from '~icons/lucide/check'
import IconUser from '~icons/lucide/user'
import { GrChip } from '@feugene/granularity'
const subscribed = ref(false)
</script>
<template>
<div class="flex flex-wrap items-center gap-2">
<GrChip label="Анна Петрова" tone="azure" closable>
<template #icon>
<IconUser class="h-full w-full" />
</template>
</GrChip>
<GrChip
v-model:selected="subscribed"
label="Подписаться на изменения"
tone="success"
selectable
>
<template v-if="subscribed" #icon>
<IconCheck class="h-full w-full" />
</template>
</GrChip>
<GrChip label="Архивная метка" tone="neutral" closable disabled />
</div>
</template>У выключенного чипа крестика нет вовсе: он обещал бы действие, которого нет.
Тона и размеры
Восемь тонов в светлом и плотном вариантах и четыре ступени размера.
<script setup lang="ts">
import { GrChip } from '@feugene/granularity'
const tones = ['neutral', 'primary', 'success', 'warning', 'danger', 'info', 'slate', 'azure'] as const
const sizes = ['xs', 'sm', 'md', 'lg'] as const
</script>
<template>
<div class="grid gap-4">
<div class="flex flex-wrap items-center gap-2">
<GrChip v-for="tone in tones" :key="tone" :tone="tone" :label="tone" />
</div>
<div class="flex flex-wrap items-center gap-2">
<GrChip v-for="tone in tones" :key="tone" :tone="tone" :label="tone" dark />
</div>
<div class="flex flex-wrap items-center gap-2">
<GrChip v-for="size in sizes" :key="size" :size="size" tone="info" :label="size" closable />
</div>
</div>
</template>Доступность
- Паттерн APG
toggle button / option- Клавиши
- Не выбираемый чип клавиш не имеет: интерактив в нём один — крестик, и он обычная кнопка со своей остановкой
Tab. Выбираемый чип — кнопка:Enter/Spaceпереключают,Delete/Backspaceснимают (еслиclosable). Крестик у него в таб-порядке **не стоит**: внутри роли-виджета вложенная кнопка запрещена, и способ снять объявлен черезaria-keyshortcuts