GrColorPicker

  • ядро
  • Формы
  • API · 15 props
  • ⌨ клавиши

Берут, когда цвет задаёт пользователь.

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

Обзор

Когда брать

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

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

НужноБерите
Выбор из нескольких фиксированных цветовGrRadioGroup / GrSegmented
Цвет — тон компонента из палитры темыпроп tone нужного компонента
Значение — произвольная строкаGrInput

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

view

  • sliders
  • area

Размеры

  • xs
  • sm
  • md
  • lg

Состояния

  • обычная
  • disabled
  • readonly
  • invalid

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

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

Сцена

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

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

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

  • Фирменный цвет, который обязан оставаться читаемым

    Значение — hex-строка: её читают CSS, токены и сервер. view="area" даёт квадрат насыщенность × светлота, presets — фирменную палитру, eyedropper — пипетку там, где она есть у браузера, а name отправляет значение с нативной формой. Белому тексту нужно 4,5:1, поэтому цвет, который не проходит, — invalid, а строка под ним говорит почему; превью показывает, куда цвет попадёт.

    BrandingShown on invoices, payment pages and emails
    White text 5.5:1 — readable
    Acme BillingInvoice INV-0418
    Amount due€1,284.50
  • Цвета меток в списке

    Маленький пикер pill помещается в строку списка, а placement="bottom-end" раскрывает панель внутрь от правого края. Системная метка — readonly: панель открывается и показывает цвет, но значение не меняется.

    LabelsBilling web · project board
    • Bug · system
      Только для чтения
    • Design
    • Backend
    • Waiting on customer
  • Вуаль с прозрачностью

    alpha добавляет четвёртый бегунок и делает значение #RRGGBBAA, которое CSS читает как есть: фото просвечивает сквозь вуаль, а заголовок остаётся читаемым. Пресеты несут свою прозрачность.

    Sign-in pageOverlay over the background photo
    Welcome back
    #0f172abf

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

Props

PropTypeпо умолчаниюОписание
placement"bottom-start" | "bottom-end" | "top-start" | "top-end" | undefined"bottom-start"Сторона, с которой раскрывается панель.
disabledboolean | undefinedfalseПоле недоступно: триггер выключен, панель не открывается. Включается и `disabled` у `GrFormField`.
openv-modelboolean | undefinedundefinedКонтролируемое состояние панели (`v-model:open`).
readonlyboolean | undefinedfalseТолько для чтения: цвет видно, панель открывается, но значение не меняется.
invalidboolean | undefinedfalseСостояние ошибки: рамка цвета ошибки и `aria-invalid`. Включается и ошибкой `GrFormField`.
requiredboolean | undefinedfalseОбязательное поле. `aria-required` у кнопки-триггера невалиден, поэтому обязательность уходит в её описание для скринридера.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedВысота триггера и масштаб панели по шкале контролов. Не задан — из `GrConfigProvider`, иначе `md`.
ariaLabelstring | undefinedundefinedДоступное имя триггера вне `GrFormField` — когда видимой подписи рядом нет.
namestring | undefinedundefinedИмя для нативной формы: значение уходит скрытым полем.
shape"box" | "pill" | undefinedundefinedФорма рамки. `box` — скругление шкалы контролов; `pill` — пилюля.
alphaboolean | undefinedfalseЧетвёртый слайдер и восьмизначная форма hex.
view"sliders" | "area" | undefinedundefinedКак задаются насыщенность и светлота: двумя бегунками или квадратом. Оттенок и прозрачность остаются бегунками в обоих видах.
eyedropperboolean | undefinedundefinedКнопка пипетки. Рисуется только там, где браузер даёт `EyeDropper`, — сегодня это Chromium.
presetsstring[] | undefined[]Палитра быстрого выбора. Пусто — блок не рендерится.

События

EventСигнатураОписание
update:modelValue(event: "update:modelValue", value: string): void—
change(event: "change", value: string): void—
update:open(event: "update:open", value: boolean): void—
focus(event: "focus", event: FocusEvent): void—
blur(event: "blur", event: FocusEvent): void—

Методы

MethodsСигнатураОписание
focus() => voidФокус на кнопку-триггер из кода.
blur() => voidСнять фокус с кнопки-триггера.

Типы

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

Импорт
import type { GrColorPickerConfigurableProps, GrColorPickerProps, GrColorPickerEmits } from '@feugene/granularity/components/GrColorPicker'

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

grColorPickerConfigconst
const grColorPickerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrColorPicker">;

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

GrColorPickerConfigurablePropsinterface

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

interface GrColorPickerConfigurableProps {
    size: GrColorPickerSize;
    /** Форма рамки — см. `components/shared/controlShape.ts`. */
    shape: GrControlShape;
    view: GrColorPickerView;
    eyedropper: boolean;
}

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

GrColorPickerPropsinterface
interface GrColorPickerProps {
    /** Цвет в hex: `#RRGGBB`, а при `alpha` — `#RRGGBBAA`. Мусор не роняет компонент. */
    modelValue: string;
    /** Четвёртый слайдер и восьмизначная форма hex. */
    alpha?: boolean;
    /**
     * Как задаются насыщенность и светлота: двумя бегунками или квадратом.
     * Оттенок и прозрачность остаются бегунками в обоих видах.
     */
    view?: GrColorPickerView;
    /**
     * Кнопка пипетки. Рисуется только там, где браузер даёт `EyeDropper`, —
     * сегодня это Chromium.
     */
    eyedropper?: boolean;
    /** Палитра быстрого выбора. Пусто — блок не рендерится. */
    presets?: string[];
    /**
     * Высота триггера и масштаб панели по шкале контролов.
     * Не задан — из `GrConfigProvider`, иначе `md`.
     */
    size?: GrColorPickerSize;
    /** Форма рамки. `box` — скругление шкалы контролов; `pill` — пилюля. */
    shape?: GrControlShape;
    /** Контролируемое состояние панели (`v-model:open`). */
    open?: boolean;
    /** Сторона, с которой раскрывается панель. */
    placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
    /** Имя для нативной формы: значение уходит скрытым полем. */
    name?: string;
    /**
     * Поле недоступно: триггер выключен, панель не открывается.
     * Включается и `disabled` у `GrFormField`.
     */
    disabled?: boolean;
    /** Только для чтения: цвет видно, панель открывается, но значение не меняется. */
    readonly?: boolean;
    /** Состояние ошибки: рамка цвета ошибки и `aria-invalid`. Включается и ошибкой `GrFormField`. */
    invalid?: boolean;
    /**
     * Обязательное поле. `aria-required` у кнопки-триггера невалиден, поэтому
     * обязательность уходит в её описание для скринридера.
     */
    required?: boolean;
    /** Доступное имя триггера вне `GrFormField` — когда видимой подписи рядом нет. */
    ariaLabel?: string;
}
GrColorPickerEmitsinterface
interface GrColorPickerEmits {
    /** Новый цвет в hex — на каждом шаге бегунка, выборе из палитры и валидном вводе в поле hex. */
    (e: 'update:modelValue', value: string): void;
    /** Цвет изменился — тот же hex и в те же моменты, что у `update:modelValue`. */
    (e: 'change', value: string): void;
    /** Панель открылась или закрылась (`v-model:open`). */
    (e: 'update:open', value: boolean): void;
    /** Фокус пришёл на кнопку-триггер. */
    (e: 'focus', event: FocusEvent): void;
    /** Фокус ушёл с кнопки-триггера — в том числе в открытую панель. */
    (e: 'blur', event: FocusEvent): void;
}

Файлdist/types/components/GrColorPicker/grColorPickerStyles.d.ts

GrColorPickerSizetype
type GrColorPickerSize = GrComponentSize;

Файлdist/types/components/GrColorPicker/color.d.ts

GrHslatype

Арифметика цвета для GrColorPicker — чистый модуль без Vue. Модель компонента — hex-строка (в такой форме цвет лежит в токенах темы), а панель работает в HSL: у оттенка, насыщенности и светлоты есть шкала, по которой можно вести слайдер, а у каналов RGB её нет. Свой парсер, а не __tests__/cssContrast.ts: тот — инфраструктура гейтов, и тянуть тестовый модуль в поставляемый код нельзя.

type GrHsla = {
    /** Оттенок, 0…359 (круговой: 360 — это 0). */
    h: number;
    /** Насыщенность, 0…100. */
    s: number;
    /** Светлота, 0…100. */
    l: number;
    /** Непрозрачность, 0…1. */
    a: number;
};

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

grColorPickerSafelistconst
const grColorPickerSafelist: string[];

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

GrColorPickerInstancetype
type GrColorPickerInstance = ComponentExposed<typeof GrColorPickerComponent>;

Доступность

Паттерн APG

dialog + slider

Клавиши

EscTab↑ / ↓

КлавишаДействие
—Enter/Space на триггере — открыть панель
Escзакрыть и вернуть фокус на триггер
Tabуйти из панели дальше по странице; внутри каналов — клавиши GrSlider. При view="area" вместо бегунков S и L стоит квадрат: ←/→ — насыщенность
↑ / ↓светлота
—Home/End и PageUp/PageDown — по оси сфокусированного поля. Осей две, и остановок Tab тоже две (как у диапазона GrSlider); стрелка поперёк оси уводит фокус на поле изменившегося канала — иначе диктор промолчал бы о нём

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

Модель — hex-строка

modelValue — #RRGGBB, а при alpha — #RRGGBBAA. Это та форма, в которой цвет лежит в токенах темы, приезжает с бэкенда и понимается CSS: потребителю не приходится конвертировать ни на входе, ни на выходе.

Невалидное значение не роняет компонент и ничего не эмитит: панель показывает #000000, модель остаётся как есть. Молча переписывать чужие данные компонент не вправе.

Внутри цвет живёт в HSLA и отдельно от модели, потому что hex — проекция с потерями: у чёрного, белого и любого серого нет оттенка. Держи компонент состояние только в hex — бегунок оттенка прыгал бы на 0° каждый раз, когда пользователь уводит насыщенность в ноль.

Каналы при этом хранятся дробными. Целая сетка HSL мельче сетки hex, и посадка на неё меняет цвет: из двенадцати типовых брендовых значений одиннадцать возвращались бы другими — #10b981 как #10b77f. Целые нужны шкалам, и их получают шкалы: бегунок читает округление своего канала, а хранимое значение остаётся точным. Поэтому выбранный пресет, вставленный hex и взятый пипеткой цвет возвращаются ровно теми же.

Два вида панели

view решает, чем задаются насыщенность и светлота. Оттенок и прозрачность — бегунки в обоих видах.

viewЧто в панели
sliders (по умолчанию)лестница каналов: H, S, L и A — каждый GrSlider
areaквадрат насыщенность × светлота вместо S и L

Дефолт остался прежним намеренно: вид меняет облик уже стоящих в приложениях панелей, и решение это приложения, а не пакета. Переключается он и глобально — <GrConfigProvider :component-defaults="{ GrColorPicker: { view: 'area' } }">.

Квадрат озвучен, а не только нарисован

Обычная беда 2D-области — что она мышиная: у неё нет ни значений, которые прочитает диктор, ни клавиш. Здесь внутри квадрата два настоящих input[type=range], по одному на ось: они визуально скрыты, но остаются в таб-порядке и со своими именами и значениями, а фокус показывает сама область через focus-within. Диктор видит два слайдера — «Saturation 90 %» и «Lightness 66 %», — а не безымянную картинку.

Отсюда и две остановки Tab на квадрат: осей две, как бегунков у диапазона GrSlider. Клавиши — ../keyboard.md.

Заливка квадрата точна, а не похожа

Квадрат собран тремя слоями: шкала насыщенности при светлоте 50 % и пара вуалей, белой сверху и чёрной снизу. Это не приём «на глаз»: осветление и затемнение в HSL — линейная интерполяция к белому и к чёрному, ровно то, что делает наложение с альфой. Цвет под ручкой совпадает с hex до последнего разряда, и это проверяется пикселем в живом браузере.

Оси — насыщенность и светлота, а не привычные по HSV насыщенность и яркость. Перевод в HSV дал бы более нарядную заливку, но принёс бы вторую проекцию с потерями: при нулевой яркости насыщенность HSV не определена, и чёрный терял бы её так же, как серый теряет оттенок.

Пипетка

eyedropper добавляет кнопку рядом с hex-полем. Она рисуется только там, где браузер даёт EyeDropper — сегодня это Chromium; в остальных её нет вовсе, а не есть и не работает. Отказ пользователя (Esc во время выбора) ничего не меняет и ни о чём не сообщает: это не ошибка.

С экрана приходит уже смешанный цвет, своей прозрачности у него нет — текущая остаётся как была.

Панель немодальная

Tab из панели уводит фокус дальше по странице, а не запирает в ней: за страницей ничего не блокируется, и запирать пользователя не за что. Esc закрывает панель и возвращает фокус на триггер — это делает общий стек слоёв через GrPopover.

Открытием можно управлять снаружи: v-model:open, положение — placement.

Пресеты

presets — массив hex-строк. Невалидные отсеиваются, у выбранного пресета aria-pressed. Пусто — блока пресетов нет вовсе.

Токены темы в presets не подставляются: --gr-primary живёт CSS-переменной и на сборке в hex не разрешается. Палитру собирает приложение — из своего конфига или из getComputedStyle.

Форма

Контракт форм-контрола целиком: disabled, readonly, invalid, required, ariaLabel, эмиты update:modelValue/change/focus/blur, экспортируемые focus()/blur(). Имя берётся от GrFormField (<label for> указывает на триггер) либо из ariaLabel.

name отдаёт текущее значение в нативную форму скрытым полем — внутри виджета интерактивных элементов быть не должно.

clearable нет намеренно: у цвета не бывает пустого состояния. Нужна «не задано» — это undefined в модели на уровне приложения, а не состояние контрола.

Границы
  • истории недавних цветов нет — это состояние приложения;
  • градиентов нет — компонент про один цвет.