GrSortableList
Берут, когда порядок задаёт пользователь.
Обзор
Когда брать
- порядок задаёт пользователь — приоритеты задач, поля отчёта, шаги маршрута, колонки конструктора;
- порядок сохраняется — модель меняется на отпускании, и её остаётся записать;
- перенос нужен с клавиатуры — ровно это и есть причина, по которой компонент живёт в дизайн-системе;
- тянуть надо за ручку —
handleOnlyоставляет текст выделяемым, а строку — кликабельной.
Когда взять другое
| Нужно | Берите |
|---|---|
| Переносить между двумя списками | GrTransfer |
| Порядок фиксирован | GrList |
| Элементы вложены | GrTree с draggable |
| Порядок колонок таблицы | GrDataTable |
| Виджеты на двумерной сетке | GrDashboard |
| Строки только выбирают, не двигают | GrDataTable |
Варианты все тона и виды — настоящим компонентом
variant
elevatedoutlinedghost
Состояния
обычнаяdisabled
Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrSortableList v-model="value" />Примеры 3 у каждого — живое демо и его код
Колонки выгрузки в CSV
Порядок здесь и есть значение: он становится первой строкой файла. В строках свои флажки и кнопки, поэтому перенос начинается только с ручки —
handle-onlyпо умолчанию;item-keyследит за колонкой по id.moveсообщает одну операцию вместо всего списка, а открытыеmove()иfocusItem()ставят колонку первой той же механикой.Columns of the CSV exportDrag a handle, or focus the list and press Space on a rowInvoice numberINV-2026-0418CustomerNorthwind TradersIssue date02 Oct 2026Due date01 Nov 2026Total€1,284.50VAT€240.18The first line of the fileInvoice number;Customer;Issue date;TotalКого будят первым
В строках нечего нажимать, поэтому
:handle-only="false"разрешает тянуть строку целиком.max-heightпревращает список в собственный скроллер, который сам прокручивается у краёв, пока строку несут;changeсообщает о завершённом переносе — момент сохранить, — а слотitemполучаетdraggingиgrabbed, чтобы отметить несомую строку.Escalation order · payments incidentsOrder saved · 7 people 1MOMina OrtizPayments · +351 912 004 118first2DKDaniel KimBilling · +44 7700 900 4313GWGrace WuPlatform · +353 87 555 01924JWJonas WeberPlatform · +49 151 2345 67895ATAiko TanakaBilling · +351 913 552 7016LMLucas MartinPayments · +33 6 12 34 56 787SLSara LindqvistSupport · +46 70 123 45 67Обложка объявления
orientation="horizontal"раскладывает фотографии рядом с переносом и переводит клавиатуру на ←/→; первая — обложка. Плиткам линии не нужны, поэтомуdividedвыключен. Опубликованное объявление держит порядок —disabledоставляет его читаемым, — а когда удалены все фотографии, слотemptyобъясняет, что это значит, и предлагает вернуть.2-room flat in Alfama · photos CoverLiving roomKitchenBedroomBalcony
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
modelValueобязательныйv-model | T[] | — | Набор в текущем порядке. `v-model`: наружу уходит новый массив, вход не мутируется. |
itemKey | string | ((item: T, index: number) => string | number) | undefined | undefined | Ключ элемента: имя поля или функция. Без него — индекс. |
disabled | boolean | undefined | false | Список только для чтения: ни указателем, ни с клавиатуры. |
orientation | GrSortableOrientation | undefined | "vertical" | Ось переноса. `horizontal` — ряд с переносом по ширине. |
variant | GrCardVariant | undefined | undefined | Поверхность под списком — вариант карточки. |
divided | boolean | undefined | true | Разделители между строками. |
handleOnly | boolean | undefined | true | Тянуть можно только за ручку. Выключено — тянется вся строка. |
maxHeight | string | number | undefined | undefined | Высота видимой части: список становится скроллером с автопрокруткой при переносе. |
emptyText | string | undefined | undefined | Текст пустого состояния. Слот `#empty` сильнее. |
ariaLabel | string | undefined | undefined | Имя списка для скринридера. Не задано — берётся из локали. |
Слоты
| Slot | Type | Описание |
|---|---|---|
item | { item: T; index: number; dragging: boolean; grabbed: boolean; } | Содержимое строки; без слота выводится сам элемент. `dragging` — строку тянут указателем, `grabbed` — она взята с клавиатуры. |
handle | { item: T; index: number; disabled: boolean; } | Значок ручки переноса. Рендерится внутрь кнопки: захват и подпись остаются за компонентом. |
empty | any | Пустое состояние вместо `emptyText`. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (e: "update:modelValue", value: T[]): void | Новый порядок после переноса — новый массив; исходный не мутируется. |
move | (e: "move", from: number, to: number): void | Строка перенесена с индекса `from` на `to`. Шлётся только по завершении переноса. |
change | (e: "change", value: T[]): void | Перестановка завершена — тот же массив, что ушёл в `update:modelValue`. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrSortableListProps, GrSortableListEmits, GrSortableOrientation } from '@feugene/granularity/components/GrSortableList'Файлdist/types/components/GrSortableList/GrSortableList.vue.d.ts
interface GrSortableListProps<TItem = unknown> {
/** Набор в текущем порядке. `v-model`: наружу уходит новый массив, вход не мутируется. */
modelValue: TItem[];
/** Ключ элемента: имя поля или функция. Без него — индекс. */
itemKey?: string | ((item: TItem, index: number) => string | number);interface GrSortableListEmits<TItem = unknown> {
/** Новый порядок после переноса — новый массив; исходный не мутируется. */
(e: 'update:modelValue', value: TItem[]): void;
/** Строка перенесена с индекса `from` на `to`. Шлётся только по завершении переноса. */
(e: 'move', from: number, to: number): void;
/** Перестановка завершена — тот же массив, что ушёл в `update:modelValue`. */
(e: 'change', value: TItem[]): void;
}Файлdist/types/components/GrSortableList/grSortableListStyles.d.ts
type GrSortableOrientation = 'horizontal' | 'vertical';Файлdist/types/components/GrSortableList/config.d.ts
const grSortableListConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSortableList">;Файлdist/types/components/GrSortableList/safelist.d.ts
const grSortableListSafelist: string[];Файлdist/types/components/GrSortableList/index.d.ts
type GrSortableListInstance = ComponentExposed<typeof GrSortableListComponent>;Доступность
Паттерн APG
list (roving tabindex)
Клавиши
↑ / ↓Space / EnterEscHome / End
| Клавиша | Действие |
|---|---|
| ↑ / ↓ | по строкам |
| Space / Enter | взять и положить, стрелки во взятом состоянии — двигать строку |
| Esc | отменить |
| Home / End | к краям; в горизонтальном списке ось стрелок — ←/→ |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Модель данных
v-model — массив в текущем порядке. Наружу уходит новый массив: вход не мутируется, поэтому
сравнение «было — стало» и история в сторе потребителя остаются рабочими. Рядом с
update:modelValue компонент отдаёт move с парой индексов — она удобнее, когда порядок хранится
на сервере и нужно послать одну операцию, а не весь список.
itemKey — имя поля или функция. Без него ключом становится индекс: для статичного набора это
нормально, но при добавлении и удалении элементов приведёт к лишним перерисовкам.
Клавиатура
| Клавиша | Действие |
|---|---|
Tab | одна остановка на весь список |
↑ / ↓ (в горизонтальном — ← / →) | перевести фокус между строками |
Space / Enter | взять строку · положить |
↑ / ↓ во взятом состоянии | двигать саму строку |
Esc | отменить перенос |
Home / End | к первой и последней строке |
Взятие, каждое движение, отпускание и отмена объявляются в живой регион
(useAnnouncer) — без этого клавиатурный перенос происходит вслепую. Уход
фокуса из списка снимает захват: строка не может остаться взятой навсегда.
Ручка
По умолчанию тянется только ручка (handleOnly). Так строка остаётся кликабельной, а внутри неё
можно держать ссылки и кнопки. :handle-only="false" делает перетаскиваемой всю строку — уместно
для коротких списков без интерактива внутри.
Ручка — кнопка вне таб-порядка (tabindex="-1"): один Tab на список принадлежит строке, а с
клавиатуры перенос начинается Space на ней же. Содержимое ручки заменяется слотом #handle.
Пропы, эмиты, слоты
Списка пропов здесь нет — он генерируется из исходников. Что стоит знать сверх сигнатур:
orientation="horizontal"переключает и раскладку, и ось клавиатуры разом;maxHeightпревращает список в скроллер и включает автопрокрутку у краёв при переносе;variantуезжает вGrCardпод списком, как уGrList;disabledзапрещает перенос обоими способами, но оставляет список читаемым; подложка ложится на весь список, а не на строки, поэтому размер поверхности не меняется — и в горизонтальной раскладке тоже;- expose:
move(from, to)— программная перестановка тем же путём, что и перенос,focusItem(index)— фокус на строку.
Границы
- Виртуализации нет. Перенос требует, чтобы цель была отрисована, а окно отсечения этого не
гарантирует. Для длинных списков без сортировки есть
GrListсvirtual.
Механика переноса вынесена в композабл useDragSort — если нужен свой список
со своей разметкой, стройте на нём, а не на этом компоненте.
Пропы этого примера