Adding menu component
- Implements Material Design spec for style and animations - Supports keyboard navigation - Supports attaching to any four corners of owner elementmaster
parent
42bd559fe6
commit
e37c594d86
|
@ -130,7 +130,7 @@ gulp.task('scripts', function () {
|
|||
'src/checkbox/checkbox.js',
|
||||
'src/column-layout/column-layout.js',
|
||||
'src/icon-toggle/icon-toggle.js',
|
||||
'src/item/item.js',
|
||||
'src/menu/menu.js',
|
||||
'src/radio/radio.js',
|
||||
'src/slider/slider.js',
|
||||
'src/spinner/spinner.js',
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
@import "../typography/typography";
|
||||
@import "../colors";
|
||||
@import "../shadow/shadow";
|
||||
|
||||
.wsk-dropdown-menu {
|
||||
list-style : none;
|
||||
background : $default-dropdown-bg-color;
|
||||
border : none;
|
||||
border-radius: 2px;
|
||||
display : inline-block;
|
||||
min-width : 124px;
|
||||
position : relative;
|
||||
overflow : hidden;
|
||||
padding : 0;
|
||||
@include shadow-z1();
|
||||
will-change: box-shadow, transform;
|
||||
transition: box-shadow 0.2s ease-out;
|
||||
}
|
||||
|
||||
.wsk-dropdown-menu .wsk-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/** TODO: DropdownMenu attached to other elements. */
|
|
@ -1,34 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dropdown Menu</title>
|
||||
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
|
||||
<link href='//fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&lang=en' rel='stylesheet' type='text/css'>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="PreviewBlock">
|
||||
|
||||
<ul class="wsk-dropdown-menu">
|
||||
<button class="wsk-item wsk-js-ripple-effect">5.0 Lollipop</button>
|
||||
<button class="wsk-item wsk-js-ripple-effect">4.4 KitKat</button>
|
||||
<button disabled class="wsk-item wsk-js-ripple-effect">4.3 Jelly Bean</button>
|
||||
<button class="wsk-item wsk-js-ripple-effect">Android History</button>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<!-- build:js(app/styleguide/dropdown-menu/) ../../scripts/main.min.js -->
|
||||
<script src="../wskComponentHandler.js"></script>
|
||||
<script src="../item/item.js"></script>
|
||||
<script src="../ripple/ripple.js"></script>
|
||||
<script src="../third_party/rAF.js"></script>
|
||||
<!-- endbuild -->
|
||||
</body>
|
||||
</html>
|
|
@ -1,3 +0,0 @@
|
|||
@import "../styleguide_demo_bp";
|
||||
@import "../item/item";
|
||||
@import "_dropdown-menu";
|
|
@ -77,13 +77,8 @@
|
|||
</div>
|
||||
|
||||
<div class="styleguide-demo">
|
||||
<h1>Dropdown Menu</h1>
|
||||
<iframe src="./dropdown-menu/demo.html" scrolling="no"></iframe>
|
||||
</div>
|
||||
|
||||
<div class="styleguide-demo">
|
||||
<h1>Item</h1>
|
||||
<iframe src="./item/demo.html" scrolling="no"></iframe>
|
||||
<h1>Menu</h1>
|
||||
<iframe src="./menu/demo.html" scrolling="no"></iframe>
|
||||
</div>
|
||||
|
||||
<div class="styleguide-demo">
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
@import "../typography/typography";
|
||||
@import "../colors";
|
||||
@import "../ripple/ripple";
|
||||
|
||||
.wsk-item {
|
||||
border : none;
|
||||
color : $default-item-text-color;
|
||||
background : transparent;
|
||||
display : inline-block;
|
||||
text-align : left;
|
||||
margin : 0;
|
||||
padding : 0.6em 1.2em;
|
||||
outline-color: $default-item-outline-color;
|
||||
position : relative;
|
||||
overflow : hidden;
|
||||
font : inherit;
|
||||
@include typo-body-1();
|
||||
text-decoration: none;
|
||||
cursor : pointer;
|
||||
}
|
||||
|
||||
.wsk-item[disabled] {
|
||||
color : $disabled-item-text-color;
|
||||
cursor : auto;
|
||||
}
|
||||
|
||||
.wsk-item:hover {
|
||||
background-color: $default-item-hover-bg-color;
|
||||
}
|
||||
|
||||
.wsk-item:focus {
|
||||
outline: none;
|
||||
background-color: $default-item-focus-bg-color;
|
||||
}
|
||||
|
||||
.wsk-item::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.wsk-item:active {
|
||||
background-color: $default-item-active-bg-color;
|
||||
}
|
||||
|
||||
.wsk-item--ripple-container {
|
||||
display: block;
|
||||
height: 100%;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Item</title>
|
||||
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
|
||||
<link href='//fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&lang=en' rel='stylesheet' type='text/css'>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="PreviewBlock">
|
||||
|
||||
<button class="wsk-item wsk-js-ripple-effect">Australia</button>
|
||||
<button class="wsk-item wsk-js-ripple-effect">Canada</button>
|
||||
<button disabled class="wsk-item wsk-js-ripple-effect">United States of America</button>
|
||||
<button class="wsk-item wsk-js-ripple-effect">United Kingdom</button>
|
||||
|
||||
<br />
|
||||
|
||||
<a href="#" class="wsk-item wsk-js-ripple-effect">Web Starter Kit</a>
|
||||
|
||||
</div>
|
||||
<!-- build:js(app/styleguide/item/) ../../scripts/main.min.js -->
|
||||
<script src="../wskComponentHandler.js"></script>
|
||||
<script src="../item/item.js"></script>
|
||||
<script src="../ripple/ripple.js"></script>
|
||||
<script src="../third_party/rAF.js"></script>
|
||||
<!-- endbuild -->
|
||||
</body>
|
||||
</html>
|
|
@ -1,2 +0,0 @@
|
|||
@import "../styleguide_demo_bp";
|
||||
@import "_item";
|
|
@ -1,64 +0,0 @@
|
|||
/**
|
||||
* Class constructor for Item WSK component.
|
||||
* Implements WSK component design pattern defined at:
|
||||
* https://github.com/jasonmayes/wsk-component-design-pattern
|
||||
* @param {HTMLElement} element The element that will be upgraded.
|
||||
*/
|
||||
function MaterialItem(element) {
|
||||
'use strict';
|
||||
|
||||
this.element_ = element;
|
||||
|
||||
// Initialize instance.
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store constants in one place so they can be updated easily.
|
||||
* @enum {string | number}
|
||||
* @private
|
||||
*/
|
||||
MaterialItem.prototype.Constant_ = {
|
||||
// None for now.
|
||||
};
|
||||
|
||||
/**
|
||||
* Store strings for class names defined by this component that are used in
|
||||
* JavaScript. This allows us to simply change it in one place should we
|
||||
* decide to modify at a later date.
|
||||
* @enum {string}
|
||||
* @private
|
||||
*/
|
||||
MaterialItem.prototype.CssClasses_ = {
|
||||
WSK_ITEM_RIPPLE_CONTAINER: 'wsk-item--ripple-container',
|
||||
|
||||
WSK_RIPPLE: 'wsk-ripple'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Initialize element.
|
||||
*/
|
||||
MaterialItem.prototype.init = function() {
|
||||
'use strict';
|
||||
|
||||
if (this.element_) {
|
||||
var rippleContainer = document.createElement('span');
|
||||
rippleContainer.classList.add(this.CssClasses_.WSK_ITEM_RIPPLE_CONTAINER);
|
||||
|
||||
var ripple = document.createElement('span');
|
||||
ripple.classList.add(this.CssClasses_.WSK_RIPPLE);
|
||||
rippleContainer.appendChild(ripple);
|
||||
|
||||
this.element_.appendChild(rippleContainer);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// The component registers itself. It can assume componentHandler is available
|
||||
// in the global scope.
|
||||
componentHandler.register({
|
||||
constructor: MaterialItem,
|
||||
classAsString: 'MaterialItem',
|
||||
cssClass: 'wsk-js-ripple-effect'
|
||||
});
|
|
@ -0,0 +1,174 @@
|
|||
@import "../typography/typography";
|
||||
@import "../animation/animation";
|
||||
@import "../colors";
|
||||
@import "../shadow/shadow";
|
||||
@import "../ripple/ripple";
|
||||
|
||||
$menu-expand-duration: 0.3s;
|
||||
$menu-fade-duration: 0.2s;
|
||||
|
||||
.wsk-menu__container {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
position: absolute;
|
||||
overflow: visible;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.wsk-menu__outline {
|
||||
display: block;
|
||||
background: $default-dropdown-bg-color;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transform-origin: 0 0;
|
||||
@include shadow-z1();
|
||||
will-change: transform;
|
||||
transition: transform $menu-expand-duration $animation-curve-default,
|
||||
opacity $menu-fade-duration $animation-curve-default;
|
||||
|
||||
.wsk-menu__container.is-visible & {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
&.wsk-menu--bottom-right {
|
||||
transform-origin: 100% 0;
|
||||
}
|
||||
|
||||
&.wsk-menu--top-left {
|
||||
transform-origin: 0 100%;
|
||||
}
|
||||
|
||||
&.wsk-menu--top-right {
|
||||
transform-origin: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.wsk-menu {
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: auto;
|
||||
width: auto;
|
||||
min-width: 124px;
|
||||
padding: 8px 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
|
||||
.wsk-menu__container.is-visible & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.is-animating {
|
||||
transition: opacity $menu-fade-duration $animation-curve-default,
|
||||
clip $menu-expand-duration $animation-curve-default;
|
||||
}
|
||||
|
||||
&.wsk-menu--bottom-right {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.wsk-menu--top-left {
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&.wsk-menu--top-right {
|
||||
top: auto;
|
||||
left: auto;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.wsk-menu--unaligned {
|
||||
top: auto;
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.wsk-menu__item {
|
||||
display: block;
|
||||
border: none;
|
||||
color: $default-item-text-color;
|
||||
background-color: transparent;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
outline-color: $default-item-outline-color;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@include typo-body-1();
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
transition: opacity $menu-fade-duration $animation-curve-default;
|
||||
user-select: none;
|
||||
|
||||
.wsk-menu__container.is-visible & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
color: $disabled-item-text-color;
|
||||
background-color: transparent;
|
||||
cursor: auto;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
& .wsk-ripple {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $default-item-hover-bg-color;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background-color: $default-item-focus-bg-color;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $default-item-active-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.wsk-menu__item--ripple-container {
|
||||
display: block;
|
||||
height: 100%;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
<!doctype html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
|
||||
<title>Menu</title>
|
||||
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
|
||||
<link href='//fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&lang=en' rel='stylesheet' type='text/css'>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="PreviewBlock">
|
||||
|
||||
<button id="clickable1" class="demo-button-left wsk-button wsk-js-button wsk-button--icon">
|
||||
<span class="wsk-icon wsk-icon--more-vert"/>
|
||||
</button>
|
||||
|
||||
<ul class="wsk-menu wsk-js-menu wsk-js-ripple-effect" for="clickable1">
|
||||
<li class="wsk-menu__item">5.0 Lollipop</li>
|
||||
<li class="wsk-menu__item">4.4 KitKat</li>
|
||||
<li disabled class="wsk-menu__item">4.3 Jelly Bean</li>
|
||||
<li class="wsk-menu__item">Android History</li>
|
||||
</ul>
|
||||
|
||||
<button id="clickable2" class="demo-button-left2 wsk-button wsk-js-button wsk-button--icon">
|
||||
<span class="wsk-icon wsk-icon--more-vert"/>
|
||||
</button>
|
||||
|
||||
<ul class="wsk-menu wsk-menu--top-left wsk-js-menu wsk-js-ripple-effect" for="clickable2">
|
||||
<li class="wsk-menu__item">5.0 Lollipop</li>
|
||||
<li class="wsk-menu__item">4.4 KitKat</li>
|
||||
<li disabled class="wsk-menu__item">4.3 Jelly Bean</li>
|
||||
<li class="wsk-menu__item">Android History</li>
|
||||
</ul>
|
||||
|
||||
<button id="clickable3" class="demo-button-right2 wsk-button wsk-js-button wsk-button--icon">
|
||||
<span class="wsk-icon wsk-icon--more-vert"/>
|
||||
</button>
|
||||
|
||||
<ul class="wsk-menu wsk-menu--bottom-right wsk-js-menu wsk-js-ripple-effect" for="clickable3">
|
||||
<li class="wsk-menu__item">5.0 Lollipop</li>
|
||||
<li class="wsk-menu__item">4.4 KitKat</li>
|
||||
<li disabled class="wsk-menu__item">4.3 Jelly Bean</li>
|
||||
<li class="wsk-menu__item">Android History</li>
|
||||
</ul>
|
||||
|
||||
<button id="clickable4" class="demo-button-right wsk-button wsk-js-button wsk-button--icon">
|
||||
<span class="wsk-icon wsk-icon--more-vert"/>
|
||||
</button>
|
||||
|
||||
<ul class="wsk-menu wsk-menu--top-right wsk-js-menu wsk-js-ripple-effect" for="clickable4">
|
||||
<li class="wsk-menu__item">5.0 Lollipop</li>
|
||||
<li class="wsk-menu__item">4.4 KitKat</li>
|
||||
<li disabled class="wsk-menu__item">4.3 Jelly Bean</li>
|
||||
<li class="wsk-menu__item">Android History</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<!-- build:js(app/styleguide/dropdown-menu/) ../../scripts/main.min.js -->
|
||||
<script src="../wskComponentHandler.js"></script>
|
||||
<script src="menu.js"></script>
|
||||
<script src="../button/button.js"></script>
|
||||
<script src="../ripple/ripple.js"></script>
|
||||
<script src="../third_party/rAF.js"></script>
|
||||
<!-- endbuild -->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,33 @@
|
|||
@import "../styleguide_demo_bp";
|
||||
@import "../button/button";
|
||||
@import "../icons/icons";
|
||||
@import "_menu";
|
||||
|
||||
.PreviewBlock {
|
||||
position: relative;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.demo-button-left {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
top: 250px;
|
||||
}
|
||||
|
||||
.demo-button-left2 {
|
||||
position: absolute;
|
||||
left: 72px;
|
||||
top: 250px;
|
||||
}
|
||||
|
||||
.demo-button-right {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
top: 250px;
|
||||
}
|
||||
|
||||
.demo-button-right2 {
|
||||
position: absolute;
|
||||
right: 72px;
|
||||
top: 250px;
|
||||
}
|
|
@ -0,0 +1,448 @@
|
|||
/**
|
||||
* Class constructor for dropdown WSK component.
|
||||
* Implements WSK component design pattern defined at:
|
||||
* https://github.com/jasonmayes/wsk-component-design-pattern
|
||||
* @param {HTMLElement} element The element that will be upgraded.
|
||||
*/
|
||||
function MaterialMenu(element) {
|
||||
'use strict';
|
||||
|
||||
this.element_ = element;
|
||||
|
||||
// Initialize instance.
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store constants in one place so they can be updated easily.
|
||||
* @enum {string | number}
|
||||
* @private
|
||||
*/
|
||||
MaterialMenu.prototype.Constant_ = {
|
||||
// Total duration of the menu animation.
|
||||
TRANSITION_DURATION_SECONDS: 0.3,
|
||||
// The fraction of the total duration we want to use for menu item animations.
|
||||
TRANSITION_DURATION_FRACTION: 0.8,
|
||||
// How long the menu stays open after choosing an option (so the user can see
|
||||
// the ripple).
|
||||
CLOSE_TIMEOUT: 150
|
||||
};
|
||||
|
||||
/**
|
||||
* Keycodes, for code readability.
|
||||
* @enum {number}
|
||||
* @private
|
||||
*/
|
||||
MaterialMenu.prototype.Keycodes_ = {
|
||||
ENTER: 13,
|
||||
ESCAPE: 27,
|
||||
SPACE: 32,
|
||||
UP_ARROW: 38,
|
||||
DOWN_ARROW: 40
|
||||
};
|
||||
|
||||
/**
|
||||
* Store strings for class names defined by this component that are used in
|
||||
* JavaScript. This allows us to simply change it in one place should we
|
||||
* decide to modify at a later date.
|
||||
* @enum {string}
|
||||
* @private
|
||||
*/
|
||||
MaterialMenu.prototype.CssClasses_ = {
|
||||
CONTAINER: 'wsk-menu__container',
|
||||
OUTLINE: 'wsk-menu__outline',
|
||||
ITEM: 'wsk-menu__item',
|
||||
ITEM_RIPPLE_CONTAINER: 'wsk-menu__item-ripple-container',
|
||||
RIPPLE_EFFECT: 'wsk-js-ripple-effect',
|
||||
RIPPLE_IGNORE_EVENTS: 'wsk-js-ripple-effect--ignore-events',
|
||||
RIPPLE: 'wsk-ripple',
|
||||
// Statuses
|
||||
IS_UPGRADED: 'is-upgraded',
|
||||
IS_VISIBLE: 'is-visible',
|
||||
IS_ANIMATING: 'is-animating',
|
||||
// Alignment options
|
||||
BOTTOM_LEFT: 'wsk-menu--bottom-left', // This is the default.
|
||||
BOTTOM_RIGHT: 'wsk-menu--bottom-right',
|
||||
TOP_LEFT: 'wsk-menu--top-left',
|
||||
TOP_RIGHT: 'wsk-menu--top-right',
|
||||
UNALIGNED: 'wsk-menu--unaligned'
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize element.
|
||||
*/
|
||||
MaterialMenu.prototype.init = function() {
|
||||
'use strict';
|
||||
|
||||
if (this.element_) {
|
||||
// Create container for the menu.
|
||||
var container = document.createElement('div');
|
||||
container.classList.add(this.CssClasses_.CONTAINER);
|
||||
this.element_.parentElement.insertBefore(container, this.element_);
|
||||
this.element_.parentElement.removeChild(this.element_);
|
||||
container.appendChild(this.element_);
|
||||
this.container_ = container;
|
||||
|
||||
// Create outline for the menu (shadow and background).
|
||||
var outline = document.createElement('div');
|
||||
outline.classList.add(this.CssClasses_.OUTLINE);
|
||||
this.outline_ = outline;
|
||||
container.insertBefore(outline, this.element_);
|
||||
|
||||
// Find the "for" element and bind events to it.
|
||||
var forElId = this.element_.getAttribute('for');
|
||||
var forEl = null;
|
||||
if (forElId) {
|
||||
forEl = document.getElementById(forElId);
|
||||
if (forEl) {
|
||||
this.forElement_ = forEl;
|
||||
forEl.addEventListener('click', this.handleForClick_.bind(this));
|
||||
forEl.addEventListener('keydown',
|
||||
this.handleForKeyboardEvent_.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
// Add a listener to each menu item.
|
||||
items[i].addEventListener('click', this.handleItemClick_.bind(this));
|
||||
// Add a tab index to each menu item.
|
||||
items[i].tabIndex = '-1';
|
||||
// Add a keyboard listener to each menu item.
|
||||
items[i].addEventListener('keydown',
|
||||
this.handleItemKeyboardEvent_.bind(this));
|
||||
}
|
||||
|
||||
// Add ripple classes to each item, if the user has enabled ripples.
|
||||
if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) {
|
||||
this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS);
|
||||
|
||||
for (i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
|
||||
var rippleContainer = document.createElement('span');
|
||||
rippleContainer.classList.add(this.CssClasses_.ITEM_RIPPLE_CONTAINER);
|
||||
|
||||
var ripple = document.createElement('span');
|
||||
ripple.classList.add(this.CssClasses_.RIPPLE);
|
||||
rippleContainer.appendChild(ripple);
|
||||
|
||||
item.appendChild(rippleContainer);
|
||||
item.classList.add(this.CssClasses_.RIPPLE_EFFECT);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy alignment classes to the container, so the outline can use them.
|
||||
if (this.element_.classList.contains(this.CssClasses_.BOTTOM_LEFT)) {
|
||||
this.outline_.classList.add(this.CssClasses_.BOTTOM_LEFT);
|
||||
}
|
||||
if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) {
|
||||
this.outline_.classList.add(this.CssClasses_.BOTTOM_RIGHT);
|
||||
}
|
||||
if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
|
||||
this.outline_.classList.add(this.CssClasses_.TOP_LEFT);
|
||||
}
|
||||
if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
|
||||
this.outline_.classList.add(this.CssClasses_.TOP_RIGHT);
|
||||
}
|
||||
if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
|
||||
this.outline_.classList.add(this.CssClasses_.UNALIGNED);
|
||||
}
|
||||
|
||||
container.classList.add(this.CssClasses_.IS_UPGRADED);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles a click on the "for" element, by positioning the menu and then
|
||||
* toggling it.
|
||||
* @private
|
||||
*/
|
||||
MaterialMenu.prototype.handleForClick_ = function(evt) {
|
||||
'use strict';
|
||||
|
||||
if (this.element_ && this.forElement_) {
|
||||
var rect = this.forElement_.getBoundingClientRect();
|
||||
var forRect = this.forElement_.parentElement.getBoundingClientRect();
|
||||
|
||||
if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
|
||||
// Do not position the menu automatically. Requires the developer to
|
||||
// manually specify position.
|
||||
} else if (this.element_.classList.contains(
|
||||
this.CssClasses_.BOTTOM_RIGHT)) {
|
||||
// Position below the "for" element, aligned to its right.
|
||||
this.container_.style.right = (forRect.right - rect.right) + 'px';
|
||||
this.container_.style.top =
|
||||
this.forElement_.offsetTop + this.forElement_.offsetHeight + 'px';
|
||||
} else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
|
||||
// Position above the "for" element, aligned to its left.
|
||||
this.container_.style.left = this.forElement_.offsetLeft + 'px';
|
||||
this.container_.style.bottom = (forRect.bottom - rect.top) + 'px';
|
||||
} else if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
|
||||
// Position above the "for" element, aligned to its right.
|
||||
this.container_.style.right = (forRect.right - rect.right) + 'px';
|
||||
this.container_.style.bottom = (forRect.bottom - rect.top) + 'px';
|
||||
} else {
|
||||
// Default: position below the "for" element, aligned to its left.
|
||||
this.container_.style.left = this.forElement_.offsetLeft + 'px';
|
||||
this.container_.style.top =
|
||||
this.forElement_.offsetTop + this.forElement_.offsetHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
this.toggle(evt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles a keyboard event on the "for" element.
|
||||
* @private
|
||||
*/
|
||||
MaterialMenu.prototype.handleForKeyboardEvent_ = function(evt) {
|
||||
'use strict';
|
||||
|
||||
if (this.element_ && this.container_ && this.forElement_) {
|
||||
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM +
|
||||
':not([disabled])');
|
||||
|
||||
if (items && items.length > 0 &&
|
||||
this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
|
||||
if (evt.keyCode === this.Keycodes_.UP_ARROW) {
|
||||
evt.preventDefault();
|
||||
items[items.length - 1].focus();
|
||||
} else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) {
|
||||
evt.preventDefault();
|
||||
items[0].focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles a keyboard event on an item.
|
||||
* @private
|
||||
*/
|
||||
MaterialMenu.prototype.handleItemKeyboardEvent_ = function(evt) {
|
||||
'use strict';
|
||||
|
||||
if (this.element_ && this.container_) {
|
||||
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM +
|
||||
':not([disabled])');
|
||||
|
||||
if (items && items.length > 0 &&
|
||||
this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
|
||||
var currentIndex = Array.prototype.slice.call(items).indexOf(evt.target);
|
||||
|
||||
if (evt.keyCode === this.Keycodes_.UP_ARROW) {
|
||||
evt.preventDefault();
|
||||
if (currentIndex > 0) {
|
||||
items[currentIndex - 1].focus();
|
||||
} else {
|
||||
items[items.length - 1].focus();
|
||||
}
|
||||
} else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) {
|
||||
evt.preventDefault();
|
||||
if (items.length > currentIndex + 1) {
|
||||
items[currentIndex + 1].focus();
|
||||
} else {
|
||||
items[0].focus();
|
||||
}
|
||||
} else if (evt.keyCode === this.Keycodes_.SPACE ||
|
||||
evt.keyCode === this.Keycodes_.ENTER) {
|
||||
evt.preventDefault();
|
||||
// Send mousedown and mouseup to trigger ripple.
|
||||
var e = new MouseEvent('mousedown');
|
||||
evt.target.dispatchEvent(e);
|
||||
e = new MouseEvent('mouseup');
|
||||
evt.target.dispatchEvent(e);
|
||||
// Send click.
|
||||
evt.target.click();
|
||||
} else if (evt.keyCode === this.Keycodes_.ESCAPE) {
|
||||
evt.preventDefault();
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles a click event on an item.
|
||||
* @private
|
||||
*/
|
||||
MaterialMenu.prototype.handleItemClick_ = function(evt) {
|
||||
'use strict';
|
||||
|
||||
if (evt.target.getAttribute('disabled') !== null) {
|
||||
evt.stopPropagation();
|
||||
} else {
|
||||
// Wait some time before closing menu, so the user can see the ripple.
|
||||
this.closing_ = true;
|
||||
window.setTimeout(function(evt) {
|
||||
this.hide();
|
||||
this.closing_ = false;
|
||||
}.bind(this), this.Constant_.CLOSE_TIMEOUT);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the initial clip (for opening the menu) or final clip (for closing
|
||||
* it), and applies it. This allows us to animate from or to the correct point,
|
||||
* that is, the point it's aligned to in the "for" element.
|
||||
* @private
|
||||
*/
|
||||
MaterialMenu.prototype.applyClip_ = function(height, width) {
|
||||
'use strict';
|
||||
|
||||
if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
|
||||
// Do not clip.
|
||||
this.element_.style.clip = null;
|
||||
} else if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) {
|
||||
// Clip to the top right corner of the menu.
|
||||
this.element_.style.clip =
|
||||
'rect(0 ' + width + 'px ' + '0 ' + width + 'px)';
|
||||
} else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
|
||||
// Clip to the bottom left corner of the menu.
|
||||
this.element_.style.clip =
|
||||
'rect(' + height + 'px 0 ' + height + 'px 0)';
|
||||
} else if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
|
||||
// Clip to the bottom right corner of the menu.
|
||||
this.element_.style.clip = 'rect(' + height + 'px ' + width + 'px ' +
|
||||
height + 'px ' + width + 'px)';
|
||||
} else {
|
||||
// Default: do not clip (same as clipping to the top left corner).
|
||||
this.element_.style.clip = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds an event listener to clean up after the animation ends.
|
||||
* @private
|
||||
*/
|
||||
MaterialMenu.prototype.addAnimationEndListener_ = function() {
|
||||
'use strict';
|
||||
|
||||
var cleanup = function() {
|
||||
this.element_.classList.remove(this.CssClasses_.IS_ANIMATING);
|
||||
}.bind(this);
|
||||
|
||||
// Remove animation class once the transition is done.
|
||||
this.element_.addEventListener('transitionend', cleanup);
|
||||
this.element_.addEventListener('webkitTransitionEnd', cleanup);
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays the menu.
|
||||
* @public
|
||||
*/
|
||||
MaterialMenu.prototype.show = function(evt) {
|
||||
'use strict';
|
||||
|
||||
if (this.element_ && this.container_ && this.outline_) {
|
||||
// Measure the inner element.
|
||||
var height = this.element_.getBoundingClientRect().height;
|
||||
var width = this.element_.getBoundingClientRect().width;
|
||||
|
||||
// Apply the inner element's size to the container and outline.
|
||||
this.container_.style.width = width + 'px';
|
||||
this.container_.style.height = height + 'px';
|
||||
this.outline_.style.width = width + 'px';
|
||||
this.outline_.style.height = height + 'px';
|
||||
|
||||
var transitionDuration = this.Constant_.TRANSITION_DURATION_SECONDS *
|
||||
this.Constant_.TRANSITION_DURATION_FRACTION;
|
||||
|
||||
// Calculate transition delays for individual menu items, so that they fade
|
||||
// in one at a time.
|
||||
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var itemDelay = null;
|
||||
if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT) ||
|
||||
this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
|
||||
itemDelay = ((height - items[i].offsetTop - items[i].offsetHeight) /
|
||||
height * transitionDuration) + 's';
|
||||
} else {
|
||||
itemDelay = (items[i].offsetTop / height * transitionDuration) + 's';
|
||||
}
|
||||
items[i].style.transitionDelay = itemDelay;
|
||||
}
|
||||
|
||||
// Apply the initial clip to the text before we start animating.
|
||||
this.applyClip_(height, width);
|
||||
|
||||
// Wait for the next frame, turn on animation, and apply the final clip.
|
||||
// Also make it visible. This triggers the transitions.
|
||||
window.requestAnimFrame(function() {
|
||||
this.element_.classList.add(this.CssClasses_.IS_ANIMATING);
|
||||
this.element_.style.clip = 'rect(0 ' + width + 'px ' + height + 'px 0)';
|
||||
this.container_.classList.add(this.CssClasses_.IS_VISIBLE);
|
||||
}.bind(this));
|
||||
|
||||
// Clean up after the animation is complete.
|
||||
this.addAnimationEndListener_();
|
||||
|
||||
// Add a click listener to the document, to close the menu.
|
||||
var callback = function(e) {
|
||||
// Check to see if the document is processing the same event that
|
||||
// displayed the menu in the first place. If so, do nothing.
|
||||
// Also check to see if the menu is in the process of closing itself, and
|
||||
// do nothing in that case.
|
||||
if (e !== evt && !this.closing_) {
|
||||
document.removeEventListener('click', callback);
|
||||
this.hide();
|
||||
}
|
||||
}.bind(this);
|
||||
document.addEventListener('click', callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides the menu.
|
||||
* @public
|
||||
*/
|
||||
MaterialMenu.prototype.hide = function(evt) {
|
||||
'use strict';
|
||||
|
||||
if (this.element_ && this.container_ && this.outline_) {
|
||||
var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
|
||||
|
||||
// Remove all transition delays; menu items fade out concurrently.
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
items[i].style.transitionDelay = null;
|
||||
}
|
||||
|
||||
// Measure the inner element.
|
||||
var height = this.element_.getBoundingClientRect().height;
|
||||
var width = this.element_.getBoundingClientRect().width;
|
||||
|
||||
// Turn on animation, and apply the final clip. Also make invisible.
|
||||
// This triggers the transitions.
|
||||
this.element_.classList.add(this.CssClasses_.IS_ANIMATING);
|
||||
this.applyClip_(height, width);
|
||||
this.container_.classList.remove(this.CssClasses_.IS_VISIBLE);
|
||||
|
||||
// Clean up after the animation is complete.
|
||||
this.addAnimationEndListener_();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays or hides the menu, depending on current state.
|
||||
* @public
|
||||
*/
|
||||
MaterialMenu.prototype.toggle = function(evt) {
|
||||
'use strict';
|
||||
|
||||
if (this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
|
||||
this.hide(evt);
|
||||
} else {
|
||||
this.show(evt);
|
||||
}
|
||||
};
|
||||
|
||||
// The component registers itself. It can assume componentHandler is available
|
||||
// in the global scope.
|
||||
componentHandler.register({
|
||||
constructor: MaterialMenu,
|
||||
classAsString: 'MaterialMenu',
|
||||
cssClass: 'wsk-js-menu'
|
||||
});
|
|
@ -11,11 +11,10 @@
|
|||
@import "card/card";
|
||||
@import "checkbox/checkbox";
|
||||
@import "column-layout/column-layout";
|
||||
@import "dropdown-menu/dropdown-menu";
|
||||
@import "footer/mega_footer";
|
||||
@import "footer/mini_footer";
|
||||
@import "icon-toggle/icon-toggle";
|
||||
@import "item/item";
|
||||
@import "menu/menu";
|
||||
@import "layout/layout";
|
||||
@import "list/list";
|
||||
@import "radio/radio";
|
||||
|
|
Loading…
Reference in New Issue