Skip to content

Commit db5544b

Browse files
authored
Merge pull request google#4917 from google/sgomes-menu-autopos
feat(menu): Menu auto-positioning based on an anchor.
2 parents 75b8fd0 + ec3c096 commit db5544b

File tree

8 files changed

+429
-58
lines changed

8 files changed

+429
-58
lines changed

demos/simple-menu.html

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,45 @@
2323
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
2424
<script src="assets/material-design-lite.css.js" charset="utf-8"></script>
2525
<style>
26+
html,
27+
body,
28+
main {
29+
display: flex;
30+
flex-direction: column;
31+
height: 100%;
32+
}
33+
2634
.demo-content {
35+
position: relative;
36+
flex: 1;
2737
margin-top: 16px;
28-
height: 500px;
29-
width: 100%;
3038
background-color: antiquewhite;
3139
}
40+
41+
.mdl-menu-anchor {
42+
position: absolute;
43+
}
3244
</style>
3345
</head>
3446
<body>
3547
<main>
3648
<h1>MDL Simple Menu</h1>
3749
<p>
50+
Button position:
3851
<label>
39-
<input type="radio" name="position" value="" checked>
52+
<input type="radio" name="position" value="top left" checked>
4053
Top left
4154
</label>
4255
<label>
43-
<input type="radio" name="position" value="mdl-simple-menu--open-from-top-right">
56+
<input type="radio" name="position" value="top right">
4457
Top right
4558
</label>
4659
<label>
47-
<input type="radio" name="position" value="mdl-simple-menu--open-from-bottom-left">
60+
<input type="radio" name="position" value="bottom left">
4861
Bottom left
4962
</label>
5063
<label>
51-
<input type="radio" name="position" value="mdl-simple-menu--open-from-bottom-right">
64+
<input type="radio" name="position" value="bottom right">
5265
Bottom right
5366
</label>
5467
</p>
@@ -59,34 +72,39 @@ <h1>MDL Simple Menu</h1>
5972
</label>
6073
</p>
6174
<div>
62-
<button class="toggle">Toggle</button>
6375
<span>Last Selected item: <em id="last-selected">&lt;none selected&gt;</em></span>
6476
</div>
65-
<div class="mdl-simple-menu" tabindex="-1">
66-
<ul class="mdl-simple-menu__items mdl-list" role="menu" aria-hidden="true">
67-
<li class="mdl-list-item" role="menuitem" tabindex="0">Back</li>
68-
<li class="mdl-list-item" role="menuitem" tabindex="0">Forward</li>
69-
<li class="mdl-list-item" role="menuitem" tabindex="0">Reload</li>
70-
<li class="mdl-list-divider" role="separator"></li>
77+
<div class="demo-content">
78+
<div class="mdl-menu-anchor">
79+
<button class="toggle">Toggle</button>
80+
81+
<div class="mdl-simple-menu" style="position: absolute;" tabindex="-1">
82+
<ul class="mdl-simple-menu__items mdl-list" role="menu" aria-hidden="true">
83+
<li class="mdl-list-item" role="menuitem" tabindex="0">Back</li>
84+
<li class="mdl-list-item" role="menuitem" tabindex="0">Forward</li>
85+
<li class="mdl-list-item" role="menuitem" tabindex="0">Reload</li>
86+
<li class="mdl-list-divider" role="separator"></li>
7187

72-
<li class="mdl-list-item" role="menuitem" tabindex="0">Save As...</li>
73-
<li class="mdl-list-item" role="menuitem" tabindex="0">Print...</li>
74-
<li class="mdl-list-item" role="menuitem" tabindex="0">Cast...</li>
75-
<li class="mdl-list-item" role="menuitem" tabindex="0">Translate to English</li>
76-
<li class="mdl-list-divider" role="separator"></li>
88+
<li class="mdl-list-item" role="menuitem" tabindex="0">Save As...</li>
89+
<li class="mdl-list-item" role="menuitem" tabindex="0">Print...</li>
90+
<li class="mdl-list-item" role="menuitem" tabindex="0">Cast...</li>
91+
<li class="mdl-list-item" role="menuitem" tabindex="0">Translate to English</li>
92+
<li class="mdl-list-divider" role="separator"></li>
7793

78-
<li class="mdl-list-item" role="menuitem" tabindex="0">View Page Source</li>
79-
<li class="mdl-list-item" role="menuitem" tabindex="0">Inspect</li>
80-
</ul>
94+
<li class="mdl-list-item" role="menuitem" tabindex="0">View Page Source</li>
95+
<li class="mdl-list-item" role="menuitem" tabindex="0">Inspect</li>
96+
</ul>
97+
</div>
98+
</div>
8199
</div>
82-
<div class="demo-content"></div>
83100
</main>
84101

85102
<script src="../assets/material-design-lite.js" charset="utf-8"></script>
86103
<script>
87104
var menuEl = document.querySelector('.mdl-simple-menu');
88105
var menu = new mdl.menu.MDLSimpleMenu(menuEl);
89-
document.querySelector('.toggle').addEventListener('click', function() {
106+
var toggle = document.querySelector('.toggle');
107+
toggle.addEventListener('click', function() {
90108
menu.open = !menu.open;
91109
});
92110

@@ -103,11 +121,17 @@ <h1>MDL Simple Menu</h1>
103121
for (var i = 0; i < radios.length; i++) {
104122
radios[i].addEventListener('change', function(evt) {
105123
if (evt.target.checked) {
106-
menuEl.classList.remove('mdl-simple-menu--open-from-top-right');
107-
menuEl.classList.remove('mdl-simple-menu--open-from-bottom-left');
108-
menuEl.classList.remove('mdl-simple-menu--open-from-bottom-right');
109124
if (evt.target.value) {
110-
menuEl.classList.add(evt.target.value);
125+
var anchor = document.querySelector('.mdl-menu-anchor');
126+
anchor.style.removeAttribute('top');
127+
anchor.style.removeAttribute('right');
128+
anchor.style.removeAttribute('bottom');
129+
anchor.style.removeAttribute('left');
130+
131+
var vertical = evt.target.value.split(' ')[0];
132+
var horizontal = evt.target.value.split(' ')[1];
133+
anchor.style.setAttribute(vertical, '0');
134+
anchor.style.setAttribute(horizontal, '0');
111135
}
112136
}
113137
});

packages/mdl-menu/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,68 @@ You can start the menu in its open state by adding the `mdl-simple-menu--open` c
4545
</div>
4646
```
4747

48+
### Positioning the menu
49+
50+
The menu can either be positioned manually, or automatically, by anchoring it to an element.
51+
52+
#### Automatic Positioning
53+
54+
The menu understands the concept of an anchor, which it can use to determine how to position itself, and which corner
55+
to open from.
56+
57+
The anchor can either be a visible element that the menu is a child of:
58+
59+
```html
60+
<div class="toolbar mdl-menu-anchor">
61+
<div class="mdl-simple-menu">
62+
...
63+
</div>
64+
</div>
65+
```
66+
67+
or a wrapper element that contains the actual visible element to attach to:
68+
69+
```html
70+
<div class="mdl-menu-anchor">
71+
<button>Open Menu</button>
72+
<div class="mdl-simple-menu">
73+
...
74+
</div>
75+
</div>
76+
```
77+
78+
> Note: `overflow: visible` and `position: relative` will be set on the element with `mdl-menu-anchor` to ensure that
79+
the menu is positioned and displayed correctly.
80+
81+
The menu will check if its parent element has the `mdl-menu-anchor` class set, and if so, it will automatically position
82+
itself relative to this anchor element. It will open from the top left (top right in RTL) corner of the anchor by
83+
default, but will choose an appropriate different corner if close to the edge of the screen.
84+
85+
#### Manual Positioning
86+
87+
The menu is `position: absolute` by default, and must be positioned by the user when doing manual positioning.
88+
89+
```html
90+
<div class="container">
91+
<div class="mdl-simple-menu" style="top:0; left: 0;">
92+
...
93+
</div>
94+
</div>
95+
```
96+
97+
The menu will open from the top left by default (top right in RTL). Depending on how you've positioned your button, you
98+
may want to change the point it opens from.
99+
To override the opening point, you can style `transform-origin` directly, or use one of the following convenience
100+
classes:
101+
102+
| class name | description |
103+
| ----------------------------------------- | ------------------------------------ |
104+
| `mdl-simple-menu--open-from-top-left` | Open the menu from the top left. |
105+
| `mdl-simple-menu--open-from-top-right` | Open the menu from the top right. |
106+
| `mdl-simple-menu--open-from-bottom-left` | Open the menu from the bottom left. |
107+
| `mdl-simple-menu--open-from-bottom-right` | Open the menu from the bottom right. |
108+
109+
48110
### Using the JS Component
49111

50112
> **N.B.**: The use of `role` on both the menu's internal items list, as well as on each item, is
@@ -162,6 +224,9 @@ The adapter for temporary drawers must provide the following functions, with cor
162224
| `hasClass(className: string) => boolean` | Returns boolean indicating whether element has a given class. |
163225
| `hasNecessaryDom() => boolean` | Returns boolean indicating whether the necessary DOM is present (namely, the `mdl-temporary-drawer__drawer` drawer container). |
164226
| `getInnerDimensions() => {width: number, height: number}` | Returns an object with the items container width and height |
227+
| `hasAnchor: () => boolean` | Returns whether the menu has an anchor for positioning. |
228+
| `getAnchorDimensions() => { width: number, height: number, top: number, right: number, bottom: number, left: number }` | Returns an object with the dimensions and position of the anchor (same semantics as `DOMRect`). |
229+
| `getWindowDimensions() => {width: number, height: number}` | Returns an object with width and height of the page, in pixels. |
165230
| `setScale(x: string, y: string) => void` | Sets the transform on the root element to the provided (x, y) scale. |
166231
| `setInnerScale(x: string, y: string) => void` | Sets the transform on the items container to the provided (x, y) scale. |
167232
| `getNumberOfItems() => numbers` | Returns the number of _item_ elements inside the items container. In our vanilla component, we determine this by counting the number of list items whose `role` attribute corresponds to the correct child role of the role present on the menu list element. For example, if the list element has a role of `menu` this queries for all elements that have a role of `menuitem`. |
@@ -180,3 +245,6 @@ The adapter for temporary drawers must provide the following functions, with cor
180245
| `focus() => void` | Focuses the root element of the simple menu. |
181246
| `getFocusedItemIndex() => number` | Returns the index of the currently focused menu item (-1 if none). |
182247
| `focusItemAtIndex(index: number) => void` | Focuses the menu item with the provided index. |
248+
| `isRtl() => boolean` | Returns boolean indicating whether the current environment is RTL. |
249+
| `setTransformOrigin(value: string) => void` | Sets the transform origin for the menu element. |
250+
| `setPosition(position: { top: string, right: string, bottom: string, left: string }) => void` | Sets the position of the menu element. |

packages/mdl-menu/mdl-menu.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,8 @@
1515
*/
1616

1717
@import "./simple/mdl-simple-menu";
18+
19+
.mdl-menu-anchor {
20+
position: relative;
21+
overflow: visible;
22+
}

packages/mdl-menu/simple/foundation.js

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ export default class MDLSimpleMenuFoundation extends MDLFoundation {
3838
hasClass: (/* className: string */) => {},
3939
hasNecessaryDom: () => /* boolean */ false,
4040
getInnerDimensions: () => /* { width: number, height: number } */ ({}),
41+
hasAnchor: () => /* boolean */ false,
42+
getAnchorDimensions: () =>
43+
/* { width: number, height: number, top: number, right: number, bottom: number, left: number } */ ({}),
44+
getWindowDimensions: () => /* { width: number, height: number } */ ({}),
4145
setScale: (/* x: number, y: number */) => {},
4246
setInnerScale: (/* x: number, y: number */) => {},
4347
getNumberOfItems: () => /* number */ 0,
@@ -55,7 +59,10 @@ export default class MDLSimpleMenuFoundation extends MDLFoundation {
5559
isFocused: () => /* boolean */ false,
5660
focus: () => {},
5761
getFocusedItemIndex: () => /* number */ -1,
58-
focusItemAtIndex: (/* index: number */) => {}
62+
focusItemAtIndex: (/* index: number */) => {},
63+
isRtl: () => /* boolean */ false,
64+
setTransformOrigin: (/* origin: string */) => {},
65+
setPosition: (/* position: { top: string, right: string, bottom: string, left: string } */) => {}
5966
};
6067
}
6168

@@ -299,13 +306,60 @@ export default class MDLSimpleMenuFoundation extends MDLFoundation {
299306
}, numbers.SELECTED_TRIGGER_DELAY);
300307
}
301308

302-
// Open the menu. Optionally focus on provided item.
309+
autoPosition_() {
310+
if (!this.adapter_.hasAnchor()) {
311+
return;
312+
}
313+
314+
// Defaults: open from the top left.
315+
let vertical = 'top';
316+
let horizontal = 'left';
317+
318+
const anchor = this.adapter_.getAnchorDimensions();
319+
const windowDimensions = this.adapter_.getWindowDimensions();
320+
321+
const topOverflow = anchor.top + this.dimensions_.height - windowDimensions.height;
322+
const bottomOverflow = this.dimensions_.height - anchor.bottom;
323+
const extendsBeyondTopBounds = topOverflow > 0;
324+
325+
if (extendsBeyondTopBounds) {
326+
if (bottomOverflow < topOverflow) {
327+
vertical = 'bottom';
328+
}
329+
}
330+
331+
const leftOverflow = anchor.left + this.dimensions_.width - windowDimensions.width;
332+
const rightOverflow = this.dimensions_.width - anchor.right;
333+
const extendsBeyondLeftBounds = leftOverflow > 0;
334+
const extendsBeyondRightBounds = rightOverflow > 0;
335+
336+
if (this.adapter_.isRtl()) {
337+
// In RTL, we prefer to open from the right.
338+
horizontal = 'right';
339+
if (extendsBeyondRightBounds && leftOverflow < rightOverflow) {
340+
horizontal = 'left';
341+
}
342+
} else if (extendsBeyondLeftBounds && rightOverflow < leftOverflow) {
343+
horizontal = 'right';
344+
}
345+
346+
const position = {
347+
[horizontal]: '0',
348+
[vertical]: '0'
349+
};
350+
351+
this.adapter_.setTransformOrigin(`${vertical} ${horizontal}`);
352+
this.adapter_.setPosition(position);
353+
}
354+
355+
// Open the menu.
303356
open({focusIndex = null} = {}) {
304357
this.adapter_.saveFocus();
305358
this.adapter_.addClass(MDLSimpleMenuFoundation.cssClasses.ANIMATING);
306359
requestAnimationFrame(() => {
307360
this.dimensions_ = this.adapter_.getInnerDimensions();
308361
this.applyTransitionDelays_();
362+
this.autoPosition_();
309363
this.animateMenu_();
310364
this.adapter_.addClass(MDLSimpleMenuFoundation.cssClasses.OPEN);
311365
this.focusOnOpen_(focusIndex);

packages/mdl-menu/simple/index.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ export class MDLSimpleMenu extends MDLComponent {
7171
const {itemsContainer_: itemsContainer} = this;
7272
return {width: itemsContainer.offsetWidth, height: itemsContainer.offsetHeight};
7373
},
74+
hasAnchor: () => this.root_.parentElement && this.root_.parentElement.classList.contains('mdl-menu-anchor'),
75+
getAnchorDimensions: () => this.root_.parentElement.getBoundingClientRect(),
76+
getWindowDimensions: () => {
77+
return {width: window.innerWidth, height: window.innerHeight};
78+
},
7479
setScale: (x, y) => {
7580
this.root_.style[getTransformPropertyName(window)] = `scale(${x}, ${y})`;
7681
},
@@ -105,7 +110,17 @@ export class MDLSimpleMenu extends MDLComponent {
105110
isFocused: () => document.activeElement === this.root_,
106111
focus: () => this.root_.focus(),
107112
getFocusedItemIndex: () => this.items.indexOf(document.activeElement),
108-
focusItemAtIndex: index => this.items[index].focus()
113+
focusItemAtIndex: index => this.items[index].focus(),
114+
isRtl: () => getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl',
115+
setTransformOrigin: origin => {
116+
this.root_.style[`${getTransformPropertyName(window)}-origin`] = origin;
117+
},
118+
setPosition: position => {
119+
this.root_.style.left = 'left' in position ? position.left : null;
120+
this.root_.style.right = 'right' in position ? position.right : null;
121+
this.root_.style.top = 'top' in position ? position.top : null;
122+
this.root_.style.bottom = 'bottom' in position ? position.bottom : null;
123+
}
109124
});
110125
}
111126

0 commit comments

Comments
 (0)