├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── TextEditor
│ ├── Link
│ │ ├── Link.tsx
│ │ └── index.ts
│ ├── TextEditor.scss
│ ├── TextEditor.tsx
│ ├── config.ts
│ ├── context.tsx
│ ├── convert.tsx
│ ├── index.ts
│ └── useEditor.tsx
├── ToolPanel
│ ├── ToolPanel.scss
│ ├── ToolPanel.tsx
│ └── index.ts
├── index.css
├── index.tsx
├── react-app-env.d.ts
├── reportWebVitals.ts
└── setupTests.ts
├── 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 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Текстовый редактор на React.js
2 | ## Запуск проекта
3 | ### `yarn start`
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "editor-ts",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "@types/jest": "^26.0.15",
10 | "@types/node": "^12.0.0",
11 | "@types/react": "^17.0.0",
12 | "@types/react-dom": "^17.0.0",
13 | "classnames": "^2.3.1",
14 | "draft-convert": "^2.1.11",
15 | "draft-js": "^0.11.7",
16 | "react": "^17.0.2",
17 | "react-dom": "^17.0.2",
18 | "react-scripts": "4.0.3",
19 | "typescript": "^4.1.2",
20 | "web-vitals": "^1.0.1"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "prettier": "prettier --write .",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject"
28 | },
29 | "eslintConfig": {
30 | "extends": [
31 | "react-app",
32 | "react-app/jest"
33 | ]
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | },
47 | "devDependencies": {
48 | "@types/draft-convert": "^2.1.3",
49 | "@types/draft-js": "^0.11.3",
50 | "node-sass": "^4.14.1",
51 | "prettier": "^2.3.2"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ktsstudio/editor-react/3628025612461c473c239289e7bc6db1c1ffa5a8/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ktsstudio/editor-react/3628025612461c473c239289e7bc6db1c1ffa5a8/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ktsstudio/editor-react/3628025612461c473c239289e7bc6db1c1ffa5a8/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import TextEditor, { TextEditorProvider } from "./TextEditor";
3 | import ToolPanel from "./ToolPanel";
4 |
5 | function App() {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | }
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/src/TextEditor/Link/Link.tsx:
--------------------------------------------------------------------------------
1 | import { ContentState } from "draft-js";
2 | import * as React from "react";
3 | import { useEditorApi } from "../context";
4 |
5 | type LinkProps = {
6 | children: React.ReactNode;
7 | contentState: ContentState;
8 | entityKey: string;
9 | };
10 |
11 | const Link: React.FC = ({ contentState, entityKey, children }) => {
12 | const { setEntityData } = useEditorApi();
13 | const { url, className } = contentState.getEntity(entityKey).getData();
14 |
15 | const handlerClick = () => {
16 | const newUrl = prompt("URL:", url);
17 | if (newUrl) {
18 | setEntityData(entityKey, { url: newUrl });
19 | }
20 | };
21 |
22 | return (
23 |
24 | {children}
25 |
26 | );
27 | };
28 |
29 | export default Link;
30 |
--------------------------------------------------------------------------------
/src/TextEditor/Link/index.ts:
--------------------------------------------------------------------------------
1 | import Link from "./Link";
2 | import { EntityType } from "../config";
3 | import { ContentBlock, ContentState, DraftDecorator } from "draft-js";
4 |
5 | function findLinkEntities(
6 | contentBlock: ContentBlock,
7 | callback: (start: number, end: number) => void,
8 | contentState: ContentState
9 | ): void {
10 | contentBlock.findEntityRanges((character) => {
11 | const entityKey = character.getEntity();
12 | return (
13 | entityKey !== null &&
14 | contentState.getEntity(entityKey).getType() === EntityType.link
15 | );
16 | }, callback);
17 | }
18 |
19 | const decorator: DraftDecorator = {
20 | strategy: findLinkEntities,
21 | component: Link,
22 | };
23 |
24 | export default decorator;
25 |
--------------------------------------------------------------------------------
/src/TextEditor/TextEditor.scss:
--------------------------------------------------------------------------------
1 | .text-editor {
2 | border: 1px solid black;
3 | padding: 15px;
4 | cursor: pointer;
5 | }
6 |
7 | .public-DraftEditor-content {
8 | min-height: 100px;
9 | }
10 |
--------------------------------------------------------------------------------
/src/TextEditor/TextEditor.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Editor } from "draft-js";
3 | import "./TextEditor.scss";
4 | import { BLOCK_RENDER_MAP, CUSTOM_STYLE_MAP } from "./config";
5 | import { useEditorApi } from "./context";
6 | import cn from "classnames";
7 |
8 | export type TextEditorProps = {
9 | className?: string;
10 | };
11 |
12 | const TextEditor: React.FC = ({ className }) => {
13 | const editorApi = useEditorApi();
14 |
15 | return (
16 |
17 |
26 |
27 | );
28 | };
29 |
30 | export default TextEditor;
31 |
--------------------------------------------------------------------------------
/src/TextEditor/config.ts:
--------------------------------------------------------------------------------
1 | import Immutable from "immutable";
2 | import { DraftEditorCommand, DefaultDraftBlockRenderMap } from "draft-js";
3 |
4 | export enum EntityType {
5 | link = "link",
6 | }
7 |
8 | export enum BlockType {
9 | h1 = "header-one",
10 | h2 = "header-two",
11 | h3 = "header-three",
12 | h4 = "header-four",
13 | h5 = "header-five",
14 | h6 = "header-six",
15 | blockquote = "blockquote",
16 | code = "code-block",
17 | list = "unordered-list-item",
18 | orderList = "ordered-list-item",
19 | cite = "cite",
20 | default = "unstyled",
21 | }
22 |
23 | export enum InlineStyle {
24 | BOLD = "BOLD",
25 | ITALIC = "ITALIC",
26 | UNDERLINE = "UNDERLINE",
27 | ACCENT = "ACCENT",
28 | }
29 |
30 | export const BLOCK_LABELS = {
31 | [BlockType.h1]: "Заголовок 1",
32 | [BlockType.h2]: "Заголовок 2",
33 | [BlockType.h3]: "Заголовок 3",
34 | [BlockType.h4]: "Заголовок 4",
35 | [BlockType.h5]: "Заголовок 5",
36 | [BlockType.h6]: "Заголовок 6",
37 | [BlockType.blockquote]: "Цитата",
38 | [BlockType.code]: "Блок с кодом",
39 | [BlockType.list]: "Маркированный список",
40 | [BlockType.orderList]: "Нумерованный список",
41 | [BlockType.cite]: "Сноска",
42 | [BlockType.default]: "Обычный текст",
43 | };
44 |
45 | export type KeyCommand = DraftEditorCommand | "accent";
46 |
47 | const CUSTOM_BLOCK_RENDER_MAP = Immutable.Map({
48 | [BlockType.cite]: {
49 | element: "cite",
50 | },
51 | });
52 |
53 | export const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.merge(
54 | CUSTOM_BLOCK_RENDER_MAP
55 | );
56 |
57 | export const CUSTOM_STYLE_MAP = {
58 | [InlineStyle.ACCENT]: {
59 | backgroundColor: "#F7F6F3",
60 | color: "#A41E68",
61 | },
62 | };
63 |
--------------------------------------------------------------------------------
/src/TextEditor/context.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { EditorApi, useEditor } from "./useEditor";
3 |
4 | const TextEditorContext = React.createContext(undefined);
5 |
6 | export const TextEditorProvider: React.FC = ({ children }) => {
7 | const editorApi = useEditor();
8 |
9 | return (
10 |
11 | {children}
12 |
13 | );
14 | };
15 |
16 | export const useEditorApi = () => {
17 | const context = React.useContext(TextEditorContext);
18 | if (context === undefined) {
19 | throw new Error("useEditorApi must be used within TextEditorProvider");
20 | }
21 |
22 | return context;
23 | };
24 |
--------------------------------------------------------------------------------
/src/TextEditor/convert.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/heading-has-content */
2 | import { convertFromHTML, convertToHTML } from "draft-convert";
3 | import { CUSTOM_STYLE_MAP, BlockType, EntityType, InlineStyle } from "./config";
4 |
5 | export const stateToHTML = convertToHTML({
6 | styleToHTML: (style) => {
7 | switch (style) {
8 | case InlineStyle.BOLD:
9 | return ;
10 | case InlineStyle.ITALIC:
11 | return ;
12 | case InlineStyle.UNDERLINE:
13 | return (
14 |
15 | );
16 | case InlineStyle.ACCENT:
17 | return (
18 |
22 | );
23 | default:
24 | return null;
25 | }
26 | },
27 | blockToHTML: (block) => {
28 | switch (block.type) {
29 | case BlockType.cite:
30 | return ;
31 | case BlockType.h1:
32 | return ;
33 | case BlockType.h2:
34 | return ;
35 | case BlockType.h3:
36 | return ;
37 | case BlockType.h4:
38 | return ;
39 | case BlockType.h5:
40 | return ;
41 | case BlockType.h6:
42 | return ;
43 | case BlockType.orderList:
44 | return {
45 | element: ,
46 | nest:
,
47 | };
48 | case BlockType.list:
49 | return {
50 | element: ,
51 | nest: ,
52 | };
53 | case BlockType.blockquote:
54 | return ;
55 | case BlockType.default:
56 | return ;
57 | default:
58 | return null;
59 | }
60 | },
61 | entityToHTML: (entity, originalText) => {
62 | if (entity.type === EntityType.link) {
63 | return {originalText};
64 | }
65 | return originalText;
66 | },
67 | });
68 |
69 | export const HTMLtoState = convertFromHTML({
70 | htmlToStyle: (nodeName, node, currentStyle) => {
71 | if (nodeName === "strong") {
72 | return currentStyle.add(InlineStyle.BOLD);
73 | }
74 |
75 | if (nodeName === "em") {
76 | return currentStyle.add(InlineStyle.ITALIC);
77 | }
78 |
79 | if (nodeName === "span" && node.classList.contains("underline")) {
80 | return currentStyle.add(InlineStyle.UNDERLINE);
81 | }
82 |
83 | if (nodeName === "span" && node.classList.contains("accent")) {
84 | return currentStyle.add(InlineStyle.ACCENT);
85 | }
86 |
87 | return currentStyle;
88 | },
89 | /** Типизация пакета не предусматривает параметр last, но он есть */
90 | // @ts-ignore
91 | htmlToBlock(nodeName, node, last) {
92 | switch (nodeName) {
93 | case "h1":
94 | return BlockType.h1;
95 | case "h2":
96 | return BlockType.h2;
97 | case "h3":
98 | return BlockType.h3;
99 | case "h4":
100 | return BlockType.h4;
101 | case "li":
102 | if (last === "ol") {
103 | return BlockType.orderList;
104 | }
105 | return BlockType.list;
106 | case "blockquote":
107 | return BlockType.blockquote;
108 | case "cite":
109 | return BlockType.cite;
110 | case "div":
111 | case "p":
112 | return BlockType.default;
113 | default:
114 | return null;
115 | }
116 | },
117 | htmlToEntity: (nodeName, node, createEntity) => {
118 | if (nodeName === "a" && node.href) {
119 | return createEntity(EntityType.link, "MUTABLE", { url: node.href });
120 | }
121 |
122 | return undefined;
123 | },
124 | });
125 |
--------------------------------------------------------------------------------
/src/TextEditor/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./TextEditor";
2 |
3 | export * from "./TextEditor";
4 | export * from "./useEditor";
5 | export * from "./context";
6 |
--------------------------------------------------------------------------------
/src/TextEditor/useEditor.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | KeyBindingUtil,
3 | getDefaultKeyBinding,
4 | DraftHandleValue,
5 | CompositeDecorator,
6 | DraftEntityMutability,
7 | EditorState,
8 | RichUtils,
9 | } from "draft-js";
10 | import * as React from "react";
11 | import { BlockType, EntityType, InlineStyle, KeyCommand } from "./config";
12 | import { HTMLtoState, stateToHTML } from "./convert";
13 | import LinkDecorator from "./Link";
14 |
15 | export type EditorApi = {
16 | state: EditorState;
17 | onChange: (state: EditorState) => void;
18 | toggleBlockType: (blockType: BlockType) => void;
19 | currentBlockType: BlockType;
20 | toHtml: () => string;
21 | toggleInlineStyle: (inlineStyle: InlineStyle) => void;
22 | hasInlineStyle: (inlineStyle: InlineStyle) => boolean;
23 | addLink: (url: string) => void;
24 | setEntityData: (entityKey: string, data: Record) => void;
25 | handleKeyCommand: (
26 | command: KeyCommand,
27 | editorState: EditorState
28 | ) => DraftHandleValue;
29 | handlerKeyBinding: (e: React.KeyboardEvent) => KeyCommand | null;
30 | };
31 |
32 | const decorator = new CompositeDecorator([LinkDecorator]);
33 |
34 | export const useEditor = (html?: string): EditorApi => {
35 | const [state, setState] = React.useState(() =>
36 | html
37 | ? EditorState.createWithContent(HTMLtoState(html), decorator)
38 | : EditorState.createEmpty(decorator)
39 | );
40 |
41 | const toggleBlockType = React.useCallback((blockType: BlockType) => {
42 | setState((currentState) =>
43 | RichUtils.toggleBlockType(currentState, blockType)
44 | );
45 | }, []);
46 |
47 | const currentBlockType = React.useMemo(() => {
48 | const selection = state.getSelection();
49 | const content = state.getCurrentContent();
50 | const block = content.getBlockForKey(selection.getStartKey());
51 | console.log(block.toJS());
52 | return block.getType() as BlockType;
53 | }, [state]);
54 |
55 | const toggleInlineStyle = React.useCallback((inlineStyle: InlineStyle) => {
56 | setState((currentState) =>
57 | RichUtils.toggleInlineStyle(currentState, inlineStyle)
58 | );
59 | }, []);
60 |
61 | const hasInlineStyle = React.useCallback(
62 | (inlineStyle: InlineStyle) => {
63 | const currentStyle = state.getCurrentInlineStyle();
64 | return currentStyle.has(inlineStyle);
65 | },
66 | [state]
67 | );
68 |
69 | const setEntityData = React.useCallback((entityKey, data) => {
70 | setState((currentState) => {
71 | const content = currentState.getCurrentContent();
72 | const contentStateUpdated = content.mergeEntityData(entityKey, data);
73 | return EditorState.push(
74 | currentState,
75 | contentStateUpdated,
76 | "apply-entity"
77 | );
78 | });
79 | }, []);
80 |
81 | const addEntity = React.useCallback(
82 | (
83 | entityType: EntityType,
84 | data: Record,
85 | mutability: DraftEntityMutability
86 | ) => {
87 | setState((currentState) => {
88 | const contentState = currentState.getCurrentContent();
89 | const contentStateWithEntity = contentState.createEntity(
90 | entityType,
91 | mutability,
92 | data
93 | );
94 | const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
95 | const newState = EditorState.set(currentState, {
96 | currentContent: contentStateWithEntity,
97 | });
98 | return RichUtils.toggleLink(
99 | newState,
100 | newState.getSelection(),
101 | entityKey
102 | );
103 | });
104 | },
105 | []
106 | );
107 |
108 | const addLink = React.useCallback(
109 | (url) => {
110 | addEntity(EntityType.link, { url }, "MUTABLE");
111 | },
112 | [addEntity]
113 | );
114 |
115 | const handleKeyCommand = React.useCallback(
116 | (command: KeyCommand, editorState: EditorState) => {
117 | if (command === "accent") {
118 | toggleInlineStyle(InlineStyle.ACCENT);
119 | return "handled";
120 | }
121 |
122 | const newState = RichUtils.handleKeyCommand(editorState, command);
123 |
124 | if (newState) {
125 | setState(newState);
126 | return "handled";
127 | }
128 |
129 | return "not-handled";
130 | },
131 | [toggleInlineStyle]
132 | );
133 |
134 | const handlerKeyBinding = React.useCallback((e: React.KeyboardEvent) => {
135 | if (e.keyCode === 81 && KeyBindingUtil.hasCommandModifier(e)) {
136 | return "accent";
137 | }
138 |
139 | return getDefaultKeyBinding(e);
140 | }, []);
141 |
142 | const toHtml = React.useCallback(
143 | () => stateToHTML(state.getCurrentContent()),
144 | [state]
145 | );
146 |
147 | return React.useMemo(
148 | () => ({
149 | state,
150 | onChange: setState,
151 | toggleBlockType,
152 | currentBlockType,
153 | toggleInlineStyle,
154 | hasInlineStyle,
155 | toHtml,
156 | addLink,
157 | setEntityData,
158 | handleKeyCommand,
159 | handlerKeyBinding,
160 | }),
161 | [
162 | state,
163 | toggleBlockType,
164 | currentBlockType,
165 | toggleInlineStyle,
166 | hasInlineStyle,
167 | toHtml,
168 | addLink,
169 | setEntityData,
170 | handleKeyCommand,
171 | handlerKeyBinding,
172 | ]
173 | );
174 | };
175 |
--------------------------------------------------------------------------------
/src/ToolPanel/ToolPanel.scss:
--------------------------------------------------------------------------------
1 | .tool-panel {
2 | display: flex;
3 | flex-wrap: wrap;
4 | align-items: center;
5 | margin-left: -5px;
6 | margin-right: -5px;
7 |
8 | &__item {
9 | flex-shrink: 0;
10 | margin: 0 5px 10px;
11 |
12 | &_active {
13 | color: red;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/ToolPanel/ToolPanel.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useEditorApi } from "../TextEditor";
3 | import cn from "classnames";
4 | import { BlockType, InlineStyle } from "../TextEditor/config";
5 | import "./ToolPanel.scss";
6 |
7 | const ToolPanel: React.FC = () => {
8 | const {
9 | toHtml,
10 | addLink,
11 | toggleBlockType,
12 | currentBlockType,
13 | toggleInlineStyle,
14 | hasInlineStyle,
15 | } = useEditorApi();
16 |
17 | return (
18 |
19 |
31 |
43 |
55 |
67 |
68 | {Object.values(InlineStyle).map((v) => (
69 |
82 | ))}
83 |
84 |
95 |
96 |
104 |
105 | );
106 | };
107 |
108 | export default ToolPanel;
109 |
--------------------------------------------------------------------------------
/src/ToolPanel/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./ToolPanel";
2 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 10px;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | * {
12 | box-sizing: border-box;
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 | import reportWebVitals from "./reportWebVitals";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from "web-vitals";
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom";
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src"]
20 | }
21 |
--------------------------------------------------------------------------------