├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── final
├── components
│ ├── Header.tsx
│ ├── Layout.tsx
│ ├── Modal.tsx
│ ├── Navigation.tsx
│ ├── ProfileButton.spec.tsx
│ ├── ProfileButton.tsx
│ └── modal.tsx
├── next-env.d.ts
├── package.json
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── blog.tsx
│ └── index.tsx
├── postcss.config.js
├── public
│ ├── avatar.png
│ ├── favicon.ico
│ └── vercel.svg
├── setup.sh
├── styles
│ └── globals.css
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
└── start
├── .eslintrc.json
├── components
├── Header.tsx
├── Layout.tsx
├── Modal.tsx
├── Navigation.tsx
└── ProfileButton.tsx
├── next-env.d.ts
├── package.json
├── pages
├── _app.tsx
├── _document.tsx
├── blog.tsx
└── index.tsx
├── postcss.config.js
├── public
├── avatar.png
├── favicon.ico
└── vercel.svg
├── setup.sh
├── styles
└── globals.css
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | .next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "igarashi"
4 | ]
5 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ryō Igarashi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Accessible React 101
2 |
3 | This is a tutorial for creating accessible app with React & Next.js
4 |
5 | ## Directories
6 |
7 | - `start` ― Unmodified, inaccessible app
8 | - `final` ― Accessible version
9 |
10 | ## Tech stacks
11 |
12 | [React]: https://reactjs.org
13 | [Next.js]: https://nextjs.org/
14 | [Tailwind CSS]: https://tailwindcss.com/
15 | [React Testing Library]: https://testing-library.com/docs/react-testing-library/intro/
16 |
17 | - [React] and [Next.js] ― UI framework
18 | - [Tailwind CSS] ― Utility-first CSS framework. Supports `sr-only` (aka visually-hidden) class
19 | - [React Testing Library] ― Behaviour-driven testing library for React
20 |
21 | ## License
22 |
23 | MIT
24 |
--------------------------------------------------------------------------------
/final/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { Navigation } from "./Navigation";
2 |
3 | // 1. Adding id and aria-labelledby
4 | export const Header = () => {
5 | return (
6 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/final/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { ReactNode } from "react"
3 | import { Header } from "./Header"
4 |
5 | export interface LayoutProps {
6 | title: string;
7 | description: string;
8 | children: ReactNode;
9 | }
10 |
11 | export const Layout = ({ title, description, children }: LayoutProps) => {
12 | return (
13 |
14 |
15 |
{title}
16 |
17 |
18 |
19 |
20 | 本文にスキップ
21 |
22 |
23 |
24 |
25 |
26 | {children}
27 |
28 |
29 |
34 | {title}を閲覧中
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/final/components/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, useCallback, useEffect, useRef } from "react";
2 | import { createPortal } from "react-dom";
3 |
4 | // 1. aria, roles
5 | // 2. focus management
6 | const Window = ({
7 | title,
8 | children,
9 | }: PropsWithChildren<{ readonly title: string }>) => {
10 | const ref = useRef(null);
11 |
12 | useEffect(() => {
13 | ref.current?.focus();
14 | }, []);
15 |
16 | return (
17 |
23 |
30 |
31 |
{children}
32 |
33 | );
34 | };
35 |
36 | const Footer = ({ onClose }: { readonly onClose: () => void }) => {
37 | return (
38 |
43 | );
44 | };
45 |
46 | interface ModalProps {
47 | readonly title: string;
48 | readonly onClose: () => void;
49 | }
50 |
51 | // 1. aria-hidden
52 | // 2. keydown events
53 | export const Modal = ({
54 | children,
55 | title,
56 | onClose,
57 | }: PropsWithChildren) => {
58 |
59 | const handleKeydown = useCallback((e: KeyboardEvent) => {
60 | if (e.key === 'Escape') onClose();
61 | }, []);
62 |
63 | useEffect(() => {
64 | document.addEventListener('keydown', handleKeydown);
65 | document.getElementById('app')?.setAttribute('aria-hidden', 'true');
66 |
67 | return (): void => {
68 | document.removeEventListener('keydown', handleKeydown);
69 | document.getElementById('app')?.removeAttribute('aria-hidden');
70 | }
71 | }, []);
72 |
73 | return createPortal(
74 |
78 |
79 | {children}
80 |
81 |
82 |
,
83 | document.body
84 | );
85 | };
86 |
--------------------------------------------------------------------------------
/final/components/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { useRouter } from "next/router";
3 |
4 | type ItemProps = {
5 | name: string;
6 | href: string;
7 | };
8 |
9 | const Item = ({ name, href }: ItemProps) => {
10 | const router = useRouter();
11 |
12 | return (
13 |
14 |
15 | {name}
16 |
17 |
18 | );
19 | };
20 |
21 | type NavigationProps = {
22 | items: ItemProps[];
23 | };
24 |
25 | // 1. Use UL and LI
26 | export const Navigation = ({ items }: NavigationProps) => {
27 | return (
28 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/final/components/ProfileButton.spec.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 | import { fireEvent, render, screen } from "@testing-library/react";
3 | import { ProfileButton } from "./ProfileButton";
4 |
5 | describe('Modal', () => {
6 | it('is accessible', () => {
7 | render();
8 | fireEvent.click(screen.getByRole('button', { name: 'スキルを表示する' }));
9 |
10 | expect(screen.getByRole('dialog', { name: '私のスキル' })).toBeVisible();
11 | expect(screen.getByText('私のスキル')).toHaveFocus();
12 |
13 | fireEvent.click(screen.getByRole('button', { name: '閉じる' }));
14 |
15 | expect(screen.queryByRole('dialog', { name: '私のスキル' })).toBeNull();
16 | expect(screen.getByRole('button', { name: 'スキルを表示する' })).toHaveFocus();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/final/components/ProfileButton.tsx:
--------------------------------------------------------------------------------
1 | import { faEllipsisH } from "@fortawesome/free-solid-svg-icons";
2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3 | import { useRef, useState } from "react";
4 | import { Modal } from "./Modal";
5 |
6 | // 1. label
7 | // 2. taking the focus back to this on close
8 | export const ProfileButton = () => {
9 | const [open, setOpen] = useState(false);
10 | const ref = useRef(null);
11 |
12 | const handleClose = () => {
13 | setOpen(false);
14 | ref.current?.focus();
15 | }
16 |
17 | return (
18 | <>
19 | {open && (
20 |
21 |
22 | - React
23 | - Next.js
24 | - Tailwind CSS
25 |
26 |
27 | )}
28 |
29 |
37 | >
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/final/components/modal.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, useCallback, useEffect, useRef } from "react";
2 | import { createPortal } from "react-dom";
3 |
4 | // 1. aria, roles
5 | // 2. focus management
6 | const Window = ({
7 | title,
8 | children,
9 | }: PropsWithChildren<{ readonly title: string }>) => {
10 | const ref = useRef(null);
11 |
12 | useEffect(() => {
13 | ref.current?.focus();
14 | }, []);
15 |
16 | return (
17 |
23 |
30 |
31 |
{children}
32 |
33 | );
34 | };
35 |
36 | const Footer = ({ onClose }: { readonly onClose: () => void }) => {
37 | return (
38 |
43 | );
44 | };
45 |
46 | interface ModalProps {
47 | readonly title: string;
48 | readonly onClose: () => void;
49 | }
50 |
51 | // 1. aria-hidden
52 | // 2. keydown events
53 | export const Modal = ({
54 | children,
55 | title,
56 | onClose,
57 | }: PropsWithChildren) => {
58 |
59 | const handleKeydown = useCallback((e: KeyboardEvent) => {
60 | if (e.key === 'Escape') onClose();
61 | }, []);
62 |
63 | useEffect(() => {
64 | document.addEventListener('keydown', handleKeydown);
65 | document.getElementById('app')?.setAttribute('aria-hidden', 'true');
66 |
67 | return (): void => {
68 | document.removeEventListener('keydown', handleKeydown);
69 | document.getElementById('app')?.removeAttribute('aria-hidden');
70 | }
71 | }, []);
72 |
73 | return createPortal(
74 |
78 |
79 | {children}
80 |
81 |
82 |
,
83 | document.body
84 | );
85 | };
86 |
--------------------------------------------------------------------------------
/final/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/final/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "accessible-react-101",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@fortawesome/fontawesome-svg-core": "^1.2.32",
12 | "@fortawesome/free-regular-svg-icons": "^5.15.1",
13 | "@fortawesome/free-solid-svg-icons": "^5.15.1",
14 | "@fortawesome/react-fontawesome": "^0.1.14",
15 | "@testing-library/jest-dom": "^5.11.8",
16 | "@testing-library/react": "^11.2.3",
17 | "@testing-library/react-hooks": "^4.0.0",
18 | "@types/react": "^17.0.0",
19 | "@types/react-dom": "^17.0.0",
20 | "@typescript-eslint/eslint-plugin": "^4.12.0",
21 | "@typescript-eslint/parser": "^4.12.0",
22 | "autoprefixer": "^10.2.1",
23 | "eslint": "^7.17.0",
24 | "eslint-plugin-jsx-a11y": "^6.4.1",
25 | "jest": "^26.6.3",
26 | "next": "10.0.5",
27 | "postcss": "^8.2.3",
28 | "react": "17.0.1",
29 | "react-dom": "^17.0.1",
30 | "tailwindcss": "^2.0.2",
31 | "ts-jest": "^26.4.4",
32 | "typescript": "^4.1.3"
33 | },
34 | "jest": {
35 | "preset": "ts-jest",
36 | "globals": {
37 | "ts-jest": {
38 | "tsconfig": {
39 | "jsx": "react-jsx"
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/final/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { AppProps } from 'next/app';
2 | import { useRouter } from 'next/router';
3 | import { useCallback, useEffect } from 'react';
4 | import '../styles/globals.css';
5 |
6 | function MyApp({ Component, pageProps }: AppProps) {
7 | const router = useRouter();
8 |
9 | const handleRouteChange = useCallback(() => {
10 | const main = document.getElementById('main');
11 | main?.focus({ preventScroll: true });
12 | }, []);
13 |
14 | useEffect(() => {
15 | router.events.on('routeChangeComplete', handleRouteChange);
16 | return () => router.events.off('routeChangeComplete', handleRouteChange);
17 | });
18 |
19 | return
20 | }
21 |
22 | export default MyApp
23 |
--------------------------------------------------------------------------------
/final/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | class CustomDocument extends Document {
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | )
14 | }
15 | }
16 |
17 | export default CustomDocument;
18 |
--------------------------------------------------------------------------------
/final/pages/blog.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { Layout } from "../components/Layout";
3 |
4 | export default function Blog() {
5 | return (
6 |
7 | 記事
8 | 最新の記事の一覧
9 |
10 |
11 | ホームに戻る
12 |
13 |
14 |
15 | {Array.from({ length: 5 }, (_, i) => i).map((i, _, a) => (
16 |
22 |
27 |
28 | あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら
29 |
30 |
31 | ))}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/final/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { Layout } from "../components/Layout";
3 | import { ProfileButton } from "../components/ProfileButton";
4 |
5 | export default function Home() {
6 | return (
7 |
8 |
9 |
10 |

11 |
12 |
13 |
14 |
ボブ
15 |
Webエンジニア・デザイナー
16 |
17 |
18 |
19 |
20 | 私のポートフォリオへようこそ
21 |
22 |
23 | 記事の一覧
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/final/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/final/public/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neet/accessible-react-101/2b347ec4c164fe0367f9cf4cababd551a55dec36/final/public/avatar.png
--------------------------------------------------------------------------------
/final/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neet/accessible-react-101/2b347ec4c164fe0367f9cf4cababd551a55dec36/final/public/favicon.ico
--------------------------------------------------------------------------------
/final/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/final/setup.sh:
--------------------------------------------------------------------------------
1 | npx create-next-app accessible-react-101
2 |
3 | yarn add next typescript react tailwind
4 |
5 | touch tsconfig.json
6 |
--------------------------------------------------------------------------------
/final/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body {
7 | padding: 0;
8 | margin: 0;
9 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
10 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
11 | }
12 |
13 | a {
14 | color: inherit;
15 | text-decoration: none;
16 | }
17 |
18 | * {
19 | box-sizing: border-box;
20 | }
21 |
--------------------------------------------------------------------------------
/final/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors')
2 |
3 | module.exports = {
4 | purge: ['./pages/**/*.tsx', './components/**/*.tsx'],
5 | darkMode: false, // or 'media' or 'class'
6 | theme: {
7 | extend: {},
8 | colors,
9 | },
10 | variants: {
11 | extend: {},
12 | },
13 | plugins: [],
14 | }
15 |
--------------------------------------------------------------------------------
/final/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve"
20 | },
21 | "include": [
22 | "next-env.d.ts",
23 | "**/*.ts",
24 | "**/*.tsx"
25 | ],
26 | "exclude": [
27 | "node_modules"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/start/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "eslint:recommended"
5 | // , "plugin:jsx-a11y/recommended"
6 | ],
7 | "env": {
8 | "browser": true,
9 | "node": true,
10 | "jest": true
11 | },
12 | "plugins": [],
13 | "parserOptions": {
14 | "ecmaVersion": 9,
15 | "sourceType": "module"
16 | },
17 | "settings": {
18 | "react": {
19 | "version": "detect"
20 | }
21 | },
22 | "rules": {
23 | "no-console": "error",
24 | "eqeqeq": ["error", "always", { "null": "ignore" }],
25 | "jsx-a11y/anchor-is-valid": "off",
26 | "react/display-name": "off",
27 | "react/prop-types": "off",
28 | "react/react-in-jsx-scope": "off"
29 | },
30 | "overrides": [
31 | {
32 | "files": ["**/*.{ts,tsx}"],
33 | "extends": [
34 | "plugin:@typescript-eslint/eslint-recommended"
35 | // "plugin:@typescript-eslint/all",
36 | // "plugin:import/typescript",
37 | // "prettier/@typescript-eslint"
38 | ],
39 | "parserOptions": {
40 | "project": "./tsconfig.json"
41 | },
42 | "rules": {
43 | "@typescript-eslint/no-type-alias": "off"
44 | }
45 | }
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/start/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { Navigation } from "./Navigation";
2 |
3 | // 1. Adding id and aria-labelledby
4 | export const Header = () => {
5 | return (
6 |
7 |
8 |
9 | Portfolio
10 |
11 |
12 |
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/start/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { ReactNode } from "react"
3 | import { Header } from "./Header"
4 |
5 | export interface LayoutProps {
6 | title: string;
7 | description: string;
8 | children: ReactNode;
9 | }
10 |
11 | export const Layout = ({ title, description, children }: LayoutProps) => {
12 | return (
13 |
14 |
15 |
{title}
16 |
17 |
18 |
19 |
20 |
21 |
22 | {children}
23 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/start/components/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from "react";
2 | import { createPortal } from "react-dom";
3 |
4 | // 1. aria, roles
5 | // 2. focus management
6 | const Window = ({
7 | title,
8 | children,
9 | }: PropsWithChildren<{ readonly title: string }>) => {
10 | return (
11 |
12 |
15 |
16 |
{children}
17 |
18 | );
19 | };
20 |
21 | const Footer = ({ onClose }: { readonly onClose: () => void }) => {
22 | return (
23 |
28 | );
29 | };
30 |
31 | interface ModalProps {
32 | readonly title: string;
33 | readonly onClose: () => void;
34 | }
35 |
36 | // 1. aria-hidden
37 | // 2. keydown events
38 | export const Modal = ({
39 | children,
40 | title,
41 | onClose,
42 | }: PropsWithChildren) => {
43 | return createPortal(
44 |
45 |
46 | {children}
47 |
48 |
49 |
,
50 | document.body
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/start/components/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { useRouter } from "next/router";
3 |
4 | type ItemProps = {
5 | name: string;
6 | href: string;
7 | };
8 |
9 | const Item = ({ name, href }: ItemProps) => {
10 | const router = useRouter();
11 |
12 | return (
13 |
14 |
15 | {name}
16 |
17 |
18 | );
19 | };
20 |
21 | type NavigationProps = {
22 | items: ItemProps[];
23 | };
24 |
25 | // 1. Use UL and LI
26 | export const Navigation = ({ items }: NavigationProps) => {
27 | return (
28 |
29 | {items.map((item, i) => (
30 |
31 | ))}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/start/components/ProfileButton.tsx:
--------------------------------------------------------------------------------
1 | import { faEllipsisH } from "@fortawesome/free-solid-svg-icons";
2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3 | import { useState } from "react";
4 | import { Modal } from "./Modal";
5 |
6 | // 1. label
7 | // 2. taking the focus back to this on close
8 | export const ProfileButton = () => {
9 | const [open, setOpen] = useState(false);
10 |
11 | return (
12 | <>
13 | {open && (
14 | void setOpen(false)}>
15 |
16 | - React
17 | - Next.js
18 | - Tailwind CSS
19 |
20 |
21 | )}
22 |
23 | void setOpen(true)}
26 | >
27 |
28 |
29 | >
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/start/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/start/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "accessible-react-101",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@fortawesome/fontawesome-svg-core": "^1.2.32",
12 | "@fortawesome/free-regular-svg-icons": "^5.15.1",
13 | "@fortawesome/free-solid-svg-icons": "^5.15.1",
14 | "@fortawesome/react-fontawesome": "^0.1.14",
15 | "@testing-library/jest-dom": "^5.11.8",
16 | "@testing-library/react": "^11.2.3",
17 | "@testing-library/react-hooks": "^4.0.0",
18 | "@types/react": "^17.0.0",
19 | "@types/react-dom": "^17.0.0",
20 | "@typescript-eslint/eslint-plugin": "^4.12.0",
21 | "@typescript-eslint/parser": "^4.12.0",
22 | "autoprefixer": "^10.2.1",
23 | "eslint": "^7.17.0",
24 | "eslint-plugin-jsx-a11y": "^6.4.1",
25 | "jest": "^26.6.3",
26 | "next": "10.0.5",
27 | "postcss": "^8.2.3",
28 | "react": "17.0.1",
29 | "react-dom": "^17.0.1",
30 | "tailwindcss": "^2.0.2",
31 | "ts-jest": "^26.4.4",
32 | "typescript": "^4.1.3"
33 | },
34 | "jest": {
35 | "preset": "ts-jest",
36 | "globals": {
37 | "ts-jest": {
38 | "tsconfig": {
39 | "jsx": "react-jsx"
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/start/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { AppProps } from 'next/app';
2 | import '../styles/globals.css';
3 |
4 | function MyApp({ Component, pageProps }: AppProps) {
5 | return
6 | }
7 |
8 | export default MyApp
9 |
--------------------------------------------------------------------------------
/start/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document';
2 |
3 | class MyDocument extends Document {
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | )
14 | }
15 | }
16 |
17 | export default MyDocument;
18 |
--------------------------------------------------------------------------------
/start/pages/blog.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { Layout } from "../components/Layout";
3 |
4 | export default function Blog() {
5 | return (
6 |
7 | 記事
8 | 最新の記事の一覧
9 |
10 |
11 | ホームに戻る
12 |
13 |
14 |
15 | {Array.from({ length: 3 }, (_, i) => i).map((i, _, a) => (
16 |
23 |
28 |
29 | あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら
30 |
31 |
32 | ))}
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/start/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { Layout } from "../components/Layout";
3 | import { ProfileButton } from "../components/ProfileButton";
4 |
5 | export default function Home() {
6 | return (
7 |
8 |
9 |
10 |

11 |
12 |
13 |
14 |
ボブ
15 |
Webエンジニア・デザイナー
16 |
17 |
18 |
19 |
20 | 私のポートフォリオへようこそ
21 |
22 |
23 | 記事の一覧
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/start/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/start/public/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neet/accessible-react-101/2b347ec4c164fe0367f9cf4cababd551a55dec36/start/public/avatar.png
--------------------------------------------------------------------------------
/start/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neet/accessible-react-101/2b347ec4c164fe0367f9cf4cababd551a55dec36/start/public/favicon.ico
--------------------------------------------------------------------------------
/start/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/start/setup.sh:
--------------------------------------------------------------------------------
1 | npx create-next-app accessible-react-101
2 |
3 | yarn add next typescript react tailwind
4 |
5 | touch tsconfig.json
6 |
--------------------------------------------------------------------------------
/start/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body {
7 | padding: 0;
8 | margin: 0;
9 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
10 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
11 | }
12 |
13 | a {
14 | color: inherit;
15 | text-decoration: none;
16 | }
17 |
18 | * {
19 | box-sizing: border-box;
20 | }
21 |
--------------------------------------------------------------------------------
/start/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors')
2 |
3 | module.exports = {
4 | purge: ['./pages/**/*.tsx', './components/**/*.tsx'],
5 | darkMode: false, // or 'media' or 'class'
6 | theme: {
7 | extend: {},
8 | colors,
9 | },
10 | variants: {
11 | extend: {},
12 | },
13 | plugins: [],
14 | }
15 |
--------------------------------------------------------------------------------
/start/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve"
20 | },
21 | "include": [
22 | "next-env.d.ts",
23 | "**/*.ts",
24 | "**/*.tsx"
25 | ],
26 | "exclude": [
27 | "node_modules"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------