GrChip

  • ядро
  • Данные
  • API · 13 props
  • ⌨ клавиши

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

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

Обзор

Когда брать

  • снимаемая метка у сущности — теги задачи, получатели письма, выбранные файлы: крестик у каждой, список ведёт приложение;
  • фильтр, который включают и выключают — selectable даёт переключатель с aria-pressed, набор таких фильтров собирает GrChipGroup;
  • быстрый выбор из коротких вариантов — период, статус, приоритет: чипы занимают строку, а не выпадающий список;
  • свой ряд тегов рядом с полем — когда ввод и хранение уже написаны, а нужна только плитка с крестиком.

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

НужноБерите
Статус или ярлык без интерактиваGrBadge
Поле, которое само набирает теги из вводаGrInputTag
Счётчик поверх кнопки или иконкиGrBadgeWrap
Один выбор из вариантов одной строкойGrSegmented
Много вариантов, которые ищут вводомGrSelect / GrAutocomplete

Варианты все тона и виды — настоящим компонентом

Варианты · GrChip
tone \ darkfalsetrue
primary
neutral
success
warning
danger
info
slate
azure

Размеры

  • xs
  • sm
  • md
  • lg

Состояния

  • обычная
  • disabled

Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.

Playground контролы выводятся из API, а не пишутся руками

Сцена

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

0 изменено· состояние — в адресе

Примеры 5 у каждого — живое демо и его код

  • Метки, которые снимают одним движением

    closable добавляет настоящую кнопку с именем метки — «Убрать vat», а не «Убрать» пять раз подряд. remove — просьба, а не факт: чип не прячет себя сам, список живёт на экране. Бейдж сообщает состояние; чип — то, чем объект пометили, поэтому его можно снять.

    Export fails for invoices with reverse-charge VAT#1824 · opened by Grace Wu 2 days ago
    Labels
    buginvoicesvatcustomer-reported
  • Получатели и кто снаружи

    Слот #icon отличает коллегу от внешнего адреса, а тон это подкрепляет. Когда одной подписи мало, чтобы понять, что снимает крестик, remove-label задаёт имя целиком: «Remove Grace Wu from recipients».

    Send invoice INV-0418
    ToAda LinGrace WuNorthwind AP

    1 recipient is outside Acme — the invoice leaves the workspace.

  • Фильтры, которые включают и выключают

    selectable превращает чип в переключатель с aria-pressed, а v-model:selected держит его состояние. Выбранный чип берёт плотную заливку своего тона — находится одним взглядом и отличается насыщением даже в монохроме. Чип из одного значка называет aria-label; disabled оставляет недоступный фильтр на виду.

    3 of 24 invoices · €4,035 due

  • Токены запроса в строке поиска

    Токен ближе к коду, чем к тегу: radius="semi" даёт ему форму ярлыка ключа, size="sm" вписывает его в поле. В слоте ключ и значение двух весов, и label не сказал бы, что снимается, — это делает remove-label.

    status: overduecustomer: Northwindamount: > €1,000
    3 filters · 9 invoices

API порождается из пакета · разойтись не может

Props

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

Слоты

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

События

EventСигнатураОписание
remove(event: "remove"): void—
update:selected(event: "update:selected", value: boolean): void—

Методы

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

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { GrChipConfigurableProps, GrChipValue, GrChipSelection } from '@feugene/granularity/components/GrChip'

Файлdist/types/components/GrChip/config.d.ts

grChipConfigconst
const grChipConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrChip">;

Файлdist/types/components/GrChip/defaults.d.ts

GrChipConfigurablePropsinterface

Пропы GrChip, настраиваемые глобально через componentDefaults. Только оформление: выбор и снятие относятся к экземпляру.

interface GrChipConfigurableProps {
    tone: GrChipTone;
    size: GrChipSize;
    radius: GrChipRadius;
    dark: boolean;
}

Файлdist/types/components/shared/chipGroupContext.d.ts

GrChipValuetype

Значение чипа в группе. Не только строка: набор фильтров чаще всего адресуется id. Объекты сюда намеренно не входят — значение уходит в data-value и в скрытое поле нативной формы, а значит обязано иметь однозначное строковое представление.

type GrChipValue = string | number | boolean;
GrChipSelectiontype

Как группа обращается со множественностью выбора.

type GrChipSelection = 'single' | 'multiple';
GrChipGroupContexttype
type GrChipGroupContext = {
    selection: ComputedRef<GrChipSelection>;
    isSelected: (value: GrChipValue) => boolean;

Файлdist/types/components/GrChip/grChipStyles.d.ts

GrChipTonetype
type GrChipTone = GrBadgeTone;
GrChipSizetype
type GrChipSize = GrComponentSize;
GrChipRadiustype
type GrChipRadius = GrBadgeRadius;

Файлdist/types/components/GrChip/safelist.d.ts

grChipSafelistconst
const grChipSafelist: string[];

Файлdist/types/components/GrChip/GrChip.vue.d.ts

GrChipPropsinterface

Интерактивный тег: снимаемый, выбираемый, с иконкой. От GrBadge отличается не видом, а природой: бейдж — метка, чип — виджет. Поэтому цвета и радиусы взяты у бейджа как есть (в одном ряду они обязаны совпадать), а роль, клавиатура и размеры — свои.

interface GrChipProps {
    /** Значение чипа в группе. Вне `GrChipGroup` не используется. */
    value?: GrChipValue;
    /**
     * Подпись. Нужна отдельно от слота: из неё собирается имя кнопки снятия —
     * «Убрать» на двадцати кнопках подряд не даёт выбрать нужную.
     */
    label?: string;
    /**
     * Тон чипа — те же цвета, что у `GrBadge`, чтобы в одном ряду они не расходились.
     * Не задан — из группы и `GrConfigProvider`, иначе `neutral`.
     */
    tone?: GrChipTone;
    /** Заливка тоном вместо мягкой подложки — для чипа, который обязан выделяться. По умолчанию `false`. */
    dark?: boolean;
    /**
     * Ступень контрольной шкалы: цель нажатия той же высоты, что у соседних полей.
     * Не задан — из группы и `GrConfigProvider`, иначе `md`.
     */
    size?: GrChipSize;
    /**
     * Форма: `round` — таблетка, `semi` — ярлык, `square` — штамп.
     * Не задан — из группы и `GrConfigProvider`, иначе `round`.
     */
    radius?: GrChipRadius;
    /** Чип становится переключателем: `aria-pressed`, `Enter`/`Space`. */
    selectable?: boolean;
    /** Состояние переключателя вне группы. Внутри группы значение ведёт группа. */
    selected?: boolean;
    /** Крестик. Внутри виджета он не кнопка — см. блок про роли ниже. */
    closable?: boolean;
    /** Чип недоступен: не выбирается и не снимается, крестик скрыт. Внутри группы — и от её `disabled`. */
    disabled?: boolean;
    /** Доступное имя, когда подпись не объясняет назначения. */
    ariaLabel?: string;
    /** Имя кнопки снятия целиком. Перебивает собранное из `label`. */
    removeLabel?: string;
    /**
     * `tabindex` кнопки снятия — для чипа внутри поля ввода.
     *
     * Там таб-стоп принадлежит `<input>`, а ряд чипов ходит стрелками: у
     * `GrInputTag` кольцо оставляет ровно один `0`, у `GrAutocomplete` их нет
     * вовсе. Проп нужен потому, что кольцом управляет родитель — он знает и
     * порядок чипов, и что стоит за краем ряда.
     */
    removeTabindex?: number;
}
GrChipEmitsinterface
interface GrChipEmits {
    /** Переключатель вне группы сменил состояние (`v-model:selected`); в группе выбор ведёт она. */
    (e: 'update:selected', value: boolean): void;
    /** Чип просят снять — крестиком или `Delete`/`Backspace`. Удалить его из списка — забота родителя. */
    (e: 'remove'): void;
}

Файлdist/types/components/GrChip/index.d.ts

GrChipInstancetype
type GrChipInstance = ComponentExposed<typeof GrChipComponent>;

Доступность

Паттерн APG

toggle button / option

КлавишаДействие
—Не выбираемый чип клавиш не имеет: интерактив в нём один — крестик, и он обычная кнопка со своей остановкой Tab. Выбираемый чип — кнопка: Enter/Space переключают
—Delete/Backspace снимают (если closable). Крестик у него в таб-порядке **не стоит**: внутри роли-виджета вложенная кнопка запрещена, и способ снять объявлен через aria-keyshortcuts

Использование из прозы библиотеки

Крестик — не всегда кнопка

Разметка чипа зависит от того, кликают ли по нему самому.

Чип не виджет (просто снимаемая метка) — корень <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.