├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .storybook ├── main.js └── manager.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ ├── __snapshots__ │ └── index.tsx.snap └── index.tsx ├── jest.config.js ├── package-lock.json ├── package.json ├── source ├── index.tsx └── types │ └── fast-shallow-equal.d.ts ├── stories ├── base.css └── index.tsx ├── tsconfig.base.json ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json └── tsconfig.umd.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "react-expanding-textarea", 3 | "projectOwner": "rpearce", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "contributors": [ 12 | { 13 | "login": "rpearce", 14 | "name": "Robert Pearce", 15 | "avatar_url": "https://avatars2.githubusercontent.com/u/592876?v=4", 16 | "profile": "https://robertwpearce.com", 17 | "contributions": [ 18 | "code", 19 | "doc", 20 | "example", 21 | "ideas", 22 | "test" 23 | ] 24 | }, 25 | { 26 | "login": "oyeanuj", 27 | "name": "Anuj", 28 | "avatar_url": "https://avatars2.githubusercontent.com/u/9633371?v=4", 29 | "profile": "http://shuffle.do/@anuj", 30 | "contributions": [ 31 | "bug" 32 | ] 33 | }, 34 | { 35 | "login": "lloydwatkin", 36 | "name": "Lloyd Watkin", 37 | "avatar_url": "https://avatars0.githubusercontent.com/u/271622?v=4", 38 | "profile": "http://www.evilprofessor.co.uk", 39 | "contributions": [ 40 | "ideas" 41 | ] 42 | }, 43 | { 44 | "login": "jch254", 45 | "name": "Jordan Hornblow", 46 | "avatar_url": "https://avatars2.githubusercontent.com/u/3821107?v=4", 47 | "profile": "https://603.nz", 48 | "contributions": [ 49 | "bug" 50 | ] 51 | }, 52 | { 53 | "login": "visgotti", 54 | "name": "visgotti", 55 | "avatar_url": "https://avatars0.githubusercontent.com/u/7028891?v=4", 56 | "profile": "https://github.com/visgotti", 57 | "contributions": [ 58 | "ideas" 59 | ] 60 | }, 61 | { 62 | "login": "thomassnielsen", 63 | "name": "Thomas Sunde Nielsen", 64 | "avatar_url": "https://avatars1.githubusercontent.com/u/626954?v=4", 65 | "profile": "http://blogg.leieting.no/om-oss", 66 | "contributions": [ 67 | "bug", 68 | "ideas" 69 | ] 70 | }, 71 | { 72 | "login": "cibulka", 73 | "name": "cibulka", 74 | "avatar_url": "https://avatars2.githubusercontent.com/u/3989833?v=4", 75 | "profile": "https://github.com/cibulka", 76 | "contributions": [ 77 | "bug", 78 | "ideas" 79 | ] 80 | }, 81 | { 82 | "login": "jbsmith731", 83 | "name": "Brett Smith", 84 | "avatar_url": "https://avatars2.githubusercontent.com/u/6562559?v=4", 85 | "profile": "https://brettsmith.me", 86 | "contributions": [ 87 | "bug" 88 | ] 89 | }, 90 | { 91 | "login": "raunofreiberg", 92 | "name": "Rauno Freiberg", 93 | "avatar_url": "https://avatars1.githubusercontent.com/u/23662329?v=4", 94 | "profile": "https://raunofreiberg.com", 95 | "contributions": [ 96 | "bug", 97 | "code" 98 | ] 99 | }, 100 | { 101 | "login": "tknuts", 102 | "name": "Thomas Kristiansen", 103 | "avatar_url": "https://avatars3.githubusercontent.com/u/3716280?v=4", 104 | "profile": "https://github.com/tknuts", 105 | "contributions": [ 106 | "ideas" 107 | ] 108 | }, 109 | { 110 | "login": "Puspendert", 111 | "name": "Puspender", 112 | "avatar_url": "https://avatars0.githubusercontent.com/u/16055344?v=4", 113 | "profile": "https://github.com/Puspendert", 114 | "contributions": [ 115 | "bug" 116 | ] 117 | }, 118 | { 119 | "login": "markathomas", 120 | "name": "Mark Thomas", 121 | "avatar_url": "https://avatars3.githubusercontent.com/u/488472?v=4", 122 | "profile": "https://github.com/markathomas", 123 | "contributions": [ 124 | "bug" 125 | ] 126 | }, 127 | { 128 | "login": "1v", 129 | "name": "Artem", 130 | "avatar_url": "https://avatars0.githubusercontent.com/u/6566370?v=4", 131 | "profile": "https://github.com/1v", 132 | "contributions": [ 133 | "bug" 134 | ] 135 | }, 136 | { 137 | "login": "elitenoire", 138 | "name": "Eva Raymond", 139 | "avatar_url": "https://avatars3.githubusercontent.com/u/25673419?v=4", 140 | "profile": "https://twitter.com/EvaRaymie", 141 | "contributions": [ 142 | "bug" 143 | ] 144 | }, 145 | { 146 | "login": "chrisdrackett", 147 | "name": "Chris Drackett", 148 | "avatar_url": "https://avatars3.githubusercontent.com/u/4378?v=4", 149 | "profile": "https://github.com/chrisdrackett", 150 | "contributions": [ 151 | "bug" 152 | ] 153 | }, 154 | { 155 | "login": "simonsmith", 156 | "name": "Simon Smith", 157 | "avatar_url": "https://avatars0.githubusercontent.com/u/360703?v=4", 158 | "profile": "http://simonsmith.io/", 159 | "contributions": [ 160 | "bug", 161 | "ideas", 162 | "review" 163 | ] 164 | }, 165 | { 166 | "login": "jordie23", 167 | "name": "jordie23", 168 | "avatar_url": "https://avatars0.githubusercontent.com/u/712360?v=4", 169 | "profile": "https://github.com/jordie23", 170 | "contributions": [ 171 | "bug", 172 | "ideas" 173 | ] 174 | }, 175 | { 176 | "login": "mat-sz", 177 | "name": "Mat Sz", 178 | "avatar_url": "https://avatars0.githubusercontent.com/u/57893590?v=4", 179 | "profile": "https://matsz.dev/", 180 | "contributions": [ 181 | "bug", 182 | "code" 183 | ] 184 | }, 185 | { 186 | "login": "crtl", 187 | "name": "crtl", 188 | "avatar_url": "https://avatars.githubusercontent.com/u/25827827?v=4", 189 | "profile": "https://github.com/crtl", 190 | "contributions": [ 191 | "bug", 192 | "ideas" 193 | ] 194 | }, 195 | { 196 | "login": "jnthnwn", 197 | "name": "Jonathan Wan", 198 | "avatar_url": "https://avatars.githubusercontent.com/u/4400604?v=4", 199 | "profile": "https://github.com/jnthnwn", 200 | "contributions": [ 201 | "bug", 202 | "code" 203 | ] 204 | }, 205 | { 206 | "login": "jamesmoss", 207 | "name": "James Moss", 208 | "avatar_url": "https://avatars.githubusercontent.com/u/629766?v=4", 209 | "profile": "http://moss.io/", 210 | "contributions": [ 211 | "bug", 212 | "ideas" 213 | ] 214 | }, 215 | { 216 | "login": "SunnyAureliusRichard", 217 | "name": "SunnyAureliusRichard", 218 | "avatar_url": "https://avatars.githubusercontent.com/u/100728856?v=4", 219 | "profile": "https://github.com/SunnyAureliusRichard", 220 | "contributions": [ 221 | "bug" 222 | ] 223 | }, 224 | { 225 | "login": "magoz", 226 | "name": "Magoz", 227 | "avatar_url": "https://avatars.githubusercontent.com/u/9190753?v=4", 228 | "profile": "https://magoz.studio/", 229 | "contributions": [ 230 | "ideas" 231 | ] 232 | }, 233 | { 234 | "login": "jeffreystorer", 235 | "name": "jeffreystorer", 236 | "avatar_url": "https://avatars.githubusercontent.com/u/13458609?v=4", 237 | "profile": "https://github.com/jeffreystorer", 238 | "contributions": [ 239 | "bug" 240 | ] 241 | } 242 | ], 243 | "commitConvention": "none" 244 | } 245 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !.eslintrc.js 2 | !.prettierrc.js 3 | !.storybook/ 4 | dist/ 5 | docs/ 6 | source/types/ 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | jest: true, 6 | node: true, 7 | }, 8 | extends: [ 9 | 'plugin:react/recommended', 10 | 'standard', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'plugin:jsx-a11y/recommended', 13 | ], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | ecmaFeatures: { 17 | jsx: true, 18 | }, 19 | ecmaVersion: 'latest', 20 | sourceType: 'module', 21 | }, 22 | plugins: [ 23 | 'react', 24 | 'react-hooks', 25 | '@typescript-eslint', 26 | ], 27 | rules: { 28 | 'comma-dangle': ['error', { 29 | arrays: 'always-multiline', 30 | exports: 'always-multiline', 31 | functions: 'ignore', 32 | imports: 'always-multiline', 33 | objects: 'always-multiline', 34 | }], 35 | 'react-hooks/exhaustive-deps': 'error', 36 | 'react-hooks/rules-of-hooks': 'error', 37 | 'react/prop-types': 0, 38 | 'space-before-function-paren': 'off', 39 | 'spaced-comment': 'off', 40 | }, 41 | settings: { 42 | react: { 43 | version: 'detect', 44 | }, 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: rpearce 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout repo under GH workspace 11 | uses: actions/checkout@v2 12 | 13 | - name: Use nodejs 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: '16' 17 | cache: 'npm' 18 | 19 | - name: Install deps without updating package-lock.json 20 | run: npm i --no-save 21 | 22 | - name: Run the CI build 23 | run: npm run ci 24 | 25 | - name: Deploy to gh-pages 26 | uses: peaceiris/actions-gh-pages@v3 27 | if: ${{ github.ref == 'refs/heads/main' }} 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: ./docs 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage/ 3 | dist/ 4 | docs/ 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | addons: [ 3 | '@storybook/addon-a11y', 4 | '@storybook/addon-controls', 5 | ], 6 | stories: ['../stories/index.tsx'], 7 | } 8 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons' 2 | 3 | addons.setConfig({ 4 | panelPosition: 'right', 5 | }) 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.3.6] - 2022-05-24 9 | 10 | ### Changed 11 | 12 | * Updated `tslib` compat to be `^2.4.0` 13 | 14 | ### Fixed 15 | 16 | * Support for React 18 17 | * Updated `react-with-forwarded-ref` to solve legacy peer deps issue 18 | 19 | ## [2.3.5] - 2022-03-02 20 | 21 | ### Fixed 22 | 23 | * Fix SSR useLayoutEffect warning in 24 | nextjs (https://github.com/rpearce/react-expanding-textarea/pull/84) 25 | 26 | ## [2.3.4] - 2022-01-03 27 | 28 | ### Changed 29 | 30 | * "Use useLayoutEffect instead of useEffect" (#72) 31 | 32 | ### Fixed 33 | 34 | * "Textarea does not automatically recalculate size if it's parent container 35 | changes size" (#71). This adds a tiny dependency on 36 | [`fast-shallow-equal`](https://www.npmjs.com/package/fast-shallow-equal) and 37 | pairs it with a custom hook to be able to detect shallow changes to a `style` 38 | object if it gets passed. We also are now resizing using a `ResizeObserver`, 39 | if it's supported, as well as if a provided `className` changes. 40 | 41 | ## [2.3.3] - 2021-10-28 42 | 43 | ### Fixed 44 | 45 | * Fix legacy peer deps issue 46 | 47 | ## [2.3.2] - 2021-08-12 48 | 49 | ### Fixed 50 | 51 | * Fix: use ChangeEvent over FormEvent (issue #61) 52 | 53 | ## [2.3.1] - 2021-01-05 54 | 55 | ### Fixed 56 | 57 | * "Force a resize if the provided value is changed" (PR #58 from @mat-sz) 58 | 59 | ## [2.3.0] - 2020-12-08 60 | 61 | ### Added 62 | 63 | * can now pass a callback-style `ref` instead of only the `createRef` or 64 | `useRef` variant object that has a `current` property; allows for tools like 65 | `react-hook-form` to work with this project (#52) 66 | 67 | ### Changed 68 | 69 | * patch upgrade to `react-with-forwarded-ref` 70 | 71 | ## [2.2.4] - 2020-12-03 72 | 73 | ### Changed 74 | 75 | * simplified `ref` logic on `textarea` element 76 | * patch upgrade to `ts-lib` 77 | 78 | ## [2.2.2] - 2020-08-28 79 | 80 | ### Fixed 81 | 82 | * textareas with `maxHeight` not having a scrollbar (#43) 83 | 84 | ## [2.2.2] - 2020-08-28 85 | 86 | ### Changed 87 | 88 | * README update 89 | 90 | ## [2.2.1] - 2020-08-28 91 | 92 | ### Changed 93 | 94 | * now building output using tsc instead of rollup 95 | 96 | ### Fixed 97 | 98 | * now exporting the textarea interface (should resolve #47) 99 | 100 | ## [2.2.0] - 2020-03-14 101 | 102 | ### Added 103 | 104 | * typescript support 105 | 106 | ### Changed 107 | 108 | * license from ISC to BSD-3 109 | 110 | ### Fixed 111 | 112 | * issue where `line-height` is `normal` and calculation breaks by falling back 113 | to `fontSize * 1.2` 114 | * upgrades to fix vulnerabilities 115 | 116 | ## [2.1.2] - 2020-02-04 117 | 118 | ### Added 119 | 120 | * Commonjs `react-expanding-textarea.min.js` build 121 | * UMD `react-expanding-textarea.min.js` build 122 | 123 | ### Fixed 124 | 125 | * auto-adjusting issue in firefox (#33) 126 | 127 | ### Changed 128 | 129 | * changed `browser` field value in package.json to point to 130 | `dist/umd/react-expanding-textarea.min.js` suffix 131 | 132 | ## [2.1.1] - 2020-01-19 133 | 134 | ### Fixed 135 | 136 | * README 137 | 138 | ## [2.1.0] - 2020-01-19 139 | 140 | ### Added 141 | 142 | * support for forwarding a ref (#36) 143 | * added `babel-plugin-transform-react-remove-prop-types` 144 | 145 | ## [2.0.4] - 2020-01-05 146 | 147 | ### Changed 148 | 149 | * bumped devDependencies 150 | * added UMD build 151 | * changed build location for commonjs and esmodules to `dist/esm/` and 152 | `dist/cjs`. 153 | 154 | ## [2.0.3] - 2019-10-14 155 | 156 | ### Fixed 157 | 158 | * Fixed textarea growing before it needed to (#31) 159 | 160 | ## [2.0.2] - 2019-09-11 161 | 162 | ### Fixed 163 | 164 | * `prop-types` was being used but not included as a dependency 165 | 166 | ## [2.0.1] - 2019-09-10 167 | 168 | ### Changed 169 | 170 | * fixed security issues for `sshpk`, `cached-path-relative` and `mixin-deep` 171 | 172 | ## [2.0.0] - 2019-09-07 173 | 174 | ### Added 175 | 176 | * dependency on `prop-types` 177 | 178 | ### Changed 179 | 180 | * build folder is now `dist/` 181 | * now building with `rollup` 182 | * now providing CJS & ESM dist files (`main` and `module` in `package.json`) 183 | 184 | ## [1.0.0] - 2019-02-24 185 | 186 | ### Added 187 | 188 | * responds to both `onChange` and `onInput` callbacks now 189 | 190 | ### Changed 191 | 192 | * complete rewrite using React hooks. Minimum react peer dependency is now 193 | `>= 16.8`. 194 | 195 | ### Fixed 196 | 197 | * includes a fix for #18 198 | 199 | ## [0.2.0] - 2018-08-08 200 | 201 | ### Added / Fixed 202 | 203 | * addressed #14 where the `rows` attribute was being disregarded. Now, it 204 | provides a means to provide a minimum/default number of `rows`. This is a 205 | minorversion bump because it will cause the component to behave differently 206 | for existing folks and is really more of an addition than a fix. 207 | 208 | ## [0.1.10] - 2018-04-29 209 | 210 | ### Fixed 211 | 212 | * fixed #10 where a change in the value prop was not recalculating the size 213 | 214 | ## [0.1.9] - 2017-10-05 215 | 216 | ### Fixed 217 | 218 | * support for react v16 219 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at me@robertwpearce.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Check out the [issues](https://github.com/rpearce/react-expanding-textarea/issues) 4 | 1. [Fork](https://guides.github.com/activities/forking/) this repository 5 | 1. [Clone](https://help.github.com/articles/cloning-a-repository/) your fork 6 | 1. Add the upstream project (this one) as a git remote: 7 | ``` 8 | $ git remote add upstream git@github.com:rpearce/react-expanding-textarea.git 9 | $ git fetch upstream 10 | $ git rebase upstream/main 11 | ``` 12 | 1. Check out a feature branch 13 | ``` 14 | $ git checkout -b my-feature 15 | ``` 16 | 1. Make your changes 17 | 1. Push your branch to your GitHub repo 18 | ``` 19 | $ git push origin my-feature 20 | ``` 21 | 1. Create a [pull request](https://help.github.com/articles/about-pull-requests/) 22 | from your branch to this repo's `main` branch 23 | 1. When all is merged, pull down the upstream changes to your main 24 | ``` 25 | $ git fetch upstream 26 | $ git merge upstream/main 27 | ``` 28 | 1. Delete your feature branch (locally and then on GitHub) 29 | ``` 30 | $ git branch -D my-feature 31 | $ git push origin :my-feature 32 | ``` 33 | 34 | ## Testing 35 | Tests are located in the `test/` folder. Here's how to run them: 36 | 37 | ``` 38 | $ yarn test 39 | ``` 40 | 41 | To test in watch mode: 42 | 43 | ``` 44 | $ yarn test --watch 45 | ``` 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Robert Pearce 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Robert Pearce nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ Retired 2 | 3 | Please consider using https://github.com/Andarist/react-textarea-autosize. There's an insurmountable issue with this approach in https://github.com/rpearce/react-expanding-textarea/issues/82 that is solved by `react-textarea-autosize`. 4 | 5 | # react-expanding-textarea 6 | 7 | [![All Contributors](https://img.shields.io/badge/all_contributors-24-orange.svg?style=flat-square)](#contributors-) [![npm version](https://img.shields.io/npm/v/react-expanding-textarea.svg?style=flat-square)](https://www.npmjs.com/package/react-expanding-textarea) [![npm downloads](https://img.shields.io/npm/dm/react-expanding-textarea.svg?style=flat-square)](https://www.npmjs.com/package/react-expanding-textarea) [![bundlephobia size](https://flat.badgen.net/bundlephobia/minzip/react-expanding-textarea)](https://bundlephobia.com/result?p=react-expanding-textarea) 8 | 9 | React textarea component to automatically expand and contract your textareas. 10 | 11 | You can [view the demo here](http://rpearce.github.io/react-expanding-textarea/). 12 | 13 | ## Links 14 | 15 | * [Installation](#installation) 16 | * [Usage](#usage) 17 | * [Using The `rows` Prop](#using-the-rows-prop) 18 | * [Manually Calling `resize`](#manually-calling-resize) 19 | * [Changelog](./CHANGELOG.md) 20 | * [Contributing](./CONTRIBUTING.md) 21 | * [Code of Conduct](./CODE_OF_CONDUCT.md) 22 | 23 | ## Installation 24 | 25 | ``` 26 | npm i react-expanding-textarea 27 | ``` 28 | 29 | ## Usage 30 | 31 | Use this exactly like you would a normal ` 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /__tests__/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties, createRef } from 'react' 2 | import { fireEvent, render } from '@testing-library/react' 3 | import '@testing-library/jest-dom/extend-expect' 4 | 5 | import ExpandingTextarea, { getHeight, resize } from '../source' 6 | 7 | test('renders with a minimum number of rows', () => { 8 | const wrapStyles: CSSProperties = { 9 | boxSizing: 'border-box', 10 | maxWidth: '500px', 11 | } 12 | const textareaStyles = { 13 | borderBottom: '4px solid', 14 | borderTop: '4px solid', 15 | lineHeight: '27.2px', 16 | padding: '10px', 17 | width: '100%', 18 | } 19 | const { getByText } = render( 20 |
21 | 26 |
27 | ) 28 | 29 | expect(getByText('Some text')).toHaveStyle('height: 109.6px') 30 | }) 31 | 32 | test('calls onChange & onInput when text changes', () => { 33 | const onChange = jest.fn() 34 | const onInput = jest.fn() 35 | const { getByText } = render( 36 | 41 | ) 42 | 43 | fireEvent.input(getByText('Some text'), { 44 | target: { value: 'Some text is here' }, 45 | }) 46 | 47 | expect(onChange).toBeCalled() 48 | expect(onInput).toBeCalled() 49 | }) 50 | 51 | test('continues to work if no onChange and no onInput', () => { 52 | const { asFragment, getByText } = render( 53 | 54 | ) 55 | 56 | fireEvent.input(getByText('Some text'), { 57 | target: { value: 'Some text is here' }, 58 | }) 59 | 60 | expect(asFragment()).toMatchSnapshot() 61 | }) 62 | 63 | test('getHeight: returns scroll height when no rows', () => { 64 | window.getComputedStyle = jest.fn().mockImplementation(() => ({ 65 | borderBottomWidth: '4px', 66 | borderTopWidth: '4px', 67 | lineHeight: '27.2px', 68 | paddingBottom: '10px', 69 | paddingTop: '10px', 70 | })) 71 | const el = document.createElement('textarea') 72 | jest.spyOn(el, 'scrollHeight', 'get').mockImplementation(() => 129) 73 | 74 | expect(getHeight(0, el)).toEqual(137) 75 | }) 76 | 77 | test('getHeight: returns scroll height when larger than row height', () => { 78 | window.getComputedStyle = jest.fn().mockImplementation(() => ({ 79 | borderBottomWidth: '4px', 80 | borderTopWidth: '4px', 81 | lineHeight: '27.2px', 82 | paddingBottom: '10px', 83 | paddingTop: '10px', 84 | })) 85 | const el = document.createElement('textarea') 86 | jest.spyOn(el, 'scrollHeight', 'get').mockImplementation(() => 129) 87 | 88 | expect(getHeight(3, el)).toEqual(137) 89 | }) 90 | 91 | test('getHeight: returns row height when larger than scroll height', () => { 92 | window.getComputedStyle = jest.fn().mockImplementation(() => ({ 93 | borderBottomWidth: '4px', 94 | borderTopWidth: '4px', 95 | lineHeight: '27.2px', 96 | paddingBottom: '10px', 97 | paddingTop: '10px', 98 | })) 99 | const el = document.createElement('textarea') 100 | jest.spyOn(el, 'scrollHeight', 'get').mockImplementation(() => 129) 101 | 102 | expect(getHeight(5, el)).toEqual(164) 103 | }) 104 | 105 | test('resize: sets style height when present', () => { 106 | window.getComputedStyle = jest.fn().mockImplementation(() => ({ 107 | borderBottomWidth: '4px', 108 | borderTopWidth: '4px', 109 | lineHeight: '27.2px', 110 | paddingBottom: '10px', 111 | paddingTop: '10px', 112 | })) 113 | 114 | const el = document.createElement('textarea') 115 | jest.spyOn(el, 'scrollHeight', 'get').mockImplementation(() => 129) 116 | el.style.height = '100px' 117 | 118 | resize(0, null) 119 | 120 | expect(el.style.height).toEqual('100px') 121 | 122 | resize(0, el) 123 | 124 | expect(el.style.height).toEqual('137px') 125 | }) 126 | 127 | test('accepts ref', () => { 128 | const ref = createRef() 129 | render() 130 | expect(ref.current?.tagName).toEqual('TEXTAREA') 131 | }) 132 | 133 | test('accepts function ref', () => { 134 | let el: HTMLTextAreaElement | { tagName: 'BROKEN' } = { tagName: 'BROKEN' } 135 | const ref = (node: HTMLTextAreaElement) => { el = node } 136 | render() 137 | expect(el.tagName).toEqual('TEXTAREA') 138 | }) 139 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | collectCoverage: true, 4 | collectCoverageFrom: ['/source/index.tsx'], 5 | coveragePathIgnorePatterns: ['/node_modules/'], 6 | moduleNameMapper: {}, 7 | preset: 'ts-jest', 8 | setupFilesAfterEnv: [], 9 | testEnvironment: 'jsdom', 10 | verbose: true, 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-expanding-textarea", 3 | "version": "2.3.6", 4 | "description": "React textarea component to automatically expand and contract your textareas", 5 | "main": "dist/cjs/index.js", 6 | "module": "dist/esm/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com:rpearce/react-expanding-textarea.git" 10 | }, 11 | "homepage": "https://github.com/rpearce/react-expanding-textarea", 12 | "bugs": "https://github.com/rpearce/react-expanding-textarea/issues", 13 | "author": "Robert Pearce (http://robertwpearce.com)", 14 | "contributors": [ 15 | "Robert Pearce (https://robertwpearce.com)", 16 | "Rauno Freiberg (https://raunofreiberg.com)", 17 | "Mat Sz (https://matsz.dev)", 18 | "Jonathan Wan (https://github.com/jnthnwn)" 19 | ], 20 | "license": "BSD-3", 21 | "keywords": [ 22 | "textarea", 23 | "textarea-component", 24 | "textarea-autoresize", 25 | "text-resize", 26 | "expand", 27 | "autosize", 28 | "expanding-textarea", 29 | "autosize-textarea", 30 | "react", 31 | "react-component" 32 | ], 33 | "tags": [ 34 | "textarea", 35 | "textarea-component", 36 | "textarea-autoresize", 37 | "text-resize", 38 | "expand", 39 | "autosize", 40 | "expanding-textarea", 41 | "autosize-textarea", 42 | "react", 43 | "react-component" 44 | ], 45 | "files": [ 46 | "LICENSE", 47 | "README.md", 48 | "dist/" 49 | ], 50 | "sideEffects": false, 51 | "scripts": { 52 | "build": "rm -rf ./dist && concurrently npm:build:cjs npm:build:esm npm:build:umd", 53 | "build:cjs": "tsc -p tsconfig.cjs.json", 54 | "build:docs": "rm -rf docs/ && mkdir -p docs && build-storybook -o docs", 55 | "build:esm": "tsc -p tsconfig.esm.json", 56 | "build:umd": "tsc -p tsconfig.umd.json", 57 | "ci": "concurrently npm:lint npm:test npm:build npm:build:docs", 58 | "contributors:add": "all-contributors add", 59 | "contributors:generate": "all-contributors generate", 60 | "lint": "eslint .", 61 | "prepublishOnly": "concurrently npm:lint npm:test npm:build", 62 | "start": "start-storybook -p 6006", 63 | "test": "jest" 64 | }, 65 | "devDependencies": { 66 | "@storybook/addon-a11y": "^6.4.22", 67 | "@storybook/addon-controls": "^6.4.22", 68 | "@storybook/react": "^6.4.22", 69 | "@testing-library/jest-dom": "^5.16.4", 70 | "@testing-library/react": "^13.2.0", 71 | "@types/jest": "^27.5.0", 72 | "@types/node": "^17.0.33", 73 | "@types/react": "^18.0.9", 74 | "@types/react-dom": "^18.0.4", 75 | "@typescript-eslint/eslint-plugin": "^5.26.0", 76 | "@typescript-eslint/parser": "^5.26.0", 77 | "all-contributors-cli": "^6.20.0", 78 | "concurrently": "^7.2.1", 79 | "eslint": "^8.16.0", 80 | "eslint-config-standard": "^17.0.0", 81 | "eslint-plugin-import": "^2.26.0", 82 | "eslint-plugin-jest": "^26.2.2", 83 | "eslint-plugin-jsx-a11y": "^6.5.1", 84 | "eslint-plugin-n": "^15.2.0", 85 | "eslint-plugin-promise": "^6.0.0", 86 | "eslint-plugin-react": "^7.30.0", 87 | "eslint-plugin-react-hooks": "^4.5.0", 88 | "jest": "^28.1.0", 89 | "jest-environment-jsdom": "^28.1.0", 90 | "react": "^18.1.0", 91 | "react-dom": "^18.1.0", 92 | "ts-jest": "^28.0.2", 93 | "typescript": "^4.6.4" 94 | }, 95 | "peerDependencies": { 96 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 97 | }, 98 | "dependencies": { 99 | "fast-shallow-equal": "^1.0.0", 100 | "react-with-forwarded-ref": "^0.3.5", 101 | "tslib": "^2.4.0" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /source/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | CSSProperties, 3 | ChangeEvent, 4 | FC, 5 | MutableRefObject, 6 | RefObject, 7 | TextareaHTMLAttributes, 8 | useCallback, 9 | useLayoutEffect, 10 | useMemo, 11 | useRef, 12 | } from 'react' 13 | import withForwardedRef from 'react-with-forwarded-ref' 14 | import { equal as isShallowEqual } from 'fast-shallow-equal' 15 | 16 | // ============================================================================= 17 | export interface GetHeight { 18 | (rows: number, el: HTMLTextAreaElement): number 19 | } 20 | 21 | export const getHeight: GetHeight = (rows, el) => { 22 | const { 23 | borderBottomWidth, 24 | borderTopWidth, 25 | fontSize, 26 | lineHeight, 27 | paddingBottom, 28 | paddingTop, 29 | } = window.getComputedStyle(el) 30 | 31 | const lh = 32 | lineHeight === 'normal' 33 | ? parseFloat(fontSize) * 1.2 34 | : parseFloat(lineHeight) 35 | 36 | const rowHeight = 37 | rows === 0 38 | ? 0 39 | : lh * rows + 40 | parseFloat(borderBottomWidth) + 41 | parseFloat(borderTopWidth) + 42 | parseFloat(paddingBottom) + 43 | parseFloat(paddingTop) 44 | 45 | const scrollHeight = 46 | el.scrollHeight + 47 | parseFloat(borderBottomWidth) + 48 | parseFloat(borderTopWidth) 49 | 50 | return Math.max(rowHeight, scrollHeight) 51 | } 52 | 53 | // ============================================================================= 54 | export interface Resize { 55 | (rows: number, el: HTMLTextAreaElement | null): void 56 | } 57 | 58 | export const resize: Resize = (rows, el) => { 59 | if (el) { 60 | let overflowY = 'hidden' 61 | const { maxHeight } = window.getComputedStyle(el) 62 | 63 | if (maxHeight !== 'none') { 64 | const maxHeightN = parseFloat(maxHeight) 65 | 66 | if (maxHeightN < el.scrollHeight) { 67 | overflowY = '' 68 | } 69 | } 70 | 71 | el.style.height = '0' 72 | el.style.overflowY = overflowY 73 | el.style.height = `${getHeight(rows, el)}px` 74 | } 75 | } 76 | 77 | // ============================================================================= 78 | const useShallowObjectMemo = (obj: A): A => { 79 | const refObject = useRef(obj) 80 | const refCounter = useRef(0) 81 | 82 | if (!isShallowEqual(obj, refObject.current)) { 83 | refObject.current = obj 84 | refCounter.current += 1 85 | } 86 | 87 | // eslint-disable-next-line react-hooks/exhaustive-deps 88 | return useMemo(() => refObject.current, [refCounter.current]) 89 | } 90 | 91 | // ============================================================================= 92 | const useSSRLayoutEffect = 93 | typeof window === 'undefined' ? Function.prototype : useLayoutEffect 94 | 95 | // ============================================================================= 96 | type RefFn = (node: HTMLTextAreaElement) => void 97 | 98 | export interface TextareaProps 99 | extends Omit, 'rows'> { 100 | forwardedRef?: RefObject | RefFn 101 | onChange?: (evt: ChangeEvent) => void 102 | onInput?: (evt: ChangeEvent) => void 103 | rows?: string | number | undefined 104 | value?: string 105 | } 106 | 107 | const ExpandingTextarea: FC = ({ 108 | forwardedRef, 109 | ...props 110 | }: TextareaProps) => { 111 | const isForwardedRefFn = typeof forwardedRef === 'function' 112 | const style = useShallowObjectMemo(props.style) 113 | const internalRef = useRef() 114 | const ref = ( 115 | isForwardedRefFn || !forwardedRef ? internalRef : forwardedRef 116 | ) as MutableRefObject 117 | const rows = props.rows ? parseInt('' + props.rows, 10) : 0 118 | const { onChange, onInput, ...rest } = props 119 | 120 | useSSRLayoutEffect(() => { 121 | resize(rows, ref.current) 122 | }, [props.className, props.value, ref, rows, style]) 123 | 124 | useSSRLayoutEffect(() => { 125 | if (!window.ResizeObserver) { 126 | return 127 | } 128 | 129 | const observer = new ResizeObserver(() => { 130 | resize(rows, ref.current) 131 | }) 132 | 133 | observer.observe(ref.current) 134 | 135 | return () => { 136 | observer.disconnect() 137 | } 138 | }, [ref, rows]) 139 | 140 | const handleInput = useCallback( 141 | (e: ChangeEvent) => { 142 | onChange?.(e) 143 | onInput?.(e) 144 | resize(rows, ref.current) 145 | }, 146 | [onChange, onInput, ref, rows] 147 | ) 148 | 149 | const handleRef = useCallback( 150 | (node: HTMLTextAreaElement) => { 151 | ref.current = node 152 | 153 | if (isForwardedRefFn) { 154 | (forwardedRef as RefFn)(node) 155 | } 156 | }, 157 | [forwardedRef, isForwardedRefFn, ref] 158 | ) 159 | 160 | return ( 161 |