UNPKG

8.71 kBJavaScriptView Raw
1import React, { Component, cloneElement } from 'react';
2import ReactDOM from 'react-dom';
3import createChainedFunction from 'tinper-bee-core/lib/createChainedFunction';
4import splitComponentProps from 'tinper-bee-core/lib/splitComponent';
5import PropTypes from 'prop-types';
6import Overlay from 'bee-overlay/build/Overlay';
7import Portal from 'bee-overlay/build/Portal';
8import Content from './Content';
9import contains from 'dom-helpers/query/contains';
10
11//TODO: 当多个Popover在一个组件内时,显示一个会触发多个渲染。见demo1.
12
13const isReact16 = ReactDOM.createPortal !== undefined;
14
15const triggerType = PropTypes.oneOf(['click', 'hover', 'focus']);
16
17/**
18 * 检查值是属于这个值,还是等于这个值
19 *
20 * @param {string} one
21 * @param {string|array} of
22 * @returns {boolean}
23 */
24function isOneOf(one, of) {
25 if (Array.isArray(of)) {
26 return of.indexOf(one) >= 0;
27 }
28 return one === of;
29}
30
31const propTypes = {
32 ...Overlay.propTypes,
33
34
35 // FIXME: This should be `defaultShow`.
36 /**
37 * 覆盖的初始可见性状态。对于更细微的可见性控制,请考虑直接使用覆盖组件。
38 */
39 defaultOverlayShown: PropTypes.bool,
40
41 /**
42 * 要覆盖在目标旁边的元素或文本。
43 */
44 content: PropTypes.node.isRequired,
45 /**
46 * 显示和隐藏覆盖一旦触发的毫秒延迟量
47 */
48 delay: PropTypes.number,
49 /**
50 * 触发后显示叠加层之前的延迟毫秒
51 */
52 delayShow: PropTypes.number,
53 /**
54 * 触发后隐藏叠加层的延迟毫秒
55 */
56 delayHide: PropTypes.number,
57
58 /**
59 * @private
60 */
61 onClick: PropTypes.func,
62 onClose: PropTypes.func,
63 onCancel: PropTypes.func,
64
65
66 // Overridden props from `<Overlay>`.
67 /**
68 * @private
69 */
70 target: PropTypes.oneOf([null]),
71 /**
72 * @private
73 */
74 onHide: PropTypes.oneOf([null]),
75 /**
76 * @private
77 */
78 show: PropTypes.bool,
79
80 trigger: PropTypes.oneOfType([
81 triggerType, PropTypes.arrayOf(triggerType),
82 ]),
83 /**
84 * @private
85 */
86 onBlur: PropTypes.func,
87 /**
88 * @private
89 */
90 onFocus: PropTypes.func,
91 /**
92 * @private
93 */
94 onMouseOut: PropTypes.func,
95 /**
96 * @private
97 */
98 onMouseOver: PropTypes.func,
99};
100
101
102const defaultProps = {
103 placement: 'right',
104 clsPrefix: 'u-popover',
105 rootClose: true,
106 defaultOverlayShown: false,
107};
108
109class Popover extends Component{
110 constructor(props, context) {
111 super(props, context);
112
113 this._mountNode = null;
114
115 this.state = {
116 show: props.defaultOverlayShown,
117 };
118
119 this.handleMouseOver = e => (
120 this.handleMouseOverOut(this.handleDelayedShow, e)
121 );
122 this.handleMouseOut = e => (
123 this.handleMouseOverOut(this.handleDelayedHide, e)
124 );
125 }
126
127 componentDidMount() {
128 this._mountNode = document.createElement('div');
129 !isReact16 && this.renderOverlay();
130 }
131
132 componentWillReceiveProps(nextProps) {
133 if(nextProps.hasOwnProperty('show')){
134 if(nextProps.show){
135 this.handleShow();
136 }else{
137 this.handleHide();
138 }
139 }
140 }
141
142 componentDidUpdate() {
143 !isReact16 && this.renderOverlay();
144 }
145
146 componentWillUnmount() {
147 !isReact16 && ReactDOM.unmountComponentAtNode(this._mountNode);
148 this._mountNode = null;
149
150 }
151
152 handleToggle = () => {
153 if (!this.state.show) {
154 this.show();
155 }else{
156 this.hide();
157 }
158 }
159
160 handleDelayedShow = () => {
161 if (this._hoverHideDelay != null) {
162 clearTimeout(this._hoverHideDelay);
163 this._hoverHideDelay = null;
164 return;
165 }
166
167 if (this.state.show || this._hoverShowDelay != null) {
168 return;
169 }
170
171 const delay = this.props.delayShow != null ?
172 this.props.delayShow : this.props.delay;
173
174 if (!delay) {
175 this.show();
176 return;
177 }
178
179 this._hoverShowDelay = setTimeout(() => {
180 this._hoverShowDelay = null;
181 this.show();
182 }, delay);
183 }
184
185 handleDelayedHide = () => {
186 if (this._hoverShowDelay != null) {
187 clearTimeout(this._hoverShowDelay);
188 this._hoverShowDelay = null;
189 return;
190 }
191
192 if (!this.state.show || this._hoverHideDelay != null) {
193 return;
194 }
195
196 const delay = this.props.delayHide != null ?
197 this.props.delayHide : this.props.delay;
198
199 if (!delay) {
200 this.hide();
201 return;
202 }
203
204 this._hoverHideDelay = setTimeout(() => {
205 this._hoverHideDelay = null;
206 this.hide();
207 }, delay);
208 }
209
210 // 简单实现mouseEnter和mouseLeave。
211 // React的内置版本是有问题的:https://github.com/facebook/react/issues/4251
212 //在触发器被禁用的情况下,mouseOut / Over可能导致闪烁
213 //从一个子元素移动到另一个子元素。
214 handleMouseOverOut = (handler, e) => {
215 const target = e.currentTarget;
216 const related = e.relatedTarget || e.nativeEvent.toElement;
217
218 if (!related || related !== target && !contains(target, related)) {
219 handler(e);
220 }
221 }
222
223
224 handleHide = () => {
225 if(this.state.show){
226 this.hide();
227 }
228 }
229
230 handleShow = () => {
231 if(!this.state.show){
232 this.show();
233 }
234 }
235
236 show = () => {
237 this.setState({ show: true });
238 }
239
240 hide = () => {
241 let { onHide } = this.props;
242 onHide && onHide()
243 this.setState({ show: false });
244 }
245
246 makeOverlay = (overlay, props) => {
247 return (
248 <Overlay
249 {...props}
250 show={this.state.show}
251 onHide={this.handleHide}
252 target={this}
253 >
254 {overlay}
255 </Overlay>
256 );
257 }
258
259 renderOverlay = () => {
260 ReactDOM.unstable_renderSubtreeIntoContainer(
261 this, this._overlay, this._mountNode
262 );
263 }
264
265 render() {
266 const {
267 content,
268 children,
269 onClick,
270 trigger,
271 onBlur,
272 onFocus,
273 onMouseOut,
274 onMouseOver,
275 ...props
276 } = this.props;
277
278 delete props.delay;
279 delete props.delayShow;
280 delete props.delayHide;
281 delete props.defaultOverlayShown;
282
283 const [overlayProps, confirmProps] = splitComponentProps(props, Overlay);
284
285 const child = React.Children.only(children);
286 const childProps = child.props;
287
288 let overlay = (
289 <Content placement={ props.placement } {...confirmProps} >
290 {content}
291 </Content>
292 );
293
294 const triggerProps = {
295 'aria-describedby': overlay.props.id
296 };
297
298 // FIXME: 这里用于传递这个组件上的处理程序的逻辑是不一致的。我们不应该通过任何这些道具。
299
300 triggerProps.onClick = createChainedFunction(childProps.onClick, onClick);
301
302 if (isOneOf('click', trigger)) {
303 triggerProps.onClick = createChainedFunction(
304 triggerProps.onClick, this.handleToggle
305 );
306 }
307
308 if (isOneOf('hover', trigger)) {
309
310 triggerProps.onMouseOver = createChainedFunction(
311 childProps.onMouseOver, onMouseOver, this.handleMouseOver
312 );
313 triggerProps.onMouseOut = createChainedFunction(
314 childProps.onMouseOut, onMouseOut, this.handleMouseOut
315 );
316 }
317
318 if (isOneOf('focus', trigger)) {
319 triggerProps.onFocus = createChainedFunction(
320 childProps.onFocus, onFocus, this.handleDelayedShow
321 );
322 triggerProps.onBlur = createChainedFunction(
323 childProps.onBlur, onBlur, this.handleDelayedHide
324 );
325 }
326
327
328 this._overlay = this.makeOverlay(overlay, overlayProps);
329
330 if (!isReact16) {
331 return cloneElement(child, triggerProps);
332 }
333 triggerProps.key = 'overlay';
334
335 let portal = (
336 <Portal
337 key="portal"
338 container={props.container}>
339 { this._overlay }
340 </Portal>
341 )
342
343
344 return [
345 cloneElement(child, triggerProps),
346 portal
347 ]
348 }
349
350}
351
352Popover.propTypes = propTypes;
353Popover.defaultProps = defaultProps;
354
355export default Popover;