1
0

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: ReactNode; ref: ForwardedRef<HTMLButtonElement>; }' is not assignable to type 'IntrinsicAttributes & SelectTriggerProps & RefAttributes<HTMLButtonElement>'.他)

Posted at

はじめに

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

前回に引き続き、Chakra UIの型エラーについてです。

  • 前回の記事

問題

テスト実行時に以下のエラーが発生しました。

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

    src/components/ui/select.tsx:17:8 - error TS2322: Type '{ children: ReactNode; ref: ForwardedRef<HTMLButtonElement>; }' is not assignable to type 'IntrinsicAttributes & SelectTriggerProps & RefAttributes<HTMLButtonElement>'.
      Property 'children' does not exist on type 'IntrinsicAttributes & SelectTriggerProps & RefAttributes<HTMLButtonElement>'.

    17       <ChakraSelect.Trigger ref={ref}>{children}</ChakraSelect.Trigger>
              ~~~~~~~~~~~~~~~~~~~~
    src/components/ui/select.tsx:28:6 - error TS2322: Type '{ children: Element; ref: ForwardedRef<HTMLButtonElement>; asChild: true; }' is not assignable to type 'IntrinsicAttributes & SelectClearTriggerProps & RefAttributes<HTMLButtonElement>'.
      Property 'children' does not exist on type 'IntrinsicAttributes & SelectClearTriggerProps & RefAttributes<HTMLButtonElement>'.

    28     <ChakraSelect.ClearTrigger asChild {...props} ref={ref}>
            ~~~~~~~~~~~~~~~~~~~~~~~~~
    src/components/ui/select.tsx:60:6 - error TS2322: Type '{ children: (ReactNode | Element)[]; ref: ForwardedRef<HTMLDivElement>; key: any; item: CollectionItem; }' is not assignable to type 'IntrinsicAttributes & SelectItemProps & RefAttributes<HTMLDivElement>'.
      Property 'children' does not exist on type 'IntrinsicAttributes & SelectItemProps & RefAttributes<HTMLDivElement>'.

    60     <ChakraSelect.Item key={item.value} item={item} {...rest} ref={ref}>
            ~~~~~~~~~~~~~~~~~
    src/components/ui/select.tsx:75:6 - error TS2322: Type '{ children: Element; ref: ForwardedRef<HTMLSpanElement>; placeholder: string; }' is not assignable to type 'IntrinsicAttributes & SelectValueTextProps & RefAttributes<HTMLSpanElement>'.
      Property 'children' does not exist on type 'IntrinsicAttributes & SelectValueTextProps & RefAttributes<HTMLSpanElement>'.

    75     <ChakraSelect.ValueText {...rest} ref={ref}>
            ~~~~~~~~~~~~~~~~~~~~~~
    src/components/ui/select.tsx:109:11 - error TS2339: Property 'children' does not exist on type 'SelectItemGroupProps'.

    109   const { children, label, ...rest } = props;
                  ~~~~~~~~
    src/components/ui/select.tsx:111:6 - error TS2322: Type '{ children: any[]; ref: ForwardedRef<HTMLDivElement>; }' is not assignable to type 'IntrinsicAttributes & SelectItemGroupProps & RefAttributes<HTMLDivElement>'.
      Property 'children' does not exist on type 'IntrinsicAttributes & SelectItemGroupProps & RefAttributes<HTMLDivElement>'.

    111     <ChakraSelect.ItemGroup {...rest} ref={ref}>
             ~~~~~~~~~~~~~~~~~~~~~~

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        5.729 s
Ran all test suites.
参考ファイル
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>({
    defaultValues: {
      github_id: '',
      qiita_id: '',
      x_id: '',
    },
  });
  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) => {
    const transformedData = {
      ...data,
      github_id: data.github_id || null,
      qiita_id: data.qiita_id || null,
      x_id: data.x_id || null,
    };
    insertUser(transformedData)
      .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} value={field.value || ''} />} />
                  </Field>
                  <Field label="Qiita ID">
                    <Controller name="qiita_id" control={control} render={({ field }) => <Input {...field} value={field.value || ''} />} />
                  </Field>
                  <Field label="X ID">
                    <Controller
                      name="x_id"
                      control={control}
                      render={({ field }) => <Input {...field} value={field.value || ''} placeholder="@は不要" />}
                    />
                  </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の内容を修正します。
なお、コメントの番号は、何番目のエラーに対応するかを表しています。

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

- import type { CollectionItem, SelectItemProps } from '@chakra-ui/react';
+ import type { CollectionItem, SelectItemProps, SelectClearTriggerProps } from '@chakra-ui/react'; // 2. SelectClearTriggerPropsをインポート
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;
}

export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerProps>(function SelectTrigger(props, ref) {
  const { children, clearable, ...rest } = props;
+ const chakraSelectTriggerProps: = { // 1. chakraSelectTriggerPropsを定義
+   children,
+ };
  return (
    <ChakraSelect.Control {...rest}>
-     <ChakraSelect.Trigger ref={ref}>{children}</ChakraSelect.Trigger>
+     <ChakraSelect.Trigger ref={ref} {...chakraSelectTriggerProps} /> {/* 1. chakraSelectTriggerPropsに置き換え */}
      <ChakraSelect.IndicatorGroup>
        {clearable && <SelectClearTrigger />}
        <ChakraSelect.Indicator />
      </ChakraSelect.IndicatorGroup>
    </ChakraSelect.Control>
  );
});

const SelectClearTrigger = React.forwardRef<HTMLButtonElement, ChakraSelect.ClearTriggerProps>(function SelectClearTrigger(props, ref) {
+ const chakraSelectClearTriggerProps: SelectClearTriggerProps = { // 2. chakraSelectClearTriggerPropsを定義
+   asChild: true,
+   children: <CloseButton size="xs" variant="plain" focusVisibleRing="inside" focusRingWidth="2px" pointerEvents="auto" />,
+ };
- return (
-   <ChakraSelect.ClearTrigger asChild {...props} ref={ref}>
-     <CloseButton size="xs" variant="plain" focusVisibleRing="inside" focusRingWidth="2px" pointerEvents="auto" />
-   </ChakraSelect.ClearTrigger>
- );
+ return <ChakraSelect.ClearTrigger {...props} ref={ref} {...chakraSelectClearTriggerProps} />; {/* 2. chakraSelectClearTriggerPropsに置き換え */}
});

interface SelectContentProps extends ChakraSelect.ContentProps {
  portalled?: boolean;
  portalRef?: React.RefObject<HTMLElement>;
  children?: React.ReactNode;
}

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>
  );
});

interface NewSelectItemProps extends SelectItemProps {
  children?: React.ReactNode;
  item: CollectionItem;
  key?: string;
}

export const SelectItem = React.forwardRef<HTMLDivElement, NewSelectItemProps>(function SelectItem(props, ref) {
  const { item, children, ...rest } = props;
+ const chakraSelectItemProps: NewSelectItemProps = { // 3. chakraSelectItemPropsを定義
+   children: (
+     <>
+       {children}
+       <ChakraSelect.ItemIndicator />
+     </>
+   ),
+   item,
+ };
- return (
-   <ChakraSelect.Item key={item.value} item={item} {...rest} ref={ref}>
-     {children}
-     <ChakraSelect.ItemIndicator />
-   </ChakraSelect.Item>
- );
+ return <ChakraSelect.Item key={item.value} {...rest} ref={ref} {...chakraSelectItemProps} />; {/* 3. chakraSelectItemPropsに置き換え */}
});

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

export const SelectValueText = React.forwardRef<HTMLSpanElement, SelectValueTextProps>(function SelectValueText(props, ref) {
  const { children, ...rest } = props;
+ const chakraSelectValueTextProps = { // 4. chakraSelectValueTextPropsを定義
+   children: (
+     <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>
+   ),
+ };
- 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>
- );
+ return <ChakraSelect.ValueText {...rest} ref={ref} {...chakraSelectValueTextProps} />; {/* 4. chakraSelectValueTextPropsに置き換え */}
});

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;
+ children: React.ReactNode; // 5. SelectItemGroupPropsにchildrenを追加
}

export const SelectItemGroup = React.forwardRef<HTMLDivElement, SelectItemGroupProps>(function SelectItemGroup(props, ref) {
  const { children, label, ...rest } = props;
+ const chakraSelectItemGroupProps = { // 6. chakraSelectItemGroupPropsを定義
+   children: (
+     <>
+       <ChakraSelect.ItemGroupLabel>{label}</ChakraSelect.ItemGroupLabel>
+       {children}
+     </>
+   ),
+ };
- return (
-   <ChakraSelect.ItemGroup {...rest} ref={ref}>
-     <ChakraSelect.ItemGroupLabel>{label}</ChakraSelect.ItemGroupLabel>
-     {children}
-   </ChakraSelect.ItemGroup>
- );
+ return <ChakraSelect.ItemGroup {...rest} ref={ref} {...chakraSelectItemGroupProps} />; {/* 6. chakraSelectItemGroupPropsに置き換え */}
});

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

おわりに

以上の修正により、テストが通るようになりました。

参考

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?