임동욱 임동욱 09-14
feat: FoxSortableList의 끌기 손잡이를 없애고 항목 전체를 집어 끌게 한다
가맹몰 추가 이미지는 타일 자체를 잡아 좌우로 옮기고 하단 [저장] 한 번에 반영하는 방식이다
(사용자 확정). 왼쪽 위 손잡이 버튼을 지우고 li가 draggable·tabindex=0이 되어 마우스는
타일을, 키보드는 초점을 둔 항목을 방향키로 옮긴다. 본문 안 버튼·입력에서 올라오는 키는
받지 않는다.

Co-Authored-By: Claude Opus 5 
@bf310a25ed2eab8ada682badfd0626aca1cc8da8
@fox/core/components/fox-sortable-list/fox-sortable-list.tsx
--- @fox/core/components/fox-sortable-list/fox-sortable-list.tsx
+++ @fox/core/components/fox-sortable-list/fox-sortable-list.tsx
@@ -15,12 +15,12 @@
   items: readonly T[];
   /** 항목의 안정적인 키. 인덱스를 쓰면 옮긴 뒤 상태가 어긋난다. */
   getKey: (item: T) => string;
-  /** 항목 본문. 핸들은 목록이 그리므로 여기서 다시 그리지 않는다. */
+  /** 항목 본문. 항목 전체가 끌리므로 본문에 끌기 손잡이를 따로 두지 않는다. */
   renderItem: (item: T, index: number) => ReactNode;
   /** 순서가 바뀔 때마다 새 배열을 준다. 목록은 상태를 갖지 않는다. */
   onReorder: (next: T[]) => void;
   direction?: FoxSortableListDirection;
-  /** 목록 전체를 읽어 줄 이름. 핸들의 안내문에도 쓰인다. */
+  /** 목록 전체를 읽어 줄 이름. 항목의 안내문에도 쓰인다. */
   ariaLabel: string;
   disabled?: boolean;
   className?: string;
@@ -29,8 +29,13 @@
 /**
  * @fox 정렬 목록 — 항목의 **순서만** 바꾼다. 내용은 `renderItem`이 그린다.
  *
- * 마우스는 핸들을 끌어 놓고(네이티브 HTML5 드래그 — 의존성 없음), 키보드는 핸들에 초점을
- * 두고 방향키로 옮긴다. 둘 다 `onReorder`로 새 배열을 돌려주며 목록은 상태를 갖지 않는다.
+ * 마우스는 항목 자체를 잡아 끌어 놓고(네이티브 HTML5 드래그 — 의존성 없음), 키보드는 항목에
+ * 초점을 두고 방향키로 옮긴다. 둘 다 `onReorder`로 새 배열을 돌려주며 목록은 상태를 갖지 않는다.
+ * 손잡이(핸들)는 두지 않는다 — 타일을 통째로 집는 편이 자연스럽다(사용자 확정). 본문에 글을
+ * 입력하는 칸이 있는 목록이 생기면 그때 손잡이 변형을 더한다.
+ *
+ * 방향키는 항목 자신에 초점이 있을 때만 받는다 — 본문 안의 버튼·입력에서 올라오는 키는
+ * 그쪽 몫이다.
  *
  * 놓는 위치는 "끌고 있는 항목을 지금 가리키는 항목 자리로" 한 가지다 — 앞뒤 절반을 나누면
  * 타일 크기가 제각각인 곳에서 예측이 안 된다.
@@ -98,7 +103,10 @@
     setOverKey(null);
   }
 
-  function handleHandleKeyDown(event: KeyboardEvent<HTMLButtonElement>, index: number) {
+  function handleItemKeyDown(event: KeyboardEvent<HTMLLIElement>, index: number) {
+    if (event.target !== event.currentTarget) {
+      return;
+    }
     const backward = direction === "horizontal" ? "ArrowLeft" : "ArrowUp";
     const forward = direction === "horizontal" ? "ArrowRight" : "ArrowDown";
 
@@ -148,23 +156,16 @@
               isDragging && "fox-sortable-list__item--dragging",
               isOver && "fox-sortable-list__item--over",
             )}
+            tabIndex={disabled ? undefined : 0}
+            aria-label={disabled ? undefined : `${index + 1}번째 항목 — 끌거나 방향키로 순서 변경`}
+            aria-describedby={disabled ? undefined : hintId}
+            draggable={!disabled}
+            onDragStart={disabled ? undefined : (event) => handleDragStart(event, key)}
+            onDragEnd={disabled ? undefined : handleDragEnd}
             onDragOver={disabled ? undefined : (event) => handleDragOver(event, key)}
             onDrop={disabled ? undefined : (event) => handleDrop(event, key)}
+            onKeyDown={disabled ? undefined : (event) => handleItemKeyDown(event, index)}
           >
-            {!disabled && (
-              <button
-                type="button"
-                className="fox-sortable-list__handle"
-                aria-label={`${index + 1}번째 항목 순서 변경`}
-                aria-describedby={hintId}
-                draggable
-                onDragStart={(event) => handleDragStart(event, key)}
-                onDragEnd={handleDragEnd}
-                onKeyDown={(event) => handleHandleKeyDown(event, index)}
-              >
-                <i className="fox-ico fox-ico-DotsSixVertical" aria-hidden="true" />
-              </button>
-            )}
             <div className="fox-sortable-list__body">{renderItem(item, index)}</div>
           </li>
         );
@fox/dev-test/component-registry.tsx
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
@@ -4341,10 +4341,10 @@
     id: "fox-sortable-list",
     name: "FoxSortableList",
     description:
-      "항목 순서를 끌어서 바꾸는 목록입니다. 시안에 정렬 목록 컴포넌트가 없어 기획 문구(추가 이미지는 드래그로 순서를 바꾼다)만으로 만들었습니다 — 핸들 하나와 끌고 있는 항목·놓을 자리 표시가 전부이고 본문은 호출부가 그립니다. 핸들에 초점을 두고 방향키(가로는 ←→, 세로는 ↑↓)·Home·End로도 옮길 수 있어 마우스 없이도 됩니다. 목록은 상태를 갖지 않고 onReorder로 새 배열만 돌려줍니다.",
+      "항목 순서를 끌어서 바꾸는 목록입니다. 시안에 정렬 목록 컴포넌트가 없어 기획 문구(추가 이미지는 드래그로 순서를 바꾼다)만으로 만들었습니다 — 손잡이 없이 항목 자체를 집어 끌고, 끌고 있는 항목·놓을 자리 표시가 전부이며 본문은 호출부가 그립니다. 항목에 초점을 두고 방향키(가로는 ←→, 세로는 ↑↓)·Home·End로도 옮길 수 있어 마우스 없이도 됩니다. 목록은 상태를 갖지 않고 onReorder로 새 배열만 돌려줍니다.",
     variants: [
       {
-        label: "가로 — 핸들을 끌거나 초점을 두고 ←→",
+        label: "가로 — 타일을 끌거나 Tab으로 초점을 두고 ←→",
         node: <SortableListLiveDemo direction="horizontal" />,
       },
       {
@fox/styles/_fox-sortable-list.scss
--- @fox/styles/_fox-sortable-list.scss
+++ @fox/styles/_fox-sortable-list.scss
@@ -1,22 +1,19 @@
 // FoxSortableList — 항목 순서를 끌어서(또는 방향키로) 바꾸는 목록.
 //
 // 시안에 정렬 목록 컴포넌트가 없어 기획 문구("추가 이미지는 드래그로 순서를 바꾼다",
-// BO-MAL-002 ⑤)만으로 만들었다(사용자 확정). 모양은 최소한으로 둔다 — 핸들 하나와
-// 끌고 있는 항목·놓을 자리 표시뿐이고, 항목 본문은 호출부가 그린다.
+// BO-MAL-002 ⑤)만으로 만들었다(사용자 확정). 모양은 최소한으로 둔다 — 끌고 있는 항목·놓을
+// 자리 표시뿐이고, 항목 본문은 호출부가 그린다. 손잡이 없이 항목 전체를 집는다(사용자 확정).
 //
 // 마크업 계약 (React 밖 소비자용):
 //   <ul class="fox-sortable-list fox-sortable-list--horizontal" aria-label="…">
-//     <li class="fox-sortable-list__item [--dragging|--over]">
-//       <button class="fox-sortable-list__handle" type="button" draggable aria-label="…">
-//         <i class="fox-ico fox-ico-DotsSixVertical"></i>
-//       </button>
+//     <li class="fox-sortable-list__item [--dragging|--over]" tabindex="0" draggable aria-label="…">
 //       <div class="fox-sortable-list__body">…항목 본문…</div>
 //     </li>
 //   </ul>
 //
 // 토큰이 아닌 값:
 //   - 끌고 있는 항목의 opacity: 상태 표현이라 색 토큰이 없다. 관례값 0.4.
-//   - 버튼 초기화(appearance/cursor/색 상속): 구조 규칙.
+//   - cursor grab/grabbing: 구조 규칙.
 
 @use "abstracts" as fox;
 
@@ -39,11 +36,20 @@
     position: relative;
     display: flex;
     align-items: flex-start;
-    gap: fox.gap(2);
     border-radius: fox.radius(3);
     // 놓을 자리 테두리가 나타날 때 항목이 밀리지 않도록 투명 테두리를 미리 둔다.
     outline: fox.border(2) solid transparent;
     outline-offset: fox.number(2);
+    cursor: grab;
+
+    &:active {
+      cursor: grabbing;
+    }
+
+    // 키보드로 잡았을 때 — 초점 테두리가 놓을 자리 표시와 같은 자리를 쓴다.
+    &:focus-visible {
+      outline-color: fox.color(border-theme-primary);
+    }
 
     &--dragging {
       opacity: 0.4;
@@ -54,31 +60,6 @@
     }
   }
 
-  &__handle {
-    display: inline-flex;
-    flex-shrink: 0;
-    align-items: center;
-    justify-content: center;
-    padding: fox.padding(1);
-    margin: 0;
-    border: 0;
-    border-radius: fox.radius(2);
-    background: transparent;
-    color: fox.color(icon-neutral-subtle);
-    font-size: fox.icon(3);
-    cursor: grab;
-    appearance: none;
-
-    &:hover {
-      background: fox.color(action-default-hover);
-      color: fox.color(icon-neutral-default);
-    }
-
-    &:active {
-      cursor: grabbing;
-    }
-  }
-
   &__body {
     min-inline-size: 0;
     flex: 1 1 auto;
@@ -86,5 +67,6 @@
 
   &--disabled &__item {
     outline: 0;
+    cursor: default;
   }
 }
Add a comment
List