Sortable | CoUI

Sortable

드래그 앤 드롭으로 항목 순서를 변경하는 정렬 컴포넌트

Sortable#

드래그 앤 드롭으로 항목의 순서를 변경할 수 있는 정렬 가능한 리스트 컴포넌트입니다.

Live Preview#

Web
Item 1
Item 2
Item 3
Flutter
Loading Flutter...
class SortableDefaultExample extends StatefulComponent {
  const SortableDefaultExample({super.key});

  @override
  State<SortableDefaultExample> createState() => _SortableDefaultExampleState();
}

class _SortableDefaultExampleState extends State<SortableDefaultExample> {
  List<String> _items = const ['Item 1', 'Item 2', 'Item 3'];

  void handleReorder(int oldIndex, int newIndex) {
    setState(() {
      final newItems = [..._items];
      final item = newItems.removeAt(oldIndex);
      newItems.insert(newIndex, item);
      _items = newItems;
    });
  }

  @override
  Component build(BuildContext context) {
    return CoSortable(
      itemCount: _items.length,
      onReorder: handleReorder,
      itemBuilder: (index, dragHandle, removeButton) => div(
        [
          dragHandle,
          span([Component.text(_items[index])]),
        ],
        classes: 'flex items-center flex-1',
      ),
    );
  }
}
class SortableDefaultExample extends StatefulWidget {
  const SortableDefaultExample({super.key});

  @override
  State<SortableDefaultExample> createState() => _SortableDefaultExampleState();
}

class _SortableDefaultExampleState extends State<SortableDefaultExample> {
  List<String> _items = const ['Item 1', 'Item 2', 'Item 3'];

  void handleReorder(int oldIndex, int newIndex) {
    setState(() {
      final newItems = [..._items];
      final item = newItems.removeAt(oldIndex);
      newItems.insert(newIndex, item);
      _items = newItems;
    });
  }

  @override
  Widget build(BuildContext context) {
    return CoSortable(
      itemCount: _items.length,
      onReorder: handleReorder,
      itemBuilder: (context, index, dragHandle, removeButton) {
        return Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            dragHandle,
            const SizedBox(width: 8),
            Text(_items[index]),
          ],
        );
      },
    );
  }
}

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 사용자가 항목의 우선순위나 순서를 직접 지정해야 하는 경우 (할 일 목록, 메뉴 순서 등)
  • 대시보드 위젯, 사이드바 메뉴 순서를 사용자가 커스터마이즈할 수 있어야 하는 경우
  • 카테고리나 태그의 노출 순서를 관리자가 직접 설정해야 하는 경우

대신 다른 컴포넌트를 사용하세요:

  • Table: 정렬이 필요 없는 단순 데이터 표시 테이블
  • Tree: 항목 간 계층 구조(부모-자식)가 있는 경우

기본 사용법 (Basic Usage)#

CoSortable(
  itemCount: items.length,
  onReorder: (oldIndex, newIndex) {
    setState(() {
      final newItems = [...items];
      final item = newItems.removeAt(oldIndex);
      newItems.insert(newIndex, item);
      items = newItems;
    });
  },
  itemBuilder: (context, index, dragHandle, removeButton) {
    return Row(
      children: [
        dragHandle,
        Expanded(child: Text(items[index])),
        if (removeButton != null) removeButton,
      ],
    );
  },
)
CoSortable(
  itemCount: items.length,
  onReorder: (oldIndex, newIndex) => reorderItems(oldIndex, newIndex),
  itemBuilder: (index, dragHandle, removeButton) => div(
    [
      dragHandle,
      span([text(items[index])]),
      if (removeButton != null) removeButton,
    ],
    classes: 'flex items-center flex-1',
  ),
)

Props / Parameters#

속성타입기본값설명
itemCountint필수정렬할 항목 수
itemBuilder Function(context, index, dragHandle, removeButton) 필수 항목 빌더 (드래그 핸들과 제거 버튼을 파라미터로 받음)
onReorder void Function(int oldIndex, int newIndex)? null 순서 변경 콜백
onRemove void Function(int index)? null 항목 제거 콜백 (removable true 시 필수)
enabled bool true 드래그 앤 드롭 활성화 여부
removable bool false 제거 버튼 표시 여부 (trueonRemove 필요)

제거 가능한 항목 (Removable Items)#

CoSortable(
  itemCount: items.length,
  onReorder: handleReorder,
  onRemove: (index) {
    setState(() {
      items = [
        ...items.sublist(0, index),
        ...items.sublist(index + 1),
      ];
    });
  },
  removable: true,
  itemBuilder: (context, index, dragHandle, removeButton) {
    return Row(
      children: [
        dragHandle,
        Expanded(child: Text(items[index])),
        if (removeButton != null) removeButton,
      ],
    );
  },
)

테마 커스터마이징 (Theme Customization)#

CoreComponentTheme.sortable을 통해 프로젝트 레벨에서 스타일을 오버라이드할 수 있습니다.

CoreComponentTheme(
  sortable: CoreSortableTheme(
    itemBackgroundColor: CoreColor(0xFFF5F5F5),
    itemBorderRadius: 8.0,
    handleColor: CoreColor(0xFF888888),
    gap: 12.0,
  ),
)

동작 스펙 (Behavior)#

인터랙션#

  • Flutter: 항목 롱프레스 후 드래그하여 순서 변경 (LongPressDraggable/DragTarget)
  • Web: HTML5 Drag and Drop API (draggable="true", dragover, drop)
  • 드래그 중 항목은 반투명 처리되며, 드롭 대상에는 강조 테두리 표시

접근성#

  • 루트 컨테이너에 role="list" 자동 적용
  • 각 항목에 role="listitem" 자동 적용
  • 드래그 핸들은 cursor: grab / cursor: grabbing 커서 표시

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
드래그 구현 LongPressDraggable + DragTarget HTML5 Drag and Drop API
드래그 핸들 아이콘Icons.drag_indicatorUnicode ⠁⠁⠁
제거 버튼 아이콘Icons.closeUnicode
  • Table: 데이터를 표 형태로 표시하며 컬럼 정렬이 필요한 경우
  • Tree: 계층 구조로 된 항목의 순서를 변경해야 하는 경우