├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ ├── build-and-test.yml │ └── deploy.yml ├── .gitignore ├── .husky └── prepare-commit-msg ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── public ├── index.html ├── script.css └── script.css.map ├── src ├── components │ ├── MultipleValueTextInput.md │ ├── MultipleValueTextInput.module.css │ ├── MultipleValueTextInput.tsx │ ├── MultipleValueTextInputItem.module.css │ ├── MultipleValueTextInputItem.tsx │ └── __tests__ │ │ └── MultipleValueTextInput.spec.tsx ├── dev │ ├── build.js │ ├── dev.tsx │ └── serve.js ├── globals.d.ts ├── index.tsx └── types.d.ts ├── styleguide.config.js ├── styleguide ├── build │ ├── bundle.9654f99d.js │ └── bundle.9654f99d.js.LICENSE.txt └── index.html ├── tsconfig.json └── utilities ├── styleMock.ts └── testSetup.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | webpack.config.js 4 | src/dev/serve.js 5 | src/dev/build.js 6 | styleguide.config.js 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:import/typescript", 7 | "prettier" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "plugins": ["@typescript-eslint", "import"], 11 | "settings": { 12 | "import/resolver": { 13 | "node": { 14 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".d.ts"], 15 | "moduleDirectory": ["node_modules", "src/"] 16 | } 17 | }, 18 | 19 | "project": {} 20 | }, 21 | "env": { 22 | "browser": true, 23 | "jest": true 24 | }, 25 | "rules": { 26 | "no-tabs": "off", 27 | "linebreak-style": ["error", "windows"], 28 | "indent": ["error", "tab"], 29 | "react/jsx-indent": ["error", "tab"], 30 | "react/jsx-indent-props": ["error", "tab"], 31 | "react/jsx-props-no-spreading": "off", 32 | "react/function-component-definition": "off", 33 | "comma-dangle": ["error", "never"], 34 | "import/extensions": ["error", "never"], 35 | "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], 36 | "react/require-default-props": "off", 37 | "import/no-extraneous-dependencies": [ 38 | "error", 39 | { "devDependencies": ["**/*.test.tsx", "**/*.spec.tsx", "utilities/*.ts"] } 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=crlf 2 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Build and Test 5 | 6 | on: 7 | push: 8 | branches: ['production', 'development'] 9 | pull_request: 10 | branches: ['production', 'development'] 11 | 12 | jobs: 13 | build-and-test: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | - run: npm ci 29 | - run: npm run build 30 | - run: npm run validate 31 | - run: npm test 32 | notify: 33 | name: Discord Notification 34 | runs-on: ubuntu-latest 35 | needs: # make sure the notification is sent AFTER the jobs you want included have completed 36 | - build-and-test 37 | if: ${{ always() }} # You always want to be notified: success, failure, or cancelled 38 | steps: 39 | - name: Notify 40 | uses: nobrayner/discord-webhook@v1 41 | with: 42 | github-token: ${{ secrets.github_token }} 43 | discord-webhook: ${{ secrets.DISCORD_WEBHOOK }} 44 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # This workflow will deploy documentation to gh-pages and release a new version on npm 2 | 3 | name: Deploy 4 | 5 | on: 6 | release: 7 | types: [published] 8 | 9 | jobs: 10 | deploy-documentation: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x] 16 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: 'npm' 25 | - run: npm ci 26 | - run: npm run docs:build 27 | - uses: peaceiris/actions-gh-pages@v3 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: . 31 | 32 | deploy-package: 33 | runs-on: ubuntu-latest 34 | strategy: 35 | matrix: 36 | node-version: [18.x] 37 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 38 | 39 | steps: 40 | - uses: actions/checkout@v3 41 | - name: Use Node.js ${{ matrix.node-version }} 42 | uses: actions/setup-node@v3 43 | with: 44 | node-version: ${{ matrix.node-version }} 45 | cache: 'npm' 46 | registry-url: 'https://registry.npmjs.org' 47 | - run: npm ci 48 | - run: npm publish 49 | env: 50 | NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 51 | 52 | notify: 53 | name: Discord Notification 54 | runs-on: ubuntu-latest 55 | needs: # make sure the notification is sent AFTER the jobs you want included have completed 56 | - deploy-documentation 57 | - deploy-package 58 | if: ${{ always() }} # You always want to be notified: success, failure, or cancelled 59 | steps: 60 | - name: Notify 61 | uses: nobrayner/discord-webhook@v1 62 | with: 63 | github-token: ${{ secrets.github_token }} 64 | discord-webhook: ${{ secrets.DISCORD_WEBHOOK }} 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dev 2 | dist 3 | coverage 4 | node_modules 5 | public/script.js 6 | public/script.js.map 7 | 8 | id_travis_rsa* 9 | /.vscode 10 | 11 | # OS 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | exec < /dev/tty && npx cz --hook || true 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /styleguide 3 | /utilities 4 | **/__tests__ 5 | styleguide.config.js 6 | build-docs.sh 7 | .eslintrc 8 | id_travis_rsa* 9 | travis.yml 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "useTabs": true, 7 | "arrowParens": "always", 8 | "printWidth": 100 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Rosalind Wills 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Multivalue Text Input 2 | 3 | ![build status](https://github.com/blackjackkent/react-multivalue-text-input/actions/workflows/build-and-test.yml/badge.svg) 4 | ![npm version](https://badge.fury.io/js/react-multivalue-text-input.svg) 5 | ![discord](https://img.shields.io/discord/375365160057176064?label=discord%20chat&style=flat) 6 | ![npm downloads](https://img.shields.io/npm/dw/react-multivalue-text-input) 7 | 8 | A text input component for React which maintains and displays a collection of entered values as an array of strings. 9 | 10 | ![demo image](https://78.media.tumblr.com/29d3a4520cf60d077875017e6027a9e7/tumblr_p7g1fef5wq1qfjrmjo1_1280.gif) 11 | 12 | ## Getting Started 13 | 14 | 1. Install the package via npm or yarn: 15 | 16 | ``` 17 | npm install react-multivalue-text-input 18 | yarn add react-multivalue-text-input 19 | ``` 20 | 21 | 2. Include the component in your React project 22 | 23 | ```js 24 | import MultipleValueTextInput from 'react-multivalue-text-input'; 25 | ``` 26 | 27 | 3. See the [demos](https://blackjackkent.github.io/react-multivalue-text-input/styleguide/) for sample usage. 28 | 29 | ## Contributing 30 | 31 | After forking this repository and checking the code out locally, you can make changes and test them locally by running `npm run dev` in the project's root directory. This will open a dev server at `http://localhost:8080` with a small test app. (The root file for this test app can be found at `src/dev/dev.tsx`. 32 | 33 | Make any changes you wish and test them in the local development server, then open a pull request from your fork and I will review your changes! 34 | 35 | ## Authors 36 | 37 | - **Rosalind Wills** - [blackjackkent](https://github.com/blackjackkent) 38 | 39 | Pull requests, suggestions, and issue feedback appreciated! 40 | 41 | ## License 42 | 43 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 44 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'jsdom', 5 | 6 | moduleNameMapper: { 7 | '\\.(css|scss)$': '/utilities/styleMock.ts' 8 | }, 9 | setupFilesAfterEnv: ['/utilities/testSetup.ts'], 10 | testPathIgnorePatterns: ['/node_modules/', '/utilities/', '/build/components/', '/dist/'] 11 | }; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-multivalue-text-input", 3 | "version": "2.5.0", 4 | "description": " A text input component for React which maintains and displays a collection of entered values as an array of strings.", 5 | "main": "dist/index.cjs.js", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "prepare": "husky install", 10 | "dev": "node src/dev/serve.js", 11 | "clean": "rimraf dist", 12 | "build": "npm run clean && node src/dev/build.js && npm run ts-types", 13 | "test": "jest", 14 | "lint": "eslint src/", 15 | "validate": "run-s test lint", 16 | "docs:serve": "npx styleguidist server", 17 | "docs:build": "npx styleguidist build", 18 | "prepublishOnly": "npm run build", 19 | "ts-types": " tsc --emitDeclarationOnly --outDir dist" 20 | }, 21 | "author": "", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "@testing-library/dom": "^8.13.0", 25 | "@testing-library/jest-dom": "^5.16.4", 26 | "@testing-library/react": "^13.2.0", 27 | "@testing-library/user-event": "^14.1.1", 28 | "@types/jest": "^27.5.0", 29 | "@types/testing-library__react": "^10.2.0", 30 | "@typescript-eslint/eslint-plugin": "^5.21.0", 31 | "@typescript-eslint/parser": "^5.21.0", 32 | "chokidar": "^3.5.3", 33 | "commitizen": "^4.2.4", 34 | "css-loader": "^6.7.1", 35 | "css-tree": "^2.3.1", 36 | "cz-conventional-changelog": "^3.3.0", 37 | "esbuild": "^0.14.38", 38 | "esbuild-css-modules-plugin": "^2.6.3", 39 | "eslint": "^8.14.0", 40 | "eslint-config-airbnb": "^19.0.4", 41 | "eslint-config-prettier": "^8.5.0", 42 | "husky": "^8.0.1", 43 | "jest": "^28.0.3", 44 | "jest-environment-jsdom": "^28.0.2", 45 | "live-server": "^1.2.2", 46 | "npm-run-all": "^4.1.5", 47 | "react-docgen-typescript": "^2.2.2", 48 | "react-styleguidist": "^11.2.0", 49 | "rimraf": "^3.0.2", 50 | "style-loader": "^3.3.1", 51 | "ts-jest": "^28.0.1", 52 | "ts-loader": "^9.3.0", 53 | "typescript": "^4.6.4", 54 | "typescript-plugin-css-modules": "^4.1.1", 55 | "webpack": "^5.72.0" 56 | }, 57 | "dependencies": { 58 | "@types/react": "^18.0.8", 59 | "@types/react-dom": "^18.0.3", 60 | "prop-types": "^15.8.1", 61 | "react": "^18.1.0", 62 | "react-dom": "^18.1.0" 63 | }, 64 | "peerDependencies": { 65 | "react": "^18.1.0", 66 | "react-dom": "^18.1.0" 67 | }, 68 | "files": [ 69 | "dist" 70 | ], 71 | "config": { 72 | "commitizen": { 73 | "path": "./node_modules/cz-conventional-changelog" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/script.css: -------------------------------------------------------------------------------- 1 | /* src/components/MultipleValueTextInput.module.css */ 2 | .inputElement, 3 | .inputLabel { 4 | width: 100%; 5 | } 6 | 7 | /* src/components/MultipleValueTextInputItem.module.css */ 8 | .inputItem { 9 | padding: 5px; 10 | background: #ccc; 11 | margin-right: 5px; 12 | } 13 | .deleteButton { 14 | cursor: pointer; 15 | } 16 | /*# sourceMappingURL=script.css.map */ 17 | -------------------------------------------------------------------------------- /public/script.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["../src/components/MultipleValueTextInput.module.css", "../src/components/MultipleValueTextInputItem.module.css"], 4 | "sourcesContent": [".inputElement,\n.inputLabel {\n\twidth: 100%;\n}\n", ".inputItem {\n\tpadding: 5px;\n\tbackground: #ccc;\n\tmargin-right: 5px;\n}\n.deleteButton {\n\tcursor: pointer;\n}\n"], 5 | "mappings": ";AAAA;AAAA;AAEC;AAAA;;;ACFD;AACC;AACA;AACA;AAAA;AAED;AACC;AAAA;", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /src/components/MultipleValueTextInput.md: -------------------------------------------------------------------------------- 1 | ### Basic example: 2 | 3 | ```js 4 | console.log(`Item added: ${item}`)} 6 | onItemDeleted={(item, allItems) => console.log(`Item removed: ${item}`)} 7 | label="Items" 8 | name="item-input" 9 | placeholder="Enter whatever items you want; separate them with COMMA or ENTER." 10 | /> 11 | ``` 12 | 13 | ### Custom delete button: 14 | 15 | ```js 16 | console.log(`Item added: ${item}`)} 18 | onItemDeleted={(item, allItems) => console.log(`Item removed: ${item}`)} 19 | label="Items" 20 | name="item-input" 21 | placeholder="Enter whatever items you want; separate them with COMMA or ENTER." 22 | deleteButton={(delete)} 23 | /> 24 | ``` 25 | 26 | ### Populate initial values 27 | 28 | ```js 29 | console.log(`Item added: ${item}`)} 31 | onItemDeleted={(item, allItems) => console.log(`Item removed: ${item}`)} 32 | label="Items" 33 | name="item-input" 34 | placeholder="Enter whatever items you want; separate them with COMMA or ENTER." 35 | values={['default value', 'another default value']} 36 | /> 37 | ``` 38 | 39 | ### Custom delimiter 40 | 41 | ```js 42 | console.log(`Item added: ${item}`)} 44 | onItemDeleted={(item, allItems) => console.log(`Item removed: ${item}`)} 45 | label="Items" 46 | name="item-input" 47 | submitKeys={[' ']} 48 | placeholder="Enter whatever items you want; separate them with SPACE." 49 | /> 50 | ``` 51 | 52 | ### Add field content on blur 53 | 54 | ```js 55 | console.log(`Item added: ${item}`)} 57 | onItemDeleted={(item, allItems) => console.log(`Item removed: ${item}`)} 58 | label="Items" 59 | name="item-input" 60 | shouldAddOnBlur={true} 61 | placeholder="Enter whatever items you want; deselect/blur the input to add item and start a new one." 62 | /> 63 | ``` 64 | 65 | ### Populate values dynamically 66 | 67 | ```js 68 | const [values, setValues] = React.useState([]); 69 | const populateValuesDynamically = () => { 70 | setValues(['test1', 'test2', 'test3']); 71 | }; 72 | 73 |
74 | 77 | setValues(resultItems)} 82 | onItemDeleted={(_, resultItems) => setValues(resultItems)} 83 | values={values} 84 | /> 85 |
; 86 | ``` 87 | -------------------------------------------------------------------------------- /src/components/MultipleValueTextInput.module.css: -------------------------------------------------------------------------------- 1 | .inputElement, 2 | .inputLabel { 3 | width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/MultipleValueTextInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './MultipleValueTextInput.module.css'; 4 | import { MultipleValueTextInputProps } from '../types'; 5 | import MultipleValueTextInputItem from './MultipleValueTextInputItem'; 6 | 7 | const propTypes = { 8 | /** Any values the input's collection should be prepopulated with. */ 9 | values: PropTypes.arrayOf(PropTypes.string), 10 | /** Method which should be called when an item is added to the collection */ 11 | onItemAdded: PropTypes.func.isRequired, 12 | /** Method which should be called when an item is removed from the collection */ 13 | onItemDeleted: PropTypes.func.isRequired, 14 | /** Label to be attached to the input, if desired */ 15 | label: PropTypes.string, 16 | /** Name attribute for the input */ 17 | name: PropTypes.string.isRequired, 18 | /** Placeholder attribute for the input, if desired */ 19 | placeholder: PropTypes.string, 20 | /** ASCII charcode for the keys which should 21 | * trigger an item to be added to the collection (defaults to comma (44) and Enter (13)) 22 | */ 23 | submitKeys: PropTypes.arrayOf(PropTypes.string), 24 | /** JSX or string which will be used as the control to delete an item from the collection */ 25 | deleteButton: PropTypes.node, 26 | /** Whether or not the blur event should trigger the added-item handler */ 27 | shouldAddOnBlur: PropTypes.bool, 28 | /** Custom class name for the input element */ 29 | className: PropTypes.string, 30 | /** Custom class name for the input label element */ 31 | labelClassName: PropTypes.string 32 | }; 33 | 34 | /** 35 | * A text input component for React which maintains and displays a collection 36 | * of entered values as an array of strings. 37 | */ 38 | function MultipleValueTextInput({ 39 | placeholder = '', 40 | label = '', 41 | name, 42 | deleteButton = ×, 43 | onItemAdded = () => null, 44 | onItemDeleted = () => null, 45 | className = '', 46 | labelClassName = '', 47 | submitKeys = ['Enter', ','], 48 | values: initialValues = [], 49 | shouldAddOnBlur, 50 | ...forwardedProps 51 | }: MultipleValueTextInputProps) { 52 | const [values, setValues] = useState(initialValues); 53 | const [value, setValue] = useState(''); 54 | const nonCharacterKeyLabels: string[] = ['Enter', 'Tab']; 55 | const delimiters: string[] = submitKeys.filter( 56 | (element) => !nonCharacterKeyLabels.includes(element) 57 | ); 58 | useEffect(() => { 59 | setValues(initialValues); 60 | }, [JSON.stringify(initialValues)]); 61 | const handleValueChange = (e: React.ChangeEvent) => { 62 | setValue(e.currentTarget.value); 63 | }; 64 | const handleItemAdd = (addedValue: string) => { 65 | if (values.includes(addedValue) || !addedValue) { 66 | setValue(''); 67 | return; 68 | } 69 | const newValues = values.concat(addedValue); 70 | setValues(newValues); 71 | setValue(''); 72 | onItemAdded(value, newValues); 73 | }; 74 | const handleItemsAdd = (addedValues: string[]) => { 75 | const uniqueValues = Array.from( 76 | new Set(addedValues.filter((elm) => elm && !values.includes(elm))) 77 | ); 78 | if (uniqueValues.length > 0) { 79 | const newValues = Array.from(new Set([...values, ...uniqueValues])); 80 | setValues(newValues); 81 | setValue(''); 82 | uniqueValues.forEach((addedValue) => { 83 | onItemAdded(addedValue, newValues); 84 | }); 85 | } else { 86 | setValue(''); 87 | } 88 | }; 89 | const handleItemRemove = (removedValue: string) => { 90 | const currentValues = values; 91 | const newValues = currentValues.filter((v) => v !== removedValue); 92 | onItemDeleted(removedValue, newValues); 93 | setValues(newValues); 94 | }; 95 | 96 | const handleKeypress = (e: React.KeyboardEvent) => { 97 | // Defaults: Enter, Comma (e.key === 'Enter' or ',') 98 | if (submitKeys.includes(e.key)) { 99 | e.preventDefault(); 100 | handleItemAdd(e.currentTarget.value); 101 | } 102 | }; 103 | const handleBlur = (e: React.FocusEvent) => { 104 | if (shouldAddOnBlur) { 105 | e.preventDefault(); 106 | handleItemAdd(e.target.value); 107 | } 108 | }; 109 | 110 | const splitMulti = (str: string) => { 111 | const tempChar = delimiters[0]; // We can use the first token as a temporary join character 112 | let result: string = str; 113 | for (let i = 1; i < delimiters.length; i += 1) { 114 | result = result.split(delimiters[i]).join(tempChar); // Handle scenarios where pasted text has more than one submitKeys in it 115 | } 116 | return result.split(tempChar); 117 | }; 118 | 119 | const handlePaste = (e: React.ClipboardEvent) => { 120 | const pastedText = e.clipboardData.getData('text/plain'); 121 | const areSubmitKeysPresent = delimiters.some((d) => pastedText.includes(d)); 122 | if (areSubmitKeysPresent) { 123 | const splitTerms = splitMulti(pastedText); 124 | if (splitTerms.length > 0) { 125 | e.preventDefault(); 126 | handleItemsAdd(splitTerms); 127 | } 128 | } 129 | }; 130 | 131 | const valueDisplays = values.map((v) => ( 132 | 138 | )); 139 | return ( 140 |
141 | 164 |
165 | ); 166 | } 167 | 168 | MultipleValueTextInput.propTypes = propTypes; 169 | export default MultipleValueTextInput; 170 | -------------------------------------------------------------------------------- /src/components/MultipleValueTextInputItem.module.css: -------------------------------------------------------------------------------- 1 | .inputItem { 2 | padding: 5px; 3 | background: #ccc; 4 | margin-right: 5px; 5 | } 6 | .deleteButton { 7 | cursor: pointer; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/MultipleValueTextInputItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { MultipleValueTextInputItemProps } from '../types'; 4 | import styles from './MultipleValueTextInputItem.module.css'; 5 | 6 | const propTypes = { 7 | value: PropTypes.string.isRequired, 8 | handleItemRemove: PropTypes.func.isRequired, 9 | deleteButton: PropTypes.node.isRequired 10 | }; 11 | 12 | const MultipleValueTextInputItem = (props: MultipleValueTextInputItemProps) => { 13 | const { value, handleItemRemove, deleteButton } = props; 14 | return ( 15 | 16 | {value}{' '} 17 | handleItemRemove(value)} 23 | onClick={() => handleItemRemove(value)} 24 | > 25 | {deleteButton} 26 | 27 | 28 | ); 29 | }; 30 | 31 | MultipleValueTextInputItem.propTypes = propTypes; 32 | export default MultipleValueTextInputItem; 33 | -------------------------------------------------------------------------------- /src/components/__tests__/MultipleValueTextInput.spec.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { render, within, waitFor } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { MultipleValueTextInputProps } from '../../types'; 5 | import MultipleValueTextInput from '../MultipleValueTextInput'; 6 | 7 | const createTestProps = (propOverrides?: MultipleValueTextInputProps) => ({ 8 | // common props 9 | onItemAdded: jest.fn(), 10 | onItemDeleted: jest.fn(), 11 | name: 'test-input', 12 | ...propOverrides 13 | }); 14 | const renderWithUserInteraction = (jsx: ReactElement) => ({ 15 | user: userEvent.setup(), 16 | ...render(jsx) 17 | }); 18 | 19 | describe('default props', () => { 20 | it('should render valid content with default props', () => { 21 | const props = createTestProps(); 22 | const { getByRole, getByTestId, queryAllByRole } = render( 23 | 24 | ); 25 | const input = getByRole('textbox'); 26 | const label = getByTestId('label'); 27 | const items = queryAllByRole('listitem'); 28 | expect(items).toHaveLength(0); 29 | expect(label).toHaveTextContent(''); 30 | expect(input).toHaveAttribute('name', 'test-input'); 31 | expect(input).toHaveAttribute('placeholder', ''); 32 | expect(input).toHaveValue(''); 33 | }); 34 | it('should render with passed props', () => { 35 | const props = createTestProps({ label: 'Test Label', placeholder: 'Test Placeholder' }); 36 | const { getByRole, getByTestId, queryAllByRole } = render( 37 | 38 | ); 39 | const input = getByRole('textbox'); 40 | const label = getByTestId('label'); 41 | const items = queryAllByRole('listitem'); 42 | expect(items).toHaveLength(0); 43 | expect(label).toHaveTextContent('Test Label'); 44 | expect(input).toHaveAttribute('name', 'test-input'); 45 | expect(input).toHaveAttribute('placeholder', 'Test Placeholder'); 46 | expect(input).toHaveValue(''); 47 | }); 48 | it('should render items', () => { 49 | const props = createTestProps({ values: ['1', '2', '3'] }); 50 | const { queryAllByRole } = render(); 51 | const items = queryAllByRole('listitem'); 52 | expect(items).toHaveLength(3); 53 | }); 54 | it('should forward props to input', () => { 55 | const props = createTestProps({ 'data-test': 'test' }); 56 | const { getByRole } = render(); 57 | const input = getByRole('textbox'); 58 | expect(input).toHaveAttribute('data-test', 'test'); 59 | }); 60 | it('should forward props to item', () => { 61 | const props = createTestProps({ values: ['1'], deleteButton: 'blah' }); 62 | const { queryAllByRole } = render(); 63 | const items = queryAllByRole('listitem'); 64 | const firstItemValue = within(items[0]).getByTestId('value'); 65 | const firstItemButton = within(items[0]).getByRole('button'); 66 | expect(firstItemValue).toHaveTextContent('1'); 67 | expect(firstItemButton).toHaveTextContent('blah'); 68 | }); 69 | }); 70 | 71 | describe('behavior', () => { 72 | it('should allow user to add item with default submitKeys', async () => { 73 | const onItemAdd = jest.fn(); 74 | const props = createTestProps({ onItemAdded: onItemAdd }); 75 | const { user, getByRole, queryAllByRole } = renderWithUserInteraction( 76 | 77 | ); 78 | const input = getByRole('textbox'); 79 | const initialItems = queryAllByRole('listitem'); 80 | expect(initialItems.length).toBe(0); 81 | await user.click(input); 82 | await user.keyboard('abc,'); 83 | await user.click(input); 84 | await user.keyboard('abc2{Enter}'); 85 | await waitFor(() => { 86 | expect(onItemAdd).toHaveBeenCalledTimes(2); 87 | const items = queryAllByRole('listitem'); 88 | expect(items).toHaveLength(2); 89 | expect(items[0]).toHaveTextContent('abc'); 90 | expect(items[1]).toHaveTextContent('abc2'); 91 | }); 92 | }); 93 | it('should allow user to add item with custom submitKeys', async () => { 94 | const onItemAdd = jest.fn(); 95 | const props = createTestProps({ onItemAdded: onItemAdd, submitKeys: [' '] }); 96 | const { user, getByRole, queryAllByRole } = renderWithUserInteraction( 97 | 98 | ); 99 | const input = getByRole('textbox'); 100 | const initialItems = queryAllByRole('listitem'); 101 | expect(initialItems.length).toBe(0); 102 | await user.click(input); 103 | await user.keyboard('abc '); 104 | await user.click(input); 105 | await user.keyboard('abc2{Enter}'); 106 | await waitFor(() => { 107 | expect(onItemAdd).toHaveBeenCalledTimes(1); 108 | const items = queryAllByRole('listitem'); 109 | expect(items).toHaveLength(1); 110 | expect(items[0]).toHaveTextContent('abc'); 111 | }); 112 | }); 113 | it('should not allow user to add item which already exists', async () => { 114 | const onItemAdd = jest.fn(); 115 | const props = createTestProps({ onItemAdded: onItemAdd }); 116 | const { user, getByRole, queryAllByRole } = renderWithUserInteraction( 117 | 118 | ); 119 | const input = getByRole('textbox'); 120 | const initialItems = queryAllByRole('listitem'); 121 | expect(initialItems.length).toBe(0); 122 | await user.click(input); 123 | await user.keyboard('abc,'); 124 | await user.click(input); 125 | await user.keyboard('abc,'); 126 | await waitFor(() => { 127 | expect(onItemAdd).toHaveBeenCalledTimes(1); 128 | const items = queryAllByRole('listitem'); 129 | expect(items).toHaveLength(1); 130 | expect(items[0]).toHaveTextContent('abc'); 131 | }); 132 | }); 133 | it('should remove item on delete button click', async () => { 134 | const onItemDelete = jest.fn(); 135 | const props = createTestProps({ onItemDeleted: onItemDelete, values: ['1', '2'] }); 136 | const { user, queryAllByRole } = renderWithUserInteraction( 137 | 138 | ); 139 | const items = queryAllByRole('listitem'); 140 | const button = within(items[0]).getByRole('button'); 141 | await user.click(button); 142 | waitFor(() => { 143 | expect(onItemDelete).toHaveBeenCalledTimes(1); 144 | expect(items).toHaveLength(1); 145 | }); 146 | }); 147 | it('should handle blur to add values when applicable', async () => { 148 | const onItemAdd = jest.fn(); 149 | const props = createTestProps({ onItemAdded: onItemAdd, shouldAddOnBlur: true }); 150 | const { user, getByRole, queryAllByRole } = renderWithUserInteraction( 151 | 152 | ); 153 | const input = getByRole('textbox'); 154 | const initialItems = queryAllByRole('listitem'); 155 | expect(initialItems).toHaveLength(0); 156 | await user.click(input); 157 | await user.keyboard('test{Tab}'); 158 | waitFor(() => { 159 | expect(onItemAdd).toHaveBeenCalledTimes(1); 160 | expect(onItemAdd).toHaveBeenLastCalledWith('test', ['test']); 161 | const items = queryAllByRole('listitem'); 162 | expect(items).toHaveLength(1); 163 | expect(items[0]).toHaveTextContent('test'); 164 | }); 165 | }); 166 | it('should not add on blur when disabled', async () => { 167 | const onItemAdd = jest.fn(); 168 | const props = createTestProps({ onItemAdded: onItemAdd }); 169 | const { user, getByRole, queryAllByRole } = renderWithUserInteraction( 170 | 171 | ); 172 | const input = getByRole('textbox'); 173 | const initialItems = queryAllByRole('listitem'); 174 | expect(initialItems).toHaveLength(0); 175 | await user.click(input); 176 | await user.keyboard('test{Tab}'); 177 | waitFor(() => { 178 | expect(onItemAdd).toHaveBeenCalledTimes(0); 179 | const items = queryAllByRole('listitem'); 180 | expect(items).toHaveLength(0); 181 | }); 182 | }); 183 | it('should allow user to add multiple unique items from clipboard if submitkeys are present', async () => { 184 | const onItemAdd = jest.fn(); 185 | const props = createTestProps({ 186 | onItemAdded: onItemAdd, 187 | submitKeys: [',', ';'], 188 | values: ['input3'] 189 | }); 190 | const { user, getByRole, queryAllByRole } = renderWithUserInteraction( 191 | 192 | ); 193 | const input = getByRole('textbox'); 194 | const initialItems = queryAllByRole('listitem'); 195 | expect(initialItems.length).toBe(1); 196 | await user.click(input); 197 | await user.paste('input1,input2;input3,input4,,input5,input1;'); 198 | await waitFor(() => { 199 | expect(onItemAdd).toHaveBeenCalledTimes(4); 200 | // const items = queryAllByRole('listitem'); 201 | // expect(items).toHaveLength(5); 202 | // expect(items[0]).toHaveTextContent('input1'); 203 | // expect(items[1]).toHaveTextContent('input2'); 204 | // expect(items[2]).toHaveTextContent('input4'); 205 | // expect(items[3]).toHaveTextContent('input5'); 206 | expect(input).toHaveValue(''); 207 | }); 208 | }); 209 | it('should allow user to view text from clipboard if submitkeys are not present', async () => { 210 | const onItemAdd = jest.fn(); 211 | const props = createTestProps({ onItemAdded: onItemAdd, submitKeys: [',', ';'] }); 212 | const { user, getByRole, queryAllByRole } = renderWithUserInteraction( 213 | 214 | ); 215 | const input = getByRole('textbox'); 216 | const initialItems = queryAllByRole('listitem'); 217 | expect(initialItems.length).toBe(0); 218 | await user.click(input); 219 | await user.paste('john doe'); 220 | await waitFor(() => { 221 | expect(onItemAdd).toHaveBeenCalledTimes(0); 222 | const items = queryAllByRole('listitem'); 223 | expect(items).toHaveLength(0); 224 | expect(input).toHaveValue('john doe'); 225 | }); 226 | }); 227 | it('should ignore non-character submit keys from being recognized as delimiters in copied text', async () => { 228 | const onItemAdd = jest.fn(); 229 | const props = createTestProps({ 230 | onItemAdded: onItemAdd, 231 | submitKeys: [',', 'Enter', 'Tab'] 232 | }); 233 | const { user, getByRole, queryAllByRole } = renderWithUserInteraction( 234 | 235 | ); 236 | const input = getByRole('textbox'); 237 | const initialItems = queryAllByRole('listitem'); 238 | expect(initialItems.length).toBe(0); 239 | await user.click(input); 240 | await user.paste('My Tablet is about to Enter the door'); 241 | await waitFor(() => { 242 | expect(onItemAdd).toHaveBeenCalledTimes(0); 243 | const items = queryAllByRole('listitem'); 244 | expect(items).toHaveLength(0); 245 | expect(input).toHaveValue('My Tablet is about to Enter the door'); 246 | }); 247 | }); 248 | it('should selectively ignore non-character submit keys from being recognized as delimiters in copied text', async () => { 249 | const onItemAdd = jest.fn(); 250 | const props = createTestProps({ 251 | onItemAdded: onItemAdd, 252 | submitKeys: [',', 'Enter', 'Tab'] 253 | }); 254 | const { user, getByRole, queryAllByRole } = renderWithUserInteraction( 255 | 256 | ); 257 | const input = getByRole('textbox'); 258 | const initialItems = queryAllByRole('listitem'); 259 | expect(initialItems.length).toBe(0); 260 | await user.click(input); 261 | await user.paste('My Tablet is about to Enter the door, value1, value2'); 262 | await waitFor(() => { 263 | expect(onItemAdd).toHaveBeenCalledTimes(3); 264 | const items = queryAllByRole('listitem'); 265 | expect(items).toHaveLength(3); 266 | expect(items[0]).toHaveTextContent('My Tablet is about to Enter the door'); 267 | expect(items[1]).toHaveTextContent('value1'); 268 | expect(items[2]).toHaveTextContent('value2'); 269 | expect(input).toHaveValue(''); 270 | }); 271 | }); 272 | it('should dynamically update values from an external prop if provided', async () => { 273 | const props = createTestProps(); 274 | const { rerender, queryAllByRole } = renderWithUserInteraction( 275 | 276 | ); 277 | const initialItems = queryAllByRole('listitem'); 278 | expect(initialItems.length).toBe(0); 279 | const updatedProps = createTestProps({ values: ['test1', 'test2', 'test3'] }); 280 | rerender(); 281 | await waitFor(() => { 282 | const items = queryAllByRole('listitem'); 283 | expect(items).toHaveLength(3); 284 | expect(items[0]).toHaveTextContent('test1'); 285 | expect(items[1]).toHaveTextContent('test2'); 286 | expect(items[2]).toHaveTextContent('test3'); 287 | }); 288 | }); 289 | }); 290 | -------------------------------------------------------------------------------- /src/dev/build.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild'); 2 | const cssModulesPlugin = require('esbuild-css-modules-plugin'); 3 | const { dependencies } = require('../../package.json'); 4 | 5 | const entryFile = 'src/index.tsx'; 6 | const shared = { 7 | bundle: true, 8 | entryPoints: [entryFile], 9 | // Treat all dependencies in package.json as externals to keep bundle size to a minimum 10 | external: Object.keys(dependencies), 11 | logLevel: 'info', 12 | minify: true, 13 | sourcemap: true, 14 | plugins: [cssModulesPlugin()] 15 | }; 16 | 17 | build({ 18 | ...shared, 19 | outdir: 'dist', 20 | splitting: true, 21 | format: 'esm', 22 | target: ['esnext'] 23 | }).catch(() => process.exit(1)); 24 | 25 | build({ 26 | ...shared, 27 | outfile: './dist/index.cjs.js', 28 | platform: 'node', 29 | target: ['node12.22.0'] 30 | }).catch(() => process.exit(1)); 31 | -------------------------------------------------------------------------------- /src/dev/dev.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import MultipleValueTextInput from '../index'; 4 | 5 | const DevEnvironmentApp = () => { 6 | const [values, setValues] = useState([]); 7 | const populateValuesDynamically = () => { 8 | setValues(['test1', 'test2', 'test3']); 9 | }; 10 | 11 | return ( 12 |
13 |

React Multivalue Test Input Testing Environment

14 |

{values.length} values entered.

15 | 18 | (close)} 21 | label="My Input" 22 | labelClassName="test-input-label" 23 | name="my-input" 24 | placeholder="Separate values with comma" 25 | onItemAdded={(_, resultItems) => setValues(resultItems)} 26 | onItemDeleted={(_, resultItems) => setValues(resultItems)} 27 | values={values} 28 | /> 29 |
30 | ); 31 | }; 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 34 | const container = document.getElementById('root')!; 35 | const root = createRoot(container); 36 | root.render(); 37 | -------------------------------------------------------------------------------- /src/dev/serve.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild'); 2 | const chokidar = require('chokidar'); 3 | const liveServer = require('live-server'); 4 | const cssModulesPlugin = require('esbuild-css-modules-plugin'); 5 | 6 | (async () => { 7 | const builder = await build({ 8 | bundle: true, 9 | // Defines env variables for bundled JavaScript; here `process.env.NODE_ENV` 10 | // is propagated with a fallback. 11 | define: { 12 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') 13 | }, 14 | entryPoints: ['src/dev/dev.tsx'], 15 | // Uses incremental compilation (see `chokidar.on`). 16 | incremental: true, 17 | // Removes whitespace, etc. depending on `NODE_ENV=...`. 18 | minify: process.env.NODE_ENV === 'production', 19 | outfile: 'public/script.js', 20 | sourcemap: true, 21 | plugins: [cssModulesPlugin()] 22 | }); 23 | // `chokidar` watcher source changes. 24 | chokidar 25 | // Watches TypeScript and React TypeScript. 26 | .watch('src/**/*.{ts,tsx}', { 27 | interval: 0 // No delay 28 | }) 29 | // Rebuilds esbuild (incrementally -- see `build.incremental`). 30 | .on('all', () => { 31 | builder.rebuild(); 32 | }); 33 | // `liveServer` local server for hot reload. 34 | liveServer.start({ 35 | // Opens the local server on start. 36 | open: true, 37 | // Uses `PORT=...` or 8080 as a fallback. 38 | port: +process.env.PORT || 8080, 39 | // Uses `public` as the local server folder. 40 | root: 'public' 41 | }); 42 | })(); 43 | -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import MultipleValueTextInput from './components/MultipleValueTextInput'; 2 | 3 | export default MultipleValueTextInput; 4 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export interface MultipleValueTextInputProps { 4 | /** Any values the input's collection should be prepopulated with. */ 5 | values?: string[]; 6 | /** Method which should be called when an item is added to the collection */ 7 | onItemAdded?: (newItem: string, resultItems: string[]) => void; 8 | /** Method which should be called when an item is removed from the collection */ 9 | onItemDeleted?: (deletedItem: string, resultItems: string[]) => void; 10 | /** Label to be attached to the input, if desired */ 11 | label?: string; 12 | /** Name attribute for the input */ 13 | name?: string; 14 | /** Placeholder attribute for the input, if desired */ 15 | placeholder?: string; 16 | /** ASCII charcode for the keys which should 17 | * trigger an item to be added to the collection (defaults to comma (44) and Enter (13)) 18 | */ 19 | submitKeys?: string[]; 20 | /** JSX or string which will be used as the control to delete an item from the collection */ 21 | deleteButton?: ReactNode; 22 | /** Whether or not the blur event should trigger the added-item handler */ 23 | shouldAddOnBlur?: boolean; 24 | /** Custom class name for the input element */ 25 | className?: string; 26 | /** Custom class name for the input label element */ 27 | labelClassName?: string; 28 | [x: string]: unknown; 29 | } 30 | 31 | export interface MultipleValueTextInputItemProps { 32 | value: string; 33 | handleItemRemove: (removedValue: string) => void; 34 | deleteButton: ReactNode; 35 | } 36 | -------------------------------------------------------------------------------- /styleguide.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | components: 'src/components/**/*.{jsx,tsx}', 4 | propsParser: (filePath, source, resolver, handlers) => { 5 | const { ext } = path.parse(filePath); 6 | return ext === '.tsx' 7 | ? require('react-docgen-typescript').parse(filePath, source, resolver, handlers) 8 | : require('react-docgen').parse(source, resolver, handlers); 9 | }, 10 | webpackConfig: { 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.tsx?$/, 15 | use: 'ts-loader', 16 | exclude: /node_modules/ 17 | }, 18 | { 19 | test: /\.css$/, 20 | use: ['style-loader', 'css-loader'] 21 | } 22 | ] 23 | } 24 | }, 25 | template: { 26 | head: { 27 | links: [ 28 | { 29 | rel: 'stylesheet', 30 | href: 'https://bootswatch.com/3/cerulean/bootstrap.min.css' 31 | } 32 | ] 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /styleguide/build/bundle.9654f99d.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | * @overview es6-promise - a tiny implementation of Promises/A+. 9 | * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) 10 | * @license Licensed under MIT license 11 | * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE 12 | * @version v4.2.8+1e68dce6 13 | */ 14 | 15 | /*! 16 | * The buffer module from node.js, for the browser. 17 | * 18 | * @author Feross Aboukhadijeh 19 | * @license MIT 20 | */ 21 | 22 | /*! 23 | * regjsgen 0.5.2 24 | * Copyright 2014-2020 Benjamin Tan 25 | * Available under the MIT license 26 | */ 27 | 28 | /*! clipboard-copy. MIT License. Feross Aboukhadijeh */ 29 | 30 | /*! https://mths.be/regenerate v1.4.2 by @mathias | MIT license */ 31 | 32 | /** 33 | * @license React 34 | * react-dom.production.min.js 35 | * 36 | * Copyright (c) Facebook, Inc. and its affiliates. 37 | * 38 | * This source code is licensed under the MIT license found in the 39 | * LICENSE file in the root directory of this source tree. 40 | */ 41 | 42 | /** 43 | * @license React 44 | * react.production.min.js 45 | * 46 | * Copyright (c) Facebook, Inc. and its affiliates. 47 | * 48 | * This source code is licensed under the MIT license found in the 49 | * LICENSE file in the root directory of this source tree. 50 | */ 51 | 52 | /** 53 | * @license React 54 | * scheduler.production.min.js 55 | * 56 | * Copyright (c) Facebook, Inc. and its affiliates. 57 | * 58 | * This source code is licensed under the MIT license found in the 59 | * LICENSE file in the root directory of this source tree. 60 | */ 61 | 62 | /** 63 | * A better abstraction over CSS. 64 | * 65 | * @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present 66 | * @website https://github.com/cssinjs/jss 67 | * @license MIT 68 | */ 69 | 70 | /** 71 | * Prism: Lightweight, robust, elegant syntax highlighting 72 | * 73 | * @license MIT 74 | * @author Lea Verou 75 | * @namespace 76 | * @public 77 | */ 78 | -------------------------------------------------------------------------------- /styleguide/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Multivalue Text Input Style Guide 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "esnext", 5 | "lib": ["esnext", "dom"], 6 | "strict": true, 7 | "noImplicitAny": false, 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "outDir": "dist", 11 | "jsx": "react", 12 | "skipLibCheck": true 13 | }, 14 | "include": ["src"], 15 | "exclude": [ 16 | "node_modules", 17 | "**/*.test.ts", 18 | "**/*.test.tsx", 19 | "**/*.spec.tsx", 20 | "src/dev/dev.tsx", 21 | "src/build/build.tsx" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /utilities/styleMock.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /utilities/testSetup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | --------------------------------------------------------------------------------