├── .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 |
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 |
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 |
--------------------------------------------------------------------------------