feat(list): drag and drop reordering (#DS-4454) - #1911
Conversation
Options of `kbq-list-selection` can be reordered by dragging or with `Alt` + arrow keys once `draggable` is set, and moved between lists connected via `connectedTo`. The list never mutates the projected data: it reports the move through the `dropped` event and the consumer applies it. The new position is announced to assistive tech only once that move has actually been applied, and `aria-keyshortcuts` advertises the keyboard alternative to dragging. Dragging inside `kbq-optgroup` or `cdk-virtual-scroll-viewport` reports indices that do not address the backing array, so both now warn in development mode.
There was a problem hiding this comment.
Pull request overview
Adds opt-in drag-and-drop (and keyboard) reordering/transfer for kbq-list-selection, including accessible announcements and documentation/examples, while keeping the list “data-immutable” (consumer applies reorders based on a dropped event).
Changes:
- Introduces
draggable,connectedTo, anddroppedonKbqListSelection, plus keyboard reordering/transfer viaAlt+ arrow keys and live-region announcements. - Adds styling for drag preview/placeholder/animations and updates a11y locale strings to support move announcements.
- Adds docs examples, dev-app demos, and unit/e2e coverage for drag/drop + keyboard flows.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/public_api_guard/components/list.api.md | Public API snapshot updated for new list drag/drop inputs/outputs and types. |
| tools/public_api_guard/components/core.api.md | Public API snapshot updated for new a11y locale key (listOptionMoved). |
| packages/e2e/routes.ts | Registers new list drag-and-drop E2E route/component. |
| packages/docs-examples/example-module.ts | Adds new live examples entries for draggable list variants. |
| packages/docs-examples/components/list/list-draggable/list-draggable-example.ts | New docs example: in-list reordering via dropped. |
| packages/docs-examples/components/list/list-draggable-connected/list-draggable-connected-example.ts | New docs example: transferring options between connected lists. |
| packages/docs-examples/components/list/index.ts | Exports and registers the new list draggable examples module-side. |
| packages/components/list/list.scss | Adds drag/drop interaction styling (placeholder, preview, cursor, transitions). |
| packages/components/list/list.ru.md | Documents draggable lists + keyboard shortcuts (RU). |
| packages/components/list/list.en.md | Documents draggable lists + keyboard shortcuts (EN). |
| packages/components/list/list-tokens.scss | Adds tokens for drag preview surface/shadow; ensures preview inherits tokens in <body>. |
| packages/components/list/list-selection.component.ts | Core implementation: CDK drag/drop wiring, keyboard move/transfer, dropped event, live-region announcements, dev warnings. |
| packages/components/list/list-selection.component.spec.ts | Unit + axe coverage for drag/drop opt-in, keyboard behavior, announcements, and shortcuts. |
| packages/components/list/e2e.ts | Adds E2E fixture component with two connected draggable lists and consumer-applied reorder/transfer. |
| packages/components/list/e2e.playwright-spec.ts | Adds Playwright coverage for pointer drag, keyboard reorder/transfer, and transition settling behavior. |
| packages/components/core/locales/types.ts | Extends KbqA11yLocaleConfiguration with listOptionMoved announcement template. |
| packages/components/core/locales/tk-TM.ts | Adds listOptionMoved locale string (tk-TM). |
| packages/components/core/locales/ru-RU.ts | Adds listOptionMoved locale string (ru-RU). |
| packages/components/core/locales/pt-BR.ts | Adds listOptionMoved locale string (pt-BR). |
| packages/components/core/locales/es-LA.ts | Adds listOptionMoved locale string (es-LA). |
| packages/components/core/locales/en-US.ts | Adds listOptionMoved locale string (en-US). |
| packages/components-dev/list/template.html | Adds dev-app demos for draggable reorder and connected transfer. |
| packages/components-dev/list/module.ts | Wires demo data + handlers for the new draggable behaviors in dev app. |
Suppressed comments (1)
packages/components/list/list-selection.component.ts:814
- To ensure unsupported-container warnings are emitted when
draggablebecomes enabled after initial render (e.g. dynamic bindings), callwarnOnUnsupportedDragContainer()fromsyncDraggableState(), which already runs whendraggable/disabledchange.
/** Keeps the underlying CDK directives in sync with the resolved `draggable` state. */
private syncDraggableState(): void {
this.dropList.disabled = !this.draggable;
this.options?.forEach((option) => option.syncDraggableState());
this.changeDetectorRef.markForCheck();
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Visit the preview URL for this PR (updated for commit c09a563): https://koobiq-next--prs-1911-o55jwo31.web.app (expires Fri, 21 Aug 2026 15:30:15 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: c9e37e518febda70d0317d07e8ceb35ac43c534c |
`KbqListOptionDragData` is only read inside the component file, so it no longer leaves the package and no longer widens the public API surface. `warnOnUnsupportedDragContainer()` now also runs from `syncDraggableState()`, so enabling `draggable` after the initial render still warns. It tolerates being called before the content children exist and reports each list only once.
Dragging an option no longer opens a gap. The surrounding options stay where they are, the dragged one keeps its slot as a faded row, and a line with a dot marks the position the option would land in — in the connected list too, and along the main axis of a horizontal list. CDK's sorting is switched off to keep the list still, which also makes the index it reports on drop unusable, so the target is derived from the pointer instead. The dragged option is left out of that measurement, which makes the resulting gap index the very index `moveItemInArray` and `transferArrayItem` expect. Three CDK behaviours had to be handled: `enter()` ignores `sortingDisabled` and pushes the placeholder into whichever list is hovered, so a foreign placeholder is hidden; `hasAnchor` keeps the faded row in the origin list during a transfer; and the drop animation is dropped, because it would fly the preview back to the slot the drag started from and read as a rejected drop.
The indicator is a plain line now. Removes the cap pseudo-element in both orientations and the size token that only fed it.
| event.preventDefault(); | ||
| } | ||
|
|
||
| if (this.draggable && isAltOnly(event) && [UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW].includes(keyCode)) { |
There was a problem hiding this comment.
Не работает на MacOS - происходит скролл страницы
There was a problem hiding this comment.
В гайде не было ничего про шорткаты, альт и стрелки — это опасные шорткаты, не рекомендую.
Альтернатива не продумана и пока никому не требовалась. Будет что-то более сложное. Или даже вход в режим перемещения по шорткатам.
Потенциально список может быть связан с инпутом как комбобокс, традиционно шорткаты с Alt должны управлять позицией каретки в ипнуте.
В Сафари вот вообще нельзя перезаписывать «быстрый скролл» по ALT+стрелки
…e-flows An overlay opened from inside a toast — a select panel, a dropdown menu — stayed where it was when another toast was dismissed, while its trigger slid up with the rest of the stack. Toasts are ordinary flow children, so a removal moves the others for as long as the leaving toast animates its height away, and CDK re-measures an overlay's origin only on a scroll or a viewport resize. The container already bridged that gap by faking a scroll event, but it only did so on the animation's start and done events: the panel was adrift for the 300ms in between, and a template toast, which carries no animation at all, never reported anything. The container now watches its own box instead, which tracks the reflow exactly, and merges that into `elementScrolled()` so `ScrollDispatcher` re-broadcasts it without a synthetic DOM event. `dispatchScrollEvent` is left in place, deprecated, because it is part of the public surface.
|
При сортировке перетаскванием нужно запретить ховер-эффекты на элементах списка. Тащить и видишь только полоску — позицию вставки и превью. Фон элементов списка под курсором не подсвечивается. Реагировать должны только элементы, в которые можно дропнуть и что-то будет. В данном примере дропнуть можно только в список или в соседний список. Подстветка может быть особенная, если дроп запрещен. (Возможно, что вообще ховеры в приложении надо отключать. Для тех жлементов на которые дроп не влиет. Но это я не уверен, можно обсудить) |
|
Вот еще для обсуждения. Полоска-индикатор должна показывает только позицию, которая изменит порядок элементов. Если перетаскиваемый элемент остаётся на прежнем месте, полоску не показываем. Пример: перетаскиваем элемент №2. перед №1 — показываем; Итого: если после отпускания порядок элементов не изменится, полоски быть не должно. ==== Так не везде, но я нашел такую реализацию https://atlassian.design/components/pragmatic-drag-and-drop/examples#list |
|
Нужна возможность запретить перетаскивание (смену порядка) для элемента. Не disabled, а просто обычное состояние, но именно перетаскивать нельзя. Такая возможность есть? |

Options of
kbq-list-selectioncan be reordered by dragging or withAlt+ arrow keys oncedraggableis set, and moved between lists connected viaconnectedTo.The list never mutates the projected data: it reports the move through the
droppedevent and the consumer applies it. The new position is announced to assistive tech only once that move has actually been applied, andaria-keyshortcutsadvertises the keyboard alternative to dragging.Dragging inside
kbq-optgrouporcdk-virtual-scroll-viewportreports indices that do not address the backing array, so both now warn in development mode.