diff --git a/docs/src/app/app-routes.jsx b/docs/src/app/app-routes.jsx index 21fb52b16302d1..cedf18b9a1279a 100644 --- a/docs/src/app/app-routes.jsx +++ b/docs/src/app/app-routes.jsx @@ -26,6 +26,7 @@ let Cards = require('./components/pages/components/cards'); let DatePicker = require('./components/pages/components/date-picker'); let Dialog = require('./components/pages/components/dialog'); let DropDownMenu = require('./components/pages/components/drop-down-menu'); +let GridList = require('./components/pages/components/grid-list'); let Icons = require('./components/pages/components/icons'); let IconButtons = require('./components/pages/components/icon-buttons'); let IconMenus = require('./components/pages/components/icon-menus'); @@ -79,6 +80,7 @@ let AppRoutes = ( + diff --git a/docs/src/app/components/pages/components.jsx b/docs/src/app/components/pages/components.jsx index 6fce441c4e70d9..960b8fbfc18530 100644 --- a/docs/src/app/components/pages/components.jsx +++ b/docs/src/app/components/pages/components.jsx @@ -12,6 +12,7 @@ class Components extends React.Component { { route: 'date-picker', text: 'Date Picker'}, { route: 'dialog', text: 'Dialog'}, { route: 'dropdown-menu', text: 'Dropdown Menu'}, + { route: 'grid-list', text: 'Grid List'}, { route: 'icons', text: 'Icons'}, { route: 'icon-buttons', text: 'Icon Buttons'}, { route: 'icon-menus', text: 'Icon Menus'}, diff --git a/docs/src/app/components/pages/components/grid-list.jsx b/docs/src/app/components/pages/components/grid-list.jsx new file mode 100644 index 00000000000000..19c30b6f21e8c0 --- /dev/null +++ b/docs/src/app/components/pages/components/grid-list.jsx @@ -0,0 +1,243 @@ +let React = require('react'); +let { GridList, GridTile } = require('material-ui'); + +let StarBorder = require('svg-icons/toggle/star-border'); +let IconButton = require('icon-button'); + +let ComponentDoc = require('../../component-doc'); + + +class GridListPage extends React.Component { + + constructor(props) { + super(props); + + this.code = ` +{/* Basic grid list with mostly default options */} + + { + tilesData.map(tile => by {tile.author}} + actionIcon={} + >) + } + +{/* Grid list with all possible overrides */} + + { + tilesData.map(tile => } + actionPosition="left" + titlePosition="top" + titleBackground={gradientBg} + cols={tile.featured ? 2 : 1} + rows={tile.featured ? 2 : 1} + >) + } + + `; + + this.desc =

Simple flex-box based Grid List implementation. Support tiles with arbitrary cell size, + but cannot implement complex layouts (like Angular Material GridList) + , is limited to flex-box limitations.

; + + this.componentInfo = [ + { + name: 'GridList Props', + infoArray: [ + { + name: 'cols', + type: 'number', + header: 'optional', + desc: 'Number of columns. Defaults to 2.' + }, + { + name: 'padding', + type: 'number', + header: 'optional', + desc: 'Number of px for the padding/spacing between items. Defaults to 4.' + }, + { + name: 'cellHeight', + type: 'number', + header: 'optional', + desc: 'Number of px for one cell height. Defaults to 180.' + } + ] + }, + { + name: 'GridTile Props', + infoArray: [ + { + name: 'title', + type: 'string', + header: 'optional', + desc: 'Title to be displayed on tile.' + }, + { + name: 'subtitle', + type: 'node', + header: 'optional', + desc: 'String or element serving as subtitle (support text).' + }, + { + name: 'titlePosition', + type: '"top"|"bottom"', + header: 'optional', + desc: 'Position of the title bar (container of title, subtitle and action icon). Defaults to "bottom".' + }, + { + name: 'titleBackground', + type: 'string', + header: 'optional', + desc: 'Style used for title bar background. Defaults to "rgba(0, 0, 0, 0.4)". Useful for setting custom gradients for example' + }, + { + name: 'actionIcon', + type: 'element', + header: 'optional', + desc: 'An IconButton element to be used as secondary action target (primary action target is the tile itself).' + }, + { + name: 'actionPosition', + type: '"left"|"right"', + header: 'optional', + desc: 'Position of secondary action IconButton. Defaults to "right".' + }, + { + name: 'cols', + type: 'number', + header: 'optional', + desc: 'Width of the tile in number of grid cells. Defaults to 1.' + }, + { + name: 'rows', + type: 'number', + header: 'optional', + desc: 'Height of the tile in number of grid cells. Defaults to 1.' + }, + { + name: 'rootClass', + type: 'string|ReactComponent', + header: 'optional', + desc: 'Either a string used as tag name for the tile root element, or a ReactComponent. Defaults to "div".' + + 'This is useful when you have, for example, a custom implementation of a navigation link (that knows' + + 'about your routes) and you want to use it as primary tile action. In case you pass a ReactComponent' + + ', please make sure that it passes all props, accepts styles overrides and render it\'s children.' + + }, + { + name: 'children', + type: 'node', + header: 'required', + desc: 'Theoretically you can pass any node as children, but the main use case is to pass an img, in which' + + 'case GridTile takes care of making the image "cover" available space (similar to background-size: cover' + + ' or to object-fit:cover)' + } + ] + } + ]; + } + + render() { + let tilesData = [ + { + img: 'images/grid-list/00-52-29-429_640.jpg', + title: 'Breakfast', + author: 'jill111', + featured: true, + },{ + img: 'images/grid-list/burger-827309_640.jpg', + title: 'Tasty burger', + author: 'pashminu' + },{ + img: 'images/grid-list/camera-813814_640.jpg', + title: 'Camera', + author: 'Danson67' + },{ + img: 'images/grid-list/morning-819362_640.jpg', + title: 'Morning', + author: 'fancycrave1', + featured: true + },{ + img: 'images/grid-list/hats-829509_640.jpg', + title: 'Hats', + author: 'Hans' + },{ + img: 'images/grid-list/honey-823614_640.jpg', + title: 'Honey', + author: 'fancycravel' + },{ + img: 'images/grid-list/vegetables-790022_640.jpg', + title: 'Vegetables', + author: 'jill111' + },{ + img: 'images/grid-list/water-plant-821293_640.jpg', + title: 'Water plant', + author: 'BkrmadtyaKarki' + }, + ]; + + let gradientBg = 'linear-gradient(to bottom, rgba(0,0,0,0.7) 0%,rgba(0,0,0,0.3) 70%,rgba(0,0,0,0) 100%);'; + + return ( + +
+ {/* Basic grid list with mostly default options */} + + { + tilesData.map(tile => by {tile.author}} + actionIcon={} + >) + } + + {/* Grid list with all possible overrides */} + + { + tilesData.map(tile => } + actionPosition="left" + titlePosition="top" + titleBackground={gradientBg} + cols={tile.featured ? 2 : 1} + rows={tile.featured ? 2 : 1} + >) + } + +
+
+ ); + } + +} + +module.exports = GridListPage; diff --git a/docs/src/www/images/grid-list/00-52-29-429_640.jpg b/docs/src/www/images/grid-list/00-52-29-429_640.jpg new file mode 100644 index 00000000000000..1ee2bb03502e5b Binary files /dev/null and b/docs/src/www/images/grid-list/00-52-29-429_640.jpg differ diff --git a/docs/src/www/images/grid-list/burger-827309_640.jpg b/docs/src/www/images/grid-list/burger-827309_640.jpg new file mode 100644 index 00000000000000..ea0d7155d7d9c0 Binary files /dev/null and b/docs/src/www/images/grid-list/burger-827309_640.jpg differ diff --git a/docs/src/www/images/grid-list/camera-813814_640.jpg b/docs/src/www/images/grid-list/camera-813814_640.jpg new file mode 100644 index 00000000000000..27fd4a7cc99ead Binary files /dev/null and b/docs/src/www/images/grid-list/camera-813814_640.jpg differ diff --git a/docs/src/www/images/grid-list/hats-829509_640.jpg b/docs/src/www/images/grid-list/hats-829509_640.jpg new file mode 100644 index 00000000000000..73ed040993e903 Binary files /dev/null and b/docs/src/www/images/grid-list/hats-829509_640.jpg differ diff --git a/docs/src/www/images/grid-list/honey-823614_640.jpg b/docs/src/www/images/grid-list/honey-823614_640.jpg new file mode 100644 index 00000000000000..832377bbd9cf76 Binary files /dev/null and b/docs/src/www/images/grid-list/honey-823614_640.jpg differ diff --git a/docs/src/www/images/grid-list/morning-819362_640.jpg b/docs/src/www/images/grid-list/morning-819362_640.jpg new file mode 100644 index 00000000000000..373d0acad10add Binary files /dev/null and b/docs/src/www/images/grid-list/morning-819362_640.jpg differ diff --git a/docs/src/www/images/grid-list/vegetables-790022_640.jpg b/docs/src/www/images/grid-list/vegetables-790022_640.jpg new file mode 100644 index 00000000000000..14c8de00c02a85 Binary files /dev/null and b/docs/src/www/images/grid-list/vegetables-790022_640.jpg differ diff --git a/docs/src/www/images/grid-list/water-plant-821293_640.jpg b/docs/src/www/images/grid-list/water-plant-821293_640.jpg new file mode 100644 index 00000000000000..993505357d14a6 Binary files /dev/null and b/docs/src/www/images/grid-list/water-plant-821293_640.jpg differ diff --git a/src/grid-list/grid-list.jsx b/src/grid-list/grid-list.jsx new file mode 100644 index 00000000000000..91d36c1a141ff1 --- /dev/null +++ b/src/grid-list/grid-list.jsx @@ -0,0 +1,95 @@ +const React = require('react'); +const StylePropable = require('../mixins/style-propable'); +const DefaultRawTheme = require('../styles/raw-themes/light-raw-theme'); +const ThemeManager = require('../styles/theme-manager'); + +const GridList = React.createClass({ + + mixins: [StylePropable], + + propTypes: { + cols: React.PropTypes.number, + padding: React.PropTypes.number, + cellHeight: React.PropTypes.number, + }, + + //for passing default theme context to children + childContextTypes: { + muiTheme: React.PropTypes.object, + }, + + getChildContext () { + return { + muiTheme: this.state.muiTheme, + }; + }, + + getDefaultProps() { + return { + cols: 2, + padding: 4, + cellHeight: '180px', + }; + }, + + getInitialState () { + return { + muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme), + }; + }, + + //to update theme inside state whenever a new theme is passed down + //from the parent / owner using context + componentWillReceiveProps (nextProps, nextContext) { + let newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme; + this.setState({muiTheme: newMuiTheme}); + }, + + getStyles() + { + return { + root: { + display: 'flex', + flexWrap: 'wrap', + margin: `-${this.props.padding/2}px`, + }, + item: { + boxSizing: 'border-box', + padding: `${this.props.padding/2}px`, + }, + }; + }, + + + render() { + const { + cols, + padding, + cellHeight, + children, + style, + ...other, + } = this.props; + + const styles = this.getStyles(); + + const mergedRootStyles = this.mergeAndPrefix(styles.root, style); + + const wrappedChildren = React.Children.map(children, (currentChild) => { + const childCols = currentChild.props.cols || 1; + const childRows = currentChild.props.rows || 1; + const itemStyle = this.mergeAndPrefix(styles.item, { + width: (100 / cols * childCols) + '%', + height: cellHeight * childRows + padding, + }); + + return
{currentChild}
; + }); + + return ( +
{wrappedChildren}
+ ); + }, +}); + +module.exports = GridList; diff --git a/src/grid-list/grid-tile.jsx b/src/grid-list/grid-tile.jsx new file mode 100644 index 00000000000000..e061c52d34b29e --- /dev/null +++ b/src/grid-list/grid-tile.jsx @@ -0,0 +1,216 @@ +const React = require('react'); +const StylePropable = require('../mixins/style-propable'); +const DefaultRawTheme = require('../styles/raw-themes/light-raw-theme'); +const ThemeManager = require('../styles/theme-manager'); + +const AutoPrefix = require('../styles/auto-prefix'); + +const GridTile = React.createClass({ + + mixins: [StylePropable], + + propTypes: { + title: React.PropTypes.string, + subtitle: React.PropTypes.node, + titlePosition: React.PropTypes.oneOf(['top', 'bottom']), + titleBackground: React.PropTypes.string, + actionIcon: React.PropTypes.element, + actionPosition: React.PropTypes.oneOf(['left', 'right']), + cols: React.PropTypes.number, + rows: React.PropTypes.number, + rootClass: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.object, + ]), + }, + + //for passing default theme context to children + childContextTypes: { + muiTheme: React.PropTypes.object, + }, + + getChildContext () { + return { + muiTheme: this.state.muiTheme, + }; + }, + + getDefaultProps() { + return { + titlePosition: 'bottom', + titleBackground: 'rgba(0, 0, 0, 0.4)', + actionPosition: 'right', + cols: 1, + rows: 1, + rootClass: 'div', + }; + }, + + getInitialState () { + return { + muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme), + }; + }, + + //to update theme inside state whenever a new theme is passed down + //from the parent / owner using context + componentWillReceiveProps (nextProps, nextContext) { + let newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme; + this.setState({muiTheme: newMuiTheme}); + }, + + getStyles() + { + const spacing = this.state.muiTheme.rawTheme.spacing; + const themeVariables = this.state.muiTheme.gridTile; + const actionPos = this.props.actionIcon ? this.props.actionPosition : null; + const gutterLess = spacing.desktopGutterLess; + + let styles = { + root: { + position: 'relative', + display: 'block', + height: '100%', + overflow: 'hidden', + }, + titleBar: { + position: 'absolute', + left: 0, + right: 0, + [this.props.titlePosition]: 0, + height: this.props.subtitle ? 68 : 48, + background: this.props.titleBackground, + display: 'flex', + alignItems: 'center', + }, + titleWrap: { + flexGrow: 1, + marginLeft: actionPos === 'right' ? gutterLess : 0, + marginRight: actionPos === 'left' ? gutterLess : 0, + color: themeVariables.textColor, + overflow: 'hidden', + }, + title: { + fontSize: '16px', + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + }, + subtitle: { + fontSize: '12px', + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + }, + actionIcon: { + order: actionPos === 'left' ? -1 : 1, + }, + childImg: { + height: '100%', + transform: 'translateX(-50%)', + position: 'relative', + left: '50%', + }, + }; + styles.titleBar = AutoPrefix.all(styles.titleBar); + styles.titleWrap = AutoPrefix.all(styles.titleWrap); + styles.actionIcon = AutoPrefix.all(styles.actionIcon); + return styles; + }, + + componentDidMount() { + this._ensureImageCover(); + }, + + componeneDidUpdate() { + this._ensureImageCover(); + }, + + _ensureImageCover() { + let imgEl = React.findDOMNode(this.refs.img); + + if (imgEl) { + let fit = () => { + if (imgEl.offsetWidth < imgEl.parentNode.offsetWidth) { + imgEl.style.height = 'auto'; + imgEl.style.left = '0'; + imgEl.style.width = '100%'; + imgEl.style.top = '50%'; + imgEl.style.transform = imgEl.style.WebkitTransform = 'translateY(-50%)'; + } + imgEl.removeEventListener('load', fit); + imgEl = null; // prevent closure memory leak + }; + if (imgEl.complete) { + fit(); + } else { + imgEl.addEventListener('load', fit); + } + } + }, + + + render() { + const { + title, + subtitle, + titlePosition, + titleBackground, + actionIcon, + actionPosition, + style, + children, + rootClass, + ...other, + } = this.props; + + const styles = this.getStyles(); + + const mergedRootStyles = this.mergeAndPrefix(styles.root, style); + + let titleBar = null; + + if (title) { + titleBar = ( +
+
+
{title}
+ { + subtitle ? (
{subtitle}
) : null + } +
+ { + actionIcon ? (
{actionIcon}
) : null + } +
+ ); + } + + let newChildren = children; + + // if there is an image passed as children + // clone it an put our styles + if (React.Children.count(children) === 1) { + newChildren = React.Children.map(children, (child) => { + if (child.type === 'img') { + return React.cloneElement(child, { + ref: 'img', + style: this.mergeStyles(styles.childImg, child.props.style), + }); + } else { + return child; + } + }); + } + + const RootTag = rootClass; + return ( + + {newChildren} + {titleBar} + + ); + }, +}); + +module.exports = GridTile; diff --git a/src/grid-list/index.js b/src/grid-list/index.js new file mode 100644 index 00000000000000..d014a3dfde2656 --- /dev/null +++ b/src/grid-list/index.js @@ -0,0 +1,4 @@ +module.exports = { + GridList: require('./grid-list'), + GridTile: require('./grid-tile'), +}; diff --git a/src/index.js b/src/index.js index 97bba55c28d2e2..31edeb6eae7fdb 100644 --- a/src/index.js +++ b/src/index.js @@ -22,6 +22,8 @@ module.exports = { FlatButton: require('./flat-button'), FloatingActionButton: require('./floating-action-button'), FontIcon: require('./font-icon'), + GridList: require('./grid-list/grid-list'), + GridTile: require('./grid-list/grid-tile'), IconButton: require('./icon-button'), IconMenu: require('./menus/icon-menu'), LeftNav: require('./left-nav'), diff --git a/src/styles/theme-manager.js b/src/styles/theme-manager.js index b0ab1cf1a30551..108d2da4c35e6c 100644 --- a/src/styles/theme-manager.js +++ b/src/styles/theme-manager.js @@ -54,6 +54,9 @@ module.exports = { secondaryIconColor: rawTheme.palette.alternateTextColor, disabledTextColor: rawTheme.palette.disabledColor, }, + gridTile: { + textColor: Colors.white, + }, inkBar: { backgroundColor: rawTheme.palette.accent1Color, },