🍰

ReactとChakra UIでカスタムラジオボタンの実装

2023/06/07に公開

はじめに

オリジナルアプリの開発で、入力フォームを実装。
フォームでアイコン選択をできるようにしたくて、ラジオボタンを使ってみたのですがイマイチ。

ラジオボタンとアイコンが並んでいると重複した感じがして違和感があるので、ラジオボタンの表示を消したいと思いました。

Chakra UIでカスタムラジオボタン(ラジオのように動作するが、ラジオのようには見えないコンポーネント)というものがあったので実装してみました。

公式ドキュメント参照:
https://chakra-ui.com/docs/components/radio/usage

開発環境

  • react: 18.2.0
  • chakra-ui/react:2.6.1

完成イメージ

完成イメージのソースコードはこちらです。

SelectRewardIcons.tsx
import { Box, HStack, Stack, useRadio, useRadioGroup } from "@chakra-ui/react";
import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
  faIceCream,
  faCookie,
  faMartiniGlass,
  faUtensils,
} from "@fortawesome/free-solid-svg-icons";

export const SelectRewardIcons = ({ setGoalsRewardItem }) => {
  function CustomRadio(props) { // ※(1)
    const { setGoalsRewardItem, ...radioProps } = props;
    const { state, getInputProps, getRadioProps } = useRadio(radioProps);

    return (
      <Box as="label">
        <input {...getInputProps({})} hidden />
        <Box
          {...getRadioProps()}
          cursor="pointer"
          borderWidth="1px"
          borderRadius="md"
          boxShadow="md"
          _checked={{
            bg: "teal.600",
            color: "white",
            borderColor: "teal.600",
          }}
          _focus={{
            boxShadow: "outline",
          }}
          px={5}
          py={3}
        >
          {props.children}
        </Box>
      </Box>
    );
  }

  const icons = [ // ※(2)
    {
      iconNumber: "1",
      name: "cookie",
      image: <FontAwesomeIcon icon={faCookie} />,
    },
    {
      iconNumber: "2",
      name: "iceCream",
      image: <FontAwesomeIcon icon={faIceCream} />,
    },
    {
      iconNumber: "3",
      name: "alcohol",
      image: <FontAwesomeIcon icon={faMartiniGlass} />,
    },
    {
      iconNumber: "4",
      name: "otherFood",
      image: <FontAwesomeIcon icon={faUtensils} />,
    },
  ];

  const handleChange = (value) => { // ※(3)
    console.log(value);
    setGoalsRewardItem(value);
  };

  const { value, getRadioProps, getRootProps } = useRadioGroup({ // ※(4)
    name: "reward",
    defaultValue: "iceCream",
    onChange: handleChange,
  });

  return (
    <Stack {...getRootProps()}>
      <HStack justify="space-around">
        {icons.map((icon) => { // ※(5)
          return (
            <CustomRadio
              key={icon.iconNumber}
              name={icon.name}
              image={icon.image}
              {...getRadioProps({ value: icon.iconNumber })}
              setGoalsRewardItem={setGoalsRewardItem}
            >
              {icon.image}
            </CustomRadio>
          );
        })}
      </HStack>
    </Stack>
  );
};

実装

■アイコン選択コンポーネントの作成

アイコンはFontawesomeを使っています。
Chakra UIが提供しているuseRadioとuseRadioGroupというHooksを使います。

(1)CustomRadio関数を用意

useRadioを使います。
これによって、一連のオプションの中で 1 つの選択肢しか選択できない機能を実装できます。

useRadioが返すプロパティ:
・state:ラジオの現在の状態を定義するすべてのプロパティを含むオブジェクト。
・getRadioProps:ラジオのプロパティを取得する関数。
・getInputProps:入力フィールドの小道具を取得する関数。

(2)表示したいアイコンを配列にして用意

(3)イベント発生時(アイコンを選択したとき)の処理をhandleChange関数で定義

ここでiconNumberの値を渡しています。

(4)useRadioGroupを用意

これによって、ラジオグループの状態管理・制御を行うことができます。

useRadioGroupが返すプロパティ:
・value:ラジオグループの値。
・name:ラジオオプションの名前。
・onChange:ラジオグループの onChange ハンドラー。
・getRadioProps:ルートプロパティを取得し、ラジオグループの変更を処理する関数(個々のラジオボタンに対して適用される)
・getRootProps:ラジオのルートプロパティを取得し、ラジオグループの変更を処理する関数(ラジオグループに対して適用される)
今回の場合、getRootPropsをStack要素に適用することで、その要素にグループ全体に関連するイベントハンドラやスタイルなどを設定しています。

(5)map関数でアイコンの情報を取り出して、(1)で書いたCustomRadio関数を適用

{icon.image}がCustomRadio関数の{props.children}に相当します。

■アイコン選択コンポーネントを入力フォームで呼び出す

setGoalsRewardItem={setGoalsRewardItem}によって、アイコンで選択した値をアイコン選択コンポーネントへ渡します。

CreateGoal.tsx
import React, { useState } from "react";

import {
  Box,
  Button,
  Card,
  CardBody,
  CardHeader,
  Flex,
  Heading,
  Input,
  Stack,
} from "@chakra-ui/react";
import { SelectRewardIcons } from "./molecules/SelectRewardIcons";

const CreateGoal = () => {
  const [todaysGoal, setTodaysGoal] = useState();
  const [goalsRewardItem, setGoalsRewardItem] = useState();
  const [goalsRewardMemo, setGoalsRewardMemo] = useState();

  // 略

  return (
    <Flex align="center" justify="center" height="100vh">
      <Box bg="gray.300" w="lg" p={5} borderRadius="lg">
        <Stack spacing={6}>
          <Card borderRadius="xl">
            <CardHeader pb={0}>
              <Heading as="h1" size="md">
                今日の目標は?
              </Heading>
            </CardHeader>
            <CardBody>
              <Input
                placeholder="内容を入力してください"
                shadow="sm"
                onChange={(e) => setTodaysGoal(e.target.value)}
              />
              {!todaysGoal && <p>入力してね!</p>}
            </CardBody>
          </Card>
          <Card borderRadius="xl">
            <CardHeader pb={0}>
              <Heading as="h1" size="md">
                ご褒美は何にしますか?
              </Heading>
            </CardHeader>
            <CardBody>
              <Stack spacing={4}>
	      // ここで渡します。
                <SelectRewardIcons setGoalsRewardItem={setGoalsRewardItem} />
                {!goalsRewardItem && <p>選んでね!</p>}
                <Input
                  placeholder="メモ"
                  shadow="sm"
                  onChange={(e) => setGoalsRewardMemo(e.target.value)}
                />
              </Stack>
            </CardBody>
          </Card>
          <Flex justify="center">
            <Button
              colorScheme="teal"
              size="md"
              fontSize="lg"
              color="white"
              px={7}
              _hover={{ opacity: 0.8 }}
              onClick={createTodaysGoal}
              disabled={!todaysGoal || !goalsRewardItem}
            >
              Save
            </Button>
          </Flex>
        </Stack>
      </Box>
    </Flex>
  );
};

export default CreateGoal;

以上で実装完了です!

Discussion