Chakra-ui v3 の画面オブジェクトを移動させる

# 記事の目的
Chakra-ui v3 を併用して画面上を移動できるオブジェクトを作る必要があった。
Web上で調べてみたが、Chakra-uiを使って実装した記事がなく実装に苦労したため、備忘録として記録を残すことにした。 (同じように困っている人の役に立てば幸いです。)

# 実装要件
1. ある一定の枠内にて、ドラッグ / ドロップで移動できるオブジェクト(アイコン)を表示
1. オブジェクトが配置された、画面上の座標を取得

# 実装バージョン
- react : 19.0.0
- chakra-ui : 3.8.0
- typescript : 5.7.2

# 必要なライブラリ
- @chakra-ui/react
- react-draggable
- react-icons

```javascript
npm i @chakra-ui/react react-draggable react-icons
```

# 実装
useState を使用して座標を管理
```javascript
const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
```

useRef によるDOM要素への参照、操作
```javascript
const dragRef = useRef<HTMLDivElement>(null!);
const containerRef = useRef<HTMLDivElement>(null!);
```

ドラッグ終了時の処理
```javascript
const handleStop = (e: DraggableEvent, data: DraggableData) => {
  if (data?.x !== undefined && data?.y !== undefined) {
  setPosition({ x: data.x, y: data.y });
  }
};
```

移動可能な範囲の作成
```javascript
<Box
  ref={containerRef}
  w="400px"
  h="300px"
  overflow="hidden"
>
```

Draggable の設定
- デフォルトの位置
- 移動終了時の座標取得
- 枠内のみの移動制限
```javascript
<Draggable
  defaultPosition={{ x: 0, y: 0 }}
  onStop={handleStop}
  nodeRef={dragRef}
  bounds="parent" // 枠内のみ移動可能
  enableUserSelectHack={false} // タッチパネル対応
>
```

アイコンの台座を作成
```javascript
<Box
  ref={dragRef}
  color="white"
  display="flex"
  alignItems="center"
  justifyContent="center"
  borderRadius="md"
  cursor="grab" // hover時のマウスカーソルをグローブに変更
  position="absolute" // Draggable の bounds を機能させる
  touchAction="none" // タッチ操作を有効化
>
```

現在の座標を取得し、表示する
```javascript
<Text>
  X: {position.x.toFixed(0)}, Y: {position.y.toFixed(0)}
</text>
```

# 完成画面

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766363/c60b66d3-152d-43d2-ab78-2173d0514c11.png)

<details>
  <summary>完成コード</summary>

  ```javascript
  import { Text, Box } from "@chakra-ui/react";
  import { FC, memo, useRef, useState } from "react";
  import { AiFillEnvironment } from "react-icons/ai";

  export const Home:FC = () => {
    const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
    const dragRef = useRef<HTMLDivElement>(null!);
    const containerRef = useRef<HTMLDivElement>(null!);

    const handleStop = (e: DraggableEvent, data: DraggableData) => {
      if (data?.x !== undefined && data?.y !== undefined) {
        setPosition({ x: data.x, y: data.y });
      }
    };

    return (
      <>
        <Box p={5} position="relative">
          <Box
            ref={containerRef} // 制限する枠の ref
            w="400px"
            h="300px"
            bg="gray.100"
            position="relative"
            border="2px solid gray"
            borderRadius="md"
            overflow="hidden"
          >
            <Draggable
              defaultPosition={{ x: 0, y: 0 }}
              onStop={handleStop}
              nodeRef={dragRef}
              bounds="parent"
            >
              <Box
                ref={dragRef}
                color="white"
                display="flex"
                alignItems="center"
                justifyContent="center"
                cursor="grab"
                position="absolute"
              >
                <Icon color="gray.500" fontSize="30px" margin="auto">
                  <AiFillEnvironment  />
                </Icon>
              </Box>
            </Draggable>
          </Box>
          <Text mt={4}>X: {position.x.toFixed(0)}, Y: {position.y.toFixed(0)}</Text>
        </Box>
  ```
</details>

# 実装できなかったこと
## dnd-kit の使用
dnd-kit を使用して Dropperble 領域を作成したが、Draggable をうまく入れ子にできなかった。
コードが複雑化したため、諦めた。

## Draggable をそのままラップ
アイコンをそのまま Draggable でラップしたが、Chakra-uiによりエラーとなってしまった。

# 今後の実装
初期の座標をDBから取得、移動したオブジェクトの座標をDBに登録する処理を実装していきます。