4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Chakra UI v3 × jest】テスト時のSelectの型エラーの解消方法①(Type '{ children: Element; }' has no properties in common with type 'IntrinsicAttributes & SelectTriggerProps & RefAttributes<HTMLButtonElement>'. 他)

Last updated at Posted at 2025-01-11

はじめに

お疲れ様です、りつです。

今回はテスト実行時に発生したChakra UI v3Selectの型エラーの解消方法について記事にまとめました。

問題

テスト実施時に、以下のようなエラーが発生しました。
今回は以下の4つのエラーの解消方法を共有いたします。

エラー内容
 FAIL  src/__tests__/sampleComponent.spec.tsx
  ● Test suite failed to run

    src/components/pages/Register.tsx:101:28 - error TS2559: Type '{ children: Element; }' has no properties in common with type 'IntrinsicAttributes & SelectTriggerProps & RefAttributes<HTMLButtonElement>'.

    101                           <SelectTrigger>
                                   ~~~~~~~~~~~~~
    src/components/pages/Register.tsx:102:46 - error TS2322: Type '{ placeholder: string; }' is not assignable to type 'IntrinsicAttributes & SelectValueTextProps & RefAttributes<HTMLSpanElement>'.
      Property 'placeholder' does not exist on type 'IntrinsicAttributes & SelectValueTextProps & RefAttributes<HTMLSpanElement>'.

    102                             <SelectValueText placeholder="Select Option" />
                                                     ~~~~~~~~~~~
    src/components/pages/Register.tsx:104:28 - error TS2559: Type '{ children: any; }' has no properties in common with type 'IntrinsicAttributes & SelectContentProps & RefAttributes<HTMLDivElement>'.

    104                           <SelectContent>
                                   ~~~~~~~~~~~~~
    src/components/pages/Register.tsx:106:32 - error TS2322: Type '{ children: any; item: any; key: any; }' is not assignable to type 'IntrinsicAttributes & SelectItemProps & RefAttributes<HTMLDivElement>'.
      Property 'children' does not exist on type 'IntrinsicAttributes & SelectItemProps & RefAttributes<HTMLDivElement>'.

    106                               <SelectItem item={skill} key={skill.value}>
                                       ~~~~~~~~~~

Test Suites: 1 failed, 1 total
参考ファイル
src/App.tsx
import { Router } from '@/router/Router';
import { Toaster } from '@/components/ui/toaster';

function App() {
  return (
    <>
      <Router />
      <Toaster />
    </>
  );
}

export default App;
src/components/pages/Register.tsx
import React, { memo, useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import { Controller, useForm } from 'react-hook-form';
import { Button, Card, Center, createListCollection, Heading, Textarea, Input, Stack, ListCollection } from '@chakra-ui/react';
import { Field } from '@/components/ui/field';
import { SelectContent, SelectItem, SelectRoot, SelectTrigger, SelectValueText } from '@/components/ui/select';
import { FormData } from '@/domain/formData';
import { useMessage } from '@/hooks/useMessage';
import { fetchSkills, insertUser } from '@/utils/supabaseFunctions';

export const Register: React.FC = memo(() => {
  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>();
  const navigate = useNavigate();
  const { showMessage } = useMessage();

  const [skills, setSkills] = useState<ListCollection<{ label: string; value: string }> | null>(null);

  useEffect(() => {
    fetchSkills()
      .then((data) => {
        setSkills(createListCollection({ items: data.map((skill) => ({ label: skill.name, value: skill.id.toString() })) }));
      })
      .catch(() => {
        showMessage({ title: 'データの取得に失敗しました', type: 'error' });
      });
  }, []);

  const onSubmit = handleSubmit((data: FormData) => {
    insertUser(data)
      .then(() => {
        showMessage({ title: '登録が完了しました', type: 'success' });
        navigate('/');
      })
      .catch(() => {
        showMessage({ title: '登録に失敗しました', type: 'error' });
      });
  });

  return (
    <>
      <Center my="5">
        <Stack>
          <Heading as="h1" mb="2" textAlign="center">
            名刺新規登録
          </Heading>
          <Card.Root width="340px" variant="elevated">
            <form onSubmit={onSubmit}>
              <Card.Body>
                <Stack gap="4" w="full">
                  <Field label="好きな英単語 *" invalid={!!errors.user_id} errorText={errors.user_id?.message}>
                    <Controller
                      name="user_id"
                      control={control}
                      rules={{
                        required: '好きな英単語の入力は必須です',
                        pattern: { value: /^[a-zA-Z]+$/, message: '好きな英単語は半角英字で入力してください。' },
                      }}
                      render={({ field }) => <Input {...field} placeholder="coffee" />}
                    />
                  </Field>
                  <Field label="名前 *" invalid={!!errors.name} errorText={errors.name?.message}>
                    <Controller
                      name="name"
                      control={control}
                      rules={{
                        required: '名前の入力は必須です',
                      }}
                      render={({ field }) => <Input {...field} />}
                    />
                  </Field>
                  <Field label="自己紹介 *" invalid={!!errors.description} errorText={errors.description?.message}>
                    <Controller
                      name="description"
                      control={control}
                      rules={{
                        required: '自己紹介の入力は必須です',
                      }}
                      render={({ field }) => <Textarea {...field} placeholder="<h1>HTMLタグも使えます</h1>" />}
                    />
                  </Field>
                  <Field label="好きな技術" invalid={!!errors.skills} errorText={errors.skills?.message}>
                    <Controller
                      name="skills"
                      control={control}
                      rules={{
                        required: '好きな技術の入力は必須です',
                      }}
                      render={({ field }) => (
                        <SelectRoot
                          name={field.name}
                          value={field.value}
                          onValueChange={({ value }) => field.onChange(value)}
                          onInteractOutside={() => field.onBlur()}
                          multiple
                          collection={skills || createListCollection({ items: [] })}
                        >
                          <SelectTrigger>
                            <SelectValueText placeholder="Select Option" />
                          </SelectTrigger>
                          <SelectContent>
                            {skills?.items.map((skill) => (
                              <SelectItem item={skill} key={skill.value}>
                                {skill.label}
                              </SelectItem>
                            ))}
                          </SelectContent>
                        </SelectRoot>
                      )}
                    />
                  </Field>
                  <Field label="GitHub ID">
                    <Controller name="github_id" control={control} render={({ field }) => <Input {...field} />} />
                  </Field>
                  <Field label="Qiita ID">
                    <Controller name="qiita_id" control={control} render={({ field }) => <Input {...field} />} />
                  </Field>
                  <Field label="X ID">
                    <Controller name="x_id" control={control} render={({ field }) => <Input {...field} />} />
                  </Field>
                  *は必須項目です
                </Stack>
              </Card.Body>
              <Card.Footer justifyContent="flex-end">
                <Button variant="solid" type="submit" aria-label="Submit" colorPalette="cyan" w="full">
                  登録
                </Button>
              </Card.Footer>
            </form>
          </Card.Root>
        </Stack>
      </Center>
    </>
  );
});
src/__tests__/sampleComponent.spec.tsx
import App from '../App';
import { render, screen } from '@testing-library/react';
import { ChakraProvider, defaultSystem } from '@chakra-ui/react';
import { BrowserRouter } from 'react-router';

describe('App', () => {
  test('タイトルがあること', async () => {
    render(
      <BrowserRouter>
        <ChakraProvider value={defaultSystem}>
          <App />
        </ChakraProvider>
      </BrowserRouter>
    );
    const title = screen.getByTestId('title');
    expect(title).toBeInTheDocument();
  });
});

解決方法

src/components/ui/select.tsxの内容を修正します。
なお、コメントの番号は、何番目のエラーに対応するかを表しています。

react:src/components/ui/select.tsx
'use client';

- import type { CollectionItem } from '@chakra-ui/react';
+ import type { CollectionItem, SelectItemProps } from '@chakra-ui/react'; // 4. SelectItemProps型をインポート
import { Select as ChakraSelect, Portal } from '@chakra-ui/react';
import { CloseButton } from './close-button';
import * as React from 'react';

interface SelectTriggerProps extends ChakraSelect.ControlProps {
  clearable?: boolean;
+ children?: React.ReactNode; // 1. children を追加
}

export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerProps>(function SelectTrigger(props, ref) {
  const { children, clearable, ...rest } = props;
  return (
    <ChakraSelect.Control {...rest}>
      <ChakraSelect.Trigger ref={ref}>{children}</ChakraSelect.Trigger>
      <ChakraSelect.IndicatorGroup>
        {clearable && <SelectClearTrigger />}
        <ChakraSelect.Indicator />
      </ChakraSelect.IndicatorGroup>
    </ChakraSelect.Control>
  );
});

const SelectClearTrigger = React.forwardRef<HTMLButtonElement, ChakraSelect.ClearTriggerProps>(function SelectClearTrigger(props, ref) {
  return (
    <ChakraSelect.ClearTrigger asChild {...props} ref={ref}>
      <CloseButton size="xs" variant="plain" focusVisibleRing="inside" focusRingWidth="2px" pointerEvents="auto" />
    </ChakraSelect.ClearTrigger>
  );
});

interface SelectContentProps extends ChakraSelect.ContentProps {
  portalled?: boolean;
  portalRef?: React.RefObject<HTMLElement>;
+ children?: React.ReactNode; // 3. children を追加
}

export const SelectContent = React.forwardRef<HTMLDivElement, SelectContentProps>(function SelectContent(props, ref) {
  const { portalled = true, portalRef, ...rest } = props;
  return (
    <Portal disabled={!portalled} container={portalRef}>
      <ChakraSelect.Positioner>
        <ChakraSelect.Content {...rest} ref={ref} />
      </ChakraSelect.Positioner>
    </Portal>
  );
});

+ // 4. SelectItemPropsを継承した新しいインターフェースを定義し、必要な項目を追加
+ interface NewSelectItemProps extends SelectItemProps {
+  children?: React.ReactNode;
+  item: CollectionItem;
+  key?: string;
+ }

- export const SelectItem = React.forwardRef<HTMLDivElement, ChakraSelect.ItemProps>(function SelectItem(props, ref) {
+ export const SelectItem = React.forwardRef<HTMLDivElement, NewSelectItemProps>(function SelectItem(props, ref) { // 4. NewSelectItemPropsに置き換え

  const { item, children, ...rest } = props;
  return (
    <ChakraSelect.Item key={item.value} item={item} {...rest} ref={ref}>
      {children}
      <ChakraSelect.ItemIndicator />
    </ChakraSelect.Item>
  );
});

interface SelectValueTextProps extends Omit<ChakraSelect.ValueTextProps, 'children'> {
  children?(items: CollectionItem[]): React.ReactNode;
+ placeholder: string; // 2. placeholder を追加
}

export const SelectValueText = React.forwardRef<HTMLSpanElement, SelectValueTextProps>(function SelectValueText(props, ref) {
  const { children, ...rest } = props;
  return (
    <ChakraSelect.ValueText {...rest} ref={ref}>
      <ChakraSelect.Context>
        {(select) => {
          const items = select.selectedItems;
          if (items.length === 0) return props.placeholder;
          if (children) return children(items);
          if (items.length === 1) return select.collection.stringifyItem(items[0]);
          return `${items.length} selected`;
        }}
      </ChakraSelect.Context>
    </ChakraSelect.ValueText>
  );
});

export const SelectRoot = React.forwardRef<HTMLDivElement, ChakraSelect.RootProps>(function SelectRoot(props, ref) {
  return (
    <ChakraSelect.Root {...props} ref={ref} positioning={{ sameWidth: true, ...props.positioning }}>
      {props.asChild ? (
        props.children
      ) : (
        <>
          <ChakraSelect.HiddenSelect />
          {props.children}
        </>
      )}
    </ChakraSelect.Root>
  );
}) as ChakraSelect.RootComponent;

interface SelectItemGroupProps extends ChakraSelect.ItemGroupProps {
  label: React.ReactNode;
}

export const SelectItemGroup = React.forwardRef<HTMLDivElement, SelectItemGroupProps>(function SelectItemGroup(props, ref) {
  const { children, label, ...rest } = props;
  return (
    <ChakraSelect.ItemGroup {...rest} ref={ref}>
      <ChakraSelect.ItemGroupLabel>{label}</ChakraSelect.ItemGroupLabel>
      {children}
    </ChakraSelect.ItemGroup>
  );
});

export const SelectLabel = ChakraSelect.Label;
export const SelectItemText = ChakraSelect.ItemText;

おわりに

上記の修正により、該当エラーは解消されましたが、別のエラーが発生しました。
こちらについては別の記事で解消していきます。

参考

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?