├── .husky ├── pre-commit └── commit-msg ├── commitlint.config.js ├── .gitignore ├── .prettierignore ├── release.config.js ├── .github └── workflows │ ├── ci.yml │ └── cd.yml ├── README.md ├── lib ├── index.tsx └── __tests__ │ └── index.tsx └── package.json /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run format 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .rpt2_cache/ 3 | .rts2_cache_cjs/ 4 | .rts2_cache_es/ 5 | .rts2_cache_umd/ 6 | node_modules 7 | .idea 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | .rpt2_cache/ 3 | .rts2_cache_cjs/ 4 | .rts2_cache_es/ 5 | .rts2_cache_umd/ 6 | node_modules 7 | dist 8 | .nyc_output 9 | .pnpm* 10 | pnpm-lock.yaml 11 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: [ 3 | "main", 4 | { name: "beta", prerelease: true }, 5 | { name: "alpha", prerelease: true }, 6 | ], 7 | plugins: [ 8 | "@semantic-release/commit-analyzer", 9 | "@semantic-release/release-notes-generator", 10 | "@semantic-release/npm", 11 | "@semantic-release/github", 12 | "@semantic-release/git", 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "**" 7 | 8 | jobs: 9 | build: 10 | name: CI 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | 20 | - name: NPM ci 21 | run: npm ci 22 | 23 | # build 24 | - name: lint 25 | run: npm run lint 26 | - name: build 27 | run: npm run build 28 | - name: test 29 | run: npm test 30 | 31 | # release 32 | - name: release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.TRUEWORK_TEAM_NPM_TOKEN }} 36 | run: npx semantic-release -d 37 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - beta 8 | 9 | jobs: 10 | build: 11 | name: CI 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 16 20 | 21 | - name: NPM ci 22 | run: npm ci 23 | 24 | # build 25 | - name: lint 26 | run: npm run lint 27 | - name: build 28 | run: npm run build 29 | - name: test 30 | run: npm test 31 | 32 | # release 33 | - name: release 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | NPM_TOKEN: ${{ secrets.TRUEWORK_TEAM_NPM_TOKEN }} 37 | run: npx semantic-release 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mounty  [](https://bundlephobia.com/result?p=mounty) 2 | 3 | A tiny React transition manager with mount/unmount support and a familiar API. 4 | 5 | ### Install 6 | 7 | ``` 8 | npm i mounty --save 9 | ``` 10 | 11 | # Usage 12 | 13 | The code below is [demoed here](https://codesandbox.io/s/mounty-demo-4zgwp). 14 | 15 | ```javascript 16 | import { Mounty } from "mounty"; 17 | 18 | function App() { 19 | const [active, setActive] = React.useState(false); 20 | 21 | return ( 22 | <> 23 | setActive(!active)}>Click to Pin 24 | 25 | 26 | {({ active, ready, entering, entered, exiting, exited }) => { 27 | return ( 28 | 34 | I'm automatically mounted & unmounted, and I fade in and out while 35 | doing it! 36 | 37 | ); 38 | }} 39 | 40 | > 41 | ); 42 | } 43 | 44 | render(, document.getElementById("root")); 45 | ``` 46 | 47 | ### License 48 | 49 | MIT License © [Truework](https://www.truework.com) 50 | -------------------------------------------------------------------------------- /lib/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export type MountyProps = { 4 | children(props: MountyState): JSX.Element; 5 | in: boolean; 6 | timeout?: number; 7 | shouldUnmount?: boolean; 8 | onEntering?: () => void; 9 | onEntered?: () => void; 10 | onExiting?: () => void; 11 | onExited?: () => void; 12 | }; 13 | 14 | export type MountyState = { 15 | active: boolean; 16 | ready: boolean; 17 | entering: boolean; 18 | entered: boolean; 19 | exiting: boolean; 20 | exited: boolean; 21 | }; 22 | 23 | export function Mounty({ 24 | children, 25 | in: isIn, 26 | timeout, 27 | shouldUnmount, 28 | ...events 29 | }: MountyProps) { 30 | const [state, setState] = React.useState({ 31 | active: isIn, 32 | ready: isIn, 33 | entering: false, 34 | entered: isIn, 35 | exiting: false, 36 | exited: false, 37 | }); 38 | 39 | React.useEffect(() => { 40 | if (isIn && !state.active) { 41 | setState((prev) => ({ 42 | ...prev, 43 | active: true, 44 | entered: false, 45 | exited: false, 46 | })); 47 | 48 | requestAnimationFrame(() => { 49 | requestAnimationFrame(() => { 50 | setState((prev) => ({ 51 | ...prev, 52 | ready: true, 53 | entering: true, 54 | })); 55 | 56 | if (events.onEntering) events.onEntering(); 57 | 58 | setTimeout(() => { 59 | setState((prev) => ({ 60 | ...prev, 61 | entering: false, 62 | entered: true, 63 | })); 64 | 65 | if (events.onEntered) events.onEntered(); 66 | }, timeout); 67 | }); 68 | }); 69 | } else if (!isIn && state.active) { 70 | setState((prev) => ({ 71 | ...prev, 72 | exiting: true, 73 | ready: false, 74 | entered: false, 75 | })); 76 | 77 | if (events.onExiting) events.onExiting(); 78 | 79 | setTimeout(() => { 80 | setState((prev) => ({ 81 | ...prev, 82 | active: false, 83 | exiting: false, 84 | exited: true, 85 | })); 86 | 87 | if (events.onExited) events.onExited(); 88 | }, timeout); 89 | } 90 | }, [isIn]); 91 | 92 | return !state.active && shouldUnmount ? null : children(state); 93 | } 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mounty", 3 | "version": "1.5.2", 4 | "description": "A tiny React transition manager with mount/unmount support and a familiar API.", 5 | "source": "lib/index.tsx", 6 | "main": "dist/mounty.js", 7 | "module": "dist/mounty.es.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "prepare": "is-ci || husky install", 14 | "build": "microbundle build", 15 | "watch": "microbundle watch --compress false", 16 | "test": "jest", 17 | "lint": "prettier --check .", 18 | "format": "prettier --write ." 19 | }, 20 | "keywords": [ 21 | "react", 22 | "react transition group", 23 | "react transition", 24 | "toggle", 25 | "animation" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "git+ssh://git@github.com/truework/mounty.git" 30 | }, 31 | "author": "estrattonbailey", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/truework/mounty/issues" 35 | }, 36 | "homepage": "https://github.com/truework/mounty#readme", 37 | "devDependencies": { 38 | "@commitlint/cli": "^16.2.4", 39 | "@commitlint/config-conventional": "^16.2.4", 40 | "@semantic-release/git": "^10.0.1", 41 | "@tsconfig/create-react-app": "^1.0.2", 42 | "@types/enzyme": "^3.10.4", 43 | "@types/enzyme-adapter-react-16": "^1.0.5", 44 | "@types/jest": "^25.1.2", 45 | "@types/react": "^16.9.19", 46 | "@types/react-dom": "^16.9.5", 47 | "commitlint": "^16.2.4", 48 | "enzyme": "^3.11.0", 49 | "enzyme-adapter-react-16": "^1.15.2", 50 | "husky": "^7.0.4", 51 | "is-ci": "^3.0.1", 52 | "jest": "^25.1.0", 53 | "microbundle": "^0.12.0-next.8", 54 | "prettier": "^2.6.2", 55 | "react": "16", 56 | "react-dom": "16", 57 | "semantic-release": "^19.0.2", 58 | "ts-jest": "^25.2.0", 59 | "ts-node": "^8.6.2", 60 | "typescript": "^3.7.5" 61 | }, 62 | "jest": { 63 | "verbose": true, 64 | "preset": "ts-jest", 65 | "globals": { 66 | "ts-jest": { 67 | "tsConfig": { 68 | "noErrorTruncation": true, 69 | "noImplicitAny": true, 70 | "strictNullChecks": true, 71 | "jsx": "react" 72 | } 73 | } 74 | } 75 | }, 76 | "peerDependencies": { 77 | "react": ">= 16.9" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/__tests__/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { configure, mount } from "enzyme"; 3 | import * as Adapter from "enzyme-adapter-react-16"; 4 | 5 | import { Mounty, MountyProps } from "../"; 6 | 7 | configure({ adapter: new Adapter() }); 8 | 9 | const wait = async (ms: number) => new Promise((r) => setTimeout(r, ms)); 10 | 11 | function Base(props: Omit) { 12 | return ( 13 | {(state) => {JSON.stringify(state)}} 14 | ); 15 | } 16 | 17 | test("default", async (done) => { 18 | const tree = mount(); 19 | 20 | expect(tree.text()).toEqual( 21 | JSON.stringify({ 22 | active: false, 23 | ready: false, 24 | entering: false, 25 | entered: false, 26 | exiting: false, 27 | exited: false, 28 | }) 29 | ); 30 | 31 | tree.setProps({ in: true }); 32 | 33 | await wait(100); 34 | tree.update(); 35 | 36 | expect(tree.text()).toEqual( 37 | JSON.stringify({ 38 | active: true, 39 | ready: true, 40 | entering: false, 41 | entered: true, 42 | exiting: false, 43 | exited: false, 44 | }) 45 | ); 46 | 47 | done(); 48 | }); 49 | 50 | test("unmounts", async (done) => { 51 | const tree = mount(); 52 | 53 | expect(tree.text()).toEqual(""); 54 | 55 | tree.setProps({ in: true, shouldUnmount: true }); 56 | 57 | await wait(100); 58 | tree.update(); 59 | 60 | expect(tree.text()).toEqual( 61 | JSON.stringify({ 62 | active: true, 63 | ready: true, 64 | entering: false, 65 | entered: true, 66 | exiting: false, 67 | exited: false, 68 | }) 69 | ); 70 | 71 | tree.setProps({ in: false, shouldUnmount: true }); 72 | 73 | await wait(100); 74 | tree.update(); 75 | 76 | expect(tree.text()).toEqual(""); 77 | 78 | done(); 79 | }); 80 | 81 | test("timeout", async (done) => { 82 | const tree = mount(); 83 | 84 | tree.setProps({ in: true }); 85 | 86 | await wait(100); 87 | tree.update(); 88 | 89 | expect(tree.text()).toEqual( 90 | JSON.stringify({ 91 | active: true, 92 | ready: true, 93 | entering: true, 94 | entered: false, 95 | exiting: false, 96 | exited: false, 97 | }) 98 | ); 99 | 100 | await wait(500); 101 | tree.update(); 102 | 103 | expect(tree.text()).toEqual( 104 | JSON.stringify({ 105 | active: true, 106 | ready: true, 107 | entering: false, 108 | entered: true, 109 | exiting: false, 110 | exited: false, 111 | }) 112 | ); 113 | 114 | tree.setProps({ in: false }); 115 | 116 | await wait(500); 117 | tree.update(); 118 | 119 | expect(tree.text()).toEqual( 120 | JSON.stringify({ 121 | active: false, 122 | ready: false, 123 | entering: false, 124 | entered: false, 125 | exiting: false, 126 | exited: true, 127 | }) 128 | ); 129 | 130 | done(); 131 | }); 132 | 133 | test("hooks", async (done) => { 134 | const onEntering = jest.fn(); 135 | const onEntered = jest.fn(); 136 | const onExiting = jest.fn(); 137 | const onExited = jest.fn(); 138 | 139 | const tree = mount( 140 | 148 | ); 149 | 150 | tree.setProps({ in: true }); 151 | 152 | await wait(100); 153 | tree.update(); 154 | 155 | expect(onEntering).toHaveBeenCalled(); 156 | 157 | await wait(100); 158 | tree.update(); 159 | 160 | expect(onEntered).toHaveBeenCalled(); 161 | 162 | tree.setProps({ in: false }); 163 | 164 | await wait(100); 165 | tree.update(); 166 | 167 | expect(onExiting).toHaveBeenCalled(); 168 | 169 | await wait(100); 170 | tree.update(); 171 | 172 | expect(onExited).toHaveBeenCalled(); 173 | 174 | done(); 175 | }); 176 | --------------------------------------------------------------------------------