本編はこちらです。
本編では扱いませんでしたが、実開発ではデザインについても考えなければいけません。
世の中には Bootstrap をはじめ様々な CSS フレームワークがありますので、それらから選択するのが無難です。ちなみに React 開発においては、Material UI という CSS フレームワークが一番人気だそうです。
参考までに、本記事の構成に Material UI の AppBar コンポーネントを取り入れた例をご紹介しておきます。
Material UI のインストール
npm で開発環境にインストールします。
Material UI 本体の他に、アイコン集もインストールします。
npm install -D @material-ui/core @material-ui/icons
App コンポーネントの実装
src/client/App.tsx
ファイルを下記の内容に書き換えます。
import * as React from "react";
import { Switch, Route, Link } from "react-router-dom";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton";
import MenuIcon from "@material-ui/icons/Menu";
import Drawer from "@material-ui/core/Drawer";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import Container from "@material-ui/core/Container";
import HomeIcon from "@material-ui/icons/Home";
import HomeWorkIcon from "@material-ui/icons/HomeWork";
import "./favicon.ico";
import * as styles from "./App.styl";
import Home from "./Home";
import HomeWork from "./HomeWork";
const App = () => {
const [drawerState, setDrawerState] = React.useState(false);
const toggleDrawer = (state: boolean) => (event: any) => {
if (event.type === "keydown" && (event.key === "Tab" || event.key === "Shift")) {
return;
}
setDrawerState(state);
};
return (
<>
<AppBar position="static">
<Toolbar>
<IconButton edge="start" className={styles.menuButton} color="inherit" aria-label="menu" onClick={toggleDrawer(true)}>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={styles.title}>
Sample
</Typography>
</Toolbar>
</AppBar>
<Drawer open={drawerState} role="presentation" className={styles.list} onClose={toggleDrawer(false)} onClick={toggleDrawer(false)} onKeyDown={toggleDrawer(false)}>
<List>
<ListItem button key="home" component={Link} to="/">
<ListItemIcon><HomeIcon /></ListItemIcon>
<ListItemText primary="Home" />
</ListItem>
<ListItem button key="homework" component={Link} to="/homework">
<ListItemIcon><HomeWorkIcon /></ListItemIcon>
<ListItemText primary="Home Work" />
</ListItem>
</List>
</Drawer>
<Container maxWidth="sm">
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/homework" component={HomeWork} />
</Switch>
</Container>
</>
);
};
export default App;
src/client/App.styl
ファイルを下記の内容に書き換えます。
$spacing = 8px
$basicBackgroundColor = #A0A0FF
$basicForegroundColor = #0000A0
$heading
color: $basicForegroundColor
:global(body)
background-color: $basicBackgroundColor
.root
flex-grow: 1
.menuButton
margin-right: $spacing * 2
.title
flex-grow: 1
.list
width: 250
実行
テスト
src/client/App.spec.tsx
ファイルを下記の内容に書き換えます。
import * as React from "react";
import { MemoryRouter } from "react-router-dom";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
const homeMock = jest.fn(() => <></>);
jest.mock("./Home", () => ({ __esModule: true, default: homeMock }));
const homeworkMock = jest.fn(() => <></>);
jest.mock("./HomeWork", () => ({ __esModule: true, default: homeworkMock }));
import App from "./App";
afterEach(cleanup);
afterEach(jest.clearAllMocks);
afterAll(() => {
jest.unmock("./Home");
jest.unmock("./HomeWork");
});
describe("App", () => {
it("最初に Home を表示すること", () => {
render(<MemoryRouter><App /></MemoryRouter>);
expect(homeMock).toBeCalled();
});
it("メニューを開けること", () => {
const root = render(<MemoryRouter><App /></MemoryRouter>);
const menuButton = root.getByLabelText("menu");
fireEvent.click(menuButton);
expect(root.getByRole("presentation")).not.toBeNull();
});
it("メニューをクリックするとメニューが閉じること", () => {
const root = render(<MemoryRouter><App /></MemoryRouter>);
const menuButton = root.getByLabelText("menu");
fireEvent.click(menuButton);
const menu = root.getByRole("presentation");
fireEvent.click(menu);
expect(() => root.getByRole("presentation")).toThrow();
});
it("キーを押下するとメニューが閉じること", () => {
const root = render(<MemoryRouter><App /></MemoryRouter>);
const menuButton = root.getByLabelText("menu");
fireEvent.click(menuButton);
const menu = root.getByRole("presentation");
fireEvent.keyDown(menu, { key: "a", code: 65 });
expect(() => root.getByRole("presentation")).toThrow();
});
it.each([
["Tab", 9],
["Shift", 16]
])("一部のキーを押下してもメニューが閉じないこと [%s, %d]", (key, code) => {
const root = render(<MemoryRouter><App /></MemoryRouter>);
const menuButton = root.getByLabelText("menu");
fireEvent.click(menuButton);
const menu = root.getByRole("presentation");
fireEvent.keyDown(menu, { key, code });
expect(root.getByRole("presentation")).not.toBeNull();
});
it("メニューから Home を表示できること", () => {
const root = render(<MemoryRouter><App /></MemoryRouter>);
expect(homeMock).toBeCalled();
homeMock.mockClear();
const menuButton = root.getByLabelText("menu");
fireEvent.click(menuButton);
const menuHomeButton = root.getByText("Home", { exact: true });
fireEvent.click(menuHomeButton);
expect(() => root.getByRole("presentation")).toThrow();
expect(homeMock).toBeCalled();
});
it("メニューから Home Work を表示できること", () => {
const root = render(<MemoryRouter><App /></MemoryRouter>);
expect(homeworkMock).not.toBeCalled();
const menuButton = root.getByLabelText("menu");
fireEvent.click(menuButton);
const menuHomeWorkButton = root.getByText("Home Work", { exact: true });
fireEvent.click(menuHomeWorkButton);
expect(() => root.getByRole("presentation")).toThrow();
expect(homeworkMock).toBeCalled();
});
});