├── .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 | [![Build Status](https://travis-ci.org/williamboman/react-typewriting.svg?branch=master)](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 | --------------------------------------------------------------------------------