diff --git a/docs/components.js b/docs/components.js
index 152e334b..65f6d56a 100644
--- a/docs/components.js
+++ b/docs/components.js
@@ -68,6 +68,14 @@ export default [
'Elevations create a sense of depth by applying a drop shadow to an element.',
component: 'src/components/component-doc',
},
+ {
+ name: 'Menu',
+ url: 'menu',
+ componentKey: 'menu',
+ description:
+ 'Menus represent a list of items. They can be used within selects or dropdowns.',
+ component: 'src/components/component-doc',
+ },
{
name: 'Chips',
url: 'chips',
diff --git a/docs/examples/menu.js b/docs/examples/menu.js
new file mode 100644
index 00000000..7ae1e32f
--- /dev/null
+++ b/docs/examples/menu.js
@@ -0,0 +1,16 @@
+class MenuExample extends Component {
+ render() {
+ return (
+
+ );
+ }
+}
+
+return ;
diff --git a/docs/readmes/menu.md b/docs/readmes/menu.md
new file mode 100644
index 00000000..65e58d66
--- /dev/null
+++ b/docs/readmes/menu.md
@@ -0,0 +1,36 @@
+## Usage
+
+```jsx
+import { Menu } from 'materialish';
+import 'materialish/menu.css';
+```
+
+# `Menu`
+
+## Props
+
+| Prop Name | Default Value | Required | Description |
+| --------- | ------------- | -------- | ----------------------------------------------------------------- |
+| className | | No | Additional class name(s) to add to the menu |
+| ...rest | | No | Other props are placed on the underlying `ul` element of the menu |
+
+## CSS Variables
+
+| Variable | Default Value | Description |
+| ---------------------- | ------------- | ----------------------------------------- |
+| --mt-baseFontSize | 1rem | The size of text within the menu |
+| --mt-fontFamily | 'Roboto' | The font family to use for text |
+| --mt-menuItemMinHeight | | The minimum allowed height for menu items |
+
+# `Menu.Item`
+
+## Props
+
+| Prop Name | Default Value | Required | Description |
+| --------- | ------------- | -------- | --------------------------------------------------------------------------- |
+| className | | No | Additional class name(s) to add to the menu item |
+| separator | false | No | Pass `true` to include a border-bottom to the menu item |
+| selected | false | No | Whether or not this menu item is currently selected |
+| ripple | true | No | Whether or not to display the "ripple" effect when the menu item is clicked |
+| children | | No | The contents to render within the menu item |
+| ...rest | | No | Other props are placed on the root element of the menu item |
diff --git a/docs/static.config.js b/docs/static.config.js
index 3787c163..e46a87b0 100644
--- a/docs/static.config.js
+++ b/docs/static.config.js
@@ -18,6 +18,7 @@ const readmes = {
radio: fs.readFileSync('./readmes/radio.md', fsOptions),
dialog: fs.readFileSync('./readmes/dialog.md', fsOptions),
elevation: fs.readFileSync('./readmes/elevation.md', fsOptions),
+ menu: fs.readFileSync('./readmes/menu.md', fsOptions),
'action-chip': fs.readFileSync('./readmes/action-chip.md', fsOptions),
'filter-chip': fs.readFileSync('./readmes/filter-chip.md', fsOptions),
'choice-chip': fs.readFileSync('./readmes/choice-chip.md', fsOptions),
@@ -33,6 +34,7 @@ const examples = {
radio: fs.readFileSync('./examples/radio.js', fsOptions),
dialog: fs.readFileSync('./examples/dialog.js', fsOptions),
elevation: fs.readFileSync('./examples/elevation.js', fsOptions),
+ menu: fs.readFileSync('./examples/menu.js', fsOptions),
'action-chip': fs.readFileSync('./examples/action-chip.js', fsOptions),
'filter-chip': fs.readFileSync('./examples/filter-chip.js', fsOptions),
'choice-chip': fs.readFileSync('./examples/choice-chip.js', fsOptions),
diff --git a/src/index.js b/src/index.js
index 9f308346..bc6eac28 100644
--- a/src/index.js
+++ b/src/index.js
@@ -10,6 +10,7 @@ import Elevation from './elevation/elevation';
import ActionChip from './action-chip/action-chip';
import ChoiceChip from './choice-chip/choice-chip';
import FilterChip from './filter-chip/filter-chip';
+import Menu from './menu/menu';
export {
Avatar,
@@ -24,4 +25,5 @@ export {
ActionChip,
ChoiceChip,
FilterChip,
+ Menu,
};
diff --git a/src/menu/menu.css b/src/menu/menu.css
new file mode 100644
index 00000000..60b4dc22
--- /dev/null
+++ b/src/menu/menu.css
@@ -0,0 +1,55 @@
+.mt-menu {
+ --mt-ripple-color: #666;
+ all: initial;
+ font-size: calc(var(--mt-baseFontSize, 1rem) * 0.875);
+ font-family: var(--mt-fontFamily, 'Roboto');
+ position: relative;
+ box-sizing: border-box;
+ border-radius: 2px;
+ text-align: left;
+ margin: 0;
+ padding: 0.55em 0;
+ display: inline-block;
+ background-color: #fff;
+ min-width: 10em;
+ overflow: hidden;
+}
+
+.mt-menu_item {
+ position: relative;
+ box-sizing: border-box;
+ display: block;
+ min-height: var(--mt-menuItemMinHeight);
+ margin: 0;
+ padding: 1.1em 1.7em;
+ z-index: 0;
+ cursor: pointer;
+}
+
+.mt-menu_item:after {
+ content: '';
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #eee;
+ z-index: -1;
+ opacity: 0;
+ will-change: opacity;
+ transition: opacity 0.1s ease-in-out;
+}
+
+.mt-menu_item:hover:after {
+ opacity: 1;
+}
+
+.mt-menu_item-separator {
+ border-bottom: 1px solid #e3e2e2;
+}
+
+.mt-menu_item-selected:after {
+ background-color: #e1e0e0;
+ opacity: 1;
+}
diff --git a/src/menu/menu.js b/src/menu/menu.js
new file mode 100644
index 00000000..2369cc47
--- /dev/null
+++ b/src/menu/menu.js
@@ -0,0 +1,68 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Ripple from '../ripple/ripple';
+import Elevation from '../elevation/elevation';
+
+export default class Menu extends Component {
+ render() {
+ const { className = '', ...props } = this.props;
+ return (
+
+
+
+ );
+ }
+}
+
+Menu.propTypes = {
+ className: PropTypes.string,
+};
+
+class Item extends Component {
+ render() {
+ const {
+ className = '',
+ separator = false,
+ selected = false,
+ ripple = true,
+ children,
+ ...props
+ } = this.props;
+ return (
+
+ {children}
+ {ripple && }
+
+ );
+ }
+
+ getRippleRef = component => {
+ this.rippleComponent = component;
+ };
+
+ onClick = e => {
+ const { onClick } = this.props;
+
+ if (this.rippleComponent) {
+ this.rippleComponent.onClick(e);
+ }
+
+ if (onClick) {
+ onClick();
+ }
+ };
+}
+
+Item.propTypes = {
+ className: PropTypes.string,
+ separator: PropTypes.bool,
+ selected: PropTypes.bool,
+ ripple: PropTypes.bool,
+};
+
+Menu.Item = Item;
diff --git a/stories/menu.stories.js b/stories/menu.stories.js
new file mode 100644
index 00000000..f6acdfb8
--- /dev/null
+++ b/stories/menu.stories.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { setOptions } from '@storybook/addon-options';
+import '../src/menu/menu.css';
+import { Menu } from '../src/index';
+
+setOptions({
+ name: 'Materialish',
+ addonPanelInRight: true,
+});
+
+storiesOf('Menu', module).add('Regular', () => (
+
+));