GrSortableList

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

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

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

Обзор

Когда брать

  • порядок задаёт пользователь — приоритеты задач, поля отчёта, шаги маршрута, колонки конструктора;
  • порядок сохраняется — модель меняется на отпускании, и её остаётся записать;
  • перенос нужен с клавиатуры — ровно это и есть причина, по которой компонент живёт в дизайн-системе;
  • тянуть надо за ручку — handleOnly оставляет текст выделяемым, а строку — кликабельной.

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

НужноБерите
Переносить между двумя спискамиGrTransfer
Порядок фиксированGrList
Элементы вложеныGrTree с draggable
Порядок колонок таблицыGrDataTable
Виджеты на двумерной сеткеGrDashboard
Строки только выбирают, не двигаютGrDataTable

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

variant

  • elevated
  • outlined
  • ghost

Состояния

  • обычная
  • disabled

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

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

Сцена

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

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

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

  • Колонки выгрузки в CSV

    Порядок здесь и есть значение: он становится первой строкой файла. В строках свои флажки и кнопки, поэтому перенос начинается только с ручки — handle-only по умолчанию; item-key следит за колонкой по id. move сообщает одну операцию вместо всего списка, а открытые move() и focusItem() ставят колонку первой той же механикой.

    Columns of the CSV export
    Invoice numberINV-2026-0418
    CustomerNorthwind Traders
    Issue date02 Oct 2026
    Due date01 Nov 2026
    Total€1,284.50
    VAT€240.18
    Drag a handle, or focus the list and press Space on a row
    The 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 118first
    2DKDaniel KimBilling · +44 7700 900 431
    3GWGrace WuPlatform · +353 87 555 0192
    4JWJonas WeberPlatform · +49 151 2345 6789
    5ATAiko TanakaBilling · +351 913 552 701
    6LMLucas MartinPayments · +33 6 12 34 56 78
    7SLSara LindqvistSupport · +46 70 123 45 67
  • Обложка объявления

    orientation="horizontal" раскладывает фотографии рядом с переносом и переводит клавиатуру на ←/→; первая — обложка. Плиткам линии не нужны, поэтому divided выключен. Опубликованное объявление держит порядок — disabled оставляет его читаемым, — а когда удалены все фотографии, слот empty объясняет, что это значит, и предлагает вернуть.

    2-room flat in Alfama · photos
    CoverLiving room
    Kitchen
    Bedroom
    Balcony

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

Props

PropTypeпо умолчаниюОписание
modelValueобязательныйv-modelT[]—Набор в текущем порядке. `v-model`: наружу уходит новый массив, вход не мутируется.
itemKeystring | ((item: T, index: number) => string | number) | undefinedundefinedКлюч элемента: имя поля или функция. Без него — индекс.
disabledboolean | undefinedfalseСписок только для чтения: ни указателем, ни с клавиатуры.
orientationGrSortableOrientation | undefined"vertical"Ось переноса. `horizontal` — ряд с переносом по ширине.
variantGrCardVariant | undefinedundefinedПоверхность под списком — вариант карточки.
dividedboolean | undefinedtrueРазделители между строками.
handleOnlyboolean | undefinedtrueТянуть можно только за ручку. Выключено — тянется вся строка.
maxHeightstring | number | undefinedundefinedВысота видимой части: список становится скроллером с автопрокруткой при переносе.
emptyTextstring | undefinedundefinedТекст пустого состояния. Слот `#empty` сильнее.
ariaLabelstring | undefinedundefinedИмя списка для скринридера. Не задано — берётся из локали.

Слоты

SlotTypeОписание
item{ item: T; index: number; dragging: boolean; grabbed: boolean; }Содержимое строки; без слота выводится сам элемент. `dragging` — строку тянут указателем, `grabbed` — она взята с клавиатуры.
handle{ item: T; index: number; disabled: boolean; }Значок ручки переноса. Рендерится внутрь кнопки: захват и подпись остаются за компонентом.
emptyanyПустое состояние вместо `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

GrSortableListPropsinterface
interface GrSortableListProps<TItem = unknown> {
    /** Набор в текущем порядке. `v-model`: наружу уходит новый массив, вход не мутируется. */
    modelValue: TItem[];
    /** Ключ элемента: имя поля или функция. Без него — индекс. */
    itemKey?: string | ((item: TItem, index: number) => string | number);
GrSortableListEmitsinterface
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

GrSortableOrientationtype
type GrSortableOrientation = 'horizontal' | 'vertical';

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

grSortableListConfigconst
const grSortableListConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSortableList">;

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

grSortableListSafelistconst
const grSortableListSafelist: string[];

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

GrSortableListInstancetype
type GrSortableListInstance = ComponentExposed<typeof GrSortableListComponent>;

Доступность

Паттерн APG

list (roving tabindex)

Клавиши

↑ / ↓Space / EnterEscHome / End

КлавишаДействие
↑ / ↓по строкам
Space / Enterвзять и положить, стрелки во взятом состоянии — двигать строку
Escотменить
Home / Endк краям; в горизонтальном списке ось стрелок — ←/→

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

Модель данных

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 — если нужен свой список со своей разметкой, стройте на нём, а не на этом компоненте.