diff --git a/docs/src/app/components/pages/components/lists.jsx b/docs/src/app/components/pages/components/lists.jsx index c6a7222f91a759..a678f5be7a8461 100644 --- a/docs/src/app/components/pages/components/lists.jsx +++ b/docs/src/app/components/pages/components/lists.jsx @@ -14,6 +14,7 @@ const ContentSend = require('svg-icons/content/send'); const EditorInsertChart = require('svg-icons/editor/insert-chart'); const FileFolder = require('svg-icons/file/folder'); const MoreVertIcon = require('svg-icons/navigation/more-vert'); +import { SelectableContainerEnhance } from 'material-ui/hoc/selectable-enhance'; const { Avatar, @@ -34,11 +35,66 @@ const { Colors } = Styles; const Code = require('lists-code'); const CodeExample = require('../../code-example/code-example'); const CodeBlock = require('../../code-example/code-block'); +let SelectableList = SelectableContainerEnhance(List); + +const Typography = Styles.Typography; +let styles = { + headline: { + fontSize: '24px', + lineHeight: '32px', + paddingTop: '16px', + marginBottom: '12px', + letterSpacing: '0', + fontWeight: Typography.fontWeightNormal, + color: Typography.textDarkBlack, + }, + subheadline: { + fontSize: '18px', + lineHeight: '27px', + paddingTop: '12px', + marginBottom: '9px', + letterSpacing: '0', + fontWeight: Typography.fontWeightNormal, + color: Typography.textDarkBlack, + }, + codeblock: { + padding: '24px', + marginBottom: '32px', + }, +} + +function wrapState(ComposedComponent) { + const StateWrapper = React.createClass({ + getInitialState() { + return { selectedIndex: 1 }; + }, + handleUpdateSelectedIndex(e, index) { + this.setState({ + selectedIndex: index, + }); + }, + render() { + return ; + }, + }); + return StateWrapper; +} + +SelectableList = wrapState(SelectableList); + export default class ListsPage extends React.Component { constructor(props) { super(props); + this.state = { selectedIndex: 1 } + + this.handleUpdateSelectedIndex = (e, index) => { + this.setState({ + selectedIndex: index, + }); + } } render() { @@ -53,6 +109,12 @@ export default class ListsPage extends React.Component { header: 'default: false', desc: 'If true, the subheader will be indented by 72px.', }, + { + name: 'selectedItemStyle', + type: 'object', + header: 'optional, only available if HOC SelectableContainerEnhance is used', + desc: 'Override the choosen inline-styles to indicate a is highlighted. You can set e.g. the background color here like this way: {{backgroundColor: #da4e49}}.', + }, { name: 'style', type: 'object', @@ -71,6 +133,13 @@ export default class ListsPage extends React.Component { header: 'optional', desc: 'The style object to override subheader styles.', }, + { + name: 'valueLink', + type: 'valueLink', + header: 'optional, only available if HOC SelectableContainerEnhance is used', + desc: 'Makes List controllable. Highlights the ListItem whose index prop matches this "selectedLink.value". ' + + '"selectedLink.requestChange" represents a callback function to change that value (e.g. in state).', + }, ], }, { @@ -182,6 +251,13 @@ export default class ListsPage extends React.Component { header: 'optional', desc: 'Override the inline-styles of the list item\'s root element.', }, + { + name: 'value', + type: 'number', + header: 'optional, only available if HOC SelectableContainerEnhance is used', + desc: 'If valueLink prop is passed to List component, this prop is also required. It assigns an identifier ' + + 'to the listItem so that it can be hightlighted by the List.', + }, ], }, { @@ -666,9 +742,105 @@ export default class ListsPage extends React.Component { secondaryTextLines={2} /> + + + + } /> + } /> + } /> + } /> + } /> + + + + +
+

Selectable Lists

+

+ Basically three steps are needed: +

+
    +
  • enhance <List> with HOC
  • +
  • decide where to put state
  • +
  • implement and set valueLink
  • +
+ + +

Enhance List

+

+ Wrapping the <List> component with the higher order component "SelectableEnhance" enables + the clicked <ListItem> to be highlighted. +

+
+ + {`import { SelectableContainerEnhance } from 'material-ui/lib/hoc/selectable-enhance'; +. +. +. +var SelectableList = SelectableContainerEnhance(List); +`} + +
+ + +

Where to put state

+

+ If this component is used in conjunction with flux or redux this is a no-brainer. The callback-handler + just has to update the store. Otherwise the state can be held within e.g the parent, but it is to be to + considered that each time a <ListItem> is clicked, the state will update and the parent - including it's + children - will rerender. +

+

+ A possible solution for this is to use another hoc. An example can be found in the sourcecode + of docs/src/app/components/pages/components/lists.jsx. +

+

The valueLink

+

+ The prop 'valueLink' of <List> has to be set, to make the highlighting controllable: +

+
+ +{`valueLink={{ + value: this.state.selectedIndex, + requestChange: this.handleUpdateSelectedIndex}} +`} + +
+ A sample implementation might look like this. +
+ +{`getInitialState() { + return { selectedIndex: 1 }; +}, +handleUpdateSelectedIndex(e,index) { + this.setState({ + selectedIndex: index, +}); +`} + +
+

Adjust the <ListItem>

+

+ The prop "value" on each ListItem has to be set. This makes the item addressable for the callback. +

+
+
); - } } +} diff --git a/docs/src/app/components/raw-code/lists-code.txt b/docs/src/app/components/raw-code/lists-code.txt index ef8c91e46c219d..2b36dc58f771de 100644 --- a/docs/src/app/components/raw-code/lists-code.txt +++ b/docs/src/app/components/raw-code/lists-code.txt @@ -97,3 +97,21 @@ ]} /> + +// List with selected indicator based on HOC + + }/> + } /> + } /> + diff --git a/docs/webpack-dev-server.config.js b/docs/webpack-dev-server.config.js index 800c7991bfdffa..82baf67d318ad4 100644 --- a/docs/webpack-dev-server.config.js +++ b/docs/webpack-dev-server.config.js @@ -83,7 +83,7 @@ var config = { loader:'babel-loader?optional=runtime&stage=0', //react-hot is like browser sync and babel loads jsx and es6-7 include: [__dirname, path.resolve(__dirname, '../src')], //include these files exclude: [nodeModulesPath] //exclude node_modules so that they are not all compiled - } + }, ] }, eslint: { diff --git a/src/hoc/selectable-enhance.js b/src/hoc/selectable-enhance.js new file mode 100644 index 00000000000000..4fee1563a28acc --- /dev/null +++ b/src/hoc/selectable-enhance.js @@ -0,0 +1,124 @@ +const React = require('react'); +const ThemeManager = require('../styles/theme-manager'); +const StylePropable = require('../mixins/style-propable'); +const ColorManipulator = require('../utils/color-manipulator'); +const DefaultRawTheme = require('../styles/raw-themes/light-raw-theme'); + +export const SelectableContainerEnhance = (Component) => { + let composed = React.createClass({ + + mixins: [StylePropable], + + contextTypes: { + muiTheme: React.PropTypes.object, + }, + + displayName: `Selectable${Component.displayName}`, + + propTypes: { + valueLink: React.PropTypes.shape({ + value: React.PropTypes.number, + requestChange: React.PropTypes.func, + }).isRequired, + selectedItemStyle: React.PropTypes.object, + }, + + childContextTypes: { + muiTheme: React.PropTypes.object, + }, + + getChildContext () { + return { + muiTheme: this.state.muiTheme, + }; + }, + + componentWillReceiveProps (nextProps, nextContext) { + let newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme; + this.setState({muiTheme: newMuiTheme}); + }, + + getInitialState() { + return { + muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme), + }; + }, + + getValueLink: function(props) { + return props.valueLink || { + value: props.value, + requestChange: props.onChange, + }; + }, + + render(){ + const { children, selectedItemStyle } = this.props + let listItems; + let keyIndex = 0; + let styles = {}; + + if (! selectedItemStyle) { + let textColor = this.state.muiTheme.rawTheme.palette.textColor; + let selectedColor = ColorManipulator.fade(textColor, 0.2); + styles = { + backgroundColor: selectedColor, + }; + } + + listItems = React.Children.map(children, (child) => { + if (child.type.displayName === "ListItem") { + let selected = this._isChildSelected(child, this.props); + let selectedChildrenStyles = {}; + if (selected) { + selectedChildrenStyles = this.mergeStyles(styles, selectedItemStyle); + } + + let mergedChildrenStyles = this.mergeStyles( + child.props.style || {}, + selectedChildrenStyles + ); + + keyIndex += 1; + + return React.cloneElement(child, { + onTouchTap: (e) => { + this._handleItemTouchTap(e, child); + if (child.props.onTouchTap) { child.props.onTouchTap(e) }; + }, + key: keyIndex, + style: mergedChildrenStyles, + }); + } + else { + return child; + } + }); + let newChildren = listItems; + + return ( + + {newChildren} + + ); + }, + + _isChildSelected(child, props) { + let itemValue = this.getValueLink(props).value; + let childValue = child.props.value; + + return (itemValue && itemValue === childValue); + }, + + _handleItemTouchTap(e, item) { + let valueLink = this.getValueLink(this.props); + let itemValue = item.props.value; + let menuValue = valueLink.value + if ( itemValue !== menuValue) { + valueLink.requestChange(e, itemValue); + } + }, + + }); + return( composed ); +}; + diff --git a/src/index.js b/src/index.js index 3af30050944d5e..49ea3b2f2b9aa4 100644 --- a/src/index.js +++ b/src/index.js @@ -44,6 +44,7 @@ module.exports = { RefreshIndicator: require('./refresh-indicator'), Ripples: require('./ripples/'), SelectField: require('./select-field'), + SelectableContainerEnhance: require('./hoc/selectable-enhance'), Slider: require('./slider'), SvgIcon: require('./svg-icon'), Icons: {