はじめに
お疲れ様です、りつです。
現在実装中のReactアプリで、テスト実行時にChakra UI関連のエラーが複数発生しました。
今回はそのうちの1つ目、Chakra UI v3のDialogContent
コンポーネントで発生したエラーの解決方法です。
【 関連記事 】 今回発生したエラーの解消記事
- 【Chakra UI v3 × jest】テスト時のDialogContentの型エラーの解消方法(Type '{ children: Element[]; }' has no properties in common with type 'IntrinsicAttributes & DialogContentProps & RefAttributes'.) ➡ この記事
- 【Chakra UI v3 × jest】テスト時のToastの型エラーの解消方法①(Type '{ children: (toast: any) => Element; toaster: any; insetInline: { mdDown: string; }; }' is not assignable to type 'IntrinsicAttributes & ToasterProps'.)
- 【Chakra UI v3 × jest】テスト時のToastの型エラーの解消方法②(Type '{ style: { width: string; height: string; color: string; }; _hover: { cursor: string; }; }' is not assignable to type 'IntrinsicAttributes & ToastCloseTriggerProps & RefAttributes'.)
- 【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法①(Type '{ children: ReactNode; asChild: boolean; ref: ForwardedRef; }' is not assignable to type 'IntrinsicAttributes & DialogContentProps & RefAttributes'.)
- 【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法②(Type '{ children: Element; asChild: true; position: string; top: string; insetEnd: string; }' is not assignable to type 'IntrinsicAttributes & DialogCloseTriggerProps & RefAttributes'.)
- 【Chakra UI v3 × jest】テスト時のDialogの型エラーの解消方法③(Property 'children' does not exist on type 'DialogCloseTriggerProps'.)
問題
npm run test
実行時に以下のエラーが発生しました。
エラー内容
FAIL src/__tests__/App.spec.tsx
● Test suite failed to run
src/App.tsx:176:10 - error TS2559: Type '{ children: Element[]; }' has no properties in common with type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.
176 <DialogContent>
~~~~~~~~~~~~~
src/App.tsx:231:10 - error TS2559: Type '{ children: Element[]; }' has no properties in common with type 'IntrinsicAttributes & DialogContentProps & RefAttributes<HTMLDivElement>'.
231 <DialogContent>
~~~~~~~~~~~~~
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 4.609 s
Ran all test suites.
なお、DialogContent
はChakra UIでモーダルを表示させる際に必要なコンポーネントの一つです。
ソースコード
src/App.tsx
src/App.tsx
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { MdEdit, MdDeleteOutline } from 'react-icons/md';
import { FiPlusCircle } from 'react-icons/fi';
import { Box, Center, Container, Flex, Heading, IconButton, Input, Spinner, Stack, Table } from '@chakra-ui/react';
import { Button } from '@/components/ui/button';
import { Field } from '@/components/ui/field';
import { NumberInputField, NumberInputRoot } from '@/components/ui/number-input';
import { Toaster } from '@/components/ui/toaster';
import { Record } from '@/domain/record';
import { useMessage } from '@/hooks/useMessage';
import { fetchAllRecords, insertRecord, deleteRecord } from '@/utils/supabaseFunctions';
import {
DialogActionTrigger,
DialogBody,
DialogCloseTrigger,
DialogContent,
DialogFooter,
DialogHeader,
DialogRoot,
DialogTitle,
} from '@/components/ui/dialog';
function App() {
const [records, setRecords] = useState<Record[]>([]);
const [deleteTargetId, setDeleteTargetId] = useState<string | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [open, setOpen] = useState(false);
const [openConfirm, setOpenConfirm] = useState(false);
const { showMessage } = useMessage();
const {
control,
handleSubmit,
reset,
formState: { errors },
} = useForm<Record>({
defaultValues: {
title: '',
time: '0',
},
});
useEffect(() => {
getAllRecords();
}, []);
const onClickOpenModal = () => {
setOpen(true);
};
const getAllRecords = () => {
setIsLoading(true);
fetchAllRecords()
.then((data) => {
setRecords(data);
})
.catch(() => {
showMessage({ title: '一覧の取得に失敗しました', type: 'error' });
})
.finally(() => {
setIsLoading(false);
});
};
const onSubmit = handleSubmit((data: Record) => {
setIsCreating(true);
insertRecord(data)
.then(() => {
showMessage({ title: '学習記録の登録が完了しました', type: 'success' });
})
.catch(() => {
showMessage({ title: '学習記録の登録に失敗しました', type: 'error' });
})
.finally(() => {
setIsCreating(false);
reset(); // 登録フォームの初期化
setOpen(false);
getAllRecords();
});
});
const onClickDeleteConfirm = (id: string) => {
setDeleteTargetId(id);
setOpenConfirm(true);
};
const onClickDelete = () => {
setIsDeleting(true);
if (typeof deleteTargetId === 'undefined') {
showMessage({ title: '削除対象のデータが見つかりません', type: 'error' });
setIsDeleting(false);
setOpenConfirm(false);
setDeleteTargetId(undefined);
return;
}
deleteRecord(deleteTargetId)
.then(() => {
showMessage({ title: '学習記録の削除が完了しました', type: 'success' });
})
.catch(() => {
showMessage({ title: '学習記録の削除に失敗しました', type: 'error' });
})
.finally(() => {
setIsDeleting(false);
setOpenConfirm(false);
setDeleteTargetId(undefined);
getAllRecords();
});
};
return (
<>
<Toaster />
<Box bg="teal.500" py="4" data-testid="title" color="gray.100">
<Container maxW="6xl">
<Flex justify="space-between" align="center">
<Heading as="h1" textAlign="left">
学習記録アプリ
</Heading>
<IconButton
aria-label="Search database"
variant="ghost"
size="lg"
color="white"
_hover={{ bg: 'teal.500', color: 'gray.200' }}
onClick={onClickOpenModal}
>
<FiPlusCircle />
</IconButton>
</Flex>
</Container>
</Box>
{isLoading ? (
<Center h="100vh">
<Spinner />
</Center>
) : (
<Container maxW="6xl">
<Table.Root size="md" variant="line" my={10} interactive>
<Table.Header>
<Table.Row>
<Table.ColumnHeader>タイトル</Table.ColumnHeader>
<Table.ColumnHeader>時間</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end"></Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{records.map((record) => (
<Table.Row key={record.id}>
<Table.Cell>{record.title}</Table.Cell>
<Table.Cell>{record.time}</Table.Cell>
<Table.Cell textAlign="end">
<Button colorPalette="blue" variant="outline" mr="4">
<MdEdit />
編集
</Button>
<Button colorPalette="red" variant="outline" onClick={() => onClickDeleteConfirm(record.id)}>
<MdDeleteOutline />
削除
</Button>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</Container>
)}
<DialogRoot lazyMount open={open} onOpenChange={(e) => setOpen(e.open)} motionPreset="slide-in-bottom" trapFocus={false}>
<DialogContent>
<DialogCloseTrigger />
<DialogHeader>
<DialogTitle>学習記録登録</DialogTitle>
</DialogHeader>
<form onSubmit={onSubmit}>
<DialogBody>
<Stack gap={4}>
<Field label="学習内容" invalid={!!errors.title} errorText={errors.title?.message}>
<Controller
name="title"
control={control}
rules={{
required: '内容の入力は必須です',
}}
render={({ field }) => <Input {...field} />}
/>
</Field>
<Field label="学習時間" invalid={!!errors.time} errorText={errors.time?.message}>
<Controller
name="time"
control={control}
rules={{
required: '時間の入力は必須です',
min: { value: 0, message: '時間は0以上である必要があります' },
}}
render={({ field }) => (
<NumberInputRoot width="100%" name={field.name} value={field.value} onValueChange={({ value }) => field.onChange(value)}>
<NumberInputField />
</NumberInputRoot>
)}
/>
</Field>
</Stack>
</DialogBody>
<DialogFooter mb="2">
<DialogActionTrigger asChild>
<Button variant="outline">キャンセル</Button>
</DialogActionTrigger>
<Button colorPalette="teal" loading={isCreating} type="submit">
登録
</Button>
</DialogFooter>
</form>
</DialogContent>
</DialogRoot>
<DialogRoot
role="alertdialog"
lazyMount
open={openConfirm}
onOpenChange={(e) => setOpenConfirm(e.open)}
motionPreset="slide-in-bottom"
trapFocus={false}
>
<DialogContent>
<DialogHeader>
<DialogTitle>削除の確認</DialogTitle>
</DialogHeader>
<DialogBody>
<p>削除したデータは戻せません。削除してもよろしいですか?</p>
</DialogBody>
<DialogFooter mb="2">
<DialogActionTrigger asChild>
<Button variant="outline">キャンセル</Button>
</DialogActionTrigger>
<Button colorPalette="red" loading={isDeleting} onClick={onClickDelete}>
削除
</Button>
</DialogFooter>
<DialogCloseTrigger />
</DialogContent>
</DialogRoot>
</>
);
}
export default App;
src/__tests__/App.spec.tsx
src/__tests__/App.spec.tsx
import App from '../App';
import { render, screen } from '@testing-library/react';
import { ChakraProvider, defaultSystem } from '@chakra-ui/react';
describe('App', () => {
test('タイトルがあること', async () => {
render(
<ChakraProvider value={defaultSystem}>
<App />
</ChakraProvider>
);
const title = screen.getByTestId('title');
expect(title).toBeInTheDocument();
});
});
解決方法
src/components/ui/dialog.tsx
の内容を修正します。
以下のように、DialogContentProps
インターフェースにchildren
の型定義を追加します。
src/components/ui/dialog.tsx
import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react"
import { CloseButton } from "./close-button"
import * as React from "react"
interface DialogContentProps extends ChakraDialog.ContentProps {
portalled?: boolean
portalRef?: React.RefObject<HTMLElement>
backdrop?: boolean
+ children?: React.ReactNode; // children を追加
}
// 省略
おわりに
上記対応により、DialogContent
コンポーネントのエラーが発生しなくなりました。
しかし、続けて別のエラーが発生したので、そちらは別の記事で解決方法をご紹介したいと思います。
➡ 記事を書きました。
参考