├── .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 |
7 |
8 |

9 | Portfolio 10 |

11 | 12 | 18 |
19 |
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 |
24 | 29 |
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 |
24 | 29 |
30 | 31 |
{children}
32 |
33 | ); 34 | }; 35 | 36 | const Footer = ({ onClose }: { readonly onClose: () => void }) => { 37 | return ( 38 |
39 | 42 |
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 |

23 | 24 | 記事 #{i} 25 | 26 |

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 | 3 | 4 | -------------------------------------------------------------------------------- /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 |
13 |

{title}

14 |
15 | 16 |
{children}
17 |
18 | ); 19 | }; 20 | 21 | const Footer = ({ onClose }: { readonly onClose: () => void }) => { 22 | return ( 23 |
24 | 27 |
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 |

24 | 25 | 記事 #{i} 26 | 27 |

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 | 3 | 4 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------