@material-ui/lab
Version:
Material-UI Lab - Incubator for Material-UI React components.
517 lines (446 loc) • 12.9 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
import * as React from 'react';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import Typography from '@material-ui/core/Typography';
import Collapse from '@material-ui/core/Collapse';
import { alpha, withStyles, useTheme } from '@material-ui/core/styles';
import { useForkRef } from '@material-ui/core/utils';
import TreeViewContext from '../TreeView/TreeViewContext';
export const styles = theme => ({
/* Styles applied to the root element. */
root: {
listStyle: 'none',
margin: 0,
padding: 0,
outline: 0,
WebkitTapHighlightColor: 'transparent',
'&:focus > $content $label': {
backgroundColor: theme.palette.action.hover
},
'&$selected > $content $label': {
backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity)
},
'&$selected > $content $label:hover, &$selected:focus > $content $label': {
backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity),
// Reset on touch devices, it doesn't add specificity
'@media (hover: none)': {
backgroundColor: 'transparent'
}
}
},
/* Pseudo-class applied to the root element when expanded. */
expanded: {},
/* Pseudo-class applied to the root element when selected. */
selected: {},
/* Styles applied to the `role="group"` element. */
group: {
margin: 0,
padding: 0,
marginLeft: 17
},
/* Styles applied to the tree node content. */
content: {
width: '100%',
display: 'flex',
alignItems: 'center',
cursor: 'pointer'
},
/* Styles applied to the tree node icon and collapse/expand icon. */
iconContainer: {
marginRight: 4,
width: 15,
display: 'flex',
flexShrink: 0,
justifyContent: 'center',
'& svg': {
fontSize: 18
}
},
/* Styles applied to the label element. */
label: {
width: '100%',
paddingLeft: 4,
position: 'relative',
'&:hover': {
backgroundColor: theme.palette.action.hover,
// Reset on touch devices, it doesn't add specificity
'@media (hover: none)': {
backgroundColor: 'transparent'
}
}
}
});
const isPrintableCharacter = str => {
return str && str.length === 1 && str.match(/\S/);
};
const TreeItem = /*#__PURE__*/React.forwardRef(function TreeItem(props, ref) {
const {
children,
classes,
className,
collapseIcon,
endIcon,
expandIcon,
icon: iconProp,
label,
nodeId,
onClick,
onLabelClick,
onIconClick,
onFocus,
onKeyDown,
onMouseDown,
TransitionComponent = Collapse,
TransitionProps
} = props,
other = _objectWithoutPropertiesLoose(props, ["children", "classes", "className", "collapseIcon", "endIcon", "expandIcon", "icon", "label", "nodeId", "onClick", "onLabelClick", "onIconClick", "onFocus", "onKeyDown", "onMouseDown", "TransitionComponent", "TransitionProps"]);
const {
icons: contextIcons,
focus,
focusFirstNode,
focusLastNode,
focusNextNode,
focusPreviousNode,
focusByFirstCharacter,
selectNode,
selectRange,
selectNextNode,
selectPreviousNode,
rangeSelectToFirst,
rangeSelectToLast,
selectAllNodes,
expandAllSiblings,
toggleExpansion,
isExpanded,
isFocused,
isSelected,
isTabbable,
multiSelect,
getParent,
mapFirstChar,
addNodeToNodeMap,
removeNodeFromNodeMap
} = React.useContext(TreeViewContext);
const nodeRef = React.useRef(null);
const contentRef = React.useRef(null);
const handleRef = useForkRef(nodeRef, ref);
let icon = iconProp;
const expandable = Boolean(Array.isArray(children) ? children.length : children);
const expanded = isExpanded ? isExpanded(nodeId) : false;
const focused = isFocused ? isFocused(nodeId) : false;
const tabbable = isTabbable ? isTabbable(nodeId) : false;
const selected = isSelected ? isSelected(nodeId) : false;
const icons = contextIcons || {};
const theme = useTheme();
if (!icon) {
if (expandable) {
if (!expanded) {
icon = expandIcon || icons.defaultExpandIcon;
} else {
icon = collapseIcon || icons.defaultCollapseIcon;
}
if (!icon) {
icon = icons.defaultParentIcon;
}
} else {
icon = endIcon || icons.defaultEndIcon;
}
}
const handleClick = event => {
if (!focused) {
focus(nodeId);
}
const multiple = multiSelect && (event.shiftKey || event.ctrlKey || event.metaKey); // If already expanded and trying to toggle selection don't close
if (expandable && !event.defaultPrevented && !(multiple && isExpanded(nodeId))) {
toggleExpansion(event, nodeId);
}
if (multiple) {
if (event.shiftKey) {
selectRange(event, {
end: nodeId
});
} else {
selectNode(event, nodeId, true);
}
} else {
selectNode(event, nodeId);
}
if (onClick) {
onClick(event);
}
};
const handleMouseDown = event => {
if (event.shiftKey || event.ctrlKey || event.metaKey) {
event.preventDefault();
}
if (onMouseDown) {
onMouseDown(event);
}
};
const handleNextArrow = event => {
if (expandable) {
if (expanded) {
focusNextNode(nodeId);
} else {
toggleExpansion(event);
}
}
return true;
};
const handlePreviousArrow = event => {
if (expanded) {
toggleExpansion(event, nodeId);
return true;
}
const parent = getParent(nodeId);
if (parent) {
focus(parent);
return true;
}
return false;
};
const handleKeyDown = event => {
let flag = false;
const key = event.key;
if (event.altKey || event.currentTarget !== event.target) {
return;
}
const ctrlPressed = event.ctrlKey || event.metaKey;
switch (key) {
case ' ':
if (nodeRef.current === event.currentTarget) {
if (multiSelect && event.shiftKey) {
flag = selectRange(event, {
end: nodeId
});
} else if (multiSelect) {
flag = selectNode(event, nodeId, true);
} else {
flag = selectNode(event, nodeId);
}
}
event.stopPropagation();
break;
case 'Enter':
if (nodeRef.current === event.currentTarget && expandable) {
toggleExpansion(event);
flag = true;
}
event.stopPropagation();
break;
case 'ArrowDown':
if (multiSelect && event.shiftKey) {
selectNextNode(event, nodeId);
}
focusNextNode(nodeId);
flag = true;
break;
case 'ArrowUp':
if (multiSelect && event.shiftKey) {
selectPreviousNode(event, nodeId);
}
focusPreviousNode(nodeId);
flag = true;
break;
case 'ArrowRight':
if (theme.direction === 'rtl') {
flag = handlePreviousArrow(event);
} else {
flag = handleNextArrow(event);
}
break;
case 'ArrowLeft':
if (theme.direction === 'rtl') {
flag = handleNextArrow(event);
} else {
flag = handlePreviousArrow(event);
}
break;
case 'Home':
if (multiSelect && ctrlPressed && event.shiftKey) {
rangeSelectToFirst(event, nodeId);
}
focusFirstNode();
flag = true;
break;
case 'End':
if (multiSelect && ctrlPressed && event.shiftKey) {
rangeSelectToLast(event, nodeId);
}
focusLastNode();
flag = true;
break;
default:
if (key === '*') {
expandAllSiblings(event, nodeId);
flag = true;
} else if (multiSelect && ctrlPressed && key.toLowerCase() === 'a') {
flag = selectAllNodes(event);
} else if (!ctrlPressed && !event.shiftKey && isPrintableCharacter(key)) {
focusByFirstCharacter(nodeId, key);
flag = true;
}
}
if (flag) {
event.preventDefault();
event.stopPropagation();
}
if (onKeyDown) {
onKeyDown(event);
}
};
const handleFocus = event => {
if (!focused && event.currentTarget === event.target) {
focus(nodeId);
}
if (onFocus) {
onFocus(event);
}
};
React.useEffect(() => {
if (addNodeToNodeMap) {
const childIds = [];
React.Children.forEach(children, child => {
if ( /*#__PURE__*/React.isValidElement(child) && child.props.nodeId) {
childIds.push(child.props.nodeId);
}
});
addNodeToNodeMap(nodeId, childIds);
}
}, [children, nodeId, addNodeToNodeMap]);
React.useEffect(() => {
if (removeNodeFromNodeMap) {
return () => {
removeNodeFromNodeMap(nodeId);
};
}
return undefined;
}, [nodeId, removeNodeFromNodeMap]);
React.useEffect(() => {
if (mapFirstChar && label) {
mapFirstChar(nodeId, contentRef.current.textContent.substring(0, 1).toLowerCase());
}
}, [mapFirstChar, nodeId, label]);
React.useEffect(() => {
if (focused) {
nodeRef.current.focus();
}
}, [focused]);
let ariaSelected;
if (multiSelect) {
ariaSelected = selected;
} else if (selected) {
// single-selection trees unset aria-selected
ariaSelected = true;
}
return /*#__PURE__*/React.createElement("li", _extends({
className: clsx(classes.root, className, expanded && classes.expanded, selected && classes.selected),
role: "treeitem",
onKeyDown: handleKeyDown,
onFocus: handleFocus,
"aria-expanded": expandable ? expanded : null,
"aria-selected": ariaSelected,
ref: handleRef,
tabIndex: tabbable ? 0 : -1
}, other), /*#__PURE__*/React.createElement("div", {
className: classes.content,
onClick: handleClick,
onMouseDown: handleMouseDown,
ref: contentRef
}, /*#__PURE__*/React.createElement("div", {
onClick: onIconClick,
className: classes.iconContainer
}, icon), /*#__PURE__*/React.createElement(Typography, {
onClick: onLabelClick,
component: "div",
className: classes.label
}, label)), children && /*#__PURE__*/React.createElement(TransitionComponent, _extends({
unmountOnExit: true,
className: classes.group,
in: expanded,
component: "ul",
role: "group"
}, TransitionProps), children));
});
process.env.NODE_ENV !== "production" ? TreeItem.propTypes = {
// ----------------------------- Warning --------------------------------
// | These PropTypes are generated from the TypeScript type definitions |
// | To update them edit the d.ts file and run "yarn proptypes" |
// ----------------------------------------------------------------------
/**
* The content of the component.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
* See [CSS API](#css) below for more details.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* The icon used to collapse the node.
*/
collapseIcon: PropTypes.node,
/**
* The icon displayed next to a end node.
*/
endIcon: PropTypes.node,
/**
* The icon used to expand the node.
*/
expandIcon: PropTypes.node,
/**
* The icon to display next to the tree node's label.
*/
icon: PropTypes.node,
/**
* The tree node label.
*/
label: PropTypes.node,
/**
* The id of the node.
*/
nodeId: PropTypes.string.isRequired,
/**
* @ignore
*/
onClick: PropTypes.func,
/**
* @ignore
*/
onFocus: PropTypes.func,
/**
* `onClick` handler for the icon container. Call `event.preventDefault()` to prevent `onNodeToggle` from being called.
*/
onIconClick: PropTypes.func,
/**
* @ignore
*/
onKeyDown: PropTypes.func,
/**
* `onClick` handler for the label container. Call `event.preventDefault()` to prevent `onNodeToggle` from being called.
*/
onLabelClick: PropTypes.func,
/**
* @ignore
*/
onMouseDown: PropTypes.func,
/**
* The component used for the transition.
* [Follow this guide](/components/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
*/
TransitionComponent: PropTypes.elementType,
/**
* Props applied to the [`Transition`](http://reactcommunity.org/react-transition-group/transition#Transition-props) element.
*/
TransitionProps: PropTypes.object
} : void 0;
export default withStyles(styles, {
name: 'MuiTreeItem'
})(TreeItem);