diff --git a/components/font_icon/FontIcon.jsx b/components/font_icon/FontIcon.jsx index 501ffac5..b68dc7eb 100644 --- a/components/font_icon/FontIcon.jsx +++ b/components/font_icon/FontIcon.jsx @@ -7,7 +7,7 @@ const FontIcon = ({ children, className, value, ...other}) => { className ); return ( - + {value} {children} diff --git a/components/list/ListItem.jsx b/components/list/ListItem.jsx index 897ad34f..afaa4f00 100644 --- a/components/list/ListItem.jsx +++ b/components/list/ListItem.jsx @@ -1,30 +1,21 @@ import React from 'react'; -import ClassNames from 'classnames'; -import FontIcon from '../font_icon'; import ListItemContent from './ListItemContent'; +import ListItemLayout from './ListItemLayout'; import Ripple from '../ripple'; import style from './style'; class ListItem extends React.Component { static propTypes = { - avatar: React.PropTypes.string, - caption: React.PropTypes.string.isRequired, children: React.PropTypes.any, - className: React.PropTypes.string, disabled: React.PropTypes.bool, - leftIcon: React.PropTypes.any, - legend: React.PropTypes.string, onClick: React.PropTypes.func, - rightIcon: React.PropTypes.any, ripple: React.PropTypes.bool, - selectable: React.PropTypes.bool, to: React.PropTypes.string }; static defaultProps = { disabled: false, - ripple: false, - selectable: false + ripple: false }; handleClick = (event) => { @@ -33,28 +24,41 @@ class ListItem extends React.Component { } }; - renderContent () { - const className = ClassNames(style.item, { - [style.withLegend]: this.props.legend, - [style.disabled]: this.props.disabled, - [style.selectable]: this.props.selectable - }, this.props.className); + groupChildren () { + const children = { + leftActions: [], + rightActions: [], + ignored: [] + }; - return ( - - {this.props.leftIcon ? : null} - {this.props.avatar ? : null} - - {this.props.rightIcon ? : null} - - ); + React.Children.forEach(this.props.children, (child, i) => { + if (!React.isValidElement(child)) { + return; + } + if (child.props.listItemIgnore) { + children.ignored.push(child); + return; + } + if (child.type === ListItemContent) { + children.itemContent = child; + return; + } + const bucket = children.itemContent ? 'rightActions' : 'leftActions'; + children[bucket].push({...child, key: i}); + }); + + return children; } render () { + const {onMouseDown, to, onClick, ripple, ...other} = this.props; + const children = this.groupChildren(); + const content = ; + return ( -
  • - {this.props.to ? {this.renderContent()} : this.renderContent()} - {this.props.children} +
  • + {to ? {content} : content} + {children.ignored}
  • ); } @@ -62,6 +66,7 @@ class ListItem extends React.Component { export default Ripple({ className: style.ripple, - centered: false + centered: false, + listItemIgnore: true })(ListItem); export {ListItem as RawListItem}; diff --git a/components/list/ListItemAction.jsx b/components/list/ListItemAction.jsx new file mode 100644 index 00000000..82388b26 --- /dev/null +++ b/components/list/ListItemAction.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import style from './style'; + +const ListItemAction = ({action}) => { + const {onClick, onMouseDown} = action.props; + const stopRipple = onClick && !onMouseDown; + const stop = e => e.stopPropagation(); + return ( + + {action} + + ); +}; + +ListItemAction.propTypes = { +}; + +ListItemAction.defaultProps = { +}; + +export default ListItemAction; diff --git a/components/list/ListItemActions.jsx b/components/list/ListItemActions.jsx new file mode 100644 index 00000000..d6df1aff --- /dev/null +++ b/components/list/ListItemActions.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import style from './style'; +import ListItemAction from './ListItemAction'; + +const ListItemActions = ({type, children}) => { + const validChildren = React.Children.toArray(children).filter(c => ( + React.isValidElement(c) + )); + + return ( + + {validChildren.map((action, i) => )} + + ); +}; + +ListItemActions.propTypes = { + children: React.PropTypes.any, + type: React.PropTypes.oneOf(['left', 'right']) +}; + +export default ListItemActions; diff --git a/components/list/ListItemContent.jsx b/components/list/ListItemContent.jsx index 6fdd5cf5..35db9b0f 100644 --- a/components/list/ListItemContent.jsx +++ b/components/list/ListItemContent.jsx @@ -1,16 +1,41 @@ import React from 'react'; import style from './style'; +import ListItemText from './ListItemText'; -const ListItemContent = ({caption, legend}) => ( - - {caption} - {legend} - -); +const types = ['auto', 'normal', 'large']; -ListItemContent.propTypes = { - caption: React.PropTypes.string.isRequired, - legend: React.PropTypes.any -}; +class ListItemContent extends React.Component { + static propTypes = { + caption: React.PropTypes.string, + children: React.PropTypes.any, + legend: React.PropTypes.string, + type: React.PropTypes.oneOf(types) + }; + type () { + const {type, children, caption, legend} = this.props; + + let count = React.Children.count(children); + [caption, legend].forEach(s => count += s ? 1 : 0); + const typeIndex = Math.min(count, types.length); + + return type || types[typeIndex]; + } + + render () { + const {children, caption, legend} = this.props; + + const className = `${style.itemContentRoot} ${style[this.type()]}`; + + return ( + + + {caption && {caption}} + {legend && {legend}} + {children} + + + ); + } +} export default ListItemContent; diff --git a/components/list/ListItemLayout.jsx b/components/list/ListItemLayout.jsx new file mode 100644 index 00000000..db043895 --- /dev/null +++ b/components/list/ListItemLayout.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import ClassNames from 'classnames'; +import FontIcon from '../font_icon'; +import Avatar from '../avatar'; +import ListItemContent from './ListItemContent'; +import ListItemActions from './ListItemActions'; +import style from './style'; + +const ListItemLayout = (props) => { + const className = ClassNames(style.item, { + [style.disabled]: props.disabled, + [style.selectable]: props.selectable + }, props.className); + + const leftActions = [ + props.leftIcon && , + props.avatar && , + ...props.leftActions + ]; + const rightActions = [ + props.rightIcon && , + ...props.rightActions + ]; + const content = props.itemContent || ; + + return ( + + {leftActions} + {content} + {rightActions} + + ); +}; + +ListItemLayout.propTypes = { + avatar: React.PropTypes.string, + caption: React.PropTypes.string, + children: React.PropTypes.any, + className: React.PropTypes.string, + disabled: React.PropTypes.bool, + itemContent: React.PropTypes.element, + leftActions: React.PropTypes.array, + leftIcon: React.PropTypes.any, + legend: React.PropTypes.string, + rightActions: React.PropTypes.array, + rightIcon: React.PropTypes.any, + selectable: React.PropTypes.bool, + to: React.PropTypes.string +}; + +ListItemLayout.defaultProps = { + disabled: false, + selectable: false +}; + +export default ListItemLayout; diff --git a/components/list/ListItemText.jsx b/components/list/ListItemText.jsx new file mode 100644 index 00000000..6e240a57 --- /dev/null +++ b/components/list/ListItemText.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import ClassNames from 'classnames'; +import style from './style'; + +const ListItemText = ({className, primary, children, ...other}) => { + const _className = ClassNames(style.itemText, {[style.primary]: primary}, className); + + return ( + + {children} + + ); +}; + +ListItemText.propTypes = { + children: React.PropTypes.any, + className: React.PropTypes.string, + primary: React.PropTypes.bool +}; + +ListItemText.defaultProps = { + primary: false +}; + +export default ListItemText; diff --git a/components/list/_config.scss b/components/list/_config.scss index 49c57c8f..bb760414 100644 --- a/components/list/_config.scss +++ b/components/list/_config.scss @@ -11,6 +11,8 @@ $list-item-hover-color: $palette-grey-200; $list-item-legend-margin-top: .3 * $unit; $list-item-icon-font-size: 2.4 * $unit; $list-item-icon-size: 1.8 * $unit; -$list-item-right-icon-margin: $list-content-left-spacing - $list-horizontal-padding - $list-item-icon-size; +$list-item-right-icon-margin: $list-content-left-spacing - 2 * $list-horizontal-padding - $list-item-icon-size; +$list-item-right-checkbox-margin: $list-item-right-icon-margin + $list-horizontal-padding; $list-item-avatar-height: 4 * $unit; $list-item-avatar-margin: .8 * $unit; +$list-item-child-margin: .8 * $unit; diff --git a/components/list/index.js b/components/list/index.js index 371643cf..38d0ddaf 100644 --- a/components/list/index.js +++ b/components/list/index.js @@ -3,3 +3,5 @@ export ListItem from './ListItem'; export ListDivider from './ListDivider'; export ListCheckbox from './ListCheckbox'; export ListSubHeader from './ListSubHeader'; +export ListItemText from './ListItemText'; +export ListItemContent from './ListItemContent'; diff --git a/components/list/style.scss b/components/list/style.scss index bef91ca8..7f03ebeb 100644 --- a/components/list/style.scss +++ b/components/list/style.scss @@ -56,9 +56,6 @@ cursor: pointer; background-color: $list-item-hover-color; } - &.withLegend { - height: $list-item-min-height-legend; - } &.disabled { pointer-events: none; &:not(.checkboxItem) { @@ -70,15 +67,74 @@ } } +.left { + [data-react-toolbox='font-icon'] { + width: $list-item-icon-size; + } + & :last-child { + > [data-react-toolbox='font-icon'] { + margin-right: $list-item-right-icon-margin; + } + } +} + +.right { + > :last-child { + margin-right: 0; + } + + > :first-child { + margin-left: $list-horizontal-padding; + } +} + +.left, .right { + display: flex; + flex: 0 0 auto; + align-items: center; + vertical-align: middle; +} + +.itemAction { + display: flex; + margin: $list-item-child-margin $list-horizontal-padding $list-item-child-margin 0; + + > * { + padding: 0; + } + + > [data-react-toolbox='font-icon'] { + font-size: $list-item-icon-font-size; + color: $color-text-secondary; + } +} + +.itemContentRoot { + display: block; + flex-grow: 1; + &.large { + height: $list-item-min-height-legend; + + .itemContent { + display: block; + position: relative; + top: 50%; + transform: translateY(-50%) + } + } +} + + .checkbox { display: flex; width: 100%; + height: 100%; min-height: $list-item-min-height; align-items: center; margin: 0; cursor: pointer; > [data-role='checkbox'] { - margin-right: $list-item-right-icon-margin; + margin-right: $list-item-right-checkbox-margin; } > [data-role='label'] { padding-left: 0; @@ -89,51 +145,18 @@ color: $color-text-secondary; } -.text { - flex-grow: 1; -} - -.caption { +.itemText { display: block; - font-size: $font-size-normal; - color: $color-text; -} -.legend { - display: block; - padding-top: $list-item-legend-margin-top; - font-size: $font-size-small; - color: $color-text-secondary; - white-space: normal; -} - -.avatar { - display: flex; - width: $list-item-avatar-height; - height: $list-item-avatar-height; - flex: 0 0 auto; - margin: $list-item-avatar-margin $list-horizontal-padding $list-item-avatar-margin 0; - overflow: hidden; - border-radius: 50%; -} - -.right, .left { - display: flex; - align-items: center; - vertical-align: middle; - &.icon { - font-size: $list-item-icon-font-size; + &:not(.primary) { + font-size: $font-size-small; color: $color-text-secondary; - } -} - -.right { - margin-left: $list-horizontal-padding; -} - -.left { - &.icon { - width: $list-item-icon-size; - margin-right: $list-item-right-icon-margin; + padding-top: $list-item-legend-margin-top; + white-space: normal; + } + + &.primary { + font-size: $font-size-normal; + color: $color-text; } } diff --git a/components/ripple/Ripple.jsx b/components/ripple/Ripple.jsx index b869bcbe..5c98ba0c 100644 --- a/components/ripple/Ripple.jsx +++ b/components/ripple/Ripple.jsx @@ -14,7 +14,8 @@ const Ripple = (options = {}) => { const { centered: defaultCentered, className: defaultClassName, - spread: defaultSpread + spread: defaultSpread, + ...props } = {...defaults, ...options}; return ComposedComponent => { @@ -76,6 +77,7 @@ const Ripple = (options = {}) => { } handleMouseDown = (event) => { + console.log('ripple'); if (!this.props.disabled) this.start(event); if (this.props.onMouseDown) this.props.onMouseDown(event); }; @@ -104,11 +106,10 @@ const Ripple = (options = {}) => { transform: `translate3d(${-width / 2 + left}px, ${-width / 2 + top}px, 0) scale(${scale})` }, {width, height: width}); - return ( {children ? children : null} - + diff --git a/spec/components/list.jsx b/spec/components/list.jsx index eebe7902..9f42ea39 100644 --- a/spec/components/list.jsx +++ b/spec/components/list.jsx @@ -1,5 +1,8 @@ import React from 'react'; -import { ListCheckbox, ListSubHeader, List, ListItem, ListDivider } from '../../components/list'; +import { ListCheckbox, ListSubHeader, List, ListItem, ListDivider, ListItemText, ListItemContent } from '../../components/list'; +import Avatar from '../../components/avatar'; +import FontIcon from '../../components/font_icon'; +import {Button} from '../../components/button'; const listStyle = { border: '1px solid #EEE', @@ -138,6 +141,47 @@ class ListTest extends React.Component { + +
    List with custom components
    +

    Using custom components in list item

    +
    + + + + + + + + + + + + + + Custom Caption + + + + + {console.log('clicked caption');}}> + Custom caption with events + + Custom legend with correct height + + + +
    ); }