├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── README.md
├── __tests__
├── Typewriting.tsx
└── utils.ts
├── package-lock.json
├── package.json
├── setup-jest.ts
├── src
├── Typewriting.tsx
└── utils.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | lib/
2 | node_modules/
3 | npm-debug.log*
4 | npm-error.log*
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | __tests__/
2 | src/
3 | npm-debug.log
4 | yarn-error.log
5 |
6 | .babelrc
7 | .eslintrc.yml
8 | .gitignore
9 | .travis.yml
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 'stable'
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## v2.2.0 (2019-01-03)
2 |
3 | - Add `fullCurrentText` argument to the render prop function signature. `fullCurrentText` holds the full value of the current string.
4 |
5 | ## v2.1.1 (2018-09-09)
6 |
7 | - Exclude __tests__ directory from npm package.
8 | - Fix TS definition exports.
9 |
10 | ## v2.1.0 (2018-09-09)
11 |
12 | - Add tests.
13 | - Refactor some code.
14 |
15 | ## v2.0.1 (2018-08-27)
16 |
17 | - Don't use React fragments to respect React v15 compatibility, as defined in `peerDependencies`.
18 |
19 | ## v2.0.0 (2018-08-11)
20 |
21 | #### Breaking changes
22 |
23 | - Use render prop pattern instead of taking `Component` as a prop. Refer to the documentation for updated examples.
24 |
25 | #### Other
26 | - Port everything to TypeScript (TS definition file is now provided in the npm package).
27 |
28 |
29 | ## v1.1.0 (2018-08-09)
30 |
31 | - Make it possible to pass a number tuple to `writeSpeedMs` and `deleteSpeedMs` for more granular control.
32 | - Add a minimum threshold for randomized timeouts to avoid UI updates that are too adjacent in time.
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | *React component for creating customizable typewriting visualizations*
2 |
3 | [](https://travis-ci.org/williamboman/react-typewriting)
4 |
5 | ## Usage
6 |
7 | ```tsx
8 | import { Typewriting } from 'react-typewriting'
9 |
10 | interface TypewritingRenderArgs {
11 | currentText: string
12 | fullCurrentText: string
13 | }
14 |
15 |
21 | {({ currentText, fullCurrentText }: TypewritingRenderArgs) => (
22 | {currentText}
23 | )}
24 |
25 | ```
26 |
27 | ## Installation
28 |
29 | ```sh
30 | $ npm install react-typewriting
31 | # or
32 | $ yarn add react-typewriting
33 | ```
34 |
35 | ## Props
36 |
37 | ### `strings` | `Array` | *required*
38 |
39 | The strings to print out, in order of appearance.
40 |
41 | ### `children` | `({ currentText: string, fullCurrentText: string }) => ReactNode` | *required*
42 |
43 | The child render prop.
44 |
45 | - `currentText` holds the latest, sliced, version of the current string
46 | - `fullCurrentText` holds the full value of the current string
47 |
48 | ### `waitBeforeDeleteMs` | `number` | default: 9000
49 |
50 | Amount of milliseconds strings will be fully readable before starting
51 | to delete the string.
52 |
53 | ### `writeSpeedMs` | `number` or `[number, number]` | default: 100
54 |
55 | This prop controls the speed at which the strings are built.
56 |
57 | If provided a `number`, this number will be the longest time to wait between writing characters.
58 |
59 | If provided a `[number, number]` tuple, a number between these two values will be the longest time to wait between writing characters.
60 |
61 | ### `deleteSpeedMs` | `number` or `[number, number]`| default: 60
62 |
63 | Same as `writeSpeedMs` (see above), but for when deleting characters.
64 |
65 | ## License
66 |
67 | Licensed under the MIT license.
68 |
69 | ## Authors
70 |
71 | **William Boman**
72 |
--------------------------------------------------------------------------------
/__tests__/Typewriting.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { mount } from "enzyme"
3 | import { Typewriting } from "../src/Typewriting"
4 |
5 | jest.useFakeTimers()
6 |
7 | test("calls child function with correct arguments", () => {
8 | const childFn = jest.fn(({ currentText }) => currentText)
9 | mount({childFn})
10 |
11 | let n = 0
12 | while (n++ < ["foo bar", "lorem ipsum"].join(" ").length * 2) {
13 | jest.runOnlyPendingTimers()
14 | }
15 |
16 | expect(childFn.mock.calls).toMatchObject([
17 | [{ currentText: "" }],
18 | [{ currentText: "f" }],
19 | [{ currentText: "fo" }],
20 | [{ currentText: "foo" }],
21 | [{ currentText: "foo " }],
22 | [{ currentText: "foo b" }],
23 | [{ currentText: "foo ba" }],
24 | [{ currentText: "foo bar" }],
25 | [{ currentText: "foo bar" }],
26 | [{ currentText: "foo ba" }],
27 | [{ currentText: "foo b" }],
28 | [{ currentText: "foo " }],
29 | [{ currentText: "foo" }],
30 | [{ currentText: "fo" }],
31 | [{ currentText: "f" }],
32 | [{ currentText: "" }],
33 | [{ currentText: "" }],
34 | [{ currentText: "l" }],
35 | [{ currentText: "lo" }],
36 | [{ currentText: "lor" }],
37 | [{ currentText: "lore" }],
38 | [{ currentText: "lorem" }],
39 | [{ currentText: "lorem " }],
40 | [{ currentText: "lorem i" }],
41 | [{ currentText: "lorem ip" }],
42 | [{ currentText: "lorem ips" }],
43 | [{ currentText: "lorem ipsu" }],
44 | [{ currentText: "lorem ipsum" }],
45 | [{ currentText: "lorem ipsum" }],
46 | [{ currentText: "lorem ipsu" }],
47 | [{ currentText: "lorem ips" }],
48 | [{ currentText: "lorem ip" }],
49 | [{ currentText: "lorem i" }],
50 | [{ currentText: "lorem " }],
51 | [{ currentText: "lorem" }],
52 | [{ currentText: "lore" }],
53 | [{ currentText: "lor" }],
54 | [{ currentText: "lo" }],
55 | [{ currentText: "l" }],
56 | ])
57 | })
58 |
59 | test("respects writeSpeedMs prop", () => {
60 | const childFn = jest.fn(({ currentText }) => currentText)
61 | mount(
62 |
63 | {childFn}
64 | ,
65 | )
66 |
67 | jest.advanceTimersByTime(300)
68 | expect(childFn.mock.calls).toMatchObject([
69 | [{ currentText: "" }],
70 | [{ currentText: "f" }],
71 | [{ currentText: "fo" }],
72 | [{ currentText: "foo" }],
73 | [{ currentText: "foo " }],
74 | ])
75 | jest.advanceTimersByTime(300)
76 | expect(childFn.mock.calls).toMatchObject([
77 | [{ currentText: "" }],
78 | [{ currentText: "f" }],
79 | [{ currentText: "fo" }],
80 | [{ currentText: "foo" }],
81 | [{ currentText: "foo " }],
82 | [{ currentText: "foo b" }],
83 | [{ currentText: "foo ba" }],
84 | [{ currentText: "foo bar" }],
85 | ])
86 | })
87 |
88 | test("respects the deleteSpeedMs prop", () => {
89 | const childFn = jest.fn(({ currentText }) => currentText)
90 | const wrapper = mount(
91 |
92 | {childFn}
93 | ,
94 | )
95 |
96 | while (wrapper.find(Typewriting).text() !== "foo bar") {
97 | jest.runOnlyPendingTimers()
98 | }
99 | jest.runOnlyPendingTimers()
100 | jest.runOnlyPendingTimers()
101 |
102 | jest.advanceTimersByTime(300)
103 | expect(childFn.mock.calls).toMatchObject([
104 | [{ currentText: "" }],
105 | [{ currentText: "f" }],
106 | [{ currentText: "fo" }],
107 | [{ currentText: "foo" }],
108 | [{ currentText: "foo " }],
109 | [{ currentText: "foo b" }],
110 | [{ currentText: "foo ba" }],
111 | [{ currentText: "foo bar" }],
112 | [{ currentText: "foo bar" }],
113 | [{ currentText: "foo ba" }],
114 | [{ currentText: "foo b" }],
115 | [{ currentText: "foo " }],
116 | [{ currentText: "foo" }],
117 | ])
118 |
119 | jest.advanceTimersByTime(300)
120 | expect(childFn.mock.calls).toMatchObject([
121 | [{ currentText: "" }],
122 | [{ currentText: "f" }],
123 | [{ currentText: "fo" }],
124 | [{ currentText: "foo" }],
125 | [{ currentText: "foo " }],
126 | [{ currentText: "foo b" }],
127 | [{ currentText: "foo ba" }],
128 | [{ currentText: "foo bar" }],
129 | [{ currentText: "foo bar" }],
130 | [{ currentText: "foo ba" }],
131 | [{ currentText: "foo b" }],
132 | [{ currentText: "foo " }],
133 | [{ currentText: "foo" }],
134 | [{ currentText: "fo" }],
135 | [{ currentText: "f" }],
136 | [{ currentText: "" }],
137 | ])
138 | })
139 |
140 | test("calls the render prop function with correct values", () => {
141 | const childFn = jest.fn(val => null)
142 | mount(
143 |
148 | {childFn}
149 | ,
150 | )
151 | expect(childFn.mock.calls[0]).toEqual([
152 | {
153 | currentText: "",
154 | fullCurrentText: "foo bar",
155 | },
156 | ])
157 | jest.advanceTimersByTime(700)
158 | expect(childFn.mock.calls[16]).toEqual([
159 | {
160 | currentText: "",
161 | fullCurrentText: "lorem ipsum",
162 | },
163 | ])
164 | })
165 |
--------------------------------------------------------------------------------
/__tests__/utils.ts:
--------------------------------------------------------------------------------
1 | import { MINIMUM_THRESHOLD, randomizeTimeout } from "../src/utils"
2 |
3 | test("randomizes single timeout range number correctly", () => {
4 | for (let i = 30; i <= 100; ++i) {
5 | const timeout = randomizeTimeout(i)
6 | expect(timeout).toBeGreaterThanOrEqual(MINIMUM_THRESHOLD)
7 | expect(timeout).toBeLessThanOrEqual(i)
8 | }
9 | })
10 |
11 | test("randomizes single timeout range above MINIMUM_THRESHOLD", () => {
12 | expect(randomizeTimeout(MINIMUM_THRESHOLD - 10)).toBe(MINIMUM_THRESHOLD)
13 | })
14 |
15 | test("randomizes timeout range number correctly", () => {
16 | let min = 0
17 | let max = 100
18 | let i = 0
19 | while (i++ < 100) {
20 | const timeout = randomizeTimeout([min, max])
21 | expect(timeout).toBeGreaterThanOrEqual(min)
22 | expect(timeout).toBeLessThanOrEqual(max)
23 | ++min
24 | ++max
25 | }
26 | })
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-typewriting",
3 | "version": "2.2.0",
4 | "homepage": "https://github.com/williamboman/react-typewriting",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/williamboman/react-typewriting.git"
8 | },
9 | "description": "React component for creating highly customizable typewriting effects.",
10 | "main": "lib/Typewriting.js",
11 | "typings": "lib/Typewriting.d.ts",
12 | "scripts": {
13 | "build": "rm -rf lib/ && tsc --declaration --outDir lib",
14 | "prepublish": "npm run build",
15 | "jest": "jest",
16 | "test": "npm run ts-lint && npm run jest",
17 | "ts-lint": "tsc --noEmit"
18 | },
19 | "keywords": [
20 | "react",
21 | "component",
22 | "typewriting",
23 | "typewriter",
24 | "animation",
25 | "typescript"
26 | ],
27 | "author": "William Boman ",
28 | "license": "MIT",
29 | "devDependencies": {
30 | "@types/enzyme": "^3.1.14",
31 | "@types/enzyme-adapter-react-16": "^1.0.3",
32 | "@types/jest": "^23.3.4",
33 | "@types/react": "15.6.19",
34 | "enzyme": "^3.7.0",
35 | "enzyme-adapter-react-16": "^1.6.0",
36 | "jest": "^23.6.0",
37 | "react": "^16.5.2",
38 | "react-dom": "^16.5.2",
39 | "ts-jest": "^23.10.4",
40 | "typescript": "3.1.1"
41 | },
42 | "peerDependenies": {
43 | "react": ">=15"
44 | },
45 | "jest": {
46 | "roots": [
47 | "/__tests__"
48 | ],
49 | "transform": {
50 | "^.+\\.tsx?$": "ts-jest"
51 | },
52 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
53 | "moduleFileExtensions": [
54 | "ts",
55 | "tsx",
56 | "js",
57 | "jsx",
58 | "json",
59 | "node"
60 | ],
61 | "setupTestFrameworkScriptFile": "/setup-jest.ts"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/setup-jest.ts:
--------------------------------------------------------------------------------
1 | import * as Enzyme from "enzyme"
2 | import * as Adapter from "enzyme-adapter-react-16"
3 |
4 | Enzyme.configure({ adapter: new Adapter() })
5 |
--------------------------------------------------------------------------------
/src/Typewriting.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { randomizeTimeout, TimeoutRange } from "./utils"
4 |
5 | enum Tick {
6 | INIT,
7 | WRITE,
8 | DELETE,
9 | START_DELETE,
10 | }
11 |
12 | const DEFAULTS = {
13 | WRITE_SPEED_MS: 100,
14 | DELETE_SPEED_MS: 60,
15 | WAIT_BEFORE_DELETE_MS: 9000,
16 | }
17 |
18 | interface RenderArgs {
19 | currentText: string
20 | fullCurrentText: string
21 | }
22 |
23 | interface Props {
24 | strings: string[]
25 | waitBeforeDeleteMs?: number
26 | writeSpeedMs?: TimeoutRange
27 | deleteSpeedMs?: TimeoutRange
28 | children: (args: RenderArgs) => React.ReactNode
29 | }
30 |
31 | interface State {
32 | currentStringIdx: number
33 | currentCharPos: number
34 | isDeleting: boolean
35 | }
36 |
37 | const moveToNextString = (
38 | prevState: State,
39 | props: Props,
40 | ): Pick => {
41 | const nextStringIdx = prevState.currentStringIdx + 1
42 | return {
43 | isDeleting: false,
44 | currentCharPos: 0,
45 | currentStringIdx: nextStringIdx < props.strings.length ? nextStringIdx : 0,
46 | }
47 | }
48 |
49 | const moveCharPos = (change: number) => (prevState: State): Pick => ({
50 | currentCharPos: prevState.currentCharPos + change,
51 | })
52 |
53 | const startDeleting = (): Pick => ({
54 | isDeleting: true,
55 | })
56 |
57 | export class Typewriting extends React.PureComponent {
58 | private tickTimeout: number | null = null
59 |
60 | state = {
61 | currentStringIdx: 0,
62 | currentCharPos: 0,
63 | isDeleting: false,
64 | }
65 |
66 | componentDidMount() {
67 | this.queueTick(Tick.INIT)
68 | }
69 |
70 | componentWillUnmount() {
71 | if (this.tickTimeout != null) {
72 | clearTimeout(this.tickTimeout)
73 | }
74 | }
75 |
76 | private queueTick(tickType: Tick) {
77 | const { writeSpeedMs, deleteSpeedMs, waitBeforeDeleteMs } = this.props
78 |
79 | const timeout =
80 | tickType === Tick.INIT
81 | ? 0
82 | : tickType === Tick.WRITE
83 | ? randomizeTimeout(writeSpeedMs != null ? writeSpeedMs : DEFAULTS.WRITE_SPEED_MS)
84 | : tickType === Tick.DELETE
85 | ? randomizeTimeout(deleteSpeedMs != null ? deleteSpeedMs : DEFAULTS.DELETE_SPEED_MS)
86 | : tickType === Tick.START_DELETE
87 | ? waitBeforeDeleteMs != null
88 | ? waitBeforeDeleteMs
89 | : DEFAULTS.WAIT_BEFORE_DELETE_MS
90 | : 0 // ¯\_(ツ)_/¯
91 |
92 | this.tickTimeout = window.setTimeout(() => this.tick(), timeout)
93 | }
94 |
95 | private tick() {
96 | const { currentStringIdx, currentCharPos, isDeleting } = this.state
97 | const currentText = this.props.strings[currentStringIdx]
98 |
99 | if (!isDeleting) {
100 | if (currentCharPos >= currentText.length) {
101 | this.setState(startDeleting, () => this.queueTick(Tick.START_DELETE))
102 | } else {
103 | this.setState(moveCharPos(1), () => this.queueTick(Tick.WRITE))
104 | }
105 | } else {
106 | if (currentCharPos <= 0) {
107 | this.setState(moveToNextString, () => this.queueTick(Tick.WRITE))
108 | } else {
109 | this.setState(moveCharPos(-1), () => this.queueTick(Tick.DELETE))
110 | }
111 | }
112 | }
113 |
114 | render() {
115 | const { strings } = this.props
116 | const { currentStringIdx, currentCharPos } = this.state
117 |
118 | const fullCurrentText = strings[currentStringIdx]
119 | const currentText = fullCurrentText.slice(0, currentCharPos)
120 |
121 | return this.props.children({ currentText, fullCurrentText }) as any
122 | }
123 | }
124 |
125 | // for backwards compatibility
126 | export default Typewriting
127 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export type TimeoutRange = number | [number, number]
2 |
3 | export const MINIMUM_THRESHOLD = 30
4 |
5 | export const randomizeTimeout = (ms: TimeoutRange): number =>
6 | Array.isArray(ms)
7 | ? // random value inside the specified min and max thresholds
8 | ms[0] + Math.random() * (ms[1] - ms[0])
9 | : // randomize the value - with a minimum threshold
10 | Math.max(Math.random() * ms, MINIMUM_THRESHOLD)
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "scripthost", "es5", "es6"],
5 | "typeRoots": ["./node_modules/@types"],
6 | "jsx": "react",
7 | "noUnusedLocals": true,
8 | "noImplicitAny": true,
9 | "strictNullChecks": true
10 | },
11 | "include": ["./src/**/*"],
12 | "exclude": ["./node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------