Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit 3f6b4af

Browse files
rschmuklerRobert Messerle
authored and
Robert Messerle
committed
feat(menu): add a basic dropdown menu component
Closes #3173 References #78
1 parent cc07e63 commit 3f6b4af

18 files changed

+1046
-2
lines changed

src/components/menu/_menu.js

+232
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/**
2+
* @ngdoc module
3+
* @name material.components.menu
4+
*/
5+
6+
angular.module('material.components.menu', [
7+
'material.core',
8+
'material.components.backdrop'
9+
])
10+
.directive('mdMenu', MenuDirective);
11+
12+
/**
13+
* @ngdoc directive
14+
* @name mdMenu
15+
* @module material.components.menu
16+
* @restrict E
17+
* @description
18+
*
19+
* Menus are elements that open when clicked. They are useful for displaying
20+
* additional options within the context of an action.
21+
*
22+
* Every `md-menu` must specify exactly two child elements. The first element is what is
23+
* left in the DOM and is used to open the menu. This element is called the origin element.
24+
* The origin element's scope has access to `$mdOpenMenu()`
25+
* which it may call to open the menu.
26+
*
27+
* The second element is the `md-menu-content` element which represents the
28+
* contents of the menu when it is open. Typically this will contain `md-menu-item`s,
29+
* but you can do custom content as well.
30+
*
31+
* <hljs lang="html">
32+
* <md-menu>
33+
* <!-- Origin element is a md-button with an icon -->
34+
* <md-button ng-click="$mdOpenMenu()" class="md-icon-button">
35+
* <md-icon md-svg-icon="call:phone"></md-icon>
36+
* </md-button>
37+
* <md-menu-content>
38+
* <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item>
39+
* </md-menu-content>
40+
* </md-menu>
41+
* </hljs>
42+
43+
* ## Sizing Menus
44+
*
45+
* The width of the menu when it is open may be specified by specifying a `width`
46+
* attribute on the `md-menu-content` element.
47+
* See the [Material Design Spec](http://www.google.com/design/spec/components/menus.html#menus-specs)
48+
* for more information.
49+
*
50+
*
51+
* ## Aligning Menus
52+
*
53+
* When a menu opens, it is important that the content aligns with the origin element.
54+
* Failure to align menus can result in jarring experiences for users as content
55+
* suddenly shifts. To help with this, `md-menu` provides serveral APIs to help
56+
* with alignment.
57+
*
58+
* ### Target Mode
59+
*
60+
* By default, `md-menu` will attempt to align the `md-menu-content` by aligning
61+
* designated child elements in both the origin and the menu content.
62+
*
63+
* To specify the alignment element in the `origin` you can use the `md-menu-origin`
64+
* attribute on a child element. If no `md-menu-origin` is specified, the `md-menu`
65+
* will be used as the origin element.
66+
*
67+
* Similarly, the `md-menu-content` may specify a `md-menu-align-target` for a
68+
* `md-menu-item` to specify the node that it should try and allign with.
69+
*
70+
* In this example code, we specify an icon to be our origin element, and an
71+
* icon in our menu content to be our alignment target. This ensures that both
72+
* icons are aligned when the menu opens.
73+
*
74+
* <hljs lang="html">
75+
* <md-menu>
76+
* <md-button ng-click="$mdOpenMenu()" class="md-icon-button">
77+
* <md-icon md-menu-origin md-svg-icon="call:phone"></md-icon>
78+
* </md-button>
79+
* <md-menu-content>
80+
* <md-menu-item>
81+
* <md-button ng-click="doSomething()">
82+
* <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon>
83+
* Do Something
84+
* </md-button>
85+
* </md-menu-item>
86+
* </md-menu-content>
87+
* </md-menu>
88+
* </hljs>
89+
*
90+
* Sometimes we want to specify alignment on the right side of an element, for example
91+
* if we have a menu on the right side a toolbar, we want to right align our menu content.
92+
*
93+
* We can specify the origin by using the `md-position-mode` attribute on both
94+
* the `x` and `y` axis. Right now only the `x-axis` has more than one option.
95+
* You may specify the default mode of `target target` or
96+
* `target-right target` to specify a right-oriented alignment target. See the
97+
* position section of the demos for more examples.
98+
*
99+
* ### Menu Offsets
100+
*
101+
* It is sometimes unavoidable to need to have a deeper level of control for
102+
* the positioning of a menu to ensure perfect alignment. `md-menu` provides
103+
* the `md-offset` attribute to allow pixel level specificty of adjusting the
104+
* exact positioning.
105+
*
106+
* This offset is provided in the format of `x y` or `n` where `n` will be used
107+
* in both the `x` and `y` axis.
108+
*
109+
* For example, to move a menu by `2px` from the top, we can use:
110+
* <hljs lang="html">
111+
* <md-menu md-offset="2 0">
112+
* <!-- menu-content -->
113+
* </md-menu>
114+
* </hljs>
115+
*
116+
* @usage
117+
* <hljs lang="html">
118+
* <md-menu>
119+
* <md-button ng-click="$mdOpenMenu()" class="md-icon-button">
120+
* <md-icon md-svg-icon="call:phone"></md-icon>
121+
* </md-button>
122+
* <md-menu-content>
123+
* <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item>
124+
* </md-menu-content>
125+
* </md-menu>
126+
* </hljs>
127+
*
128+
* @param {string} md-position-mode The position mode in the form of
129+
`x`, `y`. Default value is `target`,`target`. Right now the `x` axis
130+
also suppports `target-right`.
131+
* @param {string} md-offset An offset to apply to the dropdown after positioning
132+
`x`, `y`. Default value is `0`,`0`.
133+
*
134+
*/
135+
136+
function MenuDirective($mdMenu) {
137+
return {
138+
restrict: 'E',
139+
require: 'mdMenu',
140+
controller: function() { }, // empty function to be built by link
141+
scope: true,
142+
compile: compile
143+
};
144+
145+
function compile(tEl) {
146+
tEl.addClass('md-menu');
147+
tEl.children().eq(0).attr('aria-haspopup', 'true');
148+
return link;
149+
}
150+
151+
function link(scope, el, attrs, mdMenuCtrl) {
152+
// Se up mdMenuCtrl to keep our code squeaky clean
153+
buildCtrl();
154+
155+
// Expose a open function to the child scope for their html to use
156+
scope.$mdOpenMenu = function() {
157+
mdMenuCtrl.open();
158+
};
159+
160+
if (el.children().length != 2) {
161+
throw new Error('Invalid HTML for md-menu. Expected two children elements.');
162+
}
163+
164+
// Move everything into a md-menu-container
165+
var menuContainer = angular.element('<div class="md-open-menu-container md-whiteframe-z2"></div>');
166+
var menuContents = el.children()[1];
167+
menuContainer.append(menuContents);
168+
169+
var enabled;
170+
mdMenuCtrl.enable();
171+
172+
function buildCtrl() {
173+
mdMenuCtrl.enable = function enableMenu() {
174+
if (!enabled) {
175+
//el.on('keydown', handleKeypress);
176+
enabled = true;
177+
}
178+
};
179+
180+
mdMenuCtrl.disable = function disableMenu() {
181+
if (enabled) {
182+
//el.off('keydown', handleKeypress);
183+
enabled = false;
184+
}
185+
};
186+
187+
mdMenuCtrl.open = function openMenu() {
188+
el.attr('aria-expanded', 'true');
189+
$mdMenu.show({
190+
mdMenuCtrl: mdMenuCtrl,
191+
element: menuContainer,
192+
target: el[0]
193+
});
194+
};
195+
196+
mdMenuCtrl.close = function closeMenu(skipFocus) {
197+
el.attr('aria-expanded', 'false');
198+
$mdMenu.hide();
199+
if (!skipFocus) el.children()[0].focus();
200+
};
201+
202+
mdMenuCtrl.positionMode = function() {
203+
var attachment = (attrs.mdPositionMode || 'target').split(' ');
204+
205+
if (attachment.length == 1) { attachment.push(attachment[0]); }
206+
207+
return {
208+
left: attachment[0],
209+
top: attachment[1]
210+
};
211+
212+
};
213+
214+
mdMenuCtrl.offsets = function() {
215+
var offsets = (attrs.mdOffset || '0 0').split(' ').map(function(x) { return parseFloat(x, 10); });
216+
if (offsets.length == 2) {
217+
return {
218+
left: offsets[0],
219+
top: offsets[1]
220+
};
221+
} else if (offsets.length == 1) {
222+
return {
223+
top: offsets[0],
224+
left: offsets[0]
225+
};
226+
} else {
227+
throw new Error('Invalid offsets specified. Please follow format <x, y> or <n>');
228+
}
229+
};
230+
}
231+
}
232+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<div class="md-menu-demo" ng-controller="BasicDemoCtrl as ctrl">
2+
3+
<div class="menu-demo-container" layout-align="center center" layout="column">
4+
<h2 class="md-title">Simple dropdown menu</h2>
5+
<p>Note that applying the <code>md-menu-origin</code> and <code>md-menu-align-target</code> attributes ensure that the menu elements align</p>
6+
<md-menu>
7+
<md-button aria-label="Open phone interactions menu" class="md-icon-button" ng-click="$mdOpenMenu()">
8+
<md-icon md-menu-origin md-svg-icon="call:phone"></md-icon>
9+
</md-button>
10+
<md-menu-content width="4">
11+
<md-menu-item>
12+
<md-button ng-click="ctrl.redial($event)">
13+
<md-icon md-svg-icon="call:dialpad" md-menu-align-target></md-icon>
14+
Redial
15+
</md-button>
16+
</md-menu-item>
17+
<md-menu-item>
18+
<md-button disabled="disabled" ng-click="ctrl.checkVoicemail()">
19+
<md-icon md-svg-icon="call:voicemail"></md-icon>
20+
Check voicemail
21+
</md-button>
22+
</md-menu-item>
23+
<md-menu-divider></md-menu-divider>
24+
<md-menu-item>
25+
<md-button ng-click="ctrl.toggleNotifications()">
26+
<md-icon md-svg-icon="social:notifications-{{ctrl.notificationsEnabled ? 'off' : 'on'}}"></md-icon>
27+
{{ctrl.notificationsEnabled ? 'Disable' : 'Enable' }} notifications
28+
</md-button>
29+
</md-menu-item>
30+
</md-menu-content>
31+
</md-menu>
32+
</div>
33+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
angular.module('menuDemoBasic', ['ngMaterial'])
2+
.config(function($mdIconProvider) {
3+
$mdIconProvider
4+
.iconSet("call", '/img/icons/sets/communication-icons.svg', 24)
5+
.iconSet("social", '/img/icons/sets/social-icons.svg', 24);
6+
})
7+
.controller('BasicDemoCtrl', DemoCtrl);
8+
9+
function DemoCtrl($mdDialog) {
10+
var vm = this;
11+
vm.notificationsEnabled = true;
12+
vm.toggleNotifications = function() {
13+
vm.notificationsEnabled = !vm.notificationsEnabled;
14+
};
15+
16+
vm.redial = function(e) {
17+
$mdDialog.show(
18+
$mdDialog.alert()
19+
.title('Suddenly, a redial')
20+
.content('You just called someone back. They told you the most amazing story that has ever been told. Have a cookie.')
21+
.ok('That was easy')
22+
);
23+
};
24+
25+
vm.checkVoicemail = function() {
26+
// This never happens.
27+
};
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.md-menu-demo {
2+
padding: 24px;
3+
}
4+
5+
.menu-demo-container {
6+
min-height: 200px;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<div class="md-menu-demo" ng-controller="PositionDemoCtrl as ctrl">
2+
<div class="menu-demo-container" layout-align="center center" layout="column">
3+
<h2 class="md-title">Positon Mode Demos</h2>
4+
<p>The <code>md-position-mode</code> attribute can be used to specify the positioning along the <code>x</code> and <code>y</code> axis.</p>
5+
<h3 class="md-subhead">Target-Based Position Modes</h3>
6+
<div class="menus" layout-fill layout-wrap layout="row" layout-align="space-between center">
7+
<div layout="column" flex="33" flex-sm="100" layout-align="center center">
8+
<p>Target Mode Positioning (default)</p>
9+
<md-menu>
10+
<md-button aria-label="Open demo menu" class="md-icon-button" ng-click="$mdOpenMenu()">
11+
<md-icon md-menu-origin md-svg-icon="call:business"></md-icon>
12+
</md-button>
13+
<md-menu-content width="6">
14+
<md-menu-item ng-repeat="item in [1, 2, 3]">
15+
<md-button ng-click="ctrl.announceClick($index)">
16+
<md-icon md-menu-align-target md-svg-icon="call:no-sim"></md-icon>
17+
Option {{item}}
18+
</md-button>
19+
</md-menu-item>
20+
</md-menu-content>
21+
</md-menu>
22+
</div>
23+
<div layout="column" flex-sm="100" flex="33" layout-align="center center">
24+
<p>Target mode with <code>md-offset</code></p>
25+
<md-menu md-offset="0 -5">
26+
<md-button aria-label="Open demo menu" class="md-icon-button" ng-click="$mdOpenMenu()">
27+
<md-icon md-menu-origin md-svg-icon="call:ring-volume"></md-icon>
28+
</md-button>
29+
<md-menu-content width="4">
30+
<md-menu-item ng-repeat="item in [1, 2, 3]">
31+
<md-button ng-click="ctrl.announceClick($index)"> <span md-menu-align-target>Option</span> {{item}} </md-button>
32+
</md-menu-item>
33+
</md-menu-content>
34+
</md-menu>
35+
</div>
36+
<div layout="column" flex-sm="100" flex="33" layout-align="center center">
37+
<p><code>md-position-mode="target-right target"</code></p>
38+
<md-menu md-position-mode="target-right target">
39+
<md-button aria-label="Open demo menu" class="md-icon-button" ng-click="$mdOpenMenu()">
40+
<md-icon md-menu-origin md-svg-icon="call:portable-wifi-off"></md-icon>
41+
</md-button>
42+
<md-menu-content width="4">
43+
<md-menu-item ng-repeat="item in [1, 2, 3]">
44+
<md-button ng-click="ctrl.announceClick($index)">
45+
<p>Option {{item}}</p>
46+
<md-icon md-menu-align-target md-svg-icon="call:portable-wifi-off"></md-icon>
47+
</md-button>
48+
</md-menu-item>
49+
</md-menu-content>
50+
</md-menu>
51+
</div>
52+
</div>
53+
</div>
54+
</div>
55+
</div>
56+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
angular.module('menuDemoPosition', ['ngMaterial'])
2+
.config(function($mdIconProvider) {
3+
$mdIconProvider
4+
.iconSet("call", '/img/icons/sets/communication-icons.svg', 24)
5+
.iconSet("social", '/img/icons/sets/social-icons.svg', 24);
6+
})
7+
.controller('PositionDemoCtrl', DemoCtrl);
8+
9+
function DemoCtrl($mdDialog) {
10+
var vm = this;
11+
12+
this.announceClick = function(index) {
13+
$mdDialog.show(
14+
$mdDialog.alert()
15+
.title('You clicked!')
16+
.content('You clicked the menu item at index ' + index)
17+
.ok('Nice')
18+
);
19+
};
20+
}
21+
22+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.md-menu-demo {
2+
padding: 24px;
3+
}
4+
5+
.menu-demo-container {
6+
min-height: 200px;
7+
}

0 commit comments

Comments
 (0)