UNPKG

11.2 kBJavaScriptView Raw
1/**
2* This source code is quoted from rc-menu.
3* homepage: https://github.com/react-component/menu
4*/
5import React from 'react';
6import PropTypes from 'prop-types';
7import { connect } from 'mini-store';
8import { KeyCode } from 'tinper-bee-core';
9import createChainedFunction from 'rc-util/lib/createChainedFunction';
10import classNames from 'classnames';
11import { getKeyFromChildrenIndex, loopMenuItem, noop, menuAllProps } from './util';
12import DOMWrap from './DOMWrap';
13
14function allDisabled(arr) {
15 if (!arr.length) {
16 return true;
17 }
18 return arr.every(c => !!c.props.disabled);
19}
20
21function updateActiveKey(store, menuId, activeKey) {
22 const state = store.getState();
23 store.setState({
24 activeKey: {
25 ...state.activeKey,
26 [menuId]: activeKey,
27 },
28 });
29}
30
31function getEventKey(props) {
32 // when eventKey not available ,it's menu and return menu id '0-menu-'
33 return props.eventKey || '0-menu-';
34}
35
36export function getActiveKey(props, originalActiveKey) {
37 let activeKey = originalActiveKey;
38 const { children, eventKey } = props;
39 if (activeKey) {
40 let found;
41 loopMenuItem(children, (c, i) => {
42 if (c && !c.props.disabled && activeKey === getKeyFromChildrenIndex(c, eventKey, i)) {
43 found = true;
44 }
45 });
46 if (found) {
47 return activeKey;
48 }
49 }
50 activeKey = null;
51 if (props.defaultActiveFirst) {
52 loopMenuItem(children, (c, i) => {
53 if (!activeKey && c && !c.props.disabled) {
54 activeKey = getKeyFromChildrenIndex(c, eventKey, i);
55 }
56 });
57 return activeKey;
58 }
59 return activeKey;
60}
61
62export function saveRef(c) {
63 if (c) {
64 const index = this.instanceArray.indexOf(c);
65 if (index !== -1) {
66 // update component if it's already inside instanceArray
67 this.instanceArray[index] = c;
68 } else {
69 // add component if it's not in instanceArray yet;
70 this.instanceArray.push(c);
71 }
72 }
73}
74
75export class SubPopupMenu extends React.Component {
76 static propTypes = {
77 onSelect: PropTypes.func,
78 onClick: PropTypes.func,
79 onDeselect: PropTypes.func,
80 onOpenChange: PropTypes.func,
81 onDestroy: PropTypes.func,
82 openTransitionName: PropTypes.string,
83 openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
84 openKeys: PropTypes.arrayOf(PropTypes.string),
85 visible: PropTypes.bool,
86 children: PropTypes.any,
87 parentMenu: PropTypes.object,
88 eventKey: PropTypes.string,
89 store: PropTypes.shape({
90 getState: PropTypes.func,
91 setState: PropTypes.func,
92 }),
93
94 // adding in refactor
95 focusable: PropTypes.bool,
96 multiple: PropTypes.bool,
97 style: PropTypes.object,
98 defaultActiveFirst: PropTypes.bool,
99 activeKey: PropTypes.string,
100 selectedKeys: PropTypes.arrayOf(PropTypes.string),
101 defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string),
102 defaultOpenKeys: PropTypes.arrayOf(PropTypes.string),
103 level: PropTypes.number,
104 mode: PropTypes.oneOf(['horizontal', 'vertical', 'vertical-left', 'vertical-right', 'inline']),
105 triggerSubMenuAction: PropTypes.oneOf(['click', 'hover']),
106 inlineIndent: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
107 manualRef: PropTypes.func,
108 itemIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
109 expandIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
110 };
111
112 static defaultProps = {
113 prefixCls: 'rc-menu',
114 className: '',
115 mode: 'vertical',
116 level: 1,
117 inlineIndent: 24,
118 visible: true,
119 focusable: true,
120 style: {},
121 manualRef: noop,
122 };
123
124 constructor(props) {
125 super(props);
126
127 props.store.setState({
128 activeKey: {
129 ...props.store.getState().activeKey,
130 [props.eventKey]: getActiveKey(props, props.activeKey),
131 },
132 });
133
134 this.instanceArray = [];
135 }
136
137 componentDidMount() {
138 // invoke customized ref to expose component to mixin
139 if (this.props.manualRef) {
140 this.props.manualRef(this);
141 }
142 }
143
144 shouldComponentUpdate(nextProps) {
145 return this.props.visible || nextProps.visible;
146 }
147
148 componentDidUpdate(prevProps) {
149 const props = this.props;
150 const originalActiveKey = 'activeKey' in props ? props.activeKey :
151 props.store.getState().activeKey[getEventKey(props)];
152 const activeKey = getActiveKey(props, originalActiveKey);
153 if (activeKey !== originalActiveKey) {
154 updateActiveKey(props.store, getEventKey(props), activeKey);
155 } else if ('activeKey' in prevProps) {
156 // If prev activeKey is not same as current activeKey,
157 // we should set it.
158 const prevActiveKey = getActiveKey(prevProps, prevProps.activeKey);
159 if (activeKey !== prevActiveKey) {
160 updateActiveKey(props.store, getEventKey(props), activeKey);
161 }
162 }
163 }
164
165 // all keyboard events callbacks run from here at first
166 onKeyDown = (e, callback) => {
167 const keyCode = e.keyCode;
168 let handled;
169 this.getFlatInstanceArray().forEach((obj) => {
170 if (obj && obj.props.active && obj.onKeyDown) {
171 handled = obj.onKeyDown(e);
172 }
173 });
174 if (handled) {
175 return 1;
176 }
177 let activeItem = null;
178 if (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN) {
179 if(this.props.store.getState().keyboard){//是否启用键盘操作
180 activeItem = this.step(keyCode === KeyCode.UP ? -2 : 2)
181 }
182 }
183
184 if (activeItem) {
185 e.preventDefault();
186 updateActiveKey(this.props.store, getEventKey(this.props), activeItem.props.eventKey);
187
188 if (typeof callback === 'function') {
189 callback(activeItem);
190 }
191
192 return 1;
193 }
194 };
195
196 onItemHover = (e) => {
197 const { key, hover } = e;
198 updateActiveKey(this.props.store, getEventKey(this.props), hover ? key : null);
199 };
200
201 onDeselect = (selectInfo) => {
202 this.props.onDeselect(selectInfo);
203 };
204
205 onSelect = (selectInfo) => {
206 this.props.onSelect(selectInfo);
207 };
208
209 onClick = (e) => {
210 this.props.onClick(e);
211 };
212
213 onOpenChange = (e) => {
214 this.props.onOpenChange(e);
215 };
216
217 onDestroy = (key) => {
218 /* istanbul ignore next */
219 this.props.onDestroy(key);
220 };
221
222 getFlatInstanceArray = () => {
223 return this.instanceArray;
224 };
225
226 getOpenTransitionName = () => {
227 return this.props.openTransitionName;
228 };
229
230 step = (direction) => {
231 let children = this.getFlatInstanceArray();
232 const activeKey = this.props.store.getState().activeKey[getEventKey(this.props)];
233 const len = children.length;
234 if (!len) {
235 return null;
236 }
237 if (direction < 0) {
238 children = children.concat().reverse();
239 }
240 // find current activeIndex
241 let activeIndex = -1;
242 children.every((c, ci) => {
243 if (c && c.props.eventKey === activeKey) {
244 activeIndex = ci;
245 return false;
246 }
247 return true;
248 });
249 if (
250 !this.props.defaultActiveFirst && activeIndex !== -1
251 &&
252 allDisabled(children.slice(activeIndex, len - 1))
253 ) {
254 return undefined;
255 }
256 const start = (activeIndex + 1) % len;
257 let i = start;
258
259 do {
260 const child = children[i];
261 if (!child || child.props.disabled) {
262 i = (i + 1) % len;
263 } else {
264 return child;
265 }
266 } while (i !== start);
267
268 return null;
269 };
270
271 renderCommonMenuItem = (child, i, extraProps) => {
272 const state = this.props.store.getState();
273 const props = this.props;
274 const key = getKeyFromChildrenIndex(child, props.eventKey, i);
275 const childProps = child.props;
276 const isActive = key === state.activeKey;
277 const newChildProps = {
278 mode: childProps.mode || props.mode,
279 level: props.level,
280 inlineIndent: props.inlineIndent,
281 renderMenuItem: this.renderMenuItem,
282 rootPrefixCls: props.prefixCls,
283 index: i,
284 parentMenu: props.parentMenu,
285 // customized ref function, need to be invoked manually in child's componentDidMount
286 manualRef: childProps.disabled ? undefined :
287 createChainedFunction(child.ref, saveRef.bind(this)),
288 eventKey: key,
289 active: !childProps.disabled && isActive,
290 multiple: props.multiple,
291 onClick: (e) => {
292 (childProps.onClick || noop)(e);
293 this.onClick(e);
294 },
295 onItemHover: this.onItemHover,
296 openTransitionName: this.getOpenTransitionName(),
297 openAnimation: props.openAnimation,
298 subMenuOpenDelay: props.subMenuOpenDelay,
299 subMenuCloseDelay: props.subMenuCloseDelay,
300 forceSubMenuRender: props.forceSubMenuRender,
301 onOpenChange: this.onOpenChange,
302 onDeselect: this.onDeselect,
303 onSelect: this.onSelect,
304 builtinPlacements: props.builtinPlacements,
305 itemIcon: childProps.itemIcon || this.props.itemIcon,
306 expandIcon: childProps.expandIcon || this.props.expandIcon,
307 ...extraProps
308 };
309 if (props.mode === 'inline') {
310 newChildProps.triggerSubMenuAction = 'click';
311 }
312 return React.cloneElement(child, newChildProps);
313 };
314
315 renderMenuItem = (c, i, subMenuKey) => {
316 /* istanbul ignore if */
317
318 if (!c) {
319 return null;
320 }
321 const state = this.props.store.getState();
322 const extraProps = {
323 openKeys: state.openKeys,
324 selectedKeys: state.selectedKeys,
325 triggerSubMenuAction: this.props.triggerSubMenuAction,
326 subMenuKey,
327 };
328 return this.renderCommonMenuItem(c, i, extraProps);
329 };
330
331 render() {
332 const { ...props } = this.props;
333 this.instanceArray = [];
334 const className = classNames(
335 props.prefixCls,
336 props.className,
337 `${props.prefixCls}-${props.mode}`,
338 );
339 const domProps = {
340 className,
341 // role could be 'select' and by default set to menu
342 role: props.role || 'menu',
343 };
344 if (props.id) {
345 domProps.id = props.id;
346 }
347 if (props.focusable) {
348 domProps.tabIndex = this.props.tabIndex;
349 domProps.onKeyDown = this.onKeyDown;
350 }
351 const { prefixCls, eventKey, visible, level, mode, overflowedIndicator, theme } = props;
352 menuAllProps.forEach(key => delete props[key]);
353
354 // Otherwise, the propagated click event will trigger another onClick
355 delete props.onClick;
356 delete props.keyboard;
357
358 return (
359 // ESLint is not smart enough to know that the type of `children` was checked.
360 /* eslint-disable */
361 <DOMWrap
362 {...props}
363 prefixCls={prefixCls}
364 mode={mode}
365 tag="ul"
366 level={level}
367 theme={theme}
368 hiddenClassName={`${prefixCls}-hidden`}
369 visible={visible}
370 overflowedIndicator={overflowedIndicator}
371 {...domProps}
372 >
373 {React.Children.map(
374 props.children,
375 (c, i) => this.renderMenuItem(c, i, eventKey || '0-menu-'),
376 )}
377 </DOMWrap>
378 /*eslint-enable */
379 );
380 }
381}
382const connected = connect()(SubPopupMenu);
383
384export default connected;