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,
},