GrChip

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

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

Когда брать

  • снимаемая метка у сущности — теги задачи, получатели письма, выбранные файлы: крестик у каждой, список ведёт приложение;
  • фильтр, который включают и выключают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

PropTypeпо умолчаниюОписание
tone"primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefinedundefined
closableboolean | undefinedfalseКрестик. Внутри виджета он не кнопка — см. блок про роли ниже.
disabledboolean | undefinedfalse
size"xs" | "sm" | "md" | "lg" | undefinedundefined
ariaLabelstring | undefinedundefinedДоступное имя, когда подпись не объясняет назначения.
darkboolean | undefinedundefined
radiusGrBadgeRadius | undefinedundefined
valueGrChipValue | undefinedundefinedЗначение чипа в группе. Вне `GrChipGroup` не используется.
labelstring | undefinedundefinedПодпись. Нужна отдельно от слота: из неё собирается имя кнопки снятия — «Убрать» на двадцати кнопках подряд не даёт выбрать нужную.
selectableboolean | undefinedfalseЧип становится переключателем: `aria-pressed`, `Enter`/`Space`.
selectedboolean | undefinedfalseСостояние переключателя вне группы. Внутри группы значение ведёт группа.
removeLabelstring | undefinedundefinedИмя кнопки снятия целиком. Перебивает собранное из `label`.
removeTabindexnumber | undefinedundefined`tabindex` кнопки снятия — для чипа внутри поля ввода. Там таб-стоп принадлежит `<input>`, а ряд чипов ходит стрелками: у `GrInputTag` кольцо оставляет ровно один `0`, у `GrAutocomplete` их нет вовсе. Проп нужен потому, что кольцом управляет родитель — он знает и порядок чипов, и что стоит за краем ряда.

Slots

SlotTypeОписание
defaultanyПодпись чипа. Внутри выбираемого чипа — только фразовое содержимое.
iconanyИконка перед подписью.

Events

EventTypeОписание
remove[]
update:selected[value: boolean]

Methods / Expose

Methods / ExposeTypeОписание
focus() => void | undefined
blur() => void | undefined
removeElHTMLButtonElement | nullКнопка снятия — цель roving-фокуса у родителя. Отдаётся элементом, а не методом `focusRemove`: кольцу нужен сам узел, оно само решает, когда и куда переносить фокус.

Примеры 3

Метки записи со снятием

Крестик у каждой метки, состав ведёт приложение.

designfrontendнужен ревьюv0.25

Tags
<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 — это просьба: чип не прячет себя сам, потому что массив лежит у вас.

Иконка, переключатель, выключенный чип

Три состояния подряд: метка с иконкой, чип-переключатель и недоступный чип.

Iconзависит от окружения витрины
<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>

У выключенного чипа крестика нет вовсе: он обещал бы действие, которого нет.

Тона и размеры

Восемь тонов в светлом и плотном вариантах и четыре ступени размера.

neutralprimarysuccesswarningdangerinfoslateazure
neutralprimarysuccesswarningdangerinfoslateazure
xssmmdlg

Tones
<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

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

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