├── .npmrc ├── tests ├── setupTests.ts ├── AutowidthInput.test.tsx └── __snapshots__ │ └── AutowidthInput.test.tsx.snap ├── .commitlintrc.json ├── .prettierrc.json ├── .husky ├── pre-commit ├── commit-msg └── common.sh ├── src ├── index.tsx ├── stories │ └── AutowidthInput.stories.tsx └── AutowidthInput.tsx ├── .editorconfig ├── .storybook ├── preview.js └── main.js ├── tsconfig.json ├── tsup.config.ts ├── renovate.json ├── jest.config.js ├── .gitignore ├── .github └── workflows │ ├── main.yml │ └── publish.yml ├── .eslintrc.js ├── README.md └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true -------------------------------------------------------------------------------- /tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "singleQuote": false 5 | } -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | 5 | yarn lint-staged -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | 5 | yarn commitlint --edit $1 -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import AutowidthInput from "./AutowidthInput"; 2 | 3 | export * from "./AutowidthInput"; 4 | export default AutowidthInput; 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /.husky/common.sh: -------------------------------------------------------------------------------- 1 | command_exists () { 2 | command -v "$1" >/dev/null 2>&1 3 | } 4 | 5 | # Workaround for Windows 10, Git Bash and Yarn 6 | if command_exists winpty && test -t 1; then 7 | exec < /dev/tty 8 | fi -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: "^on[A-Z].*" }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "allowJs": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.tsx"], 5 | treeshake: true, 6 | sourcemap: true, 7 | minify: true, 8 | clean: true, 9 | dts: true, 10 | splitting: false, 11 | format: ["cjs", "esm"], 12 | external: ["react"], 13 | }); 14 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "timezone": "Asia/Kuala_Lumpur", 4 | "schedule": ["on the first day of the month"], 5 | "packageRules": [ 6 | { 7 | "matchPackagePatterns": ["*"], 8 | "matchUpdateTypes": ["minor", "patch"], 9 | "groupName": "all non-major dependencies", 10 | "groupSlug": "all-minor-patch" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 3 | addons: [ 4 | "@storybook/addon-links", 5 | "@storybook/addon-essentials", 6 | "@storybook/addon-interactions", 7 | ], 8 | framework: "@storybook/react", 9 | core: { 10 | builder: "webpack5", 11 | }, 12 | typescript: { 13 | check: false, 14 | reactDocgen: false, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "jest-environment-jsdom", 5 | setupFilesAfterEnv: ["./tests/setupTests.ts"], 6 | transformIgnorePatterns: ["node_modules/(?!react-merge-refs)"], 7 | transform: { 8 | // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` 9 | // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` 10 | "^.+\\.m?[tj]sx?$": [ 11 | "ts-jest", 12 | { 13 | useESM: true, 14 | }, 15 | ], 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /.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 | # JetBrains IDE files 9 | .idea/ 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | /dist 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | tsconfig.tsbuildinfo 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | storybook-static -------------------------------------------------------------------------------- /src/stories/AutowidthInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ComponentStory, ComponentMeta } from "@storybook/react"; 3 | 4 | import { AutowidthInput } from ".."; 5 | 6 | export default { 7 | title: "AutowidthInput", 8 | component: AutowidthInput, 9 | argTypes: {}, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ( 13 | 14 | ); 15 | 16 | export const WithOptions = Template.bind({}); 17 | 18 | WithOptions.args = { 19 | extraWidth: 20, 20 | minWidth: 0, 21 | placeholderIsMinWidth: true, 22 | placeholder: "Sample placeholder", 23 | }; 24 | -------------------------------------------------------------------------------- /tests/AutowidthInput.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import renderer from "react-test-renderer"; 3 | import { render, screen } from "@testing-library/react"; 4 | import { AutowidthInput } from "../src"; 5 | 6 | it("renders correctly", () => { 7 | const tree = renderer.create().toJSON(); 8 | 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | 12 | it(`renders default`, () => { 13 | render(); 14 | 15 | expect(screen.getByTestId("wrapper")).toBeInTheDocument(); 16 | expect(screen.getByTestId("sizer")).toBeInTheDocument(); 17 | expect(screen.getByTestId("input")).toBeInTheDocument(); 18 | }); 19 | 20 | it(`renders placeholder`, () => { 21 | render(); 22 | 23 | expect(screen.getByTestId("placeholder-sizer")).toBeInTheDocument(); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/__snapshots__/AutowidthInput.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |
13 |
27 | 38 |
39 | `; 40 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: [push] 4 | 5 | jobs: 6 | run-ci: 7 | env: 8 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 9 | 10 | name: Run Type Check & Linters 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set up Node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: lts/* 23 | 24 | - name: Install dependencies (with cache) 25 | uses: bahmutov/npm-install@v1 26 | 27 | - name: Check types 28 | run: yarn type-check 29 | 30 | - name: Check linting 31 | run: yarn lint 32 | 33 | - name: Run tests 34 | run: yarn test:ci 35 | 36 | - name: Build storybook 37 | run: yarn build-storybook 38 | 39 | - name: Build package 40 | run: yarn build 41 | 42 | - name: Check commits messages 43 | uses: wagoid/commitlint-github-action@v5 44 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Publish 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | build-storybook: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - run: yarn 16 | - run: yarn build-storybook 17 | publish-storybook: 18 | needs: build-storybook 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version: lts/* 25 | - run: yarn 26 | - run: yarn deploy-storybook --ci 27 | env: 28 | GH_TOKEN: ${{ github.actor }}:${{ secrets.GITHUB_TOKEN }} 29 | publish-npm: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v3 33 | - uses: actions/setup-node@v3 34 | with: 35 | node-version: lts/* 36 | registry-url: https://registry.npmjs.org/ 37 | - run: yarn 38 | - run: yarn build 39 | - run: npm publish --access public 40 | env: 41 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 42 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | jest: true, 6 | }, 7 | extends: [ 8 | "plugin:react/recommended", 9 | "airbnb", 10 | "plugin:storybook/recommended", 11 | "prettier", 12 | ], 13 | parser: "@typescript-eslint/parser", 14 | parserOptions: { 15 | ecmaFeatures: { 16 | jsx: true, 17 | }, 18 | ecmaVersion: "latest", 19 | sourceType: "module", 20 | }, 21 | plugins: [ 22 | "react", 23 | "@typescript-eslint", 24 | "typescript-sort-keys", 25 | "unused-imports", 26 | ], 27 | rules: { 28 | "typescript-sort-keys/interface": "error", 29 | "typescript-sort-keys/string-enum": "error", 30 | "react/jsx-filename-extension": "off", 31 | "react/jsx-props-no-spreading": "off", 32 | "react/function-component-definition": "off", 33 | "react/destructuring-assignment": "off", 34 | "react/require-default-props": "off", 35 | "import/no-unresolved": "off", 36 | "import/extensions": "off", 37 | "import/no-extraneous-dependencies": "off", 38 | "import/prefer-default-export": "off", 39 | "no-unused-vars": "off", 40 | "no-nested-ternary": "off", 41 | "no-param-reassign": "off", 42 | "@typescript-eslint/no-unused-vars": "error", 43 | "unused-imports/no-unused-imports": "error", 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Autowidth Input 2 | 3 | Highly configurable & extensible automatically sized input field. 4 | 5 | [![npm version](https://img.shields.io/npm/v/react-autowidth-input)](https://www.npmjs.com/package/react-autowidth-input) 6 | 7 | ## Features 8 | 9 | - Works out of the box with zero config 10 | - Supports usage as a controlled or uncontrolled input 11 | - Supports custom refs 12 | - Miniscule bundle size 13 | 14 | ## Install 15 | 16 | npm i react-autowidth-input 17 | 18 | ## Quick Start 19 | 20 | ```jsx 21 | import AutowidthInput from "react-autowidth-input"; 22 | 23 | { 26 | // event.target.value contains the new value 27 | }} 28 | />; 29 | ``` 30 | 31 | ## Additional Props 32 | 33 | The component supports the following props in extension to the regular html input props. 34 | 35 | ### extraWidth 36 | 37 | _extraWidth={number}_ 38 | 39 | Default: 16 40 | 41 | The amount of additional space rendered after the input. 42 | 43 | ```jsx 44 | import React from "react"; 45 | import AutowidthInput from "react-autowidth-input"; 46 | 47 | const MyComponent = () => { 48 | return ; 49 | }; 50 | 51 | ... 52 | ``` 53 | 54 | ### wrapperClassName 55 | 56 | _wrapperClassName={string}_ 57 | 58 | Class provided to the wrapper element encapsulating the input. 59 | 60 | ```jsx 61 | import React from "react"; 62 | import AutowidthInput from "react-autowidth-input"; 63 | 64 | const MyComponent = () => { 65 | return ; 66 | }; 67 | 68 | ... 69 | ``` 70 | 71 | ### wrapperStyle 72 | 73 | _wrapperStyle={{}}_ 74 | 75 | Inline styles provided to the wrapper element encapsulating the input. 76 | 77 | ```jsx 78 | import React from "react"; 79 | import AutowidthInput from "react-autowidth-input"; 80 | 81 | const myStyles = { 82 | padding: "1rem" 83 | } 84 | 85 | const MyComponent = () => { 86 | return 87 | }; 88 | 89 | ... 90 | ``` 91 | 92 | ### onAutoSize 93 | 94 | _onAutoSize={(newWidth) => {}}_ 95 | 96 | Callback function to be fired on input resize. `newWidth` does not include width specified by `extraWidth` (see [above for `extraWidth` prop](#extrawidth)) 97 | 98 | ```jsx 99 | import React, {useState} from "react"; 100 | import AutowidthInput from "react-autowidth-input"; 101 | 102 | const MyComponent = () => { 103 | const [width, setWidth] = useState(0); 104 | 105 | const myFunction = (newWidth) => { 106 | setWidth(newWidth); 107 | } 108 | 109 | return 110 | }; 111 | 112 | ... 113 | ``` 114 | 115 | ### placeholderIsMinWidth 116 | 117 | _placeholderIsMinWidth={boolean}_ 118 | 119 | If set to true, the input will never resize to be smaller than the width of the placeholder. 120 | 121 | ```jsx 122 | import React from "react"; 123 | import AutowidthInput from "react-autowidth-input"; 124 | 125 | const MyComponent = () => { 126 | return 127 | }; 128 | 129 | ... 130 | ``` 131 | 132 | ### minWidth 133 | 134 | _minWidth={number}_ 135 | 136 | If set, specifies the minimum width of input element. Width specified by `extraWidth` is applied anyway, so actual minimum width is actually `extraWidth + minWidth` (see [above for `extraWidth` prop](#extrawidth)) 137 | 138 | ```jsx 139 | import React from "react"; 140 | import AutowidthInput from "react-autowidth-input"; 141 | 142 | const MyComponent = () => { 143 | return 144 | }; 145 | 146 | ... 147 | ``` 148 | 149 | ## Notes 150 | 151 | This component was inspired by Jed Watson's react-input-autosize, but rebuilt with modern react APIs. 152 | 153 | ## Contributors 154 | 155 | - [kierien](https://github.com/kierien) 156 | - [burtek](https://github.com/burtek) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-autowidth-input", 3 | "version": "1.0.10", 4 | "description": "Highly configurable & extensible automatically sized input field built with hooks.", 5 | "keywords": [ 6 | "input", 7 | "autowidth", 8 | "react", 9 | "reactjs" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/inkiear/react-autowidth-input" 14 | }, 15 | "author": { 16 | "name": "Kierian Lee", 17 | "email": "hey@kierian.me", 18 | "url": "https://kierian.me" 19 | }, 20 | "license": "ISC", 21 | "types": "./dist/index.d.ts", 22 | "exports": { 23 | ".": { 24 | "types": "./dist/index.d.ts", 25 | "require": "./dist/index.js", 26 | "import": "./dist/index.mjs" 27 | } 28 | }, 29 | "files": [ 30 | "dist" 31 | ], 32 | "lint-staged": { 33 | "./{src,tests}/**/*.{ts,js,jsx,tsx}": [ 34 | "eslint --ignore-path .gitignore --fix" 35 | ] 36 | }, 37 | "config": { 38 | "commitizen": { 39 | "path": "./node_modules/cz-conventional-changelog" 40 | } 41 | }, 42 | "release-it": { 43 | "git": { 44 | "commitMessage": "chore(release): v${version}" 45 | }, 46 | "github": { 47 | "release": true 48 | }, 49 | "npm": { 50 | "publish": false 51 | } 52 | }, 53 | "engines": { 54 | "node": ">=14.0.0" 55 | }, 56 | "scripts": { 57 | "dev": "concurrently \"yarn build --watch\" \"yarn storybook\"", 58 | "build": "tsup", 59 | "type-check": "tsc --noEmit", 60 | "lint": "eslint --ignore-path .gitignore \"{src,tests}/**/*.+(ts|js|tsx)\"", 61 | "lint:fix": "yarn lint --fix", 62 | "test": "jest --coverage", 63 | "test:ci": "yarn test --ci", 64 | "test:watch": "jest --watch", 65 | "prepare": "husky install", 66 | "commit": "cz", 67 | "storybook": "start-storybook -p 6006 --quiet", 68 | "build-storybook": "build-storybook", 69 | "deploy-storybook": "storybook-to-ghpages", 70 | "release": "yarn release-it" 71 | }, 72 | "devDependencies": { 73 | "@commitlint/cli": "17.2.0", 74 | "@commitlint/config-conventional": "17.2.0", 75 | "@storybook/addon-actions": "6.5.13", 76 | "@storybook/addon-essentials": "6.5.13", 77 | "@storybook/addon-interactions": "6.5.13", 78 | "@storybook/addon-links": "6.5.13", 79 | "@storybook/builder-webpack5": "6.5.13", 80 | "@storybook/manager-webpack5": "6.5.13", 81 | "@storybook/react": "6.5.13", 82 | "@storybook/storybook-deployer": "2.8.16", 83 | "@storybook/testing-library": "0.0.13", 84 | "@testing-library/jest-dom": "5.16.5", 85 | "@testing-library/react": "13.4.0", 86 | "@types/jest": "29.2.2", 87 | "@types/node": "18.11.9", 88 | "@types/react": "18.0.25", 89 | "@types/react-dom": "18.0.8", 90 | "@types/react-test-renderer": "18.0.0", 91 | "@typescript-eslint/eslint-plugin": "5.42.1", 92 | "@typescript-eslint/parser": "5.42.1", 93 | "babel-jest": "29.3.1", 94 | "babel-loader": "9.1.0", 95 | "commitizen": "^4.2.5", 96 | "concurrently": "^7.5.0", 97 | "cz-conventional-changelog": "^3.3.0", 98 | "eslint": "8.27.0", 99 | "eslint-config-airbnb": "19.0.4", 100 | "eslint-config-prettier": "8.5.0", 101 | "eslint-plugin-import": "2.26.0", 102 | "eslint-plugin-jsx-a11y": "6.6.1", 103 | "eslint-plugin-react": "7.31.10", 104 | "eslint-plugin-react-hooks": "4.6.0", 105 | "eslint-plugin-storybook": "0.6.7", 106 | "eslint-plugin-typescript-sort-keys": "2.1.0", 107 | "eslint-plugin-unused-imports": "2.0.0", 108 | "husky": "^8.0.2", 109 | "jest": "^29.3.1", 110 | "jest-environment-jsdom": "29.3.1", 111 | "lint-staged": "^13.0.3", 112 | "react": "18.2.0", 113 | "react-dom": "18.2.0", 114 | "react-test-renderer": "18.2.0", 115 | "release-it": "^15.5.0", 116 | "ts-jest": "29.0.3", 117 | "tsup": "^6.4.0", 118 | "typescript": "4.8.4" 119 | }, 120 | "peerDependencies": { 121 | "react": ">=17", 122 | "react-dom": ">=17", 123 | "react-merge-refs": ">=2" 124 | }, 125 | "dependencies": { 126 | "react-merge-refs": "2.0.1" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/AutowidthInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | InputHTMLAttributes, 3 | CSSProperties, 4 | useState, 5 | useEffect, 6 | useRef, 7 | useLayoutEffect, 8 | forwardRef, 9 | } from "react"; 10 | import { mergeRefs } from "react-merge-refs"; 11 | 12 | interface AutowidthInputProps extends InputHTMLAttributes { 13 | extraWidth?: number | string; 14 | minWidth?: number | string; 15 | onAutosize?: (newWidth: number) => void; 16 | placeholderIsMinWidth?: boolean; 17 | value?: string | number; 18 | wrapperClassName?: string; 19 | wrapperStyle?: CSSProperties; 20 | } 21 | 22 | const sizerStyle: CSSProperties = { 23 | position: "absolute", 24 | top: 0, 25 | left: 0, 26 | visibility: "hidden", 27 | height: 0, 28 | overflow: "scroll", 29 | whiteSpace: "pre", 30 | }; 31 | 32 | /** 33 | * Automatically sized input field. 34 | */ 35 | export const AutowidthInput = forwardRef( 36 | ( 37 | { 38 | extraWidth = 16, 39 | wrapperClassName, 40 | wrapperStyle: wrapperStyleProp, 41 | onAutosize, 42 | placeholderIsMinWidth, 43 | minWidth = 0, 44 | ...props 45 | }, 46 | forwardedRef 47 | ) => { 48 | const inputRef = useRef(null); 49 | const sizerRef = useRef(null); 50 | const placeholderSizerRef = useRef(null); 51 | 52 | const [input, setInput] = useState(""); 53 | const [inputWidth, setInputWidth] = useState(0); 54 | 55 | const usedValue = `${props.value ?? input}`; 56 | 57 | const handleInput = (e: React.ChangeEvent) => { 58 | setInput(e.target.value); 59 | 60 | if (props.onChange) props.onChange(e); 61 | }; 62 | 63 | /* Copy styles of the input field to the sizer, ensuring that the width of the input adjusts accordingly */ 64 | useLayoutEffect(() => { 65 | if (inputRef.current && sizerRef.current) { 66 | const computedStyle = window.getComputedStyle(inputRef.current); 67 | 68 | sizerRef.current.style.fontSize = computedStyle.fontSize; 69 | sizerRef.current.style.fontFamily = computedStyle.fontFamily; 70 | sizerRef.current.style.fontWeight = computedStyle.fontWeight; 71 | sizerRef.current.style.fontStyle = computedStyle.fontStyle; 72 | sizerRef.current.style.letterSpacing = computedStyle.letterSpacing; 73 | sizerRef.current.style.textTransform = computedStyle.textTransform; 74 | } 75 | }, []); 76 | 77 | useEffect(() => { 78 | const sizerWidth = sizerRef.current?.scrollWidth; 79 | const placeholderWidth = placeholderSizerRef.current?.scrollWidth; 80 | 81 | if (sizerWidth && usedValue.length) { 82 | /* If the input field has content, update the sizer to match its width */ 83 | 84 | let width = sizerWidth; 85 | 86 | if ( 87 | placeholderIsMinWidth && 88 | placeholderWidth && 89 | sizerWidth < placeholderWidth && 90 | placeholderSizerRef.current 91 | ) { 92 | width = placeholderWidth; 93 | } 94 | 95 | if (width < +minWidth) { 96 | width = +minWidth; 97 | } 98 | 99 | if (width) { 100 | setInputWidth(width + +extraWidth); 101 | if (onAutosize) onAutosize(width); 102 | } 103 | } else if ( 104 | props.placeholder && 105 | placeholderWidth && 106 | placeholderIsMinWidth 107 | ) { 108 | /* If no input value exists, check for placeholder value and update the sizer accordingly */ 109 | 110 | setInputWidth(Math.max(+minWidth, placeholderWidth) + +extraWidth); 111 | 112 | if (onAutosize) onAutosize(placeholderWidth); 113 | } else if (sizerRef.current) { 114 | /* If no input value or placeholder exists, update the sizer to the width of the "minWidth" + "extraWidth" prop (default is 16) */ 115 | 116 | setInputWidth(+minWidth + +extraWidth); 117 | if (onAutosize) onAutosize(+minWidth); 118 | } 119 | }, [ 120 | usedValue, 121 | props.placeholder, 122 | extraWidth, 123 | placeholderIsMinWidth, 124 | onAutosize, 125 | setInputWidth, 126 | minWidth, 127 | ]); 128 | 129 | const wrapperStyle: CSSProperties = { 130 | ...wrapperStyleProp, 131 | position: "relative", 132 | display: props.style?.display ?? "inline-block", 133 | }; 134 | 135 | const inputStyle: CSSProperties = { 136 | boxSizing: "content-box", 137 | width: inputWidth, 138 | ...props.style, 139 | }; 140 | 141 | return ( 142 |
147 |
148 | {usedValue} 149 |
150 | 158 | {props.placeholder ? ( 159 |
164 | {props.placeholder} 165 |
166 | ) : null} 167 |
168 | ); 169 | } 170 | ); 171 | 172 | export default AutowidthInput; 173 | --------------------------------------------------------------------------------