1<!-- 2@license 3Copyright (c) 2015 The Polymer Project Authors. All rights reserved. 4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7Code distributed by Google as part of the polymer project is also 8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9--> 10 11<link rel="import" href="../polymer/polymer.html"> 12<link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> 13<link rel="import" href="../iron-behaviors/iron-control-state.html"> 14<link rel="import" href="../iron-dropdown/iron-dropdown.html"> 15<link rel="import" href="../neon-animation/animations/fade-in-animation.html"> 16<link rel="import" href="../neon-animation/animations/fade-out-animation.html"> 17<link rel="import" href="../paper-styles/default-theme.html"> 18<link rel="import" href="../paper-styles/shadow.html"> 19<link rel="import" href="paper-menu-button-animations.html"> 20 21<!-- 22Material design: [Dropdown buttons](https://www.google.com/design/spec/components/buttons.html#buttons-dropdown-buttons) 23 24`paper-menu-button` allows one to compose a designated "trigger" element with 25another element that represents "content", to create a dropdown menu that 26displays the "content" when the "trigger" is clicked. 27 28The child element with the class `dropdown-trigger` will be used as the 29"trigger" element. The child element with the class `dropdown-content` will be 30used as the "content" element. 31 32The `paper-menu-button` is sensitive to its content's `iron-select` events. If 33the "content" element triggers an `iron-select` event, the `paper-menu-button` 34will close automatically. 35 36Example: 37 38 <paper-menu-button> 39 <paper-icon-button icon="menu" class="dropdown-trigger"></paper-icon-button> 40 <paper-menu class="dropdown-content"> 41 <paper-item>Share</paper-item> 42 <paper-item>Settings</paper-item> 43 <paper-item>Help</paper-item> 44 </paper-menu> 45 </paper-menu-button> 46 47### Styling 48 49The following custom properties and mixins are also available for styling: 50 51Custom property | Description | Default 52----------------|-------------|---------- 53`--paper-menu-button-dropdown-background` | Background color of the paper-menu-button dropdown | `--primary-background-color` 54`--paper-menu-button` | Mixin applied to the paper-menu-button | `{}` 55`--paper-menu-button-disabled` | Mixin applied to the paper-menu-button when disabled | `{}` 56`--paper-menu-button-dropdown` | Mixin applied to the paper-menu-button dropdown | `{}` 57`--paper-menu-button-content` | Mixin applied to the paper-menu-button content | `{}` 58 59@hero hero.svg 60@demo demo/index.html 61--> 62 63<dom-module id="paper-menu-button"> 64 <template> 65 <style> 66 :host { 67 display: inline-block; 68 position: relative; 69 padding: 8px; 70 outline: none; 71 72 @apply(--paper-menu-button); 73 } 74 75 :host([disabled]) { 76 cursor: auto; 77 color: var(--disabled-text-color); 78 79 @apply(--paper-menu-button-disabled); 80 } 81 82 iron-dropdown { 83 @apply(--paper-menu-button-dropdown); 84 } 85 86 .dropdown-content { 87 @apply(--shadow-elevation-2dp); 88 89 position: relative; 90 border-radius: 2px; 91 background-color: var(--paper-menu-button-dropdown-background, --primary-background-color); 92 93 @apply(--paper-menu-button-content); 94 } 95 96 :host([vertical-align="top"]) .dropdown-content { 97 margin-bottom: 20px; 98 margin-top: -10px; 99 top: 10px; 100 } 101 102 :host([vertical-align="bottom"]) .dropdown-content { 103 bottom: 10px; 104 margin-bottom: -10px; 105 margin-top: 20px; 106 } 107 108 #trigger { 109 cursor: pointer; 110 } 111 </style> 112 113 <div id="trigger" on-tap="toggle"> 114 <content select=".dropdown-trigger"></content> 115 </div> 116 117 <iron-dropdown 118 id="dropdown" 119 opened="{{opened}}" 120 horizontal-align="[[horizontalAlign]]" 121 vertical-align="[[verticalAlign]]" 122 dynamic-align="[[dynamicAlign]]" 123 horizontal-offset="[[horizontalOffset]]" 124 vertical-offset="[[verticalOffset]]" 125 no-overlap="[[noOverlap]]" 126 open-animation-config="[[openAnimationConfig]]" 127 close-animation-config="[[closeAnimationConfig]]" 128 no-animations="[[noAnimations]]" 129 focus-target="[[_dropdownContent]]" 130 allow-outside-scroll="[[allowOutsideScroll]]" 131 restore-focus-on-close="[[restoreFocusOnClose]]" 132 on-iron-overlay-canceled="__onIronOverlayCanceled"> 133 <div class="dropdown-content"> 134 <content id="content" select=".dropdown-content"></content> 135 </div> 136 </iron-dropdown> 137 </template> 138 139 <script> 140 (function() { 141 'use strict'; 142 143 var config = { 144 ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)', 145 MAX_ANIMATION_TIME_MS: 400 146 }; 147 148 var PaperMenuButton = Polymer({ 149 is: 'paper-menu-button', 150 151 /** 152 * Fired when the dropdown opens. 153 * 154 * @event paper-dropdown-open 155 */ 156 157 /** 158 * Fired when the dropdown closes. 159 * 160 * @event paper-dropdown-close 161 */ 162 163 behaviors: [ 164 Polymer.IronA11yKeysBehavior, 165 Polymer.IronControlState 166 ], 167 168 properties: { 169 /** 170 * True if the content is currently displayed. 171 */ 172 opened: { 173 type: Boolean, 174 value: false, 175 notify: true, 176 observer: '_openedChanged' 177 }, 178 179 /** 180 * The orientation against which to align the menu dropdown 181 * horizontally relative to the dropdown trigger. 182 */ 183 horizontalAlign: { 184 type: String, 185 value: 'left', 186 reflectToAttribute: true 187 }, 188 189 /** 190 * The orientation against which to align the menu dropdown 191 * vertically relative to the dropdown trigger. 192 */ 193 verticalAlign: { 194 type: String, 195 value: 'top', 196 reflectToAttribute: true 197 }, 198 199 /** 200 * If true, the `horizontalAlign` and `verticalAlign` properties will 201 * be considered preferences instead of strict requirements when 202 * positioning the dropdown and may be changed if doing so reduces 203 * the area of the dropdown falling outside of `fitInto`. 204 */ 205 dynamicAlign: { 206 type: Boolean 207 }, 208 209 /** 210 * A pixel value that will be added to the position calculated for the 211 * given `horizontalAlign`. Use a negative value to offset to the 212 * left, or a positive value to offset to the right. 213 */ 214 horizontalOffset: { 215 type: Number, 216 value: 0, 217 notify: true 218 }, 219 220 /** 221 * A pixel value that will be added to the position calculated for the 222 * given `verticalAlign`. Use a negative value to offset towards the 223 * top, or a positive value to offset towards the bottom. 224 */ 225 verticalOffset: { 226 type: Number, 227 value: 0, 228 notify: true 229 }, 230 231 /** 232 * If true, the dropdown will be positioned so that it doesn't overlap 233 * the button. 234 */ 235 noOverlap: { 236 type: Boolean 237 }, 238 239 /** 240 * Set to true to disable animations when opening and closing the 241 * dropdown. 242 */ 243 noAnimations: { 244 type: Boolean, 245 value: false 246 }, 247 248 /** 249 * Set to true to disable automatically closing the dropdown after 250 * a selection has been made. 251 */ 252 ignoreSelect: { 253 type: Boolean, 254 value: false 255 }, 256 257 /** 258 * Set to true to enable automatically closing the dropdown after an 259 * item has been activated, even if the selection did not change. 260 */ 261 closeOnActivate: { 262 type: Boolean, 263 value: false 264 }, 265 266 /** 267 * An animation config. If provided, this will be used to animate the 268 * opening of the dropdown. 269 */ 270 openAnimationConfig: { 271 type: Object, 272 value: function() { 273 return [{ 274 name: 'fade-in-animation', 275 timing: { 276 delay: 100, 277 duration: 200 278 } 279 }, { 280 name: 'paper-menu-grow-width-animation', 281 timing: { 282 delay: 100, 283 duration: 150, 284 easing: config.ANIMATION_CUBIC_BEZIER 285 } 286 }, { 287 name: 'paper-menu-grow-height-animation', 288 timing: { 289 delay: 100, 290 duration: 275, 291 easing: config.ANIMATION_CUBIC_BEZIER 292 } 293 }]; 294 } 295 }, 296 297 /** 298 * An animation config. If provided, this will be used to animate the 299 * closing of the dropdown. 300 */ 301 closeAnimationConfig: { 302 type: Object, 303 value: function() { 304 return [{ 305 name: 'fade-out-animation', 306 timing: { 307 duration: 150 308 } 309 }, { 310 name: 'paper-menu-shrink-width-animation', 311 timing: { 312 delay: 100, 313 duration: 50, 314 easing: config.ANIMATION_CUBIC_BEZIER 315 } 316 }, { 317 name: 'paper-menu-shrink-height-animation', 318 timing: { 319 duration: 200, 320 easing: 'ease-in' 321 } 322 }]; 323 } 324 }, 325 326 /** 327 * By default, the dropdown will constrain scrolling on the page 328 * to itself when opened. 329 * Set to true in order to prevent scroll from being constrained 330 * to the dropdown when it opens. 331 */ 332 allowOutsideScroll: { 333 type: Boolean, 334 value: false 335 }, 336 337 /** 338 * Whether focus should be restored to the button when the menu closes. 339 */ 340 restoreFocusOnClose: { 341 type: Boolean, 342 value: true 343 }, 344 345 /** 346 * This is the element intended to be bound as the focus target 347 * for the `iron-dropdown` contained by `paper-menu-button`. 348 */ 349 _dropdownContent: { 350 type: Object 351 } 352 }, 353 354 hostAttributes: { 355 role: 'group', 356 'aria-haspopup': 'true' 357 }, 358 359 listeners: { 360 'iron-activate': '_onIronActivate', 361 'iron-select': '_onIronSelect' 362 }, 363 364 /** 365 * The content element that is contained by the menu button, if any. 366 */ 367 get contentElement() { 368 return Polymer.dom(this.$.content).getDistributedNodes()[0]; 369 }, 370 371 /** 372 * Toggles the drowpdown content between opened and closed. 373 */ 374 toggle: function() { 375 if (this.opened) { 376 this.close(); 377 } else { 378 this.open(); 379 } 380 }, 381 382 /** 383 * Make the dropdown content appear as an overlay positioned relative 384 * to the dropdown trigger. 385 */ 386 open: function() { 387 if (this.disabled) { 388 return; 389 } 390 391 this.$.dropdown.open(); 392 }, 393 394 /** 395 * Hide the dropdown content. 396 */ 397 close: function() { 398 this.$.dropdown.close(); 399 }, 400 401 /** 402 * When an `iron-select` event is received, the dropdown should 403 * automatically close on the assumption that a value has been chosen. 404 * 405 * @param {CustomEvent} event A CustomEvent instance with type 406 * set to `"iron-select"`. 407 */ 408 _onIronSelect: function(event) { 409 if (!this.ignoreSelect) { 410 this.close(); 411 } 412 }, 413 414 /** 415 * Closes the dropdown when an `iron-activate` event is received if 416 * `closeOnActivate` is true. 417 * 418 * @param {CustomEvent} event A CustomEvent of type 'iron-activate'. 419 */ 420 _onIronActivate: function(event) { 421 if (this.closeOnActivate) { 422 this.close(); 423 } 424 }, 425 426 /** 427 * When the dropdown opens, the `paper-menu-button` fires `paper-open`. 428 * When the dropdown closes, the `paper-menu-button` fires `paper-close`. 429 * 430 * @param {boolean} opened True if the dropdown is opened, otherwise false. 431 * @param {boolean} oldOpened The previous value of `opened`. 432 */ 433 _openedChanged: function(opened, oldOpened) { 434 if (opened) { 435 // TODO(cdata): Update this when we can measure changes in distributed 436 // children in an idiomatic way. 437 // We poke this property in case the element has changed. This will 438 // cause the focus target for the `iron-dropdown` to be updated as 439 // necessary: 440 this._dropdownContent = this.contentElement; 441 this.fire('paper-dropdown-open'); 442 } else if (oldOpened != null) { 443 this.fire('paper-dropdown-close'); 444 } 445 }, 446 447 /** 448 * If the dropdown is open when disabled becomes true, close the 449 * dropdown. 450 * 451 * @param {boolean} disabled True if disabled, otherwise false. 452 */ 453 _disabledChanged: function(disabled) { 454 Polymer.IronControlState._disabledChanged.apply(this, arguments); 455 if (disabled && this.opened) { 456 this.close(); 457 } 458 }, 459 460 __onIronOverlayCanceled: function(event) { 461 var uiEvent = event.detail; 462 var target = Polymer.dom(uiEvent).rootTarget; 463 var trigger = this.$.trigger; 464 var path = Polymer.dom(uiEvent).path; 465 466 if (path.indexOf(trigger) > -1) { 467 event.preventDefault(); 468 } 469 } 470 }); 471 472 Object.keys(config).forEach(function (key) { 473 PaperMenuButton[key] = config[key]; 474 }); 475 476 Polymer.PaperMenuButton = PaperMenuButton; 477 })(); 478 </script> 479</dom-module> 480