material-ui
Version:
Material Design UI components built with React
426 lines (367 loc) • 13.2 kB
JSX
let React = require('react/addons');
let ColorManipulator = require('../utils/color-manipulator');
let StylePropable = require('../mixins/style-propable');
let Colors = require('../styles/colors');
let Transitions = require('../styles/transitions');
let Typography = require('../styles/typography');
let EnhancedButton = require('../enhanced-button');
let IconButton = require('../icon-button');
let OpenIcon = require('../svg-icons/navigation/arrow-drop-up');
let CloseIcon = require('../svg-icons/navigation/arrow-drop-down');
let ListNested = require('./list-nested');
let ListItem = React.createClass({
mixins: [StylePropable],
contextTypes: {
muiTheme: React.PropTypes.object,
},
propTypes: {
autoGenerateNestedIndicator: React.PropTypes.bool,
disabled: React.PropTypes.bool,
disableKeyboardFocus: React.PropTypes.bool,
innerDivStyle: React.PropTypes.object,
insetChildren: React.PropTypes.bool,
innerStyle: React.PropTypes.object,
leftAvatar: React.PropTypes.element,
leftCheckbox: React.PropTypes.element,
leftIcon: React.PropTypes.element,
nestedLevel: React.PropTypes.number,
onKeyboardFocus: React.PropTypes.func,
onMouseOut: React.PropTypes.func,
onMouseOver: React.PropTypes.func,
onNestedListToggle: React.PropTypes.func,
onTouchStart: React.PropTypes.func,
open: React.PropTypes.bool,
rightAvatar: React.PropTypes.element,
rightIcon: React.PropTypes.element,
rightIconButton: React.PropTypes.element,
rightToggle: React.PropTypes.element,
primaryText: React.PropTypes.node,
secondaryText: React.PropTypes.node,
secondaryTextLines: React.PropTypes.oneOf([1, 2]),
},
getDefaultProps() {
return {
autoGenerateNestedIndicator: true,
nestedLevel: 0,
open: false,
secondaryTextLines: 1,
};
},
getInitialState() {
return {
hovered: false,
isKeyboardFocused: false,
open: this.props.open,
rightIconButtonHovered: false,
rightIconButtonKeyboardFocused: false,
touch: false,
};
},
render() {
let {
autoGenerateNestedIndicator,
disabled,
disableKeyboardFocus,
innerDivStyle,
insetChildren,
leftAvatar,
leftCheckbox,
leftIcon,
nestedLevel,
onKeyboardFocus,
onMouseOut,
onMouseOver,
onTouchStart,
rightAvatar,
rightIcon,
rightIconButton,
rightToggle,
primaryText,
secondaryText,
secondaryTextLines,
style,
...other
} = this.props;
let textColor = this.context.muiTheme.palette.textColor;
let hoverColor = ColorManipulator.fade(textColor, 0.1);
let singleAvatar = !secondaryText && (leftAvatar || rightAvatar);
let singleNoAvatar = !secondaryText && !(leftAvatar || rightAvatar);
let twoLine = secondaryText && secondaryTextLines === 1;
let threeLine = secondaryText && secondaryTextLines > 1;
let hasCheckbox = leftCheckbox || rightToggle;
let styles = {
root: {
backgroundColor: (this.state.isKeyboardFocused || this.state.hovered) &&
!this.state.rightIconButtonHovered &&
!this.state.rightIconButtonKeyboardFocused ? hoverColor : null,
color: textColor,
display: 'block',
fontSize: 16,
lineHeight: '16px',
position: 'relative',
transition: Transitions.easeOut(),
},
//This inner div is needed so that ripples will span the entire container
innerDiv: {
marginLeft: nestedLevel * this.context.muiTheme.component.listItem.nestedLevelDepth,
paddingLeft: leftIcon || leftAvatar || leftCheckbox || insetChildren ? 72 : 16,
paddingRight: rightIcon || rightAvatar || rightIconButton ? 56 : rightToggle ? 72 : 16,
paddingBottom: singleAvatar ? 20 : 16,
paddingTop: singleNoAvatar || threeLine ? 16 : 20,
position: 'relative',
},
label: {
cursor: 'pointer',
},
icons: {
height: 24,
width: 24,
display: 'block',
position: 'absolute',
top: twoLine ? 12 : singleAvatar ? 4 : 0,
padding: 12,
},
leftIcon: {
color: Colors.grey600,
fill: Colors.grey600,
left: 4,
},
rightIcon: {
color: Colors.grey400,
fill: Colors.grey400,
right: 4,
},
avatars: {
position: 'absolute',
top: singleAvatar ? 8 : 16,
},
leftAvatar: {
left: 16,
},
rightAvatar: {
right: 16,
},
leftCheckbox: {
position: 'absolute',
display: 'block',
width: 24,
top: twoLine ? 24 : singleAvatar ? 16 : 12,
left: 16,
},
primaryText: {
margin: 0,
},
rightIconButton: {
position: 'absolute',
display: 'block',
top: twoLine ? 12 : singleAvatar ? 4 : 0,
right: 4,
},
rightToggle: {
position: 'absolute',
display: 'block',
width: 54,
top: twoLine ? 25 : singleAvatar ? 17 : 13,
right: 8,
},
secondaryText: {
fontSize: 14,
lineHeight: threeLine ? '18px' : '16px',
height: threeLine ? 36 : 16,
margin: 0,
marginTop: 4,
color: Typography.textLightBlack,
//needed for 2 and 3 line ellipsis
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: threeLine ? null : 'nowrap',
display: threeLine ? '-webkit-box' : null,
WebkitLineClamp: threeLine ? 2 : null,
WebkitBoxOrient: threeLine ? 'vertical' : null,
},
};
let primaryTextIsAnElement = React.isValidElement(primaryText);
let secondaryTextIsAnElement = React.isValidElement(secondaryText);
let mergedRootStyles = this.mergeAndPrefix(styles.root, style);
let mergedInnerDivStyles = this.mergeAndPrefix(styles.innerDiv, innerDivStyle);
let mergedDivStyles = this.mergeAndPrefix(styles.root, mergedInnerDivStyles, style);
let mergedLabelStyles = this.mergeAndPrefix(styles.root, mergedInnerDivStyles, styles.label, style);
let mergedPrimaryTextStyles = primaryTextIsAnElement ?
this.mergeStyles(styles.primaryText, primaryText.props.style) : null;
let mergedSecondaryTextStyles = secondaryTextIsAnElement ?
this.mergeStyles(styles.secondaryText, secondaryText.props.style) : null;
let contentChildren = [];
let nestedListItems = [];
let nestedList;
React.Children.forEach(this.props.children, (child) => {
if (child === null) return;
if (React.isValidElement(child) && child.type.displayName === 'ListItem') {
nestedListItems.push(child);
}
else {
contentChildren.push(child);
}
});
let rightIconButtonHandlers = {
onKeyboardFocus: this._handleRightIconButtonKeyboardFocus,
onMouseOver: this._handleRightIconButtonMouseOver,
onMouseOut: this._handleRightIconButtonMouseOut,
onTouchTap: this._handleRightIconButtonTouchTap,
onMouseDown: this._handleRightIconButtonMouseUp,
onMouseUp: this._handleRightIconButtonMouseUp,
};
// Create a nested list indicator icon if we don't have an icon on the right
if (nestedListItems.length > 0 &&
autoGenerateNestedIndicator &&
rightIcon === undefined &&
rightAvatar === undefined &&
rightIconButton === undefined) {
if (this.state.open) {
rightIconButton = <IconButton><OpenIcon /></IconButton>;
}
else {
rightIconButton = <IconButton><CloseIcon /></IconButton>;
}
rightIconButtonHandlers.onTouchTap = this._handleNestedListToggle;
}
this._pushElement(contentChildren, leftIcon, this.mergeStyles(styles.icons, styles.leftIcon));
this._pushElement(contentChildren, rightIcon, this.mergeStyles(styles.icons, styles.rightIcon));
this._pushElement(contentChildren, leftAvatar, this.mergeStyles(styles.avatars, styles.leftAvatar));
this._pushElement(contentChildren, rightAvatar, this.mergeStyles(styles.avatars, styles.rightAvatar));
this._pushElement(contentChildren, leftCheckbox, this.mergeStyles(styles.leftCheckbox));
this._pushElement(contentChildren, rightIconButton, this.mergeStyles(styles.rightIconButton), rightIconButtonHandlers);
this._pushElement(contentChildren, rightToggle, this.mergeStyles(styles.rightToggle));
if (nestedListItems.length) {
nestedList = (
<ListNested nestedLevel={nestedLevel + 1} open={this.state.open}>
{nestedListItems}
</ListNested>
);
}
if (primaryText) {
contentChildren.push(
React.isValidElement(primaryText) ?
React.cloneElement(primaryText, {key: 'primaryText', style: mergedPrimaryTextStyles}) :
<div key="primaryText" style={styles.primaryText}>{primaryText}</div>
);
}
if (secondaryText) {
contentChildren.push(
React.isValidElement(secondaryText) ?
React.cloneElement(secondaryText, {key: 'secondaryText', style: mergedSecondaryTextStyles}) :
<div key="secondaryText" style={styles.secondaryText}>{secondaryText}</div>
);
}
return hasCheckbox || disabled ?
React.createElement(
hasCheckbox ? 'label' : 'div',
{ style: hasCheckbox ? mergedLabelStyles : mergedDivStyles },
contentChildren
) : (
<div>
<EnhancedButton
{...other}
disabled={disabled}
disableKeyboardFocus={disableKeyboardFocus || this.state.rightIconButtonKeyboardFocused}
linkButton={true}
onKeyboardFocus={this._handleKeyboardFocus}
onMouseOut={this._handleMouseOut}
onMouseOver={this._handleMouseOver}
onTouchStart={this._handleTouchStart}
ref="enhancedButton"
style={mergedRootStyles}>
<div style={mergedInnerDivStyles}>
{contentChildren}
</div>
</EnhancedButton>
{nestedList}
</div>
);
},
applyFocusState(focusState) {
let button = this.refs.enhancedButton;
let buttonEl = React.findDOMNode(button);
if (button) {
switch(focusState) {
case 'none':
buttonEl.blur();
break;
case 'focused':
buttonEl.focus();
break;
case 'keyboard-focused':
button.setKeyboardFocus();
buttonEl.focus();
break;
}
}
},
_pushElement(children, element, baseStyles, additionalProps) {
if (element) {
let styles = this.mergeStyles(baseStyles, element.props.style);
children.push(
React.cloneElement(element, {
key: children.length,
style: styles,
...additionalProps,
})
);
}
},
_handleKeyboardFocus(e, isKeyboardFocused) {
this.setState({isKeyboardFocused: isKeyboardFocused});
if (this.props.onKeyboardFocus) this.props.onKeyboardFocus(e, isKeyboardFocused);
},
_handleMouseOver(e) {
if (!this.state.touch) this.setState({hovered: true});
if (this.props.onMouseOver) this.props.onMouseOver(e);
},
_handleMouseOut(e) {
this.setState({hovered: false});
if (this.props.onMouseOut) this.props.onMouseOut(e);
},
_handleRightIconButtonKeyboardFocus(e, isKeyboardFocused) {
let iconButton = this.props.rightIconButton;
let newState = {};
newState.rightIconButtonKeyboardFocused = isKeyboardFocused;
if (isKeyboardFocused) newState.isKeyboardFocused = false;
this.setState(newState);
if (iconButton && iconButton.props.onKeyboardFocus) iconButton.props.onKeyboardFocus(e, isKeyboardFocused);
},
_handleRightIconButtonMouseDown(e) {
let iconButton = this.props.rightIconButton;
e.stopPropagation();
if (iconButton && iconButton.props.onMouseDown) iconButton.props.onMouseDown(e);
},
_handleRightIconButtonMouseOut(e) {
let iconButton = this.props.rightIconButton;
this.setState({rightIconButtonHovered: false});
if (iconButton && iconButton.props.onMouseOut) iconButton.props.onMouseOut(e);
},
_handleRightIconButtonMouseOver(e) {
let iconButton = this.props.rightIconButton;
this.setState({rightIconButtonHovered: true});
if (iconButton && iconButton.props.onMouseOver) iconButton.props.onMouseOver(e);
},
_handleRightIconButtonMouseUp(e) {
let iconButton = this.props.rightIconButton;
e.stopPropagation();
if (iconButton && iconButton.props.onMouseUp) iconButton.props.onMouseUp(e);
},
_handleRightIconButtonTouchTap(e) {
let iconButton = this.props.rightIconButton;
//Stop the event from bubbling up to the list-item
e.stopPropagation();
if (iconButton && iconButton.props.onTouchTap) iconButton.props.onTouchTap(e);
},
_handleTouchStart(e) {
this.setState({touch: true});
if (this.props.onTouchStart) this.props.onTouchStart(e);
},
_handleNestedListToggle(e) {
e.stopPropagation();
this.setState({open : !this.state.open});
if (this.props.onNestedListToggle) this.props.onNestedListToggle(this);
},
});
module.exports = ListItem;