├── .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 |
4 |
--------------------------------------------------------------------------------
/src/assets/arrowUp.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/check.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------