はじめに
お疲れ様です、りつです。
前回に引き続き、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;
おわりに
以上の修正により、テストが通るようになりました。
参考