UNPKG

8.87 kBJavaScriptView Raw
1import _objectWithoutProperties from 'babel-runtime/helpers/objectWithoutProperties';
2import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
3import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
4import _inherits from 'babel-runtime/helpers/inherits';
5import _extends from 'babel-runtime/helpers/extends';
6import contains from 'dom-helpers/query/contains';
7import React, { cloneElement } from 'react';
8import PropTypes from 'prop-types';
9import ReactDOM from 'react-dom';
10import warning from 'warning';
11
12import Overlay from './Overlay';
13
14import createChainedFunction from './utils/createChainedFunction';
15
16/**
17 * Check if value one is inside or equal to the of value
18 *
19 * @param {string} one
20 * @param {string|array} of
21 * @returns {boolean}
22 */
23function isOneOf(one, of) {
24 if (Array.isArray(of)) {
25 return of.indexOf(one) >= 0;
26 }
27 return one === of;
28}
29
30var triggerType = PropTypes.oneOf(['click', 'hover', 'focus']);
31
32var propTypes = _extends({}, Overlay.propTypes, {
33
34 /**
35 * Specify which action or actions trigger Overlay visibility
36 */
37 trigger: PropTypes.oneOfType([triggerType, PropTypes.arrayOf(triggerType)]),
38
39 /**
40 * A millisecond delay amount to show and hide the Overlay once triggered
41 */
42 delay: PropTypes.number,
43 /**
44 * A millisecond delay amount before showing the Overlay once triggered.
45 */
46 delayShow: PropTypes.number,
47 /**
48 * A millisecond delay amount before hiding the Overlay once triggered.
49 */
50 delayHide: PropTypes.number,
51
52 // FIXME: This should be `defaultShow`.
53 /**
54 * The initial visibility state of the Overlay. For more nuanced visibility
55 * control, consider using the Overlay component directly.
56 */
57 defaultOverlayShown: PropTypes.bool,
58
59 /**
60 * An element or text to overlay next to the target.
61 */
62 overlay: PropTypes.node.isRequired,
63
64 /**
65 * @private
66 */
67 onBlur: PropTypes.func,
68 /**
69 * @private
70 */
71 onClick: PropTypes.func,
72 /**
73 * @private
74 */
75 onFocus: PropTypes.func,
76 /**
77 * @private
78 */
79 onMouseOut: PropTypes.func,
80 /**
81 * @private
82 */
83 onMouseOver: PropTypes.func,
84
85 // Overridden props from `<Overlay>`.
86 /**
87 * @private
88 */
89 target: PropTypes.oneOf([null]),
90 /**
91 * @private
92 */
93 onHide: PropTypes.oneOf([null]),
94 /**
95 * @private
96 */
97 show: PropTypes.oneOf([null])
98});
99
100var defaultProps = {
101 defaultOverlayShown: false,
102 trigger: ['hover', 'focus']
103};
104
105var OverlayTrigger = function (_React$Component) {
106 _inherits(OverlayTrigger, _React$Component);
107
108 function OverlayTrigger(props, context) {
109 _classCallCheck(this, OverlayTrigger);
110
111 var _this = _possibleConstructorReturn(this, _React$Component.call(this, props, context));
112
113 _this.handleToggle = _this.handleToggle.bind(_this);
114 _this.handleDelayedShow = _this.handleDelayedShow.bind(_this);
115 _this.handleDelayedHide = _this.handleDelayedHide.bind(_this);
116 _this.handleHide = _this.handleHide.bind(_this);
117
118 _this.handleMouseOver = function (e) {
119 return _this.handleMouseOverOut(_this.handleDelayedShow, e, 'fromElement');
120 };
121 _this.handleMouseOut = function (e) {
122 return _this.handleMouseOverOut(_this.handleDelayedHide, e, 'toElement');
123 };
124
125 _this._mountNode = null;
126
127 _this.state = {
128 show: props.defaultOverlayShown
129 };
130 return _this;
131 }
132
133 OverlayTrigger.prototype.componentDidMount = function componentDidMount() {
134 this._mountNode = document.createElement('div');
135 this.renderOverlay();
136 };
137
138 OverlayTrigger.prototype.componentDidUpdate = function componentDidUpdate() {
139 this.renderOverlay();
140 };
141
142 OverlayTrigger.prototype.componentWillUnmount = function componentWillUnmount() {
143 ReactDOM.unmountComponentAtNode(this._mountNode);
144 this._mountNode = null;
145
146 clearTimeout(this._hoverShowDelay);
147 clearTimeout(this._hoverHideDelay);
148 };
149
150 OverlayTrigger.prototype.handleDelayedHide = function handleDelayedHide() {
151 var _this2 = this;
152
153 if (this._hoverShowDelay != null) {
154 clearTimeout(this._hoverShowDelay);
155 this._hoverShowDelay = null;
156 return;
157 }
158
159 if (!this.state.show || this._hoverHideDelay != null) {
160 return;
161 }
162
163 var delay = this.props.delayHide != null ? this.props.delayHide : this.props.delay;
164
165 if (!delay) {
166 this.hide();
167 return;
168 }
169
170 this._hoverHideDelay = setTimeout(function () {
171 _this2._hoverHideDelay = null;
172 _this2.hide();
173 }, delay);
174 };
175
176 OverlayTrigger.prototype.handleDelayedShow = function handleDelayedShow() {
177 var _this3 = this;
178
179 if (this._hoverHideDelay != null) {
180 clearTimeout(this._hoverHideDelay);
181 this._hoverHideDelay = null;
182 return;
183 }
184
185 if (this.state.show || this._hoverShowDelay != null) {
186 return;
187 }
188
189 var delay = this.props.delayShow != null ? this.props.delayShow : this.props.delay;
190
191 if (!delay) {
192 this.show();
193 return;
194 }
195
196 this._hoverShowDelay = setTimeout(function () {
197 _this3._hoverShowDelay = null;
198 _this3.show();
199 }, delay);
200 };
201
202 OverlayTrigger.prototype.handleHide = function handleHide() {
203 this.hide();
204 };
205
206 // Simple implementation of mouseEnter and mouseLeave.
207 // React's built version is broken: https://github.com/facebook/react/issues/4251
208 // for cases when the trigger is disabled and mouseOut/Over can cause flicker
209 // moving from one child element to another.
210
211
212 OverlayTrigger.prototype.handleMouseOverOut = function handleMouseOverOut(handler, e, relatedNative) {
213 var target = e.currentTarget;
214 var related = e.relatedTarget || e.nativeEvent[relatedNative];
215
216 if ((!related || related !== target) && !contains(target, related)) {
217 handler(e);
218 }
219 };
220
221 OverlayTrigger.prototype.handleToggle = function handleToggle() {
222 if (this.state.show) {
223 this.hide();
224 } else {
225 this.show();
226 }
227 };
228
229 OverlayTrigger.prototype.hide = function hide() {
230 this.setState({ show: false });
231 };
232
233 OverlayTrigger.prototype.makeOverlay = function makeOverlay(overlay, props) {
234 return React.createElement(
235 Overlay,
236 _extends({}, props, {
237 show: this.state.show,
238 onHide: this.handleHide,
239 target: this
240 }),
241 overlay
242 );
243 };
244
245 OverlayTrigger.prototype.show = function show() {
246 this.setState({ show: true });
247 };
248
249 OverlayTrigger.prototype.renderOverlay = function renderOverlay() {
250 ReactDOM.unstable_renderSubtreeIntoContainer(this, this._overlay, this._mountNode);
251 };
252
253 OverlayTrigger.prototype.render = function render() {
254 var _props = this.props,
255 trigger = _props.trigger,
256 overlay = _props.overlay,
257 children = _props.children,
258 onBlur = _props.onBlur,
259 onClick = _props.onClick,
260 onFocus = _props.onFocus,
261 onMouseOut = _props.onMouseOut,
262 onMouseOver = _props.onMouseOver,
263 props = _objectWithoutProperties(_props, ['trigger', 'overlay', 'children', 'onBlur', 'onClick', 'onFocus', 'onMouseOut', 'onMouseOver']);
264
265 delete props.delay;
266 delete props.delayShow;
267 delete props.delayHide;
268 delete props.defaultOverlayShown;
269
270 var child = React.Children.only(children);
271 var childProps = child.props;
272 var triggerProps = {};
273
274 if (this.state.show) {
275 triggerProps['aria-describedby'] = overlay.props.id;
276 }
277
278 // FIXME: The logic here for passing through handlers on this component is
279 // inconsistent. We shouldn't be passing any of these props through.
280
281 triggerProps.onClick = createChainedFunction(childProps.onClick, onClick);
282
283 if (isOneOf('click', trigger)) {
284 triggerProps.onClick = createChainedFunction(triggerProps.onClick, this.handleToggle);
285 }
286
287 if (isOneOf('hover', trigger)) {
288 process.env.NODE_ENV !== 'production' ? warning(!(trigger === 'hover'), '[react-bootstrap] Specifying only the `"hover"` trigger limits the ' + 'visibility of the overlay to just mouse users. Consider also ' + 'including the `"focus"` trigger so that touch and keyboard only ' + 'users can see the overlay as well.') : void 0;
289
290 triggerProps.onMouseOver = createChainedFunction(childProps.onMouseOver, onMouseOver, this.handleMouseOver);
291 triggerProps.onMouseOut = createChainedFunction(childProps.onMouseOut, onMouseOut, this.handleMouseOut);
292 }
293
294 if (isOneOf('focus', trigger)) {
295 triggerProps.onFocus = createChainedFunction(childProps.onFocus, onFocus, this.handleDelayedShow);
296 triggerProps.onBlur = createChainedFunction(childProps.onBlur, onBlur, this.handleDelayedHide);
297 }
298
299 this._overlay = this.makeOverlay(overlay, props);
300
301 return cloneElement(child, triggerProps);
302 };
303
304 return OverlayTrigger;
305}(React.Component);
306
307OverlayTrigger.propTypes = propTypes;
308OverlayTrigger.defaultProps = defaultProps;
309
310export default OverlayTrigger;
\No newline at end of file