├── .babelrc ├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .stylelintrc ├── README.md ├── example ├── gulpfile.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.jsx │ ├── index.js │ └── styles │ ├── base.styl │ ├── global.css │ ├── global.styl │ └── stylus │ └── App.styl ├── package-lock.json ├── package.json ├── src ├── Dropdown.jsx ├── DropdownMultiple.jsx ├── assets │ ├── arrowDown.svg │ ├── arrowUp.svg │ └── check.svg ├── index.js └── styles │ ├── base.sass │ ├── dropdown.sass │ └── global.sass └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb"], 4 | "env": { 5 | "browser": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.paypal.me/dbilgili 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /example/node_modules 4 | /build 5 | /example/build 6 | 7 | # misc 8 | .DS_Store 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | example 3 | webpack.config.js 4 | .babelrc 5 | .gitignore 6 | .nvmrc 7 | .stylelintrc 8 | .eslintrc.json 9 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v6.10 2 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-recommended", 3 | "plugins": [ 4 | "stylelint-order" 5 | ], 6 | "rules": { 7 | "order/properties-order": [ 8 | "content", 9 | "quotes", 10 | "box-sizing", 11 | "display", 12 | "align-content", 13 | "align-items", 14 | "align-self", 15 | "flex", 16 | "flex-basis", 17 | "flex-direction", 18 | "flex-flow", 19 | "flex-grow", 20 | "flex-shrink", 21 | "flex-wrap", 22 | "justify-content", 23 | "grid", 24 | "grid-area", 25 | "grid-template", 26 | "grid-template-areas", 27 | "grid-template-rows", 28 | "grid-template-columns", 29 | "grid-column", 30 | "grid-column-start", 31 | "grid-column-end", 32 | "grid-row", 33 | "grid-row-start", 34 | "grid-row-end", 35 | "grid-auto-rows", 36 | "grid-auto-columns", 37 | "grid-auto-flow", 38 | "grid-gap", 39 | "grid-row-gap", 40 | "grid-column-gap", 41 | "order", 42 | "columns", 43 | "column-gap", 44 | "column-fill", 45 | "column-rule", 46 | "column-rule-width", 47 | "column-rule-style", 48 | "column-rule-color", 49 | "column-span", 50 | "column-count", 51 | "column-width", 52 | "position", 53 | "left", 54 | "right", 55 | "top", 56 | "bottom", 57 | "z-index", 58 | "float", 59 | "clear", 60 | "overflow", 61 | "overflow-x", 62 | "overflow-y", 63 | "resize", 64 | "visibility", 65 | "opacity", 66 | "width", 67 | "min-width", 68 | "max-width", 69 | "height", 70 | "min-height", 71 | "max-height", 72 | "margin", 73 | "margin-top", 74 | "margin-right", 75 | "margin-bottom", 76 | "margin-left", 77 | "padding", 78 | "padding-top", 79 | "padding-right", 80 | "padding-bottom", 81 | "padding-left", 82 | "border", 83 | "border-top", 84 | "border-right", 85 | "border-bottom", 86 | "border-left", 87 | "border-width", 88 | "border-top-width", 89 | "border-right-width", 90 | "border-bottom-width", 91 | "border-left-width", 92 | "border-style", 93 | "border-top-style", 94 | "border-right-style", 95 | "border-bottom-style", 96 | "border-left-style", 97 | "border-radius", 98 | "border-top-left-radius", 99 | "border-top-right-radius", 100 | "border-bottom-left-radius", 101 | "border-bottom-right-radius", 102 | "border-color", 103 | "border-top-color", 104 | "border-right-color", 105 | "border-bottom-color", 106 | "border-left-color", 107 | "border-image", 108 | "border-image-source", 109 | "border-image-width", 110 | "border-image-outset", 111 | "border-image-repeat", 112 | "border-image-slice", 113 | "outline", 114 | "outline-offset", 115 | "outline-width", 116 | "outline-style", 117 | "outline-color", 118 | "box-shadow", 119 | "background", 120 | "background-attachment", 121 | "background-clip", 122 | "background-color", 123 | "background-image", 124 | "background-repeat", 125 | "background-position", 126 | "background-size", 127 | "list-style", 128 | "list-style-type", 129 | "list-style-position", 130 | "list-style-image", 131 | "caption-side", 132 | "table-layout", 133 | "border-collapse", 134 | "border-spacing", 135 | "empty-cells", 136 | "vertical-align", 137 | "font", 138 | "font-family", 139 | "font-size", 140 | "font-size-adjust", 141 | "font-stretch", 142 | "font-weight", 143 | "font-smoothing", 144 | "osx-font-smoothing", 145 | "font-variant", 146 | "font-style", 147 | "line-height", 148 | "word-spacing", 149 | "letter-spacing", 150 | "white-space", 151 | "word-break", 152 | "word-wrap", 153 | "color", 154 | "direction", 155 | "tab-size", 156 | "text-align", 157 | "text-align-last", 158 | "text-justify", 159 | "text-indent", 160 | "text-transform", 161 | "text-decoration", 162 | "text-decoration-color", 163 | "text-decoration-line", 164 | "text-decoration-style", 165 | "text-rendering", 166 | "text-shadow", 167 | "text-overflow", 168 | "counter-reset", 169 | "counter-increment", 170 | "page-break-before", 171 | "page-break-after", 172 | "page-break-inside", 173 | "backface-visibility", 174 | "perspective", 175 | "perspective-origin", 176 | "transform", 177 | "transform-origin", 178 | "transform-style", 179 | "transition", 180 | "transition-delay", 181 | "transition-duration", 182 | "transition-property", 183 | "transition-timing-function", 184 | "animation", 185 | "animation-name", 186 | "animation-duration", 187 | "animation-timing-function", 188 | "animation-delay", 189 | "animation-iteration-count", 190 | "animation-direction", 191 | "animation-fill-mode", 192 | "animation-play-state", 193 | "cursor" 194 | ] 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This package features two custom dropdown menu components for ReactJS. 2 | 3 | __WARNING:__ Breaking changes take effect from version `1.1.7`. If you are using any of the earlier versions, refer to the [previous README files](https://www.npmjs.com/package/reactjs-dropdown-component?activeTab=explore). 4 | 5 | [Online demo](https://dbilgili.github.io/Custom-ReactJS-Dropdown-Components/index.html) 6 | 7 | __Single-selection__ | __Multi-selection__ 8 | :-------------------------:|:-------------------------: 9 | | 10 | __Single-selection searchable__ | __Multi-selection searchable__ 11 | | 12 | 13 | # Installation 14 | 15 | `npm install --save reactjs-dropdown-component` 16 | 17 | # Usage 18 | 19 | Import the component you want to use; 20 | 21 | ```javascript 22 | import { DropdownMultiple, Dropdown } from 'reactjs-dropdown-component'; 23 | ``` 24 | 25 | If you are using NextJS, import them dynamically instead; 26 | 27 | ```javascript 28 | import dynamic from 'next/dynamic'; 29 | 30 | const Dropdown = dynamic( 31 | async () => { 32 | const module = await import('reactjs-dropdown-component'); 33 | const DD = module.Dropdown; 34 | 35 | return ({ forwardedRef, ...props }) =>
; 36 | }, 37 | { ssr: false }, 38 | ); 39 | 40 | const DropdownMultiple = dynamic( 41 | async () => { 42 | const module = await import('reactjs-dropdown-component'); 43 | const DDM = module.DropdownMultiple; 44 | 45 | return ({ forwardedRef, ...props }) => ; 46 | }, 47 | { ssr: false }, 48 | ); 49 | ``` 50 | 51 | The shape of the objects in the data array should be as follows: 52 | 53 | ```javascript 54 | const locations = [ 55 | { 56 | label: 'New York', 57 | value: 'newYork', 58 | }, 59 | { 60 | label: 'Oslo', 61 | value: 'oslo', 62 | }, 63 | { 64 | label: 'Istanbul', 65 | value: 'istanbul', 66 | } 67 | ]; 68 | ``` 69 | 70 | Use a function to pass to `onChange` prop. 71 | For `` component item is an object, whereas for `` it is an array of objects. 72 | 73 | ```javascript 74 | onChange = (item, name) => { 75 | ... 76 | } 77 | ``` 78 | 79 | Finally use the components as follows: 80 | 81 | ```javascript 82 | 88 | 89 | 96 | ``` 97 | 98 | Note that when multiple options are selected in ``, `titleSingular` value automatically becomes the plural form of the noun. If you want to use a custom plural title, define `titlePlural` as well. 99 | 100 | ```javascript 101 | 109 | ``` 110 | 111 | ## Search functionality 112 | 113 | Using `searchable` prop enables the search bar. 114 | Pass an array of strings corresponding to __place holder__ and __not found message__ respectively. 115 | 116 | ```javascript 117 | 124 | ``` 125 | 126 | ## Selecting item(s) by default 127 | 128 | Use the `select` prop to define the initally selected item(s). 129 | 130 | For `` 131 | ```javascript 132 | select={{value: 'istanbul'}} 133 | ``` 134 | 135 | For `` 136 | ```javascript 137 | select={[{value: 'oslo'}, {value: 'istanbul'}]} 138 | ``` 139 | 140 | ## Calling internal functions 141 | 142 | Pass a ref and use it to call the internal functions. 143 | 144 | For `` 145 | ```javascript 146 | 150 | 151 | this.dropdownRef.current.selectSingleItem({ value: 'oslo' }); 152 | this.dropdownRef.current.clearSelection(); 153 | ``` 154 | 155 | For `` 156 | ```javascript 157 | 161 | 162 | this.dropdownRef.current.selectMultipleItems([ 163 | { value: 'istanbul' } 164 | { value: 'oslo' }, 165 | ]); 166 | 167 | this.dropdownRef.current.selectAll(); 168 | this.dropdownRef.current.deselectAll(); 169 | ``` 170 | 171 | ## Custom styling 172 | 173 | Use the following keys in an object passed to `styles` prop 174 | 175 | ```javascript 176 | wrapper 177 | header 178 | headerTitle 179 | headerArrowUpIcon 180 | headerArrowDownIcon 181 | checkIcon 182 | list 183 | listSearchBar 184 | scrollList 185 | listItem 186 | listItemNoResult 187 | ``` 188 | 189 | Example: 190 | 191 | ```javascript 192 | 201 | ``` 202 | 203 | Note that `styles` prop is meant for basic styling. Use stylesheet by targeting the specific [class names](https://github.com/dbilgili/Custom-ReactJS-Dropdown-Components/blob/master/src/styles/dropdown.sass) for more complicated stylings. 204 | 205 | Use `id` prop to set an additional class name to every element in the dropdown menu. That way you can style multiple dropdown menus with different stylings rules. 206 | 207 | In order to define your own arrow and check icons, use `arrowUpIcon`, `arrowDownIcon` and `checkIcon` props. These props accept an element type. 208 | -------------------------------------------------------------------------------- /example/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const stylus = require('gulp-stylus'); 3 | const autoprefixer = require('gulp-autoprefixer'); 4 | 5 | function handleError(err) { 6 | console.log(err.toString()); 7 | this.emit('end'); 8 | } 9 | 10 | gulp.task('stylus', function () { 11 | return gulp.src('./src/styles/global.styl') 12 | .pipe(stylus()) 13 | .pipe(autoprefixer('last 2 versions')) 14 | .on('error', handleError) 15 | .pipe(gulp.dest('./src/styles/')); 16 | }); 17 | 18 | gulp.task('watch-stylus', function () { 19 | gulp.watch('./src/styles/**/*.styl', ['stylus']); 20 | }); 21 | 22 | gulp.task('default', ['watch-stylus']); 23 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-dropdown-component-example", 3 | "homepage": "https://dbilgili.github.io/Custom-ReactJS-Dropdown-Components", 4 | "version": "1.0.0", 5 | "dependencies": { 6 | "react": "^16.5.2", 7 | "react-dom": "^16.5.2", 8 | "react-scripts": "1.1.5", 9 | "reactjs-dropdown-component": "^1.1.9" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test --env=jsdom", 15 | "eject": "react-scripts eject", 16 | "predeploy": "npm run build", 17 | "deploy": "gh-pages -d build" 18 | }, 19 | "devDependencies": { 20 | "gh-pages": "^2.0.0", 21 | "gulp": "^3.9.1", 22 | "gulp-autoprefixer": "^6.0.0", 23 | "gulp-stylus": "^2.7.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbilgili/Custom-ReactJS-Dropdown-Components/607660aa1faf3e6c2e86979243c4a023fcda1dda/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 24 | React App 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { DropdownMultiple, Dropdown } from 'reactjs-dropdown-component'; 3 | 4 | class App extends Component { 5 | constructor() { 6 | super(); 7 | this.state = { 8 | locations: [ 9 | { 10 | label: 'New York', 11 | value: 'newYork', 12 | }, 13 | { 14 | label: 'Dublin', 15 | value: 'dublin', 16 | }, 17 | { 18 | label: 'Istanbul', 19 | value: 'istanbul', 20 | }, 21 | { 22 | label: 'California', 23 | value: 'colifornia', 24 | }, 25 | { 26 | label: 'Izmir', 27 | value: 'izmir', 28 | }, 29 | { 30 | label: 'Oslo', 31 | value: 'oslo', 32 | }, 33 | ], 34 | }; 35 | } 36 | 37 | componentDidMount() { 38 | window.addEventListener('keydown', this.tabKeyPressed); 39 | window.addEventListener('mousedown', this.mouseClicked); 40 | } 41 | 42 | tabKeyPressed = (e) => { 43 | if (e.keyCode === 9) { 44 | document.querySelector('body').classList.remove('noFocus'); 45 | window.removeEventListener('keydown', this.tabKeyPressed); 46 | window.addEventListener('mousedown', this.mouseClicked); 47 | } 48 | } 49 | 50 | mouseClicked = () => { 51 | document.querySelector('body').classList.add('noFocus'); 52 | window.removeEventListener('mousedown', this.mouseClicked); 53 | window.addEventListener('keydown', this.tabKeyPressed); 54 | } 55 | 56 | onChange = (item, name) => { console.log(item, name); } 57 | 58 | render() { 59 | const { locations } = this.state; 60 | 61 | return ( 62 |
63 |

Dropdown menu examples

64 | 65 |

Regular

66 | 67 |
68 | 75 | 76 | 82 |
83 | 84 |

Searchable

85 | 86 |
87 | 95 | 96 | 103 |
104 |
105 | ); 106 | } 107 | } 108 | 109 | export default App; 110 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './styles/global.css'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /example/src/styles/base.styl: -------------------------------------------------------------------------------- 1 | * 2 | -moz-osx-font-smoothing: grayscale 3 | -webkit-font-smoothing: antialiased 4 | box-sizing: border-box 5 | text-rendering: optimizeLegibility 6 | 7 | html 8 | font-family: 'Nunito Sans', sans-serif 9 | font-size: 62.5% 10 | 11 | body 12 | font-size: 1.6rem 13 | margin: 0 14 | padding: 0 15 | -webkit-font-smoothing: antialiased 16 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0) 17 | 18 | &.noFocus 19 | *:focus 20 | outline: none 21 | 22 | ul, li 23 | margin: 0 24 | padding: 0 25 | list-style: none 26 | 27 | p 28 | margin: 0 29 | padding: 0 30 | -------------------------------------------------------------------------------- /example/src/styles/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | -moz-osx-font-smoothing: grayscale; 3 | -webkit-font-smoothing: antialiased; 4 | -webkit-box-sizing: border-box; 5 | box-sizing: border-box; 6 | text-rendering: optimizeLegibility; 7 | } 8 | html { 9 | font-family: 'Nunito Sans', sans-serif; 10 | font-size: 62.5%; 11 | } 12 | body { 13 | font-size: 1.6rem; 14 | margin: 0; 15 | padding: 0; 16 | -webkit-font-smoothing: antialiased; 17 | -webkit-tap-highlight-color: rgba(0,0,0,0); 18 | } 19 | body.noFocus *:focus { 20 | outline: none; 21 | } 22 | ul, 23 | li { 24 | margin: 0; 25 | padding: 0; 26 | list-style: none; 27 | } 28 | p { 29 | margin: 0; 30 | padding: 0; 31 | } 32 | .App { 33 | display: -webkit-box; 34 | display: -ms-flexbox; 35 | display: flex; 36 | -webkit-box-orient: vertical; 37 | -webkit-box-direction: normal; 38 | -ms-flex-direction: column; 39 | flex-direction: column; 40 | -webkit-box-align: center; 41 | -ms-flex-align: center; 42 | align-items: center; 43 | margin-top: 100px; 44 | } 45 | .App h3:nth-of-type(2) { 46 | margin-top: 50px; 47 | } 48 | .App p { 49 | margin-bottom: 50px; 50 | font-size: 2.4rem; 51 | } 52 | .wrapper { 53 | display: -webkit-box; 54 | display: -ms-flexbox; 55 | display: flex; 56 | -webkit-box-pack: justify; 57 | -ms-flex-pack: justify; 58 | justify-content: space-between; 59 | width: 460px; 60 | } 61 | -------------------------------------------------------------------------------- /example/src/styles/global.styl: -------------------------------------------------------------------------------- 1 | @import 'base' 2 | @import 'stylus/*' 3 | -------------------------------------------------------------------------------- /example/src/styles/stylus/App.styl: -------------------------------------------------------------------------------- 1 | .App 2 | display: flex 3 | flex-direction: column 4 | align-items: center 5 | margin-top: 100px 6 | 7 | h3 8 | &:nth-of-type(2) 9 | margin-top: 50px 10 | 11 | p 12 | margin-bottom: 50px 13 | font-size: 2.4rem 14 | 15 | .wrapper 16 | display: flex 17 | justify-content: space-between 18 | width: 460px 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-dropdown-component", 3 | "version": "1.2.0", 4 | "description": "Single and multi select dropwdown menu components", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack --watch", 9 | "build": "webpack" 10 | }, 11 | "author": "Dogacan Bilgili", 12 | "keywords": [ 13 | "react", 14 | "dropdown", 15 | "menu" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/dbilgili/Custom-ReactJS-Dropdown-Components.git" 20 | }, 21 | "license": "MIT", 22 | "peerDependencies": { 23 | "react": "^15.0.0 || ^16.0.0", 24 | "react-dom": "^15.0.0 || ^16.0.0" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.7.2", 28 | "@babel/plugin-proposal-class-properties": "^7.8.3", 29 | "@babel/preset-env": "^7.7.1", 30 | "@babel/preset-react": "^7.7.0", 31 | "@svgr/webpack": "^4.3.3", 32 | "autoprefixer": "^9.0.0", 33 | "babel-eslint": "^10.0.3", 34 | "babel-loader": "^8.0.6", 35 | "babel-polyfill": "^6.26.0", 36 | "css-loader": "^3.2.0", 37 | "eslint": "^6.6.0", 38 | "eslint-config-airbnb": "^18.1.0", 39 | "eslint-plugin-import": "^2.20.2", 40 | "eslint-plugin-jsx-a11y": "^6.2.3", 41 | "eslint-plugin-react": "^7.19.0", 42 | "node-sass": "^4.13.1", 43 | "postcss-loader": "^3.0.0", 44 | "react": "^16.5.2", 45 | "react-dom": "^16.5.2", 46 | "resolve-url-loader": "^3.1.1", 47 | "sass-loader": "^7.3.1", 48 | "style-loader": "^0.23.1", 49 | "stylelint": "^13.3.3", 50 | "stylelint-config-recommended": "^3.0.0", 51 | "stylelint-order": "^4.0.0", 52 | "webpack": "^4.20.2", 53 | "webpack-cli": "^3.1.2" 54 | }, 55 | "dependencies": { 56 | "pluralize": "^8.0.0", 57 | "prop-types": "^15.7.2" 58 | }, 59 | "browserslist": [ 60 | "> 1%", 61 | "last 2 versions" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /src/Dropdown.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/destructuring-assignment */ 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import ArrowDown from './assets/arrowDown.svg'; 5 | import ArrowUp from './assets/arrowUp.svg'; 6 | import Check from './assets/check.svg'; 7 | import './styles/global.sass'; 8 | 9 | class Dropdown extends Component { 10 | constructor(props) { 11 | super(props); 12 | const { title, list } = this.props; 13 | 14 | this.state = { 15 | isListOpen: false, 16 | title, 17 | selectedItem: null, 18 | keyword: '', 19 | list, 20 | }; 21 | 22 | this.searchField = React.createRef(); 23 | } 24 | 25 | componentDidMount() { 26 | const { select } = this.props; 27 | 28 | if (select) { 29 | this.selectSingleItem(select); 30 | } 31 | } 32 | 33 | componentDidUpdate() { 34 | const { isListOpen } = this.state; 35 | 36 | setTimeout(() => { 37 | if (isListOpen) { 38 | window.addEventListener('click', this.close); 39 | } else { 40 | window.removeEventListener('click', this.close); 41 | } 42 | }, 0); 43 | } 44 | 45 | componentWillUnmount() { 46 | window.removeEventListener('click', this.close); 47 | } 48 | 49 | static getDerivedStateFromProps(nextProps, prevState) { 50 | const { list } = nextProps; 51 | 52 | if (JSON.stringify(list) !== JSON.stringify(prevState.list)) { 53 | return { list }; 54 | } 55 | 56 | return null; 57 | } 58 | 59 | close = () => { 60 | this.setState({ 61 | isListOpen: false, 62 | }); 63 | } 64 | 65 | clearSelection = () => { 66 | const { name, title, onChange } = this.props; 67 | 68 | this.setState({ 69 | selectedItem: null, 70 | title, 71 | }, () => { 72 | onChange(null, name); 73 | }); 74 | } 75 | 76 | selectSingleItem = (item) => { 77 | const { list } = this.props; 78 | 79 | const selectedItem = list.find((i) => i.value === item.value); 80 | this.selectItem(selectedItem); 81 | } 82 | 83 | selectItem = (item) => { 84 | const { label, value } = item; 85 | const { list, selectedItem } = this.state; 86 | const { name, onChange } = this.props; 87 | 88 | let foundItem; 89 | 90 | if (!label) { 91 | foundItem = list.find((i) => i.value === item.value); 92 | } 93 | 94 | this.setState({ 95 | title: label || foundItem.label, 96 | isListOpen: false, 97 | selectedItem: item, 98 | }, () => selectedItem?.value !== value && onChange(item, name)); 99 | } 100 | 101 | toggleList = () => { 102 | this.setState((prevState) => ({ 103 | isListOpen: !prevState.isListOpen, 104 | keyword: '', 105 | }), () => { 106 | if (this.state.isListOpen && this.searchField.current) { 107 | this.searchField.current.focus(); 108 | this.setState({ 109 | keyword: '', 110 | }); 111 | } 112 | }); 113 | } 114 | 115 | filterList = (e) => { 116 | this.setState({ 117 | keyword: e.target.value.toLowerCase(), 118 | }); 119 | } 120 | 121 | listItems = () => { 122 | const { 123 | id, 124 | searchable, 125 | checkIcon, 126 | styles, 127 | } = this.props; 128 | const { listItem, listItemNoResult } = styles; 129 | const { keyword, list } = this.state; 130 | let tempList = [...list]; 131 | const selectedItemValue = this.state.selectedItem?.value; 132 | 133 | if (keyword.length) { 134 | tempList = list.filter((item) => item.label.toLowerCase().includes(keyword.toLowerCase())); 135 | } 136 | 137 | if (tempList.length) { 138 | return ( 139 | tempList.map((item) => ( 140 | 155 | )) 156 | ); 157 | } 158 | 159 | return ( 160 |
164 | {searchable[1]} 165 |
166 | ); 167 | } 168 | 169 | render() { 170 | const { 171 | id, 172 | searchable, 173 | arrowUpIcon, 174 | arrowDownIcon, 175 | styles, 176 | } = this.props; 177 | const { isListOpen, title } = this.state; 178 | 179 | const { 180 | wrapper, 181 | header, 182 | headerTitle, 183 | headerArrowUpIcon, 184 | headerArrowDownIcon, 185 | list, 186 | listSearchBar, 187 | scrollList, 188 | } = styles; 189 | 190 | return ( 191 |
195 | 211 | {isListOpen && ( 212 |
216 | {searchable 217 | && ( 218 | e.stopPropagation()} 224 | onChange={(e) => this.filterList(e)} 225 | /> 226 | )} 227 |
231 | {this.listItems()} 232 |
233 |
234 | )} 235 |
236 | ); 237 | } 238 | } 239 | 240 | Dropdown.defaultProps = { 241 | id: '', 242 | select: undefined, 243 | searchable: undefined, 244 | styles: {}, 245 | arrowUpIcon: null, 246 | arrowDownIcon: null, 247 | checkIcon: null, 248 | }; 249 | 250 | Dropdown.propTypes = { 251 | id: PropTypes.string, 252 | styles: PropTypes.shape({ 253 | wrapper: PropTypes.string, 254 | header: PropTypes.string, 255 | headerTitle: PropTypes.string, 256 | headerArrowUpIcon: PropTypes.string, 257 | headerArrowDownIcon: PropTypes.string, 258 | checkIcon: PropTypes.string, 259 | list: PropTypes.string, 260 | listSearchBar: PropTypes.string, 261 | scrollList: PropTypes.string, 262 | listItem: PropTypes.string, 263 | listItemNoResult: PropTypes.string, 264 | }), 265 | title: PropTypes.string.isRequired, 266 | list: PropTypes.arrayOf(PropTypes.shape({ 267 | value: PropTypes.string.isRequired, 268 | label: PropTypes.string.isRequired, 269 | })).isRequired, 270 | name: PropTypes.string.isRequired, 271 | onChange: PropTypes.func.isRequired, 272 | select: PropTypes.shape({ value: PropTypes.string }), 273 | searchable: PropTypes.shape([PropTypes.string, PropTypes.string]), 274 | checkIcon: PropTypes.elementType, 275 | arrowUpIcon: PropTypes.elementType, 276 | arrowDownIcon: PropTypes.elementType, 277 | }; 278 | 279 | export default Dropdown; 280 | -------------------------------------------------------------------------------- /src/DropdownMultiple.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/destructuring-assignment */ 2 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 3 | /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ 4 | import React, { Component } from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import pluralize from 'pluralize'; 7 | import ArrowDown from './assets/arrowDown.svg'; 8 | import ArrowUp from './assets/arrowUp.svg'; 9 | import Check from './assets/check.svg'; 10 | import './styles/global.sass'; 11 | 12 | class DropdownMultiple extends Component { 13 | constructor(props) { 14 | super(props); 15 | const { title, list } = this.props; 16 | 17 | this.state = { 18 | isListOpen: false, 19 | title, 20 | keyword: '', 21 | selectedItems: [], 22 | list, 23 | }; 24 | 25 | this.searchField = React.createRef(); 26 | } 27 | 28 | componentDidMount() { 29 | const { select } = this.props; 30 | 31 | if (select.length) { 32 | this.selectMultipleItems(select); 33 | } 34 | } 35 | 36 | componentDidUpdate() { 37 | const { isListOpen } = this.state; 38 | 39 | setTimeout(() => { 40 | if (isListOpen) { 41 | window.addEventListener('click', this.close); 42 | } else { 43 | window.removeEventListener('click', this.close); 44 | } 45 | }, 0); 46 | } 47 | 48 | componentWillUnmount() { 49 | window.removeEventListener('click', this.close); 50 | } 51 | 52 | static getDerivedStateFromProps(nextProps, prevState) { 53 | const { list } = nextProps; 54 | 55 | if (JSON.stringify(list) !== JSON.stringify(prevState.list)) { 56 | return { list }; 57 | } 58 | 59 | return null; 60 | } 61 | 62 | close = () => { 63 | this.setState({ 64 | isListOpen: false, 65 | }); 66 | } 67 | 68 | selectAll = () => { 69 | const { name, onChange } = this.props; 70 | 71 | this.setState((prevState) => ({ 72 | selectedItems: prevState.list, 73 | }), () => { 74 | this.handleTitle(); 75 | onChange(this.state.selectedItems, name); 76 | }); 77 | } 78 | 79 | deselectAll = () => { 80 | const { name, onChange } = this.props; 81 | 82 | this.setState({ 83 | selectedItems: [], 84 | }, () => { 85 | this.handleTitle(); 86 | onChange(this.state.selectedItems, name); 87 | }); 88 | } 89 | 90 | selectMultipleItems = (items) => { 91 | const { list } = this.state; 92 | 93 | items.forEach((item) => { 94 | const selectedItem = list.find((i) => i.value === item.value); 95 | setTimeout(() => { 96 | this.selectItem(selectedItem, true); 97 | }); 98 | }); 99 | } 100 | 101 | selectItem = (item, noCloseOnSelection = false) => { 102 | const { closeOnSelection } = this.props; 103 | 104 | this.setState({ 105 | isListOpen: (!noCloseOnSelection && !closeOnSelection) || false, 106 | }, () => this.handleSelection(item, this.state.selectedItems)); 107 | } 108 | 109 | handleSelection = (item, selectedItems) => { 110 | const { name, onChange } = this.props; 111 | 112 | const index = selectedItems.findIndex((i) => i.value === item.value); 113 | 114 | if (index !== -1) { 115 | const selectedItemsCopy = [...selectedItems]; 116 | selectedItemsCopy.splice(index, 1); 117 | this.setState(() => ({ 118 | selectedItems: selectedItemsCopy, 119 | }), () => { 120 | onChange(this.state.selectedItems, name); 121 | this.handleTitle(); 122 | }); 123 | } else { 124 | this.setState((prevState) => ({ 125 | selectedItems: [...prevState.selectedItems, item], 126 | }), () => { 127 | onChange(this.state.selectedItems, name); 128 | this.handleTitle(); 129 | }); 130 | } 131 | } 132 | 133 | handleTitle = () => { 134 | const { selectedItems } = this.state; 135 | const { title, titleSingular, titlePlural } = this.props; 136 | 137 | const { length } = selectedItems; 138 | 139 | if (!length) { 140 | this.setState({ 141 | title, 142 | }); 143 | } else if (length === 1) { 144 | this.setState({ 145 | title: `${length} ${titleSingular}`, 146 | }); 147 | } else if (titlePlural) { 148 | this.setState({ 149 | title: `${length} ${titlePlural}`, 150 | }); 151 | } else { 152 | const pluralizedTitle = pluralize(titleSingular, length); 153 | this.setState({ 154 | title: `${length} ${pluralizedTitle}`, 155 | }); 156 | } 157 | } 158 | 159 | toggleList = () => { 160 | this.setState((prevState) => ({ 161 | isListOpen: !prevState.isListOpen, 162 | }), () => { 163 | if (this.state.isListOpen && this.searchField.current) { 164 | this.searchField.current.focus(); 165 | this.setState({ 166 | keyword: '', 167 | }); 168 | } 169 | }); 170 | } 171 | 172 | filterList = (e) => { 173 | this.setState({ 174 | keyword: e.target.value.toLowerCase(), 175 | }); 176 | } 177 | 178 | listItems = () => { 179 | const { 180 | id, 181 | searchable, 182 | checkIcon, 183 | styles, 184 | } = this.props; 185 | const { listItem, listItemNoResult } = styles; 186 | const { keyword, list, selectedItems } = this.state; 187 | let tempList = [...list]; 188 | 189 | if (keyword.length) { 190 | tempList = list.filter((item) => item.label.toLowerCase().includes(keyword.toLowerCase())); 191 | } 192 | 193 | if (tempList.length) { 194 | return ( 195 | tempList.map((item) => ( 196 | 211 | )) 212 | ); 213 | } 214 | 215 | return ( 216 |
220 | {searchable[1]} 221 |
222 | ); 223 | } 224 | 225 | render() { 226 | const { 227 | id, 228 | searchable, 229 | arrowUpIcon, 230 | arrowDownIcon, 231 | styles, 232 | } = this.props; 233 | const { isListOpen, title } = this.state; 234 | 235 | const { 236 | wrapper, 237 | header, 238 | headerTitle, 239 | headerArrowUpIcon, 240 | headerArrowDownIcon, 241 | list, 242 | listSearchBar, 243 | scrollList, 244 | } = styles; 245 | 246 | return ( 247 |
251 | 267 | {isListOpen && ( 268 |
e.stopPropagation()} 274 | > 275 | {searchable 276 | && ( 277 | this.filterList(e)} 283 | /> 284 | )} 285 |
289 | {this.listItems()} 290 |
291 |
292 | )} 293 |
294 | ); 295 | } 296 | } 297 | 298 | DropdownMultiple.defaultProps = { 299 | id: '', 300 | select: [], 301 | closeOnSelection: false, 302 | titlePlural: undefined, 303 | searchable: undefined, 304 | styles: {}, 305 | arrowUpIcon: null, 306 | arrowDownIcon: null, 307 | checkIcon: null, 308 | }; 309 | 310 | DropdownMultiple.propTypes = { 311 | id: PropTypes.string, 312 | styles: PropTypes.shape({ 313 | wrapper: PropTypes.string, 314 | header: PropTypes.string, 315 | headerTitle: PropTypes.string, 316 | headerArrowUpIcon: PropTypes.string, 317 | headerArrowDownIcon: PropTypes.string, 318 | checkIcon: PropTypes.string, 319 | list: PropTypes.string, 320 | listSearchBar: PropTypes.string, 321 | scrollList: PropTypes.string, 322 | listItem: PropTypes.string, 323 | listItemNoResult: PropTypes.string, 324 | }), 325 | title: PropTypes.string.isRequired, 326 | titleSingular: PropTypes.string.isRequired, 327 | titlePlural: PropTypes.string, 328 | list: PropTypes.shape([{ value: PropTypes.string, label: PropTypes.string }]).isRequired, 329 | name: PropTypes.string.isRequired, 330 | onChange: PropTypes.func.isRequired, 331 | closeOnSelection: PropTypes.bool, 332 | searchable: PropTypes.shape([PropTypes.string, PropTypes.string]), 333 | select: PropTypes.arrayOf(PropTypes.shape({ 334 | value: PropTypes.string.isRequired, 335 | })), 336 | checkIcon: PropTypes.elementType, 337 | arrowUpIcon: PropTypes.elementType, 338 | arrowDownIcon: PropTypes.elementType, 339 | }; 340 | 341 | export default DropdownMultiple; 342 | -------------------------------------------------------------------------------- /src/assets/arrowDown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/arrowUp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Dropdown from './Dropdown'; 2 | import DropdownMultiple from './DropdownMultiple'; 3 | 4 | export { Dropdown, DropdownMultiple }; 5 | -------------------------------------------------------------------------------- /src/styles/base.sass: -------------------------------------------------------------------------------- 1 | .dd-wrapper 2 | 3 | button 4 | overflow: visible 5 | width: inherit 6 | margin: inherit 7 | padding: inherit 8 | border: none 9 | background: inherit 10 | font: inherit 11 | line-height: normal 12 | color: inherit 13 | text-align: inherit 14 | -webkit-appearance: none 15 | 16 | ul, li 17 | margin: 0 18 | padding: 0 19 | list-style: none 20 | 21 | p 22 | margin: 0 23 | padding: 0 24 | 25 | * 26 | box-sizing: border-box 27 | -moz-osx-font-smoothing: grayscale 28 | -webkit-font-smoothing: antialiased 29 | text-rendering: optimizeLegibility 30 | -------------------------------------------------------------------------------- /src/styles/dropdown.sass: -------------------------------------------------------------------------------- 1 | .dd-wrapper 2 | position: relative 3 | width: 222px 4 | font-size: 1.6rem 5 | user-select: none 6 | 7 | .dd-header 8 | display: flex 9 | align-items: center 10 | justify-content: space-between 11 | position: relative 12 | border: 1px solid rgb(223, 223, 223) 13 | border-radius: 3px 14 | background-color: white 15 | line-height: 38px 16 | cursor: default 17 | cursor: pointer 18 | 19 | span 20 | display: flex 21 | margin-right: 20px 22 | 23 | .dd-header-title 24 | margin: 2px 20px 25 | margin-right: 30px 26 | font-weight: 300 27 | 28 | .angle-down 29 | margin-right: 20px 30 | color: black 31 | 32 | .dd-list 33 | position: absolute 34 | z-index: 10 35 | width: 100% 36 | max-height: 215px 37 | border: 1px solid rgb(223, 223, 223) 38 | border-top: none 39 | border-bottom-left-radius: 3px 40 | border-bottom-right-radius: 3px 41 | box-shadow: 0 2px 5px -1px rgb(232, 232, 232) 42 | background-color: white 43 | font-weight: 700 44 | text-align: left 45 | -webkit-overflow-scrolling: touch 46 | 47 | .dd-scroll-list 48 | overflow-y: scroll 49 | max-height: 215px 50 | padding: 15px 0 51 | 52 | .dd-list-item 53 | display: inline-block 54 | overflow: hidden 55 | width: 100% 56 | padding: 8px 10px 57 | font-size: 1.5rem 58 | line-height: 1.6rem 59 | white-space: nowrap 60 | text-overflow: ellipsis 61 | cursor: default 62 | cursor: pointer 63 | 64 | &.no-result 65 | font-weight: normal 66 | cursor: default 67 | 68 | &:hover 69 | background-color: transparent 70 | color: black 71 | 72 | &:hover 73 | background-color: #FFCC01 74 | color: white 75 | 76 | & > span > svg > path 77 | fill: white 78 | 79 | .dd-list-search-bar 80 | width: 100% 81 | height: 40px 82 | padding: 0 10px 83 | border: none 84 | border-bottom: 1px solid #DFDFDF 85 | font-size: inherit 86 | 87 | &::placeholder 88 | color: rgb(200, 200, 200) 89 | 90 | &.searchable 91 | overflow-y: hidden 92 | padding: 0 93 | 94 | .dd-scroll-list 95 | max-height: calc(215px - 40px) 96 | padding: 10px 0 97 | -------------------------------------------------------------------------------- /src/styles/global.sass: -------------------------------------------------------------------------------- 1 | @import './base' 2 | @import './dropdown' 3 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const autoprefixer = require('autoprefixer'); 3 | 4 | module.exports = { 5 | mode: 'production', 6 | entry: ['babel-polyfill', './src/index.js'], 7 | output: { 8 | path: path.resolve(__dirname, 'build'), 9 | filename: 'index.js', 10 | libraryTarget: 'commonjs2', 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(js|jsx)$/, 16 | include: path.resolve(__dirname, 'src'), 17 | exclude: /(node_modules|bower_components|build)/, 18 | use: { 19 | loader: 'babel-loader', 20 | options: { 21 | presets: [ 22 | '@babel/preset-env', 23 | '@babel/preset-react', 24 | ], 25 | }, 26 | }, 27 | }, 28 | { 29 | test: /\.sass$/, 30 | use: [ 31 | 'style-loader', 32 | 'css-loader', 33 | { 34 | loader: 'postcss-loader', 35 | options: { 36 | plugins: () => [autoprefixer()], 37 | }, 38 | }, 39 | 'resolve-url-loader', 40 | 'sass-loader', 41 | ], 42 | }, 43 | { 44 | test: /\.svg$/, 45 | use: ['@svgr/webpack'], 46 | }, 47 | ], 48 | }, 49 | resolve: { 50 | extensions: ['*', '.js', '.jsx'], 51 | }, 52 | externals: { 53 | react: 'commonjs react', 54 | }, 55 | }; 56 | --------------------------------------------------------------------------------