UNPKG

10.9 kBJavaScriptView Raw
1import _extends from 'babel-runtime/helpers/extends';
2import _objectWithoutProperties from 'babel-runtime/helpers/objectWithoutProperties';
3import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
4import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
5import _inherits from 'babel-runtime/helpers/inherits';
6import classNames from 'classnames';
7import activeElement from 'dom-helpers/activeElement';
8import contains from 'dom-helpers/query/contains';
9import keycode from 'keycode';
10import React, { cloneElement } from 'react';
11import PropTypes from 'prop-types';
12import ReactDOM from 'react-dom';
13import all from 'prop-types-extra/lib/all';
14import elementType from 'prop-types-extra/lib/elementType';
15import isRequiredForA11y from 'prop-types-extra/lib/isRequiredForA11y';
16import uncontrollable from 'uncontrollable';
17import warning from 'warning';
18
19import ButtonGroup from './ButtonGroup';
20import DropdownMenu from './DropdownMenu';
21import DropdownToggle from './DropdownToggle';
22import { bsClass as setBsClass, prefix } from './utils/bootstrapUtils';
23import createChainedFunction from './utils/createChainedFunction';
24import { exclusiveRoles, requiredRoles } from './utils/PropTypes';
25import ValidComponentChildren from './utils/ValidComponentChildren';
26
27var TOGGLE_ROLE = DropdownToggle.defaultProps.bsRole;
28var MENU_ROLE = DropdownMenu.defaultProps.bsRole;
29
30var propTypes = {
31 /**
32 * The menu will open above the dropdown button, instead of below it.
33 */
34 dropup: PropTypes.bool,
35
36 /**
37 * An html id attribute, necessary for assistive technologies, such as screen readers.
38 * @type {string|number}
39 * @required
40 */
41 id: isRequiredForA11y(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
42
43 componentClass: elementType,
44
45 /**
46 * The children of a Dropdown may be a `<Dropdown.Toggle>` or a `<Dropdown.Menu>`.
47 * @type {node}
48 */
49 children: all(requiredRoles(TOGGLE_ROLE, MENU_ROLE), exclusiveRoles(MENU_ROLE)),
50
51 /**
52 * Whether or not component is disabled.
53 */
54 disabled: PropTypes.bool,
55
56 /**
57 * Align the menu to the right side of the Dropdown toggle
58 */
59 pullRight: PropTypes.bool,
60
61 /**
62 * Whether or not the Dropdown is visible.
63 *
64 * @controllable onToggle
65 */
66 open: PropTypes.bool,
67
68 defaultOpen: PropTypes.bool,
69
70 /**
71 * A callback fired when the Dropdown wishes to change visibility. Called with the requested
72 * `open` value, the DOM event, and the source that fired it: `'click'`,`'keydown'`,`'rootClose'`, or `'select'`.
73 *
74 * ```js
75 * function(Boolean isOpen, Object event, { String source }) {}
76 * ```
77 * @controllable open
78 */
79 onToggle: PropTypes.func,
80
81 /**
82 * A callback fired when a menu item is selected.
83 *
84 * ```js
85 * (eventKey: any, event: Object) => any
86 * ```
87 */
88 onSelect: PropTypes.func,
89
90 /**
91 * If `'menuitem'`, causes the dropdown to behave like a menu item rather than
92 * a menu button.
93 */
94 role: PropTypes.string,
95
96 /**
97 * Which event when fired outside the component will cause it to be closed
98 *
99 * *Note: For custom dropdown components, you will have to pass the
100 * `rootCloseEvent` to `<RootCloseWrapper>` in your custom dropdown menu
101 * component ([similarly to how it is implemented in `<Dropdown.Menu>`](https://github.com/react-bootstrap/react-bootstrap/blob/v0.31.5/src/DropdownMenu.js#L115-L119)).*
102 */
103 rootCloseEvent: PropTypes.oneOf(['click', 'mousedown']),
104
105 /**
106 * @private
107 */
108 onMouseEnter: PropTypes.func,
109 /**
110 * @private
111 */
112 onMouseLeave: PropTypes.func
113};
114
115var defaultProps = {
116 componentClass: ButtonGroup
117};
118
119var Dropdown = function (_React$Component) {
120 _inherits(Dropdown, _React$Component);
121
122 function Dropdown(props, context) {
123 _classCallCheck(this, Dropdown);
124
125 var _this = _possibleConstructorReturn(this, _React$Component.call(this, props, context));
126
127 _this.handleClick = _this.handleClick.bind(_this);
128 _this.handleKeyDown = _this.handleKeyDown.bind(_this);
129 _this.handleClose = _this.handleClose.bind(_this);
130
131 _this._focusInDropdown = false;
132 _this.lastOpenEventType = null;
133 return _this;
134 }
135
136 Dropdown.prototype.componentDidMount = function componentDidMount() {
137 this.focusNextOnOpen();
138 };
139
140 Dropdown.prototype.componentWillUpdate = function componentWillUpdate(nextProps) {
141 if (!nextProps.open && this.props.open) {
142 this._focusInDropdown = contains(ReactDOM.findDOMNode(this.menu), activeElement(document));
143 }
144 };
145
146 Dropdown.prototype.componentDidUpdate = function componentDidUpdate(prevProps) {
147 var open = this.props.open;
148
149 var prevOpen = prevProps.open;
150
151 if (open && !prevOpen) {
152 this.focusNextOnOpen();
153 }
154
155 if (!open && prevOpen) {
156 // if focus hasn't already moved from the menu let's return it
157 // to the toggle
158 if (this._focusInDropdown) {
159 this._focusInDropdown = false;
160 this.focus();
161 }
162 }
163 };
164
165 Dropdown.prototype.focus = function focus() {
166 var toggle = ReactDOM.findDOMNode(this.toggle);
167
168 if (toggle && toggle.focus) {
169 toggle.focus();
170 }
171 };
172
173 Dropdown.prototype.focusNextOnOpen = function focusNextOnOpen() {
174 var menu = this.menu;
175
176 if (!menu.focusNext) {
177 return;
178 }
179
180 if (this.lastOpenEventType === 'keydown' || this.props.role === 'menuitem') {
181 menu.focusNext();
182 }
183 };
184
185 Dropdown.prototype.handleClick = function handleClick(event) {
186 if (this.props.disabled) {
187 return;
188 }
189
190 this.toggleOpen(event, { source: 'click' });
191 };
192
193 Dropdown.prototype.handleClose = function handleClose(event, eventDetails) {
194 if (!this.props.open) {
195 return;
196 }
197
198 this.toggleOpen(event, eventDetails);
199 };
200
201 Dropdown.prototype.handleKeyDown = function handleKeyDown(event) {
202 if (this.props.disabled) {
203 return;
204 }
205
206 switch (event.keyCode) {
207 case keycode.codes.down:
208 if (!this.props.open) {
209 this.toggleOpen(event, { source: 'keydown' });
210 } else if (this.menu.focusNext) {
211 this.menu.focusNext();
212 }
213 event.preventDefault();
214 break;
215 case keycode.codes.esc:
216 case keycode.codes.tab:
217 this.handleClose(event, { source: 'keydown' });
218 break;
219 default:
220 }
221 };
222
223 Dropdown.prototype.toggleOpen = function toggleOpen(event, eventDetails) {
224 var open = !this.props.open;
225
226 if (open) {
227 this.lastOpenEventType = eventDetails.source;
228 }
229
230 if (this.props.onToggle) {
231 this.props.onToggle(open, event, eventDetails);
232 }
233 };
234
235 Dropdown.prototype.renderMenu = function renderMenu(child, _ref) {
236 var _this2 = this;
237
238 var id = _ref.id,
239 onSelect = _ref.onSelect,
240 rootCloseEvent = _ref.rootCloseEvent,
241 props = _objectWithoutProperties(_ref, ['id', 'onSelect', 'rootCloseEvent']);
242
243 var ref = function ref(c) {
244 _this2.menu = c;
245 };
246
247 if (typeof child.ref === 'string') {
248 process.env.NODE_ENV !== 'production' ? warning(false, 'String refs are not supported on `<Dropdown.Menu>` components. ' + 'To apply a ref to the component use the callback signature:\n\n ' + 'https://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute') : void 0;
249 } else {
250 ref = createChainedFunction(child.ref, ref);
251 }
252
253 return cloneElement(child, _extends({}, props, {
254 ref: ref,
255 labelledBy: id,
256 bsClass: prefix(props, 'menu'),
257 onClose: createChainedFunction(child.props.onClose, this.handleClose),
258 onSelect: createChainedFunction(child.props.onSelect, onSelect, function (key, event) {
259 return _this2.handleClose(event, { source: 'select' });
260 }),
261 rootCloseEvent: rootCloseEvent
262 }));
263 };
264
265 Dropdown.prototype.renderToggle = function renderToggle(child, props) {
266 var _this3 = this;
267
268 var ref = function ref(c) {
269 _this3.toggle = c;
270 };
271
272 if (typeof child.ref === 'string') {
273 process.env.NODE_ENV !== 'production' ? warning(false, 'String refs are not supported on `<Dropdown.Toggle>` components. ' + 'To apply a ref to the component use the callback signature:\n\n ' + 'https://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute') : void 0;
274 } else {
275 ref = createChainedFunction(child.ref, ref);
276 }
277
278 return cloneElement(child, _extends({}, props, {
279 ref: ref,
280 bsClass: prefix(props, 'toggle'),
281 onClick: createChainedFunction(child.props.onClick, this.handleClick),
282 onKeyDown: createChainedFunction(child.props.onKeyDown, this.handleKeyDown)
283 }));
284 };
285
286 Dropdown.prototype.render = function render() {
287 var _classes,
288 _this4 = this;
289
290 var _props = this.props,
291 Component = _props.componentClass,
292 id = _props.id,
293 dropup = _props.dropup,
294 disabled = _props.disabled,
295 pullRight = _props.pullRight,
296 open = _props.open,
297 onSelect = _props.onSelect,
298 role = _props.role,
299 bsClass = _props.bsClass,
300 className = _props.className,
301 rootCloseEvent = _props.rootCloseEvent,
302 children = _props.children,
303 props = _objectWithoutProperties(_props, ['componentClass', 'id', 'dropup', 'disabled', 'pullRight', 'open', 'onSelect', 'role', 'bsClass', 'className', 'rootCloseEvent', 'children']);
304
305 delete props.onToggle;
306
307 var classes = (_classes = {}, _classes[bsClass] = true, _classes.open = open, _classes.disabled = disabled, _classes);
308
309 if (dropup) {
310 classes[bsClass] = false;
311 classes.dropup = true;
312 }
313
314 // This intentionally forwards bsSize and bsStyle (if set) to the
315 // underlying component, to allow it to render size and style variants.
316
317 return React.createElement(
318 Component,
319 _extends({}, props, { className: classNames(className, classes) }),
320 ValidComponentChildren.map(children, function (child) {
321 switch (child.props.bsRole) {
322 case TOGGLE_ROLE:
323 return _this4.renderToggle(child, {
324 id: id,
325 disabled: disabled,
326 open: open,
327 role: role,
328 bsClass: bsClass
329 });
330 case MENU_ROLE:
331 return _this4.renderMenu(child, {
332 id: id,
333 open: open,
334 pullRight: pullRight,
335 bsClass: bsClass,
336 onSelect: onSelect,
337 rootCloseEvent: rootCloseEvent
338 });
339 default:
340 return child;
341 }
342 })
343 );
344 };
345
346 return Dropdown;
347}(React.Component);
348
349Dropdown.propTypes = propTypes;
350Dropdown.defaultProps = defaultProps;
351
352setBsClass('dropdown', Dropdown);
353
354var UncontrolledDropdown = uncontrollable(Dropdown, { open: 'onToggle' });
355
356UncontrolledDropdown.Toggle = DropdownToggle;
357UncontrolledDropdown.Menu = DropdownMenu;
358
359export default UncontrolledDropdown;
\No newline at end of file