├── .all-contributorsrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ ├── codeql-analysis.yml │ ├── node.yml │ └── publish.yml ├── .gitignore ├── .prettierrc ├── .travis.yml ├── README.md ├── SECURITY.md ├── example ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.test.tsx │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── setupTests.ts │ └── theme.tsx └── tsconfig.json ├── img ├── basic.gif └── custom-example.gif ├── package.json ├── src ├── .eslintrc ├── __snapshots__ │ └── index.test.tsx.snap ├── index.test.tsx ├── index.tsx ├── react-app-env.d.ts ├── styles.module.css └── typings.d.ts ├── tsconfig.json ├── tsconfig.test.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "kuasha420", 10 | "name": "Arafat Zahan", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/6187401?v=4", 12 | "profile": "http://kuasha.xyz", 13 | "contributions": [ 14 | "code" 15 | ] 16 | }, 17 | { 18 | "login": "joaoviana", 19 | "name": "João Viana", 20 | "avatar_url": "https://avatars.githubusercontent.com/u/7611706?v=4", 21 | "profile": "https://github.com/joaoviana", 22 | "contributions": [ 23 | "code" 24 | ] 25 | }, 26 | { 27 | "login": "herol3oy", 28 | "name": "Hamed Sedighi", 29 | "avatar_url": "https://avatars.githubusercontent.com/u/17513392?v=4", 30 | "profile": "https://demah.ir", 31 | "contributions": [ 32 | "code" 33 | ] 34 | }, 35 | { 36 | "login": "akash4393", 37 | "name": "Akash Singh", 38 | "avatar_url": "https://avatars.githubusercontent.com/u/9202515?v=4", 39 | "profile": "http://akashsingh.blog", 40 | "contributions": [ 41 | "code" 42 | ] 43 | }, 44 | { 45 | "login": "amaster507", 46 | "name": "Anthony Master", 47 | "avatar_url": "https://avatars.githubusercontent.com/u/2472115?v=4", 48 | "profile": "https://ifbmt.info", 49 | "contributions": [ 50 | "doc", 51 | "test", 52 | "code" 53 | ] 54 | }, 55 | { 56 | "login": "vyder", 57 | "name": "Vidur Murali", 58 | "avatar_url": "https://avatars.githubusercontent.com/u/587136?v=4", 59 | "profile": "https://vidur.io", 60 | "contributions": [ 61 | "code" 62 | ] 63 | }, 64 | { 65 | "login": "koolamusic", 66 | "name": "U.M Andrew", 67 | "avatar_url": "https://avatars.githubusercontent.com/u/8960757?v=4", 68 | "profile": "https://github.com/koolamusic", 69 | "contributions": [ 70 | "doc", 71 | "test", 72 | "code", 73 | "doc" 74 | ] 75 | }, 76 | { 77 | 78 | "login": "srcristofher", 79 | "name": "sr.cristofher", 80 | "avatar_url": "https://avatars.githubusercontent.com/u/67389482?v=4", 81 | "profile": "https://believeplus.com/", 82 | "contributions": [ 83 | "doc" 84 | ] 85 | } 86 | ], 87 | "contributorsPerLine": 7, 88 | "projectName": "chakra-ui-autocomplete", 89 | "projectOwner": "koolamusic", 90 | "repoType": "github", 91 | "repoHost": "https://github.com", 92 | "skipCi": true 93 | } 94 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "env": { 12 | "node": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "ecmaFeatures": { 17 | "legacyDecorators": true, 18 | "jsx": true 19 | } 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "16" 24 | } 25 | }, 26 | "rules": { 27 | "space-before-function-paren": 0, 28 | "react/prop-types": 0, 29 | "react/jsx-handler-names": 0, 30 | "react/jsx-fragments": 0, 31 | "no-unused-vars": 0, 32 | "react/no-unused-prop-types": 0, 33 | "import/export": 0 34 | } 35 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: chakra-ui-autocomplete 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '23 6 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | 2 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 3 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 4 | 5 | name: Node.js CI 6 | 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ dev, master ] 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | node-version: [12.x, 14.x] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: yarn install 29 | - run: yarn test 30 | - run: yarn build 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | registry-url: https://registry.npmjs.org/ 16 | - run: yarn install 17 | # - run: npm publish --access public 18 | env: 19 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 10 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chakra-UI AutoComplete 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) 4 | 5 | 6 | [![Financial Contributors on Open Collective](https://opencollective.com/chakra-ui-autocomplete/all/badge.svg?label=financial+contributors)](https://opencollective.com/chakra-ui-autocomplete) [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) 7 | 8 | 9 | > An Accessible Autocomplete Utility for [Chakra UI](github.com/chakra-ui/chakra-ui) that composes [Downshift](https://github.com/downshift-js/downshift) ComboBox 10 | 11 | [![NPM](https://img.shields.io/npm/v/chakra-ui-autocomplete.svg)](https://www.npmjs.com/package/chakra-ui-autocomplete) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 12 | 13 | ![demo-image](./img/basic.gif) 14 | 15 | ## Install 16 | 17 | **Warning* This Package is still WIP at the Moment and there might be some missing features 18 | 19 | ```bash 20 | npm install --save chakra-ui-autocomplete 21 | ``` 22 | 23 | ## Usage 24 | 25 | - Usage Example with TSX/Typescript 26 | 27 | ```tsx 28 | import React from 'react' 29 | import { CUIAutoComplete } from 'chakra-ui-autocomplete' 30 | 31 | 32 | export interface Item { 33 | label: string; 34 | value: string; 35 | } 36 | const countries = [ 37 | { value: "ghana", label: "Ghana" }, 38 | { value: "nigeria", label: "Nigeria" }, 39 | { value: "kenya", label: "Kenya" }, 40 | { value: "southAfrica", label: "South Africa" }, 41 | { value: "unitedStates", label: "United States" }, 42 | { value: "canada", label: "Canada" }, 43 | { value: "germany", label: "Germany" } 44 | ]; 45 | 46 | export default function App() { 47 | const [pickerItems, setPickerItems] = React.useState(countries); 48 | const [selectedItems, setSelectedItems] = React.useState([]); 49 | 50 | const handleCreateItem = (item: Item) => { 51 | setPickerItems((curr) => [...curr, item]); 52 | setSelectedItems((curr) => [...curr, item]); 53 | }; 54 | 55 | const handleSelectedItemsChange = (selectedItems?: Item[]) => { 56 | if (selectedItems) { 57 | setSelectedItems(selectedItems); 58 | } 59 | }; 60 | 61 | return ( 62 | 69 | handleSelectedItemsChange(changes.selectedItems) 70 | } 71 | /> 72 | ); 73 | } 74 | ``` 75 | 76 | --- 77 | 78 | - Usage Example with JSX/Javascript 79 | 80 | ```jsx 81 | import React from 'react' 82 | import { CUIAutoComplete } from 'chakra-ui-autocomplete' 83 | 84 | const countries = [ 85 | { value: "ghana", label: "Ghana" }, 86 | { value: "nigeria", label: "Nigeria" }, 87 | { value: "kenya", label: "Kenya" }, 88 | { value: "southAfrica", label: "South Africa" }, 89 | { value: "unitedStates", label: "United States" }, 90 | { value: "canada", label: "Canada" }, 91 | { value: "germany", label: "Germany" } 92 | ]; 93 | 94 | export default function App() { 95 | const [pickerItems, setPickerItems] = React.useState(countries); 96 | const [selectedItems, setSelectedItems] = React.useState([]); 97 | 98 | const handleCreateItem = (item) => { 99 | setPickerItems((curr) => [...curr, item]); 100 | setSelectedItems((curr) => [...curr, item]); 101 | }; 102 | 103 | const handleSelectedItemsChange = (selectedItems) => { 104 | if (selectedItems) { 105 | setSelectedItems(selectedItems); 106 | } 107 | }; 108 | 109 | return ( 110 | { 117 | handleSelectedItemsChange(changes.selectedItems) 118 | }}} 119 | /> 120 | ); 121 | } 122 | ``` 123 | 124 | [View on CodeSandbox](https://codesandbox.io/s/chakra-ui-autocomplete-example1-8uxbs) 125 | [![109296467-3cdbe100-7828-11eb-9491-1bd069bf90a4](https://user-images.githubusercontent.com/587136/109479134-1922d000-7aa0-11eb-9a7f-14d3a5f0d63d.png)](https://codesandbox.io/s/chakra-ui-autocomplete-example1-8uxbs) 126 | 127 | ### Usage Example with Custom Item Renderer 128 | 129 | ![custom-render-image](./img/custom-example.gif) 130 | 131 | 132 | ```jsx 133 | import React from 'react' 134 | import { Text, Flex, Avatar } from '@chakra-ui/react' 135 | import { CUIAutoComplete } from 'chakra-ui-autocomplete' 136 | 137 | 138 | const countries = [ 139 | { value: "ghana", label: "Ghana" }, 140 | { value: "nigeria", label: "Nigeria" }, 141 | { value: "kenya", label: "Kenya" }, 142 | { value: "southAfrica", label: "South Africa" }, 143 | { value: "unitedStates", label: "United States" }, 144 | { value: "canada", label: "Canada" }, 145 | { value: "germany", label: "Germany" } 146 | ]; 147 | 148 | export default function App() { 149 | const [pickerItems, setPickerItems] = React.useState(countries); 150 | const [selectedItems, setSelectedItems] = React.useState([]); 151 | 152 | const handleCreateItem = (item) => { 153 | setPickerItems((curr) => [...curr, item]); 154 | setSelectedItems((curr) => [...curr, item]); 155 | }; 156 | 157 | const handleSelectedItemsChange = (selectedItems) => { 158 | if (selectedItems) { 159 | setSelectedItems(selectedItems); 160 | } 161 | }; 162 | 163 | const customRender = (selected) => { 164 | return ( 165 | 166 | 167 | {selected.label} 168 | 169 | ) 170 | } 171 | 172 | const customCreateItemRender = (value) => { 173 | return ( 174 | 175 | Create{' '} 176 | 177 | "{value}" 178 | 179 | 180 | ) 181 | } 182 | 183 | 184 | 185 | return ( 186 | 198 | handleSelectedItemsChange(changes.selectedItems) 199 | } 200 | /> 201 | ); 202 | } 203 | 204 | ``` 205 | 206 | ## Props 207 | 208 | | Property | Type | Required | Decscription | 209 | | ---------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | 210 | | items | Array | Yes | An array of the items to be selected within the input field | 211 | | placeholder | string | | The placeholder for the input field | 212 | | label | string | Yes | Input Form Label to describe the activity or process | 213 | | highlightItemBg | string | | For accessibility, you can define a custom color for the highlight color when user is typing also accept props like `yellow.300` based on chakra theme provider | 214 | | onCreateItem | Function | Yes | Function to handle creating new Item | 215 | | optionFilterFunc | Function | | You can define a custom Function to handle filter logic | 216 | | itemRenderer | Function | | Custom Function that can either return a JSX Element or String, in order to control how the list items within the Dropdown is rendered | 217 | | labelStyleProps | Object | | Custom style props based on chakra-ui for labels, Example `{{ bg: 'gray.100', pt: '4'}} | 218 | | inputStyleProps | Object | | Custom style props based on chakra-ui for input field, Example`{{ bg: 'gray.100', pt: '4'}} | 219 | | toggleButtonStyleProps | Object | | Custom style props based on chakra-ui for toggle button, Example `{{ bg: 'gray.100', pt: '4'}} | 220 | | tagStyleProps | Object | | Custom style props based on chakra-ui for multi option tags, Example`{{ bg: 'gray.100', pt: '4'}} | 221 | | listStyleProps | Object | | Custom style props based on chakra-ui for dropdown list, Example `{{ bg: 'gray.100', pt: '4'}} | 222 | | listItemStyleProps | Object | | Custom style props based on chakra-ui for single list item in dropdown, Example`{{ bg: 'gray.100', pt: '4'}} | 223 | | selectedIconProps | Object | | Custom style props based on chakra-ui for the green tick icon in dropdown list, Example `{{ bg: 'gray.100', pt: '4'}} | 224 | | icon | Object | CheckCircleIcon | @chakra-ui/icons Icon to be displayed instead of CheckCircleIcon | 225 | | hideToggleButton | boolean | | Hide the toggle button | 226 | | disableCreateItem | boolean | | Disable the "create new" list Item. Default is `false` | 227 | | createItemRenderer | Function | | Custom Function that can either return a JSX Element or String, in order to control how the create new item within the Dropdown is rendered. The input value is passed as the first function parameter, Example: ``` (value) => `Create ${value}` ``` | 228 | | renderCustomInput | Function | | Custom function to render input from outside chakra-ui-autocomplete. Receives input props for the input element and toggleButtonProps for the toggle button. Can use this to render chakra-ui's ``````. Example: ```(inputProps) => (} />)``` 229 | 230 | ## Todo 231 | 232 | - [ ] Add Combobox Support for Single Select [Downshift Combobox](https://downshift.netlify.app/use-combobox) 233 | - [x] Multi Select Support 234 | - [x] Feature to Create when not in list 235 | - [x] Add prop for Items Renderer to enable rendering of React Element 236 | - [ ] Ability to define `chakra-ui components` that will render in place of `Tags, MenuList, TextInput, Form Label` will check render props or headless UI patterns. 237 | 238 | ## Contributors 239 | 240 | ### Code Contributors 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 |

Arafat Zahan

💻

João Viana

💻

Hamed Sedighi

💻

Akash Singh

💻

Anthony Master

📖 ⚠️ 💻

Vidur Murali

💻

Marco Nalon

⚠️ 💻 📖

U.M Andrew

📖 ⚠️ 💻

sr.cristofher

📖
257 | 258 | 259 | 260 | 261 | 262 | 263 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. 264 | 265 | 266 | ### Financial Contributors 267 | 268 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/chakra-ui-autocomplete/contribute)] 269 | 270 | #### Individuals 271 | 272 | 273 | 274 | #### Organizations 275 | 276 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/chakra-ui-autocomplete/contribute)] 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | ## License 290 | 291 | MIT © [koolamusic](https://github.com/koolamusic) 292 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This example was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | It is linked to the chakra-ui-autocomplete package in the parent directory for development purposes. 4 | 5 | You can run `npm install` and then `npm start` to test your package. 6 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chakra-ui-autocomplete-example", 3 | "homepage": ".", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "node ../node_modules/react-scripts/bin/react-scripts.js start", 8 | "build": "node ../node_modules/react-scripts/bin/react-scripts.js build", 9 | "test": "node ../node_modules/react-scripts/bin/react-scripts.js test", 10 | "eject": "node ../node_modules/react-scripts/bin/react-scripts.js eject" 11 | }, 12 | "dependencies": { 13 | "@chakra-ui/react": "^1.0.1", 14 | "framer-motion": "^2.9.4", 15 | "@testing-library/jest-dom": "file:../node_modules/@testing-library/jest-dom", 16 | "@testing-library/react": "file:../node_modules/@testing-library/react", 17 | "@testing-library/user-event": "file:../node_modules/@testing-library/user-event", 18 | "@types/jest": "file:../node_modules/@types/jest", 19 | "@types/node": "file:../node_modules/@types/node", 20 | "@types/react": "file:../node_modules/@types/react", 21 | "@types/react-dom": "file:../node_modules/@types/react-dom", 22 | "chakra-ui-autocomplete": "file:../dist", 23 | "react": "file:../node_modules/react", 24 | "react-dom": "file:../node_modules/react-dom", 25 | "react-scripts": "file:../node_modules/react-scripts", 26 | "typescript": "file:../node_modules/typescript" 27 | }, 28 | "devDependencies": { 29 | "@babel/plugin-syntax-object-rest-spread": "^7.8.3" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koolamusic/chakra-ui-autocomplete/3ee294806993169164b5e2ccb68755d57d20340c/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 16 | 17 | 19 | 20 | 29 | chakra-ui-autocomplete 30 | 31 | 32 | 33 | 36 | 37 |
38 | 39 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "chakra-ui-autocomplete", 3 | "name": "chakra-ui-autocomplete", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div') 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ThemeProvider, CSSReset, Text, Flex, Heading, Avatar } from '@chakra-ui/react' 3 | import { CUIAutoComplete } from '../../dist' 4 | import theme from './theme' 5 | 6 | 7 | export interface Item { 8 | label: string; 9 | value: string; 10 | } 11 | const countries = [ 12 | { value: "ghana", label: "Ghana" }, 13 | { value: "nigeria", label: "Nigeria" }, 14 | { value: "kenya", label: "Kenya" }, 15 | { value: "southAfrica", label: "South Africa" }, 16 | { value: "unitedStates", label: "United States" }, 17 | { value: "canada", label: "Canada" }, 18 | { value: "germany", label: "Germany" } 19 | ]; 20 | 21 | export default function App() { 22 | const [pickerItems, setPickerItems] = React.useState(countries); 23 | const [selectedItems, setSelectedItems] = React.useState([]); 24 | 25 | const handleCreateItem = (item: Item) => { 26 | setPickerItems((curr) => [...curr, item]); 27 | setSelectedItems((curr) => [...curr, item]); 28 | }; 29 | 30 | const handleSelectedItemsChange = (selectedItems?: Item[]) => { 31 | if (selectedItems) { 32 | setSelectedItems(selectedItems); 33 | } 34 | }; 35 | 36 | const customRender = (selected: T) => { 37 | return ( 38 | 39 | 40 | {selected.label} 41 | 42 | ) 43 | } 44 | 45 | 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | 53 | 54 | Basic Example with Style Props 55 | 56 | 66 | handleSelectedItemsChange(changes.selectedItems) 67 | } 68 | /> 69 | 70 | 71 | 72 | Basic Example with Custom Renderer 73 | 84 | handleSelectedItemsChange(changes.selectedItems) 85 | } 86 | /> 87 | 88 | 89 | 90 | 91 | 92 | 93 | ); 94 | } -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import App from './App' 6 | 7 | ReactDOM.render(, document.getElementById('root')) 8 | -------------------------------------------------------------------------------- /example/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /example/src/theme.tsx: -------------------------------------------------------------------------------- 1 | // import { theme as chakraTheme } from '@chakra-ui/react'; 2 | import { extendTheme } from '@chakra-ui/react' 3 | import { createBreakpoints } from '@chakra-ui/theme-tools' 4 | 5 | // const fonts = { mono: `'Menlo', monospace`, } 6 | 7 | // const theme = extendTheme({ 8 | // colors: { 9 | // black: '#16161D', 10 | // }, 11 | // fonts, 12 | // breakpoints, 13 | // }) 14 | 15 | // export default theme 16 | 17 | const fonts = { 18 | mono: `'Menlo', Monaco, Fira Code, Ubuntu Mono, monospace`, 19 | heading: `"Graphik Web", 'Inter', 'Ubuntu', Cantarell, Oxygen, sans-serif`, 20 | body: `"Graphik Web", 'Inter', 'Ubuntu', Segoe UI, Cantarell, Oxygen, Roboto, Fira Sans, Helvetica Neue, sans-serif` 21 | }; 22 | const fontSizes = { 23 | xs: '0.65rem', 24 | sm: '0.875rem', 25 | md: '1rem', 26 | lg: '1.125rem', 27 | xl: '1.25rem', 28 | '2xl': '1.5rem', 29 | '3xl': '1.875rem', 30 | '4xl': '2.25rem', 31 | '5xl': '3rem', 32 | '6xl': '4rem' 33 | }; 34 | 35 | const colors = { 36 | black: '#40474e', 37 | default: '#1fdc6b', 38 | tomato: 'FF5238', 39 | text: '#1D1D1D', 40 | background: '#F7FAFC', 41 | altBackground: '#fafffd' 42 | }; 43 | 44 | const breakpoints = createBreakpoints({ 45 | sm: '40em', 46 | md: '52em', 47 | lg: '64em', 48 | xl: '80em', 49 | }) 50 | 51 | 52 | const theme = extendTheme({ 53 | colors, 54 | fonts, 55 | fontSizes, 56 | breakpoints, 57 | }) 58 | 59 | export default theme; 60 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": [ 6 | "dom", 7 | "esnext" 8 | ], 9 | "moduleResolution": "node", 10 | "jsx": "react", 11 | "sourceMap": true, 12 | "declaration": true, 13 | "esModuleInterop": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "allowSyntheticDefaultImports": true, 22 | "target": "es5", 23 | "allowJs": true, 24 | "skipLibCheck": true, 25 | "strict": true, 26 | "forceConsistentCasingInFileNames": true, 27 | "resolveJsonModule": true, 28 | "isolatedModules": true, 29 | "noEmit": true 30 | }, 31 | "include": [ 32 | "src" 33 | ], 34 | "exclude": [ 35 | "node_modules", 36 | "build" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /img/basic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koolamusic/chakra-ui-autocomplete/3ee294806993169164b5e2ccb68755d57d20340c/img/basic.gif -------------------------------------------------------------------------------- /img/custom-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koolamusic/chakra-ui-autocomplete/3ee294806993169164b5e2ccb68755d57d20340c/img/custom-example.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chakra-ui-autocomplete", 3 | "version": "1.4.6", 4 | "description": "An accessible autocomplete utility library built for chakra UI", 5 | "author": "Andrew Miracle ", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "typings": "dist/index.d.ts", 9 | "module": "dist/index.js", 10 | "source": "src/index.tsx", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "start": "tsdx watch", 16 | "build": "rimraf dist && tsdx build", 17 | "test": "tsdx test", 18 | "test:tsc": "tsc --pretty --noEmit", 19 | "prepare": "run-s build", 20 | "test:build": "run-s build", 21 | "test:lint": "eslint ./src --ext ts --ext tsx --fix", 22 | "test:unit": "cross-env CI=1 react-scripts test --env=jsdom", 23 | "test:watch": "react-scripts test --env=jsdom", 24 | "predeploy": "cd example && npm install && npm run build", 25 | "deploy": "gh-pages -d example/build" 26 | }, 27 | "peerDependencies": { 28 | "@chakra-ui/react": ">= 1.0.0", 29 | "framer-motion": ">= 2.9.4", 30 | "react": ">= 16.8.1" 31 | }, 32 | "devDependencies": { 33 | "@chakra-ui/react": "^2.8.2", 34 | "@emotion/react": "^11.11.1", 35 | "@emotion/styled": "^11.11.0", 36 | "@testing-library/jest-dom": "^4.2.4", 37 | "@testing-library/react": "^9.5.0", 38 | "@testing-library/user-event": "^7.2.1", 39 | "@types/jest": "^25.1.4", 40 | "@types/match-sorter": "^4.0.0", 41 | "@types/node": "^20.10.3", 42 | "@types/react": "^18.2.42", 43 | "@types/react-dom": "^18.2.17", 44 | "@types/react-highlight-words": "^0.16.7", 45 | "@types/react-test-renderer": "^16.9.3", 46 | "@typescript-eslint/eslint-plugin": "^2.26.0", 47 | "@typescript-eslint/parser": "^2.26.0", 48 | "cross-env": "^7.0.3", 49 | "eslint": "^8.55.0", 50 | "eslint-config-prettier": "^6.7.0", 51 | "eslint-config-standard": "^17.1.0", 52 | "eslint-plugin-import": "^2.29.0", 53 | "eslint-plugin-node": "^11.1.0", 54 | "eslint-plugin-prettier": "^5.0.1", 55 | "eslint-plugin-react": "^7.33.2", 56 | "framer-motion": "^10.16.14", 57 | "gh-pages": "^2.2.0", 58 | "npm-run-all": "^4.1.5", 59 | "prettier": "^3.1.0", 60 | "react": "^18.2.0", 61 | "react-dom": "^18.2.0", 62 | "react-scripts": "^4.0.3", 63 | "react-test-renderer": "^17.0.1", 64 | "rimraf": "^5.0.5 ", 65 | "tsdx": "^0.14.1", 66 | "tslib": "^2.6.2", 67 | "typescript": "^5.3.2" 68 | }, 69 | "files": [ 70 | "dist" 71 | ], 72 | "publishConfig": { 73 | "access": "public" 74 | }, 75 | "keywords": [ 76 | "Chakra UI", 77 | "downshift", 78 | "autocomplete", 79 | "combobox", 80 | "react", 81 | "chakra-ui", 82 | "multiselect-chakra-ui" 83 | ], 84 | "repository": { 85 | "type": "git", 86 | "url": "git+https://github.com/koolamusic/chakra-ui-autocomplete.git" 87 | }, 88 | "dependencies": { 89 | "@chakra-ui/icons": "^2.1.1", 90 | "downshift": "^8.2.3", 91 | "match-sorter": "^6.3.1", 92 | "react-highlight-words": "^0.20.0", 93 | "react-use": "^17.4.2" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ChakraUI AutoComplete it renders 1`] = ` 4 |
7 |
10 |

13 | Basic Example with Style Props 14 |

15 |
18 | 25 |
28 |
35 | 50 | 69 |
70 |
73 |
    80 |
81 |
82 |
85 |

88 | Basic Example with Custom Renderer 89 |

90 |
93 | 100 |
103 |
110 | 125 | 144 |
145 |
148 |
    155 |
156 |
157 |
158 |
159 | `; 160 | -------------------------------------------------------------------------------- /src/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { CUIAutoComplete } from './index' 2 | 3 | import React from 'react' 4 | import renderer from 'react-test-renderer' 5 | import { Text, Flex, Heading, Avatar } from '@chakra-ui/react' 6 | 7 | export interface Item { 8 | label: string 9 | value: string 10 | } 11 | const countries = [ 12 | { value: 'ghana', label: 'Ghana' }, 13 | { value: 'nigeria', label: 'Nigeria' }, 14 | { value: 'kenya', label: 'Kenya' }, 15 | { value: 'southAfrica', label: 'South Africa' }, 16 | { value: 'unitedStates', label: 'United States' }, 17 | { value: 'canada', label: 'Canada' }, 18 | { value: 'germany', label: 'Germany' } 19 | ] 20 | 21 | export default function MultiSelect() { 22 | const [pickerItems, setPickerItems] = React.useState(countries) 23 | const [selectedItems, setSelectedItems] = React.useState([]) 24 | 25 | const handleCreateItem = (item: Item) => { 26 | setPickerItems((curr) => [...curr, item]) 27 | setSelectedItems((curr) => [...curr, item]) 28 | } 29 | 30 | const handleSelectedItemsChange = (selectedItems?: Item[]) => { 31 | if (selectedItems) { 32 | setSelectedItems(selectedItems) 33 | } 34 | } 35 | 36 | const customRender = (selected: T) => { 37 | return ( 38 | 39 | 40 | {selected.label} 41 | 42 | ) 43 | } 44 | 45 | return ( 46 | 55 | 56 | 57 | Basic Example with Style Props 58 | 59 | 60 | 73 | handleSelectedItemsChange(changes.selectedItems) 74 | } 75 | /> 76 | 77 | 78 | 79 | Basic Example with Custom Renderer 80 | 81 | 92 | handleSelectedItemsChange(changes.selectedItems) 93 | } 94 | /> 95 | 96 | 97 | ) 98 | } 99 | 100 | describe('ChakraUI AutoComplete ', () => { 101 | test('it renders', () => { 102 | const tree = renderer.create().toJSON() 103 | expect(tree).toMatchSnapshot() 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useEffect } from 'react'; 2 | import { 3 | useCombobox, 4 | useMultipleSelection, 5 | UseMultipleSelectionProps, 6 | } from 'downshift'; 7 | import { matchSorter } from 'match-sorter'; 8 | import Highlighter from 'react-highlight-words/index'; 9 | import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; 10 | import { 11 | FormLabel, 12 | Text, 13 | Stack, 14 | Box, 15 | List, 16 | ListItem, 17 | ListIcon, 18 | Button, 19 | Input, 20 | Tag, 21 | TagLabel, 22 | TagCloseButton, 23 | ResponsiveValue, 24 | useColorMode, 25 | BoxProps, 26 | } from '@chakra-ui/react'; 27 | import { CheckCircleIcon, ArrowDownIcon, IconProps } from '@chakra-ui/icons'; 28 | 29 | /** 30 | * Interface for each item in the autocomplete dropdown. 31 | */ 32 | export interface Item { 33 | label: string; 34 | value: string; 35 | } 36 | 37 | /** 38 | * Props for the CUIAutoComplete component. 39 | * 40 | * @template T - The type of items in the autocomplete dropdown. 41 | * @extends UseMultipleSelectionProps - Props from the useMultipleSelection hook. 42 | * 43 | * @property {T[]} items - An array of items for the autocomplete dropdown. 44 | * @property {string} placeholder - The placeholder text for the input field. 45 | * @property {string} label - The label for the autocomplete component. 46 | * @property {string} [highlightItemBg] - Background color for the highlighted item in the dropdown. 47 | * @property {(item: T) => void} [onCreateItem] - Callback function when a new item is created. 48 | * @property {(items: T[], inputValue: string) => T[]} [optionFilterFunc] - Function to filter items based on input value. 49 | * @property {(item: T) => string | JSX.Element} [itemRenderer] - Custom renderer for each item in the dropdown. 50 | * @property {any} [labelStyleProps] - Additional style props for the label element. 51 | * @property {any} [inputStyleProps] - Additional style props for the input element. 52 | * @property {any} [toggleButtonStyleProps] - Additional style props for the toggle button (dropdown button). 53 | * @property {any} [tagStyleProps] - Additional style props for the selected tags. 54 | * @property {any} [listStyleProps] - Additional style props for the list (dropdown menu). 55 | * @property {() => any} [onClearAll] - Callback function when the "Clear All" button is clicked. 56 | * @property {any} [listItemStyleProps] - Additional style props for each item in the dropdown menu. 57 | * @property {(inputValue: string) => React.ReactNode} [emptyState] - Custom content to display when no items match the input. 58 | * @property {Omit & { icon: IconProps['name'] | React.ComponentType }} [selectedIconProps] - Props for the icon indicating a selected item. 59 | * @property {React.ComponentType} [icon] - Custom icon component for the selected items. 60 | * @property {boolean} [hideToggleButton] - Whether to hide the toggle button (dropdown button). 61 | * @property {(value: string) => string | JSX.Element} [createItemRenderer] - Custom renderer for creating a new item. 62 | * @property {boolean} [disableCreateItem] - Whether to disable the creation of new items. 63 | * @property {(inputProps: any, toggleButtonProps: any) => JSX.Element} [renderCustomInput] - Custom renderer for the input field and toggle button. 64 | */ 65 | export interface CUIAutoCompleteProps 66 | extends UseMultipleSelectionProps { 67 | /** 68 | * An array of items for the autocomplete dropdown. 69 | * 70 | * @type {T[]} 71 | */ 72 | items: T[]; 73 | 74 | /** 75 | * Should the clear all button be enabled 76 | * 77 | * @type {boolean} 78 | * @default false 79 | */ 80 | clearAll?: boolean; 81 | 82 | /** 83 | * Should Keyboard shortcuts be enabled 84 | * 85 | * @type {boolean} 86 | * @default true 87 | */ 88 | keyboardShortcuts?: boolean; 89 | 90 | /** 91 | * The placeholder text for the input field. 92 | * 93 | * @type {string} 94 | */ 95 | placeholder: string; 96 | 97 | /** 98 | * The label for the autocomplete component. 99 | * 100 | * @type {string} 101 | */ 102 | label: string; 103 | 104 | /** 105 | * Background color for the highlighted item in the dropdown. 106 | * 107 | * @type {string|undefined} 108 | */ 109 | highlightItemBg?: string; 110 | 111 | /** 112 | * Callback function when a new item is created. 113 | * 114 | * @type {(item: T) => void|undefined} 115 | */ 116 | onCreateItem?: (item: T) => void; 117 | 118 | /** 119 | * Function to filter items based on the input value. 120 | * 121 | * @type {(items: T[], inputValue: string) => T[]|undefined} 122 | */ 123 | optionFilterFunc?: (items: T[], inputValue: string) => T[]; 124 | 125 | /** 126 | * Custom renderer for each item in the dropdown. 127 | * 128 | * @type {(item: T) => string | JSX.Element|undefined} 129 | */ 130 | itemRenderer?: (item: T) => string | JSX.Element; 131 | 132 | /** 133 | * Additional style props for the label element. 134 | * 135 | * @type {React.HTMLAttributes|undefined} 136 | */ 137 | labelStyleProps?: React.HTMLAttributes; 138 | 139 | /** 140 | * Additional style props for the input element. 141 | * 142 | * @type {React.InputHTMLAttributes & { size?: ResponsiveValue } | undefined} 143 | */ 144 | inputStyleProps?: React.InputHTMLAttributes & { 145 | size?: ResponsiveValue; 146 | }; 147 | 148 | /** 149 | * Additional style props for the toggle button (dropdown button). 150 | * 151 | * @type {React.ButtonHTMLAttributes|undefined} 152 | */ 153 | toggleButtonStyleProps?: React.ButtonHTMLAttributes; 154 | 155 | /** 156 | * Additional style props for the selected tags. 157 | * 158 | * @type {React.HTMLAttributes|undefined|BoxProps} 159 | */ 160 | tagStyleProps?: React.HTMLAttributes | BoxProps; 161 | 162 | /** 163 | * Additional style props for the list (dropdown menu). 164 | * 165 | * @type {React.HTMLAttributes|undefined} 166 | */ 167 | listStyleProps?: React.HTMLAttributes; 168 | 169 | /** 170 | * Callback function when the "Clear All" button is clicked. 171 | * 172 | * @type {() => any|undefined} 173 | */ 174 | onClearAll?: () => any; 175 | 176 | /** 177 | * Additional style props for each item in the dropdown menu. 178 | * 179 | * @type {React.HTMLAttributes|undefined} 180 | */ 181 | listItemStyleProps?: React.HTMLAttributes; 182 | 183 | /** 184 | * Custom content to display when no items match the input. 185 | * 186 | * @type {(inputValue: string) => React.ReactNode|undefined} 187 | */ 188 | emptyState?: (inputValue: string) => React.ReactNode; 189 | 190 | /** 191 | * Props for the icon indicating a selected item. 192 | * 193 | * @type {Omit & { icon: IconProps['name'] | React.ComponentType }|undefined} 194 | */ 195 | selectedIconProps?: Omit & { 196 | icon: IconProps['name'] | React.ComponentType; 197 | }; 198 | 199 | /** 200 | * Custom icon component for the selected items. 201 | * 202 | * @type {React.ComponentType|undefined} 203 | */ 204 | icon?: React.ComponentType; 205 | 206 | /** 207 | * Whether to hide the toggle button (dropdown button). 208 | * 209 | * @type {boolean|undefined} 210 | */ 211 | hideToggleButton?: boolean; 212 | 213 | /** 214 | * Custom renderer for creating a new item. 215 | * 216 | * @type {(value: string) => string | JSX.Element|undefined} 217 | */ 218 | createItemRenderer?: (value: string) => string | JSX.Element; 219 | 220 | /** 221 | * Whether to disable the creation of new items. 222 | * 223 | * @type {boolean|undefined} 224 | */ 225 | disableCreateItem?: boolean; 226 | 227 | /** 228 | * Custom renderer for the input field and toggle button. 229 | * 230 | * @type {(inputProps: React.InputHTMLAttributes, toggleButtonProps: React.ButtonHTMLAttributes) => JSX.Element|undefined} 231 | */ 232 | renderCustomInput?: ( 233 | inputProps: React.InputHTMLAttributes, 234 | toggleButtonProps: React.ButtonHTMLAttributes 235 | ) => JSX.Element; 236 | } 237 | 238 | /** 239 | * Default option filter function to match and sort items based on label and value. 240 | */ 241 | function defaultOptionFilterFunc(items: T[], inputValue: string) { 242 | return matchSorter(items, inputValue, { keys: ['value', 'label'] }); 243 | } 244 | 245 | /** 246 | * Default renderer for creating a new item. 247 | */ 248 | function defaultCreateItemRenderer(value: string) { 249 | return ( 250 | 251 | Create{' '} 252 | 253 | "{value}" 254 | 255 | 256 | ); 257 | } 258 | 259 | /** 260 | * Chakra UI Autocomplete Component. 261 | * 262 | * @component 263 | * @example 264 | * // Basic usage 265 | * console.log('Created:', item)} 271 | * onClearAll={() => console.log('Cleared all items')} 272 | * clearAll={true} // Enable the clear all button 273 | * /> 274 | * 275 | * @param {CUIAutoCompleteProps} props - Component properties. 276 | * @returns {React.ReactElement} - Autocomplete component. 277 | */ 278 | export const CUIAutoComplete = ( 279 | props: CUIAutoCompleteProps 280 | ): React.ReactElement> => { 281 | const { 282 | items, 283 | optionFilterFunc = defaultOptionFilterFunc, 284 | itemRenderer, 285 | highlightItemBg = 'gray.100', 286 | placeholder, 287 | label, 288 | listStyleProps, 289 | labelStyleProps, 290 | inputStyleProps, 291 | toggleButtonStyleProps, 292 | tagStyleProps, 293 | selectedIconProps, 294 | listItemStyleProps, 295 | onClearAll, 296 | clearAll = false, 297 | keyboardShortcuts = true, 298 | onCreateItem, 299 | icon: CustomIcon, 300 | hideToggleButton = false, 301 | disableCreateItem = false, 302 | createItemRenderer = defaultCreateItemRenderer, 303 | renderCustomInput, 304 | ...downshiftProps 305 | } = props; 306 | 307 | const [isCreating, setIsCreating] = useState(false); 308 | const [inputValue, setInputValue] = useState(''); 309 | const [inputItems, setInputItems] = useState(items); 310 | const [error, setError] = useState(''); 311 | 312 | const disclosureRef = useRef(null); 313 | 314 | const { colorMode } = useColorMode(); // Check the color mode for the CUI instance (light or dark) 315 | const borderColor = colorMode === 'dark' ? 'whiteAlpha.400' : 'gray.300'; 316 | 317 | const { 318 | getSelectedItemProps, 319 | getDropdownProps, 320 | addSelectedItem, 321 | removeSelectedItem, 322 | selectedItems, 323 | } = useMultipleSelection(downshiftProps); 324 | const selectedItemValues = selectedItems.map((item) => item.value); 325 | 326 | const { 327 | isOpen, 328 | getToggleButtonProps, 329 | getLabelProps, 330 | getMenuProps, 331 | getInputProps, 332 | highlightedIndex, 333 | getItemProps, 334 | openMenu, 335 | selectItem, 336 | setHighlightedIndex, 337 | } = useCombobox({ 338 | inputValue, 339 | selectedItem: undefined, 340 | items: inputItems, 341 | onInputValueChange: ({ inputValue, selectedItem }) => { 342 | const filteredItems = optionFilterFunc(items, inputValue || ''); 343 | 344 | if (isCreating && filteredItems.length > 0) { 345 | setIsCreating(false); 346 | } 347 | 348 | if (!selectedItem) { 349 | setInputItems(filteredItems); 350 | } 351 | }, 352 | stateReducer: (state, actionAndChanges) => { 353 | const { changes, type } = actionAndChanges; 354 | switch (type) { 355 | case useCombobox.stateChangeTypes.InputBlur: 356 | return { 357 | ...changes, 358 | isOpen: false, 359 | }; 360 | case useCombobox.stateChangeTypes.InputKeyDownEnter: 361 | case useCombobox.stateChangeTypes.ItemClick: 362 | return { 363 | ...changes, 364 | highlightedIndex: state.highlightedIndex, 365 | inputValue, 366 | isOpen: true, 367 | }; 368 | case useCombobox.stateChangeTypes.FunctionSelectItem: 369 | return { 370 | ...changes, 371 | inputValue, 372 | }; 373 | default: 374 | return changes; 375 | } 376 | }, 377 | onStateChange: ({ inputValue, type, selectedItem }) => { 378 | switch (type) { 379 | case useCombobox.stateChangeTypes.InputChange: 380 | setInputValue(inputValue || ''); 381 | break; 382 | case useCombobox.stateChangeTypes.InputKeyDownEnter: 383 | case useCombobox.stateChangeTypes.ItemClick: 384 | if (selectedItem) { 385 | // Validate input value 386 | if (selectedItemValues.includes(selectedItem.value)) { 387 | setError('Item already selected.'); 388 | } else { 389 | setError(''); 390 | } 391 | 392 | if (selectedItemValues.includes(selectedItem.value)) { 393 | removeSelectedItem(selectedItem); 394 | } else { 395 | if (onCreateItem && isCreating) { 396 | onCreateItem(selectedItem); 397 | setIsCreating(false); 398 | setInputItems(items); 399 | setInputValue(''); 400 | } else { 401 | addSelectedItem(selectedItem); 402 | } 403 | } 404 | 405 | selectItem(null); 406 | } 407 | break; 408 | default: 409 | break; 410 | } 411 | }, 412 | }); 413 | 414 | const clearSelection = () => { 415 | setInputValue(''); 416 | setInputItems(items); 417 | setHighlightedIndex(0); 418 | selectItem(null); 419 | onClearAll && onClearAll(); 420 | }; 421 | 422 | useEffect(() => { 423 | if (inputItems.length === 0 && !disableCreateItem) { 424 | setIsCreating(true); 425 | setInputItems([{ label: `${inputValue}`, value: inputValue }] as T[]); 426 | setHighlightedIndex(0); 427 | } 428 | }, [ 429 | inputItems, 430 | setIsCreating, 431 | setHighlightedIndex, 432 | inputValue, 433 | disableCreateItem, 434 | ]); 435 | 436 | useDeepCompareEffect(() => { 437 | setInputItems(items); 438 | }, [items]); 439 | 440 | function defaultItemRenderer(selected: T) { 441 | return selected.label; 442 | } 443 | 444 | const handleKeyDown = (e: { key: string }) => { 445 | if (keyboardShortcuts === false) return; 446 | 447 | if (e.key === 'Escape') { 448 | // Handle escape key (e.g., close dropdown) 449 | if (isOpen) { 450 | // Close the dropdown 451 | setInputValue(''); 452 | setIsCreating(false); 453 | } 454 | } else if (e.key === 'Enter') { 455 | // Handle enter key (e.g., select highlighted item or create new item) 456 | if (highlightedIndex !== null) { 457 | const selectedItem = inputItems[highlightedIndex]; 458 | if (selectedItem) { 459 | if (selectedItemValues.includes(selectedItem.value)) { 460 | removeSelectedItem(selectedItem); 461 | } else { 462 | if (onCreateItem && isCreating) { 463 | onCreateItem(selectedItem); 464 | setIsCreating(false); 465 | setInputItems(items); 466 | setInputValue(''); 467 | } else { 468 | addSelectedItem(selectedItem); 469 | } 470 | } 471 | } 472 | } else if (isCreating) { 473 | // Create a new item with the current input value 474 | const newItem = { label: inputValue, value: inputValue } as T; 475 | if (onCreateItem) { 476 | onCreateItem(newItem); 477 | setInputValue(''); 478 | } 479 | } 480 | } else if (e.key === 'ArrowDown') { 481 | // Handle arrow down key (e.g., navigate to the next item) 482 | //@ts-ignore 483 | setHighlightedIndex((prevIndex: number | null) => 484 | prevIndex === null || prevIndex === inputItems.length - 1 485 | ? 0 486 | : ((prevIndex + 1) as number) 487 | ); 488 | } else if (e.key === 'ArrowUp') { 489 | // Handle arrow up key (e.g., navigate to the previous item) 490 | //@ts-ignore 491 | setHighlightedIndex((prevIndex: number | null) => 492 | prevIndex === null || prevIndex === 0 493 | ? ((inputItems.length - 1) as number) 494 | : prevIndex - 1 495 | ); 496 | } else if (e.key === 'Tab') { 497 | // Handle tab key (e.g., close dropdown if no item is highlighted) 498 | if (isOpen && highlightedIndex === null) { 499 | // Close the dropdown 500 | setInputValue(''); 501 | setIsCreating(false); 502 | } 503 | } 504 | }; 505 | 506 | return ( 507 | 508 | {/* Label */} 509 | {label} 510 | 511 | {/* Selected Tags */} 512 | {selectedItems && ( 513 | 514 | {selectedItems.map((selectedItem, index) => ( 515 | 521 | {selectedItem.label} 522 | { 524 | e.stopPropagation(); 525 | removeSelectedItem(selectedItem); 526 | }} 527 | aria-label="Remove menu selection badge" 528 | /> 529 | 530 | ))} 531 | 532 | )} 533 | 534 | {/* @ts-ignore - Input */} 535 | openMenu() })}> 536 | {renderCustomInput ? ( 537 | renderCustomInput( 538 | { 539 | ...inputStyleProps, 540 | ...getInputProps( 541 | getDropdownProps({ 542 | placeholder, 543 | onClick: isOpen ? () => {} : openMenu, 544 | onFocus: isOpen ? () => {} : openMenu, 545 | ref: disclosureRef, 546 | }) 547 | ), 548 | }, 549 | { 550 | ...toggleButtonStyleProps, 551 | ...getToggleButtonProps(), 552 | 'aria-label': 'toggle menu', 553 | } 554 | ) 555 | ) : ( 556 | <> 557 | {} : openMenu, 563 | onFocus: isOpen ? () => {} : openMenu, 564 | onKeyDown: handleKeyDown, // Attach the keyboard shortcut handler 565 | ref: disclosureRef, 566 | }) 567 | )} 568 | /> 569 | {!hideToggleButton && ( 570 | 577 | )} 578 | 579 | )} 580 | 581 | 582 | {/* Clear All Button */} 583 | {clearAll && selectedItems.length > 0 && ( 584 | 587 | )} 588 | 589 | {error && {error}} 590 | 591 | {/* Menu Lists Component */} 592 | 593 | 603 | {isOpen && 604 | inputItems.map((item, index) => ( 605 | 614 | {isCreating ? ( 615 | createItemRenderer(item.label) 616 | ) : ( 617 | 618 | {selectedItemValues.includes(item.value) && ( 619 | 627 | )} 628 | 629 | {itemRenderer ? ( 630 | itemRenderer(item) 631 | ) : ( 632 | //@ts-expect-error This is a valid package but showing its not. 633 | 638 | )} 639 | 640 | )} 641 | 642 | ))} 643 | 644 | 645 | 646 | ); 647 | }; -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/styles.module.css: -------------------------------------------------------------------------------- 1 | /* add css module styles here (optional) */ 2 | 3 | .test { 4 | margin: 2em; 5 | padding: 0.5em; 6 | border: 2px solid #000; 7 | font-size: 2em; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default CSS definition for typescript, 3 | * will be overridden with file-specific definitions by rollup 4 | */ 5 | declare module '*.css' { 6 | const content: { [className: string]: string } 7 | export default content 8 | } 9 | 10 | interface SvgrComponent 11 | extends React.StatelessComponent> {} 12 | 13 | declare module '*.svg' { 14 | const svgUrl: string 15 | const svgComponent: SvgrComponent 16 | export default svgUrl 17 | export { svgComponent as ReactComponent } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": [ 6 | "dom", 7 | "esnext" 8 | ], 9 | "moduleResolution": "node", 10 | "jsx": "react", 11 | "sourceMap": true, 12 | "declaration": true, 13 | "esModuleInterop": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "allowSyntheticDefaultImports": true, 21 | "target": "es5", 22 | "allowJs": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "forceConsistentCasingInFileNames": true, 26 | "resolveJsonModule": true, 27 | "isolatedModules": true, 28 | "noEmit": true 29 | }, 30 | "include": [ 31 | "src" 32 | ], 33 | "exclude": [ 34 | "node_modules", 35 | "dist", 36 | "example" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } --------------------------------------------------------------------------------