UNPKG

12.9 kBJavaScriptView Raw
1import _extends from "@babel/runtime/helpers/esm/extends";
2import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
3
4/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
5import * as React from 'react';
6import clsx from 'clsx';
7import PropTypes from 'prop-types';
8import Typography from '@material-ui/core/Typography';
9import Collapse from '@material-ui/core/Collapse';
10import { alpha, withStyles, useTheme } from '@material-ui/core/styles';
11import { useForkRef } from '@material-ui/core/utils';
12import TreeViewContext from '../TreeView/TreeViewContext';
13export const styles = theme => ({
14 /* Styles applied to the root element. */
15 root: {
16 listStyle: 'none',
17 margin: 0,
18 padding: 0,
19 outline: 0,
20 WebkitTapHighlightColor: 'transparent',
21 '&:focus > $content $label': {
22 backgroundColor: theme.palette.action.hover
23 },
24 '&$selected > $content $label': {
25 backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity)
26 },
27 '&$selected > $content $label:hover, &$selected:focus > $content $label': {
28 backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity),
29 // Reset on touch devices, it doesn't add specificity
30 '@media (hover: none)': {
31 backgroundColor: 'transparent'
32 }
33 }
34 },
35
36 /* Pseudo-class applied to the root element when expanded. */
37 expanded: {},
38
39 /* Pseudo-class applied to the root element when selected. */
40 selected: {},
41
42 /* Styles applied to the `role="group"` element. */
43 group: {
44 margin: 0,
45 padding: 0,
46 marginLeft: 17
47 },
48
49 /* Styles applied to the tree node content. */
50 content: {
51 width: '100%',
52 display: 'flex',
53 alignItems: 'center',
54 cursor: 'pointer'
55 },
56
57 /* Styles applied to the tree node icon and collapse/expand icon. */
58 iconContainer: {
59 marginRight: 4,
60 width: 15,
61 display: 'flex',
62 flexShrink: 0,
63 justifyContent: 'center',
64 '& svg': {
65 fontSize: 18
66 }
67 },
68
69 /* Styles applied to the label element. */
70 label: {
71 width: '100%',
72 paddingLeft: 4,
73 position: 'relative',
74 '&:hover': {
75 backgroundColor: theme.palette.action.hover,
76 // Reset on touch devices, it doesn't add specificity
77 '@media (hover: none)': {
78 backgroundColor: 'transparent'
79 }
80 }
81 }
82});
83
84const isPrintableCharacter = str => {
85 return str && str.length === 1 && str.match(/\S/);
86};
87
88const TreeItem = /*#__PURE__*/React.forwardRef(function TreeItem(props, ref) {
89 const {
90 children,
91 classes,
92 className,
93 collapseIcon,
94 endIcon,
95 expandIcon,
96 icon: iconProp,
97 label,
98 nodeId,
99 onClick,
100 onLabelClick,
101 onIconClick,
102 onFocus,
103 onKeyDown,
104 onMouseDown,
105 TransitionComponent = Collapse,
106 TransitionProps
107 } = props,
108 other = _objectWithoutPropertiesLoose(props, ["children", "classes", "className", "collapseIcon", "endIcon", "expandIcon", "icon", "label", "nodeId", "onClick", "onLabelClick", "onIconClick", "onFocus", "onKeyDown", "onMouseDown", "TransitionComponent", "TransitionProps"]);
109
110 const {
111 icons: contextIcons,
112 focus,
113 focusFirstNode,
114 focusLastNode,
115 focusNextNode,
116 focusPreviousNode,
117 focusByFirstCharacter,
118 selectNode,
119 selectRange,
120 selectNextNode,
121 selectPreviousNode,
122 rangeSelectToFirst,
123 rangeSelectToLast,
124 selectAllNodes,
125 expandAllSiblings,
126 toggleExpansion,
127 isExpanded,
128 isFocused,
129 isSelected,
130 isTabbable,
131 multiSelect,
132 getParent,
133 mapFirstChar,
134 addNodeToNodeMap,
135 removeNodeFromNodeMap
136 } = React.useContext(TreeViewContext);
137 const nodeRef = React.useRef(null);
138 const contentRef = React.useRef(null);
139 const handleRef = useForkRef(nodeRef, ref);
140 let icon = iconProp;
141 const expandable = Boolean(Array.isArray(children) ? children.length : children);
142 const expanded = isExpanded ? isExpanded(nodeId) : false;
143 const focused = isFocused ? isFocused(nodeId) : false;
144 const tabbable = isTabbable ? isTabbable(nodeId) : false;
145 const selected = isSelected ? isSelected(nodeId) : false;
146 const icons = contextIcons || {};
147 const theme = useTheme();
148
149 if (!icon) {
150 if (expandable) {
151 if (!expanded) {
152 icon = expandIcon || icons.defaultExpandIcon;
153 } else {
154 icon = collapseIcon || icons.defaultCollapseIcon;
155 }
156
157 if (!icon) {
158 icon = icons.defaultParentIcon;
159 }
160 } else {
161 icon = endIcon || icons.defaultEndIcon;
162 }
163 }
164
165 const handleClick = event => {
166 if (!focused) {
167 focus(nodeId);
168 }
169
170 const multiple = multiSelect && (event.shiftKey || event.ctrlKey || event.metaKey); // If already expanded and trying to toggle selection don't close
171
172 if (expandable && !event.defaultPrevented && !(multiple && isExpanded(nodeId))) {
173 toggleExpansion(event, nodeId);
174 }
175
176 if (multiple) {
177 if (event.shiftKey) {
178 selectRange(event, {
179 end: nodeId
180 });
181 } else {
182 selectNode(event, nodeId, true);
183 }
184 } else {
185 selectNode(event, nodeId);
186 }
187
188 if (onClick) {
189 onClick(event);
190 }
191 };
192
193 const handleMouseDown = event => {
194 if (event.shiftKey || event.ctrlKey || event.metaKey) {
195 event.preventDefault();
196 }
197
198 if (onMouseDown) {
199 onMouseDown(event);
200 }
201 };
202
203 const handleNextArrow = event => {
204 if (expandable) {
205 if (expanded) {
206 focusNextNode(nodeId);
207 } else {
208 toggleExpansion(event);
209 }
210 }
211
212 return true;
213 };
214
215 const handlePreviousArrow = event => {
216 if (expanded) {
217 toggleExpansion(event, nodeId);
218 return true;
219 }
220
221 const parent = getParent(nodeId);
222
223 if (parent) {
224 focus(parent);
225 return true;
226 }
227
228 return false;
229 };
230
231 const handleKeyDown = event => {
232 let flag = false;
233 const key = event.key;
234
235 if (event.altKey || event.currentTarget !== event.target) {
236 return;
237 }
238
239 const ctrlPressed = event.ctrlKey || event.metaKey;
240
241 switch (key) {
242 case ' ':
243 if (nodeRef.current === event.currentTarget) {
244 if (multiSelect && event.shiftKey) {
245 flag = selectRange(event, {
246 end: nodeId
247 });
248 } else if (multiSelect) {
249 flag = selectNode(event, nodeId, true);
250 } else {
251 flag = selectNode(event, nodeId);
252 }
253 }
254
255 event.stopPropagation();
256 break;
257
258 case 'Enter':
259 if (nodeRef.current === event.currentTarget && expandable) {
260 toggleExpansion(event);
261 flag = true;
262 }
263
264 event.stopPropagation();
265 break;
266
267 case 'ArrowDown':
268 if (multiSelect && event.shiftKey) {
269 selectNextNode(event, nodeId);
270 }
271
272 focusNextNode(nodeId);
273 flag = true;
274 break;
275
276 case 'ArrowUp':
277 if (multiSelect && event.shiftKey) {
278 selectPreviousNode(event, nodeId);
279 }
280
281 focusPreviousNode(nodeId);
282 flag = true;
283 break;
284
285 case 'ArrowRight':
286 if (theme.direction === 'rtl') {
287 flag = handlePreviousArrow(event);
288 } else {
289 flag = handleNextArrow(event);
290 }
291
292 break;
293
294 case 'ArrowLeft':
295 if (theme.direction === 'rtl') {
296 flag = handleNextArrow(event);
297 } else {
298 flag = handlePreviousArrow(event);
299 }
300
301 break;
302
303 case 'Home':
304 if (multiSelect && ctrlPressed && event.shiftKey) {
305 rangeSelectToFirst(event, nodeId);
306 }
307
308 focusFirstNode();
309 flag = true;
310 break;
311
312 case 'End':
313 if (multiSelect && ctrlPressed && event.shiftKey) {
314 rangeSelectToLast(event, nodeId);
315 }
316
317 focusLastNode();
318 flag = true;
319 break;
320
321 default:
322 if (key === '*') {
323 expandAllSiblings(event, nodeId);
324 flag = true;
325 } else if (multiSelect && ctrlPressed && key.toLowerCase() === 'a') {
326 flag = selectAllNodes(event);
327 } else if (!ctrlPressed && !event.shiftKey && isPrintableCharacter(key)) {
328 focusByFirstCharacter(nodeId, key);
329 flag = true;
330 }
331
332 }
333
334 if (flag) {
335 event.preventDefault();
336 event.stopPropagation();
337 }
338
339 if (onKeyDown) {
340 onKeyDown(event);
341 }
342 };
343
344 const handleFocus = event => {
345 if (!focused && event.currentTarget === event.target) {
346 focus(nodeId);
347 }
348
349 if (onFocus) {
350 onFocus(event);
351 }
352 };
353
354 React.useEffect(() => {
355 if (addNodeToNodeMap) {
356 const childIds = [];
357 React.Children.forEach(children, child => {
358 if ( /*#__PURE__*/React.isValidElement(child) && child.props.nodeId) {
359 childIds.push(child.props.nodeId);
360 }
361 });
362 addNodeToNodeMap(nodeId, childIds);
363 }
364 }, [children, nodeId, addNodeToNodeMap]);
365 React.useEffect(() => {
366 if (removeNodeFromNodeMap) {
367 return () => {
368 removeNodeFromNodeMap(nodeId);
369 };
370 }
371
372 return undefined;
373 }, [nodeId, removeNodeFromNodeMap]);
374 React.useEffect(() => {
375 if (mapFirstChar && label) {
376 mapFirstChar(nodeId, contentRef.current.textContent.substring(0, 1).toLowerCase());
377 }
378 }, [mapFirstChar, nodeId, label]);
379 React.useEffect(() => {
380 if (focused) {
381 nodeRef.current.focus();
382 }
383 }, [focused]);
384 let ariaSelected;
385
386 if (multiSelect) {
387 ariaSelected = selected;
388 } else if (selected) {
389 // single-selection trees unset aria-selected
390 ariaSelected = true;
391 }
392
393 return /*#__PURE__*/React.createElement("li", _extends({
394 className: clsx(classes.root, className, expanded && classes.expanded, selected && classes.selected),
395 role: "treeitem",
396 onKeyDown: handleKeyDown,
397 onFocus: handleFocus,
398 "aria-expanded": expandable ? expanded : null,
399 "aria-selected": ariaSelected,
400 ref: handleRef,
401 tabIndex: tabbable ? 0 : -1
402 }, other), /*#__PURE__*/React.createElement("div", {
403 className: classes.content,
404 onClick: handleClick,
405 onMouseDown: handleMouseDown,
406 ref: contentRef
407 }, /*#__PURE__*/React.createElement("div", {
408 onClick: onIconClick,
409 className: classes.iconContainer
410 }, icon), /*#__PURE__*/React.createElement(Typography, {
411 onClick: onLabelClick,
412 component: "div",
413 className: classes.label
414 }, label)), children && /*#__PURE__*/React.createElement(TransitionComponent, _extends({
415 unmountOnExit: true,
416 className: classes.group,
417 in: expanded,
418 component: "ul",
419 role: "group"
420 }, TransitionProps), children));
421});
422process.env.NODE_ENV !== "production" ? TreeItem.propTypes = {
423 // ----------------------------- Warning --------------------------------
424 // | These PropTypes are generated from the TypeScript type definitions |
425 // | To update them edit the d.ts file and run "yarn proptypes" |
426 // ----------------------------------------------------------------------
427
428 /**
429 * The content of the component.
430 */
431 children: PropTypes.node,
432
433 /**
434 * Override or extend the styles applied to the component.
435 * See [CSS API](#css) below for more details.
436 */
437 classes: PropTypes.object,
438
439 /**
440 * @ignore
441 */
442 className: PropTypes.string,
443
444 /**
445 * The icon used to collapse the node.
446 */
447 collapseIcon: PropTypes.node,
448
449 /**
450 * The icon displayed next to a end node.
451 */
452 endIcon: PropTypes.node,
453
454 /**
455 * The icon used to expand the node.
456 */
457 expandIcon: PropTypes.node,
458
459 /**
460 * The icon to display next to the tree node's label.
461 */
462 icon: PropTypes.node,
463
464 /**
465 * The tree node label.
466 */
467 label: PropTypes.node,
468
469 /**
470 * The id of the node.
471 */
472 nodeId: PropTypes.string.isRequired,
473
474 /**
475 * @ignore
476 */
477 onClick: PropTypes.func,
478
479 /**
480 * @ignore
481 */
482 onFocus: PropTypes.func,
483
484 /**
485 * `onClick` handler for the icon container. Call `event.preventDefault()` to prevent `onNodeToggle` from being called.
486 */
487 onIconClick: PropTypes.func,
488
489 /**
490 * @ignore
491 */
492 onKeyDown: PropTypes.func,
493
494 /**
495 * `onClick` handler for the label container. Call `event.preventDefault()` to prevent `onNodeToggle` from being called.
496 */
497 onLabelClick: PropTypes.func,
498
499 /**
500 * @ignore
501 */
502 onMouseDown: PropTypes.func,
503
504 /**
505 * The component used for the transition.
506 * [Follow this guide](/components/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
507 */
508 TransitionComponent: PropTypes.elementType,
509
510 /**
511 * Props applied to the [`Transition`](http://reactcommunity.org/react-transition-group/transition#Transition-props) element.
512 */
513 TransitionProps: PropTypes.object
514} : void 0;
515export default withStyles(styles, {
516 name: 'MuiTreeItem'
517})(TreeItem);
\No newline at end of file