├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist └── index.js ├── example ├── customArrowExample.js ├── flatArrayExample.js ├── index.html ├── main.js ├── objectArrayExample.js ├── style.css └── zeroValObjectArrayExample.js ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | *.module-cache 10 | 11 | # Coverage tools 12 | lib-cov 13 | coverage 14 | 15 | # Compiled binary addons (http://nodejs.org/api/addons.html) 16 | build/Release 17 | 18 | # Dependency directory 19 | node_modules 20 | 21 | example/bundle.js 22 | 23 | # This project doesn't use Yarn 24 | yarn.lock 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '9' 5 | - '8' 6 | cache: 7 | directories: 8 | - 'node_modules' 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 xvfeng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-dropdown 2 | ============== 3 | 4 | [![NPM version][npm-image]][npm-url] 5 | [![Downloads][downloads-image]][downloads-url] 6 | 7 | Simple Dropdown component for React, inspired by [react-select](https://github.com/JedWatson/react-select) 8 | 9 | 10 | ### Why 11 | 12 | * The default HTML select element is hard to style 13 | * And sometime we also want grouped menus 14 | * if you want more advanced select, check [react-select](https://github.com/JedWatson/react-select) 15 | 16 | ### Installation 17 | 18 | ``` 19 | // with npm 20 | $ npm install react-dropdown --save 21 | 22 | // with yarn 23 | $ yarn add react-dropdown 24 | ``` 25 | 26 | ### Changelog 27 | 28 | If you want to support React version under v0.13, use react-dropdown@v0.6.1 29 | 30 | ### Usage 31 | 32 | This is the basic usage of react-dropdown 33 | 34 | ```Javascript 35 | import Dropdown from 'react-dropdown'; 36 | import 'react-dropdown/style.css'; 37 | 38 | const options = [ 39 | 'one', 'two', 'three' 40 | ]; 41 | const defaultOption = options[0]; 42 | ; 43 | ``` 44 | 45 | **Options** 46 | 47 | Flat Array options 48 | 49 | ```JavaScript 50 | 51 | const options = [ 52 | 'one', 'two', 'three' 53 | ]; 54 | ``` 55 | 56 | Object Array options 57 | 58 | ```JavaScript 59 | 60 | const options = [ 61 | { value: 'one', label: 'One' }, 62 | { value: 'two', label: 'Two', className: 'myOptionClassName' }, 63 | { 64 | type: 'group', name: 'group1', items: [ 65 | { value: 'three', label: 'Three', className: 'myOptionClassName' }, 66 | { value: 'four', label: 'Four' } 67 | ] 68 | }, 69 | { 70 | type: 'group', name: 'group2', items: [ 71 | { value: 'five', label: 'Five' }, 72 | { value: 'six', label: 'Six' } 73 | ] 74 | } 75 | ]; 76 | ``` 77 | 78 | When using Object options you can add to each option a className string to further customize the dropdown, e.g. adding icons to options 79 | 80 | **Disabling the Dropdown** 81 | 82 | Just pass a disabled boolean value to the Dropdown to disable it. This will also give you a `.Dropdown-disabled` class on the element, so you can style it yourself. 83 | 84 | ```JavaScript 85 | ; 86 | ``` 87 | 88 | ### Customizing the dropdown 89 | 90 | **className** 91 | 92 | The `className` prop is passed down to the wrapper `div`, which also has the `Dropdown-root` class. 93 | 94 | ```JavaScript 95 | ; 96 | ``` 97 | 98 | **controlClassName** 99 | 100 | The `controlClassName` prop is passed down to the control `div`, which also has the `Dropdown-control` class. 101 | 102 | ```JavaScript 103 | ; 104 | ``` 105 | 106 | **placeholderClassName** 107 | 108 | The `placeholderClassName` prop is passed down to the placeholder `div`, which also has the `Dropdown-placeholder` class. 109 | 110 | ```JavaScript 111 | ; 112 | ``` 113 | 114 | **menuClassName** 115 | 116 | The `menuClassName` prop is passed down to the menu `div` (the one that opens and closes and holds the options), which also has the `Dropdown-menu` class. 117 | 118 | ```JavaScript 119 | ; 120 | ``` 121 | 122 | **arrowClassName** 123 | 124 | The `arrowClassName` prop is passed down to the arrow `span` , which also has the `Dropdown-arrow` class. 125 | 126 | ```JavaScript 127 | ; 128 | ``` 129 | 130 | **arrowClosed**, **arrowOpen** 131 | 132 | The `arrowClosed` & `arrowOpen` props enable passing in custom elements for the open/closed state arrows. 133 | 134 | ```JavaScript 135 | } 137 | arrowOpen={} 138 | />; 139 | ``` 140 | 141 | Check more examples in the example folder. 142 | 143 | **Run example** 144 | 145 | ``` 146 | $ npm start 147 | ``` 148 | 149 | ### License 150 | 151 | MIT | Build for [CSViz](https://csviz.org) project @[Wiredcraft](http://wiredcraft.com) 152 | 153 | [npm-image]: https://img.shields.io/npm/v/react-dropdown.svg?style=flat-square 154 | [npm-url]: https://npmjs.org/package/react-dropdown 155 | [downloads-image]: http://img.shields.io/npm/dm/react-dropdown.svg?style=flat-square 156 | [downloads-url]: https://npmjs.org/package/react-dropdown 157 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _react = _interopRequireWildcard(require("react")); 9 | 10 | var _classnames = _interopRequireDefault(require("classnames")); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 13 | 14 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 15 | 16 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 17 | 18 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 19 | 20 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 21 | 22 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 23 | 24 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 25 | 26 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 27 | 28 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 29 | 30 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 31 | 32 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 33 | 34 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 35 | 36 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 37 | 38 | var DEFAULT_PLACEHOLDER_STRING = 'Select...'; 39 | 40 | var Dropdown = 41 | /*#__PURE__*/ 42 | function (_Component) { 43 | _inherits(Dropdown, _Component); 44 | 45 | function Dropdown(props) { 46 | var _this; 47 | 48 | _classCallCheck(this, Dropdown); 49 | 50 | _this = _possibleConstructorReturn(this, _getPrototypeOf(Dropdown).call(this, props)); 51 | _this.state = { 52 | selected: _this.parseValue(props.value, props.options) || { 53 | label: typeof props.placeholder === 'undefined' ? DEFAULT_PLACEHOLDER_STRING : props.placeholder, 54 | value: '' 55 | }, 56 | isOpen: false 57 | }; 58 | _this.dropdownRef = (0, _react.createRef)(); 59 | _this.mounted = true; 60 | _this.handleDocumentClick = _this.handleDocumentClick.bind(_assertThisInitialized(_this)); 61 | _this.fireChangeEvent = _this.fireChangeEvent.bind(_assertThisInitialized(_this)); 62 | return _this; 63 | } 64 | 65 | _createClass(Dropdown, [{ 66 | key: "componentDidUpdate", 67 | value: function componentDidUpdate(prevProps) { 68 | if (this.props.value !== prevProps.value) { 69 | if (this.props.value) { 70 | var selected = this.parseValue(this.props.value, this.props.options); 71 | 72 | if (selected !== this.state.selected) { 73 | this.setState({ 74 | selected: selected 75 | }); 76 | } 77 | } else { 78 | this.setState({ 79 | selected: { 80 | label: typeof this.props.placeholder === 'undefined' ? DEFAULT_PLACEHOLDER_STRING : this.props.placeholder, 81 | value: '' 82 | } 83 | }); 84 | } 85 | } 86 | } 87 | }, { 88 | key: "componentDidMount", 89 | value: function componentDidMount() { 90 | document.addEventListener('click', this.handleDocumentClick, false); 91 | document.addEventListener('touchend', this.handleDocumentClick, false); 92 | } 93 | }, { 94 | key: "componentWillUnmount", 95 | value: function componentWillUnmount() { 96 | this.mounted = false; 97 | document.removeEventListener('click', this.handleDocumentClick, false); 98 | document.removeEventListener('touchend', this.handleDocumentClick, false); 99 | } 100 | }, { 101 | key: "handleMouseDown", 102 | value: function handleMouseDown(event) { 103 | if (this.props.onFocus && typeof this.props.onFocus === 'function') { 104 | this.props.onFocus(this.state.isOpen); 105 | } 106 | 107 | if (event.type === 'mousedown' && event.button !== 0) return; 108 | event.stopPropagation(); 109 | event.preventDefault(); 110 | 111 | if (!this.props.disabled) { 112 | this.setState({ 113 | isOpen: !this.state.isOpen 114 | }); 115 | } 116 | } 117 | }, { 118 | key: "parseValue", 119 | value: function parseValue(value, options) { 120 | var option; 121 | 122 | if (typeof value === 'string') { 123 | for (var i = 0, num = options.length; i < num; i++) { 124 | if (options[i].type === 'group') { 125 | var match = options[i].items.filter(function (item) { 126 | return item.value === value; 127 | }); 128 | 129 | if (match.length) { 130 | option = match[0]; 131 | } 132 | } else if (typeof options[i].value !== 'undefined' && options[i].value === value) { 133 | option = options[i]; 134 | } 135 | } 136 | } 137 | 138 | return option || value; 139 | } 140 | }, { 141 | key: "setValue", 142 | value: function setValue(value, label) { 143 | var newState = { 144 | selected: { 145 | value: value, 146 | label: label 147 | }, 148 | isOpen: false 149 | }; 150 | this.fireChangeEvent(newState); 151 | this.setState(newState); 152 | } 153 | }, { 154 | key: "fireChangeEvent", 155 | value: function fireChangeEvent(newState) { 156 | if (newState.selected !== this.state.selected && this.props.onChange) { 157 | this.props.onChange(newState.selected); 158 | } 159 | } 160 | }, { 161 | key: "renderOption", 162 | value: function renderOption(option) { 163 | var _classes; 164 | 165 | var value = option.value; 166 | 167 | if (typeof value === 'undefined') { 168 | value = option.label || option; 169 | } 170 | 171 | var label = option.label || option.value || option; 172 | var isSelected = value === this.state.selected.value || value === this.state.selected; 173 | var classes = (_classes = {}, _defineProperty(_classes, "".concat(this.props.baseClassName, "-option"), true), _defineProperty(_classes, option.className, !!option.className), _defineProperty(_classes, 'is-selected', isSelected), _classes); 174 | var optionClass = (0, _classnames["default"])(classes); 175 | return _react["default"].createElement("div", { 176 | key: value, 177 | className: optionClass, 178 | onMouseDown: this.setValue.bind(this, value, label), 179 | onClick: this.setValue.bind(this, value, label), 180 | role: "option", 181 | "aria-selected": isSelected ? 'true' : 'false' 182 | }, label); 183 | } 184 | }, { 185 | key: "buildMenu", 186 | value: function buildMenu() { 187 | var _this2 = this; 188 | 189 | var _this$props = this.props, 190 | options = _this$props.options, 191 | baseClassName = _this$props.baseClassName; 192 | var ops = options.map(function (option) { 193 | if (option.type === 'group') { 194 | var groupTitle = _react["default"].createElement("div", { 195 | className: "".concat(baseClassName, "-title") 196 | }, option.name); 197 | 198 | var _options = option.items.map(function (item) { 199 | return _this2.renderOption(item); 200 | }); 201 | 202 | return _react["default"].createElement("div", { 203 | className: "".concat(baseClassName, "-group"), 204 | key: option.name, 205 | role: "listbox", 206 | tabIndex: "-1" 207 | }, groupTitle, _options); 208 | } else { 209 | return _this2.renderOption(option); 210 | } 211 | }); 212 | return ops.length ? ops : _react["default"].createElement("div", { 213 | className: "".concat(baseClassName, "-noresults") 214 | }, "No options found"); 215 | } 216 | }, { 217 | key: "handleDocumentClick", 218 | value: function handleDocumentClick(event) { 219 | if (this.mounted) { 220 | if (!this.dropdownRef.current.contains(event.target)) { 221 | if (this.state.isOpen) { 222 | this.setState({ 223 | isOpen: false 224 | }); 225 | } 226 | } 227 | } 228 | } 229 | }, { 230 | key: "isValueSelected", 231 | value: function isValueSelected() { 232 | return typeof this.state.selected === 'string' || this.state.selected.value !== ''; 233 | } 234 | }, { 235 | key: "render", 236 | value: function render() { 237 | var _classNames, _classNames2, _classNames3, _classNames4, _classNames5; 238 | 239 | var _this$props2 = this.props, 240 | baseClassName = _this$props2.baseClassName, 241 | controlClassName = _this$props2.controlClassName, 242 | placeholderClassName = _this$props2.placeholderClassName, 243 | menuClassName = _this$props2.menuClassName, 244 | arrowClassName = _this$props2.arrowClassName, 245 | arrowClosed = _this$props2.arrowClosed, 246 | arrowOpen = _this$props2.arrowOpen, 247 | className = _this$props2.className; 248 | var disabledClass = this.props.disabled ? 'Dropdown-disabled' : ''; 249 | var placeHolderValue = typeof this.state.selected === 'string' ? this.state.selected : this.state.selected.label; 250 | var dropdownClass = (0, _classnames["default"])((_classNames = {}, _defineProperty(_classNames, "".concat(baseClassName, "-root"), true), _defineProperty(_classNames, className, !!className), _defineProperty(_classNames, 'is-open', this.state.isOpen), _classNames)); 251 | var controlClass = (0, _classnames["default"])((_classNames2 = {}, _defineProperty(_classNames2, "".concat(baseClassName, "-control"), true), _defineProperty(_classNames2, controlClassName, !!controlClassName), _defineProperty(_classNames2, disabledClass, !!disabledClass), _classNames2)); 252 | var placeholderClass = (0, _classnames["default"])((_classNames3 = {}, _defineProperty(_classNames3, "".concat(baseClassName, "-placeholder"), true), _defineProperty(_classNames3, placeholderClassName, !!placeholderClassName), _defineProperty(_classNames3, 'is-selected', this.isValueSelected()), _classNames3)); 253 | var menuClass = (0, _classnames["default"])((_classNames4 = {}, _defineProperty(_classNames4, "".concat(baseClassName, "-menu"), true), _defineProperty(_classNames4, menuClassName, !!menuClassName), _classNames4)); 254 | var arrowClass = (0, _classnames["default"])((_classNames5 = {}, _defineProperty(_classNames5, "".concat(baseClassName, "-arrow"), true), _defineProperty(_classNames5, arrowClassName, !!arrowClassName), _classNames5)); 255 | 256 | var value = _react["default"].createElement("div", { 257 | className: placeholderClass 258 | }, placeHolderValue); 259 | 260 | var menu = this.state.isOpen ? _react["default"].createElement("div", { 261 | className: menuClass, 262 | "aria-expanded": "true" 263 | }, this.buildMenu()) : null; 264 | return _react["default"].createElement("div", { 265 | ref: this.dropdownRef, 266 | className: dropdownClass 267 | }, _react["default"].createElement("div", { 268 | className: controlClass, 269 | onMouseDown: this.handleMouseDown.bind(this), 270 | onTouchEnd: this.handleMouseDown.bind(this), 271 | "aria-haspopup": "listbox" 272 | }, value, _react["default"].createElement("div", { 273 | className: "".concat(baseClassName, "-arrow-wrapper") 274 | }, arrowOpen && arrowClosed ? this.state.isOpen ? arrowOpen : arrowClosed : _react["default"].createElement("span", { 275 | className: arrowClass 276 | }))), menu); 277 | } 278 | }]); 279 | 280 | return Dropdown; 281 | }(_react.Component); 282 | 283 | Dropdown.defaultProps = { 284 | baseClassName: 'Dropdown' 285 | }; 286 | var _default = Dropdown; 287 | exports["default"] = _default; 288 | -------------------------------------------------------------------------------- /example/customArrowExample.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Dropdown from '../index.js' 3 | 4 | const options = [ 5 | 'one', 'two', 'three' 6 | ] 7 | 8 | const arrowClosed = ( 9 | 10 | ) 11 | const arrowOpen = ( 12 | 13 | ) 14 | 15 | class CustomArrowExample extends Component { 16 | constructor (props) { 17 | super(props) 18 | this.state = { 19 | selected: '' 20 | } 21 | } 22 | 23 | render () { 24 | const defaultOption = this.state.selected 25 | 26 | return ( 27 |
28 |

Custom Arrow Example

29 | 36 | 37 |
38 |

Usage:

39 |
40 |
41 |               {`
42 | const arrowClosed = (
43 |   
44 | )
45 | const arrowOpen = (
46 |   
47 | )
48 | 
49 | 
56 |               `}
57 |             
58 |
59 |
60 |
61 | ) 62 | } 63 | } 64 | 65 | export default CustomArrowExample 66 | -------------------------------------------------------------------------------- /example/flatArrayExample.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Dropdown from '../index.js' 3 | 4 | const options = [ 5 | 'one', 'two', 'three' 6 | ] 7 | 8 | class FlatArrayExample extends Component { 9 | constructor (props) { 10 | super(props) 11 | this.state = { 12 | selected: '' 13 | } 14 | this._onSelect = this._onSelect.bind(this) 15 | } 16 | 17 | _onSelect (option) { 18 | console.log('You selected ', option.label) 19 | this.setState({selected: option}) 20 | } 21 | 22 | render () { 23 | const defaultOption = this.state.selected 24 | const placeHolderValue = typeof this.state.selected === 'string' ? this.state.selected : this.state.selected.label 25 | 26 | return ( 27 |
28 |

Flat Array Example

29 | 30 |
31 | You selected 32 | {placeHolderValue} 33 |
34 | 35 |
36 |

Options:

37 |
38 |
39 |               {`
40 | const options = [
41 |   'one', 'two', 'three'
42 | ]
43 |               `}
44 |             
45 |
46 |
47 |
48 | ) 49 | } 50 | } 51 | 52 | export default FlatArrayExample 53 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | react-dropdown 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import FlatArrayExample from './flatArrayExample' 5 | import ObjectArrayExample from './objectArrayExample' 6 | import ZeroValObjectArrayExample from './zeroValObjectArrayExample' 7 | import CustomArrowExample from './CustomArrowExample' 8 | 9 | class App extends Component { 10 | render () { 11 | return ( 12 |
13 |
14 |

React Dropdown

15 |
16 |
17 |

18 | Simple Dropdown component for React, inspired by react-select 19 |

20 |
21 |
22 |               { "$ npm install react-dropdown --save" }
23 |             
24 |
25 |
26 | 27 |
28 |

Examples:

29 |

Usage:

30 |
31 |
32 |               {`
33 | 
34 |               `}
35 |             
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |

License:

46 |
47 | 50 |
51 | 52 | ) 53 | } 54 | 55 | } 56 | 57 | ReactDOM.render(, document.querySelector('#app')) 58 | -------------------------------------------------------------------------------- /example/objectArrayExample.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Dropdown from '../index.js' 3 | 4 | class ObjectArrayExample extends Component { 5 | constructor (props) { 6 | super(props) 7 | this.state = { 8 | selected: { value: 'two', label: 'Two'} 9 | } 10 | this._onSelect = this._onSelect.bind(this) 11 | } 12 | 13 | _onSelect (option) { 14 | console.log('You selected ', option.label) 15 | this.setState({selected: option}) 16 | } 17 | 18 | render () { 19 | const { toggleClassName, togglePlaholderClassName, toggleMenuClassName, toggleOptionsClassName } = this.state 20 | 21 | const options = [ 22 | { value: 'one', label: 'One' }, 23 | { value: 'two', label: 'Two', className: toggleOptionsClassName && 'my-custom-class' }, 24 | { 25 | type: 'group', name: 'group1', items: [ 26 | { value: 'three', label: 'Three', className: toggleOptionsClassName && 'my-custom-class' }, 27 | { value: 'four', label: 'Four' } 28 | ] 29 | }, 30 | { 31 | type: 'group', name: 'group2', items: [ 32 | { value: 'five', label: 'Five' }, 33 | { value: 'six', label: 'Six' } 34 | ] 35 | } 36 | ] 37 | 38 | 39 | const defaultOption = this.state.selected 40 | const placeHolderValue = typeof this.state.selected === 'string' ? this.state.selected : this.state.selected.label 41 | 42 | return ( 43 |
44 |

Object Array and Custom ClassNames Example

45 |
46 | 49 | 52 | 55 | 58 |
59 | 68 |
69 | You selected 70 | {placeHolderValue} 71 |
72 |
73 |

Options:

74 |
75 |
 76 |               {`
 77 | const options = [
 78 |   { value: 'one', label: 'One' },
 79 |   { value: 'two', label: 'Two'${toggleOptionsClassName ? ', classNames \'my-custom-class\'' : ''} },
 80 |   {
 81 |     type: 'group', name: 'group1', items: [
 82 |       { value: 'three', label: 'Three' },
 83 |       { value: 'four', label: 'Four'${toggleOptionsClassName ? ', className: \'my-custom-class\'' : ''} }
 84 |     ]
 85 |   },
 86 |   {
 87 |     type: 'group', name: 'group2', items: [
 88 |       { value: 'five', label: 'Five' },
 89 |       { value: 'six', label: 'Six' }
 90 |     ]
 91 |   }
 92 | ]
 93 | `}
 94 |             
95 |
96 |

Usage with custom classeNames:

97 |
98 |
{
 99 | `
100 | 
108 | `}
109 |             
110 |
111 |
112 |
113 | ) 114 | } 115 | } 116 | 117 | export default ObjectArrayExample 118 | -------------------------------------------------------------------------------- /example/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px auto; 3 | width: 60%; 4 | } 5 | 6 | header h2 { 7 | text-align: center; 8 | margin: 30px auto; 9 | } 10 | 11 | pre { 12 | background: #f7f7f7; 13 | padding: 10px 20px; 14 | border-radius: 2px; 15 | border: 1px solid #ccc; 16 | } 17 | 18 | pre code { 19 | line-height: 20px; 20 | } 21 | 22 | .result { 23 | padding: 8px 0; 24 | } 25 | 26 | .arrow-closed, .arrow-open { 27 | border: solid #999; 28 | border-width: 0 2px 2px 0; 29 | display: inline-block; 30 | padding: 4px; 31 | position: absolute; 32 | right: 10px; 33 | } 34 | 35 | .arrow-closed { 36 | top: 10px; 37 | transform: rotate(45deg); 38 | -webkit-transform: rotate(45deg); 39 | } 40 | 41 | .arrow-open { 42 | top: 14px; 43 | transform: rotate(-135deg); 44 | -webkit-transform: rotate(-135deg); 45 | } 46 | 47 | .Dropdown-root { 48 | position: relative; 49 | } 50 | 51 | .Dropdown-control { 52 | position: relative; 53 | overflow: hidden; 54 | background-color: white; 55 | border: 1px solid #ccc; 56 | border-radius: 2px; 57 | box-sizing: border-box; 58 | color: #333; 59 | cursor: default; 60 | outline: none; 61 | padding: 8px 52px 8px 10px; 62 | transition: all 200ms ease; 63 | } 64 | 65 | .Dropdown-control:hover { 66 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); 67 | } 68 | 69 | .Dropdown-arrow { 70 | border-color: #999 transparent transparent; 71 | border-style: solid; 72 | border-width: 5px 5px 0; 73 | content: ' '; 74 | display: block; 75 | height: 0; 76 | margin-top: -ceil(2.5); 77 | position: absolute; 78 | right: 10px; 79 | top: 14px; 80 | width: 0 81 | } 82 | 83 | .is-open .Dropdown-arrow { 84 | border-color: transparent transparent #999; 85 | border-width: 0 5px 5px; 86 | } 87 | 88 | .Dropdown-menu { 89 | background-color: white; 90 | border: 1px solid #ccc; 91 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); 92 | box-sizing: border-box; 93 | margin-top: -1px; 94 | max-height: 200px; 95 | overflow-y: auto; 96 | position: absolute; 97 | top: 100%; 98 | width: 100%; 99 | z-index: 1000; 100 | -webkit-overflow-scrolling: touch; 101 | } 102 | 103 | .Dropdown-menu .Dropdown-group > .Dropdown-title { 104 | padding: 8px 10px; 105 | color: rgba(51, 51, 51, 1.2); 106 | font-weight: bold; 107 | text-transform: capitalize; 108 | } 109 | 110 | .Dropdown-option { 111 | box-sizing: border-box; 112 | color: rgba(51, 51, 51, 0.8); 113 | cursor: pointer; 114 | display: block; 115 | padding: 8px 10px; 116 | } 117 | 118 | .Dropdown-option:last-child { 119 | border-bottom-right-radius: 2px; 120 | border-bottom-left-radius: 2px; 121 | } 122 | 123 | .Dropdown-option:hover { 124 | background-color: #f2f9fc; 125 | color: #333; 126 | } 127 | 128 | .Dropdown-option.is-selected { 129 | background-color: #f2f9fc; 130 | color: #333; 131 | } 132 | 133 | .Dropdown-noresults { 134 | box-sizing: border-box; 135 | color: #ccc; 136 | cursor: default; 137 | display: block; 138 | padding: 8px 10px; 139 | } 140 | 141 | .my-custom-class { 142 | border: 2px solid red; 143 | font-weight: bold; 144 | color: purple; 145 | } 146 | 147 | -------------------------------------------------------------------------------- /example/zeroValObjectArrayExample.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Dropdown from '../index.js' 3 | 4 | class ZeroValObjectArrayExample extends Component { 5 | constructor (props) { 6 | super(props) 7 | this.state = { 8 | selected: { value: 0, label: 'Zero'} 9 | } 10 | this._onSelect = this._onSelect.bind(this) 11 | } 12 | 13 | _onSelect (option) { 14 | console.log(`You selected ${option.label}, with value ${option.value}`) 15 | this.setState({selected: option}) 16 | } 17 | 18 | render () { 19 | const options = [ 20 | { value: 0, label: 'Zero' }, 21 | { value: 1, label: 'One' } 22 | ] 23 | 24 | const defaultOption = this.state.selected 25 | const placeHolderValue = typeof this.state.selected === 'string' ? this.state.selected : this.state.selected.label 26 | 27 | return ( 28 |
29 |

Zero-Value Object Array Example

30 | 31 |
32 | You selected 33 | {placeHolderValue} 34 |
35 |
36 |

Options:

37 |
38 |
39 |               {`
40 | const options = [
41 | { value: 0, label: 'Zero' },
42 | { value: 1, label: 'One' }
43 | ]
44 |               `}
45 |             
46 |
47 |
48 |
49 | ) 50 | } 51 | } 52 | 53 | export default ZeroValObjectArrayExample 54 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-dropdown" { 2 | import * as React from "react"; 3 | export interface Option { 4 | label: React.ReactNode; 5 | value: string; 6 | className?: string; 7 | data?: { 8 | [dataAttribute: string]: string | number 9 | }; 10 | } 11 | export interface Group { 12 | type: "group"; 13 | name: string; 14 | items: Option[]; 15 | } 16 | export interface ReactDropdownProps { 17 | options: (Group | Option | string)[]; 18 | baseClassName?: string; 19 | className?: string; 20 | controlClassName?: string; 21 | placeholderClassName?: string; 22 | menuClassName?: string; 23 | arrowClassName?: string; 24 | disabled?: boolean; 25 | arrowClosed?: React.ReactNode, 26 | arrowOpen?: React.ReactNode, 27 | onChange?: (arg: Option) => void; 28 | onFocus?: (arg: boolean) => void; 29 | value?: Option | string; 30 | placeholder?: String; 31 | } 32 | 33 | class ReactDropdown extends React.Component { 34 | } 35 | 36 | export default ReactDropdown; 37 | } 38 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react' 2 | import classNames from 'classnames' 3 | 4 | const DEFAULT_PLACEHOLDER_STRING = 'Select...' 5 | 6 | class Dropdown extends Component { 7 | constructor (props) { 8 | super(props) 9 | this.state = { 10 | selected: this.parseValue(props.value, props.options) || { 11 | label: typeof props.placeholder === 'undefined' ? DEFAULT_PLACEHOLDER_STRING : props.placeholder, 12 | value: '' 13 | }, 14 | isOpen: false 15 | } 16 | this.dropdownRef = createRef() 17 | this.mounted = true 18 | this.handleDocumentClick = this.handleDocumentClick.bind(this) 19 | this.fireChangeEvent = this.fireChangeEvent.bind(this) 20 | } 21 | 22 | componentDidUpdate (prevProps) { 23 | if (this.props.value !== prevProps.value) { 24 | if (this.props.value) { 25 | let selected = this.parseValue(this.props.value, this.props.options) 26 | if (selected !== this.state.selected) { 27 | this.setState({ selected }) 28 | } 29 | } else { 30 | this.setState({ 31 | selected: { 32 | label: typeof this.props.placeholder === 'undefined' ? DEFAULT_PLACEHOLDER_STRING : this.props.placeholder, 33 | value: '' 34 | } 35 | }) 36 | } 37 | } 38 | } 39 | 40 | componentDidMount () { 41 | document.addEventListener('click', this.handleDocumentClick, false) 42 | document.addEventListener('touchend', this.handleDocumentClick, false) 43 | } 44 | 45 | componentWillUnmount () { 46 | this.mounted = false 47 | document.removeEventListener('click', this.handleDocumentClick, false) 48 | document.removeEventListener('touchend', this.handleDocumentClick, false) 49 | } 50 | 51 | handleMouseDown (event) { 52 | if (this.props.onFocus && typeof this.props.onFocus === 'function') { 53 | this.props.onFocus(this.state.isOpen) 54 | } 55 | if (event.type === 'mousedown' && event.button !== 0) return 56 | event.stopPropagation() 57 | event.preventDefault() 58 | 59 | if (!this.props.disabled) { 60 | this.setState({ 61 | isOpen: !this.state.isOpen 62 | }) 63 | } 64 | } 65 | 66 | parseValue (value, options) { 67 | let option 68 | 69 | if (typeof value === 'string') { 70 | for (var i = 0, num = options.length; i < num; i++) { 71 | if (options[i].type === 'group') { 72 | const match = options[i].items.filter(item => item.value === value) 73 | if (match.length) { 74 | option = match[0] 75 | } 76 | } else if (typeof options[i].value !== 'undefined' && options[i].value === value) { 77 | option = options[i] 78 | } 79 | } 80 | } 81 | 82 | return option || value 83 | } 84 | 85 | setValue (value, label) { 86 | let newState = { 87 | selected: { 88 | value, 89 | label}, 90 | isOpen: false 91 | } 92 | this.fireChangeEvent(newState) 93 | this.setState(newState) 94 | } 95 | 96 | fireChangeEvent (newState) { 97 | if (newState.selected !== this.state.selected && this.props.onChange) { 98 | this.props.onChange(newState.selected) 99 | } 100 | } 101 | 102 | renderOption (option) { 103 | let value = option.value 104 | if (typeof value === 'undefined') { 105 | value = option.label || option 106 | } 107 | let label = option.label || option.value || option 108 | let isSelected = value === this.state.selected.value || value === this.state.selected 109 | 110 | const classes = { 111 | [`${this.props.baseClassName}-option`]: true, 112 | [option.className]: !!option.className, 113 | 'is-selected': isSelected 114 | } 115 | 116 | const optionClass = classNames(classes) 117 | 118 | const dataAttributes = Object.keys(option.data || {}).reduce( 119 | (acc, dataKey) => ({ 120 | ...acc, 121 | [`data-${dataKey}`]: option.data[dataKey] 122 | }), 123 | {} 124 | ) 125 | 126 | return ( 127 |
136 | {label} 137 |
138 | ) 139 | } 140 | 141 | buildMenu () { 142 | let { options, baseClassName } = this.props 143 | let ops = options.map((option) => { 144 | if (option.type === 'group') { 145 | let groupTitle = (
146 | {option.name} 147 |
) 148 | let _options = option.items.map((item) => this.renderOption(item)) 149 | 150 | return ( 151 |
152 | {groupTitle} 153 | {_options} 154 |
155 | ) 156 | } else { 157 | return this.renderOption(option) 158 | } 159 | }) 160 | 161 | return ops.length ? ops :
162 | No options found 163 |
164 | } 165 | 166 | handleDocumentClick (event) { 167 | if (this.mounted) { 168 | if (!this.dropdownRef.current.contains(event.target)) { 169 | if (this.state.isOpen) { 170 | this.setState({ isOpen: false }) 171 | } 172 | } 173 | } 174 | } 175 | 176 | isValueSelected () { 177 | return typeof this.state.selected === 'string' || this.state.selected.value !== '' 178 | } 179 | 180 | render () { 181 | const { baseClassName, controlClassName, placeholderClassName, menuClassName, arrowClassName, arrowClosed, arrowOpen, className } = this.props 182 | 183 | const disabledClass = this.props.disabled ? 'Dropdown-disabled' : '' 184 | const placeHolderValue = typeof this.state.selected === 'string' ? this.state.selected : this.state.selected.label 185 | 186 | const dropdownClass = classNames({ 187 | [`${baseClassName}-root`]: true, 188 | [className]: !!className, 189 | 'is-open': this.state.isOpen 190 | }) 191 | const controlClass = classNames({ 192 | [`${baseClassName}-control`]: true, 193 | [controlClassName]: !!controlClassName, 194 | [disabledClass]: !!disabledClass 195 | }) 196 | const placeholderClass = classNames({ 197 | [`${baseClassName}-placeholder`]: true, 198 | [placeholderClassName]: !!placeholderClassName, 199 | 'is-selected': this.isValueSelected() 200 | }) 201 | const menuClass = classNames({ 202 | [`${baseClassName}-menu`]: true, 203 | [menuClassName]: !!menuClassName 204 | }) 205 | const arrowClass = classNames({ 206 | [`${baseClassName}-arrow`]: true, 207 | [arrowClassName]: !!arrowClassName 208 | }) 209 | 210 | const value = (
211 | {placeHolderValue} 212 |
) 213 | const menu = this.state.isOpen ?
214 | {this.buildMenu()} 215 |
: null 216 | 217 | return ( 218 |
219 |
220 | {value} 221 |
222 | {arrowOpen && arrowClosed 223 | ? this.state.isOpen ? arrowOpen : arrowClosed 224 | : } 225 |
226 |
227 | {menu} 228 |
229 | ) 230 | } 231 | } 232 | 233 | Dropdown.defaultProps = { baseClassName: 'Dropdown' } 234 | export default Dropdown 235 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dropdown", 3 | "version": "1.11.0", 4 | "description": "React dropdown component", 5 | "main": "dist/index.js", 6 | "style": "style.css", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/fraserxu/react-dropdown.git" 10 | }, 11 | "files": [ 12 | "dist/index.js", 13 | "index.d.ts", 14 | "style.css" 15 | ], 16 | "keywords": [ 17 | "react", 18 | "react-component", 19 | "component", 20 | "dropdown", 21 | "select" 22 | ], 23 | "author": { 24 | "name": "Fraser Xu", 25 | "email": "xvfeng123@gmail.com", 26 | "url": "https://fraserxu.me" 27 | }, 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/fraserxu/react-dropdown/issues" 31 | }, 32 | "homepage": "https://github.com/fraserxu/react-dropdown", 33 | "dependencies": { 34 | "classnames": "^2.2.3" 35 | }, 36 | "peerDependencies": { 37 | "react": "^0.14.7 || ^15.0.0-0 || ^16.0.0 || ^17.0.0 || ^18.0.0", 38 | "react-dom": "^0.14.7 || ^15.0.0-0 || ^16.0.0 || ^17.0.0|| ^18.0.0" 39 | }, 40 | "browserify": { 41 | "transform": [ 42 | "babelify" 43 | ] 44 | }, 45 | "babel": { 46 | "presets": [ 47 | "@babel/react", 48 | "@babel/env" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "@babel/cli": "^7.7.0", 53 | "@babel/core": "^7.7.2", 54 | "@babel/preset-env": "^7.7.1", 55 | "@babel/preset-react": "^7.7.0", 56 | "babelify": "^7.2.0", 57 | "browserify": "^13.0.0", 58 | "browserify-hmr": "^0.3.1", 59 | "ecstatic": "^1.4.0", 60 | "gh-pages": "^0.11.0", 61 | "react": "^0.14.7 || ^15.0.0-0", 62 | "react-dom": "^0.14.7 || ^15.0.0-0", 63 | "standard": "^11.0.1", 64 | "watchify": "^3.7.0" 65 | }, 66 | "typings": "./index.d.ts", 67 | "scripts": { 68 | "build": "babel index.js -o dist/index.js", 69 | "test": "standard index.js", 70 | "watch": "watchify example/main.js -p browserify-hmr -o example/bundle.js -dv", 71 | "start": "ecstatic -p 8080 example & npm run watch", 72 | "prepublishOnly": "npm test && npm run build", 73 | "predeploy": "npm test && browserify example/main.js -o example/bundle.js", 74 | "deploy": "gh-pages -d example", 75 | "lint-fix": "standard --fix index.js" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | .Dropdown-root { 2 | position: relative; 3 | } 4 | 5 | .Dropdown-control { 6 | position: relative; 7 | overflow: hidden; 8 | background-color: white; 9 | border: 1px solid #ccc; 10 | border-radius: 2px; 11 | box-sizing: border-box; 12 | color: #333; 13 | cursor: default; 14 | outline: none; 15 | padding: 8px 52px 8px 10px; 16 | transition: all 200ms ease; 17 | } 18 | 19 | .Dropdown-control:hover { 20 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); 21 | } 22 | 23 | .Dropdown-arrow { 24 | border-color: #999 transparent transparent; 25 | border-style: solid; 26 | border-width: 5px 5px 0; 27 | content: ' '; 28 | display: block; 29 | height: 0; 30 | margin-top: -ceil(2.5); 31 | position: absolute; 32 | right: 10px; 33 | top: 14px; 34 | width: 0 35 | } 36 | 37 | .is-open .Dropdown-arrow { 38 | border-color: transparent transparent #999; 39 | border-width: 0 5px 5px; 40 | } 41 | 42 | .Dropdown-menu { 43 | background-color: white; 44 | border: 1px solid #ccc; 45 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); 46 | box-sizing: border-box; 47 | margin-top: -1px; 48 | max-height: 200px; 49 | overflow-y: auto; 50 | position: absolute; 51 | top: 100%; 52 | width: 100%; 53 | z-index: 1000; 54 | -webkit-overflow-scrolling: touch; 55 | } 56 | 57 | .Dropdown-menu .Dropdown-group > .Dropdown-title{ 58 | padding: 8px 10px; 59 | color: rgba(51, 51, 51, 1); 60 | font-weight: bold; 61 | text-transform: capitalize; 62 | } 63 | 64 | .Dropdown-option { 65 | box-sizing: border-box; 66 | color: rgba(51, 51, 51, 0.8); 67 | cursor: pointer; 68 | display: block; 69 | padding: 8px 10px; 70 | } 71 | 72 | .Dropdown-option:last-child { 73 | border-bottom-right-radius: 2px; 74 | border-bottom-left-radius: 2px; 75 | } 76 | 77 | .Dropdown-option:hover { 78 | background-color: #f2f9fc; 79 | color: #333; 80 | } 81 | 82 | .Dropdown-option.is-selected { 83 | background-color: #f2f9fc; 84 | color: #333; 85 | } 86 | 87 | .Dropdown-noresults { 88 | box-sizing: border-box; 89 | color: #ccc; 90 | cursor: default; 91 | display: block; 92 | padding: 8px 10px; 93 | } 94 | --------------------------------------------------------------------------------