UNPKG

@material-ui/lab

Version:

Material-UI Lab - Incubator for Material-UI React components.

517 lines (446 loc) 12.9 kB
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);