1<!-- 2@license 3Copyright (c) 2016 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-button-state.html"> 14<link rel="import" href="../iron-behaviors/iron-control-state.html"> 15<link rel="import" href="../iron-form-element-behavior/iron-form-element-behavior.html"> 16<link rel="import" href="../iron-icon/iron-icon.html"> 17<link rel="import" href="../iron-validatable-behavior/iron-validatable-behavior.html"> 18<link rel="import" href="../paper-behaviors/paper-ripple-behavior.html"> 19<link rel="import" href="../paper-menu-button/paper-menu-button.html"> 20<link rel="import" href="../paper-styles/default-theme.html"> 21 22<link rel="import" href="paper-dropdown-menu-icons.html"> 23<link rel="import" href="paper-dropdown-menu-shared-styles.html"> 24 25<!-- 26Material design: [Dropdown menus](https://www.google.com/design/spec/components/buttons.html#buttons-dropdown-buttons) 27 28This is a faster, lighter version of `paper-dropdown-menu`, that does not 29use a `<paper-input>` internally. Use this element if you're concerned about 30the performance of this element, i.e., if you plan on using many dropdowns on 31the same page. Note that this element has a slightly different styling API 32than `paper-dropdown-menu`. 33 34`paper-dropdown-menu-light` is similar to a native browser select element. 35`paper-dropdown-menu-light` works with selectable content. The currently selected 36item is displayed in the control. If no item is selected, the `label` is 37displayed instead. 38 39Example: 40 41 <paper-dropdown-menu-light label="Your favourite pastry"> 42 <paper-listbox class="dropdown-content"> 43 <paper-item>Croissant</paper-item> 44 <paper-item>Donut</paper-item> 45 <paper-item>Financier</paper-item> 46 <paper-item>Madeleine</paper-item> 47 </paper-listbox> 48 </paper-dropdown-menu-light> 49 50This example renders a dropdown menu with 4 options. 51 52The child element with the class `dropdown-content` is used as the dropdown 53menu. This can be a [`paper-listbox`](paper-listbox), or any other or 54element that acts like an [`iron-selector`](iron-selector). 55 56Specifically, the menu child must fire an 57[`iron-select`](iron-selector#event-iron-select) event when one of its 58children is selected, and an [`iron-deselect`](iron-selector#event-iron-deselect) 59event when a child is deselected. The selected or deselected item must 60be passed as the event's `detail.item` property. 61 62Applications can listen for the `iron-select` and `iron-deselect` events 63to react when options are selected and deselected. 64 65### Styling 66 67The following custom properties and mixins are also available for styling: 68 69Custom property | Description | Default 70----------------|-------------|---------- 71`--paper-dropdown-menu` | A mixin that is applied to the element host | `{}` 72`--paper-dropdown-menu-disabled` | A mixin that is applied to the element host when disabled | `{}` 73`--paper-dropdown-menu-ripple` | A mixin that is applied to the internal ripple | `{}` 74`--paper-dropdown-menu-button` | A mixin that is applied to the internal menu button | `{}` 75`--paper-dropdown-menu-icon` | A mixin that is applied to the internal icon | `{}` 76`--paper-dropdown-menu-disabled-opacity` | The opacity of the dropdown when disabled | `0.33` 77`--paper-dropdown-menu-color` | The color of the input/label/underline when the dropdown is unfocused | `--primary-text-color` 78`--paper-dropdown-menu-focus-color` | The color of the label/underline when the dropdown is focused | `--primary-color` 79`--paper-dropdown-error-color` | The color of the label/underline when the dropdown is invalid | `--error-color` 80`--paper-dropdown-menu-label` | Mixin applied to the label | `{}` 81`--paper-dropdown-menu-input` | Mixin appled to the input | `{}` 82 83Note that in this element, the underline is just the bottom border of the "input". 84To style it: 85 86 <style is=custom-style> 87 paper-dropdown-menu-light.custom { 88 --paper-dropdown-menu-input: { 89 border-bottom: 2px dashed lavender; 90 }; 91 </style> 92 93@group Paper Elements 94@element paper-dropdown-menu-light 95@hero hero.svg 96@demo demo/index.html 97--> 98 99<dom-module id="paper-dropdown-menu-light"> 100 <template> 101 <style include="paper-dropdown-menu-shared-styles"> 102 :host(:focus) { 103 outline: none; 104 } 105 106 :host { 107 width: 200px; /* Default size of an <input> */ 108 } 109 110 /** 111 * All of these styles below are for styling the fake-input display 112 */ 113 .dropdown-trigger { 114 box-sizing: border-box; 115 position: relative; 116 width: 100%; 117 padding: 16px 0 8px 0; 118 } 119 120 :host([disabled]) .dropdown-trigger { 121 pointer-events: none; 122 opacity: var(--paper-dropdown-menu-disabled-opacity, 0.33); 123 } 124 125 :host([no-label-float]) .dropdown-trigger { 126 padding-top: 8px; /* If there's no label, we need less space up top. */ 127 } 128 129 #input { 130 @apply(--paper-font-subhead); 131 @apply(--paper-font-common-nowrap); 132 line-height: 1.5; 133 border-bottom: 1px solid var(--paper-dropdown-menu-color, --secondary-text-color); 134 color: var(--paper-dropdown-menu-color, --primary-text-color); 135 width: 100%; 136 box-sizing: border-box; 137 padding: 12px 20px 0 0; /* Right padding so that text doesn't overlap the icon */ 138 outline: none; 139 @apply(--paper-dropdown-menu-input); 140 } 141 142 :host-context([dir="rtl"]) #input { 143 padding-right: 0px; 144 padding-left: 20px; 145 } 146 147 :host([disabled]) #input { 148 border-bottom: 1px dashed var(--paper-dropdown-menu-color, --secondary-text-color); 149 } 150 151 :host([invalid]) #input { 152 border-bottom: 2px solid var(--paper-dropdown-error-color, --error-color); 153 } 154 155 :host([no-label-float]) #input { 156 padding-top: 0; /* If there's no label, we need less space up top. */ 157 } 158 159 label { 160 @apply(--paper-font-subhead); 161 @apply(--paper-font-common-nowrap); 162 display: block; 163 position: absolute; 164 bottom: 0; 165 left: 0; 166 right: 0; 167 /** 168 * The container has a 16px top padding, and there's 12px of padding 169 * between the input and the label (from the input's padding-top) 170 */ 171 top: 28px; 172 box-sizing: border-box; 173 width: 100%; 174 padding-right: 20px; /* Right padding so that text doesn't overlap the icon */ 175 text-align: left; 176 transition-duration: .2s; 177 transition-timing-function: cubic-bezier(.4,0,.2,1); 178 color: var(--paper-dropdown-menu-color, --secondary-text-color); 179 @apply(--paper-dropdown-menu-label); 180 } 181 182 :host-context([dir="rtl"]) label { 183 padding-right: 0px; 184 padding-left: 20px; 185 } 186 187 :host([no-label-float]) label { 188 top: 8px; 189 /* Since the label doesn't need to float, remove the animation duration 190 which slows down visibility changes (i.e. when a selection is made) */ 191 transition-duration: 0s; 192 } 193 194 label.label-is-floating { 195 font-size: 12px; 196 top: 8px; 197 } 198 199 label.label-is-hidden { 200 visibility: hidden; 201 } 202 203 :host([focused]) label.label-is-floating { 204 color: var(--paper-dropdown-menu-focus-color, --primary-color); 205 } 206 207 :host([invalid]) label.label-is-floating { 208 color: var(--paper-dropdown-error-color, --error-color); 209 } 210 211 /** 212 * Sets up the focused underline. It's initially hidden, and becomes 213 * visible when it's focused. 214 */ 215 label:after { 216 background-color: var(--paper-dropdown-menu-focus-color, --primary-color); 217 bottom: 7px; /* The container has an 8px bottom padding */ 218 content: ''; 219 height: 2px; 220 left: 45%; 221 position: absolute; 222 transition-duration: .2s; 223 transition-timing-function: cubic-bezier(.4,0,.2,1); 224 visibility: hidden; 225 width: 8px; 226 z-index: 10; 227 } 228 229 :host([invalid]) label:after { 230 background-color: var(--paper-dropdown-error-color, --error-color); 231 } 232 233 :host([no-label-float]) label:after { 234 bottom: 7px; /* The container has a 8px bottom padding */ 235 } 236 237 :host([focused]:not([disabled])) label:after { 238 left: 0; 239 visibility: visible; 240 width: 100%; 241 } 242 243 iron-icon { 244 position: absolute; 245 right: 0px; 246 bottom: 8px; /* The container has an 8px bottom padding */ 247 @apply(--paper-font-subhead); 248 color: var(--disabled-text-color); 249 @apply(--paper-dropdown-menu-icon); 250 } 251 252 :host-context([dir="rtl"]) iron-icon { 253 left: 0; 254 right: auto; 255 } 256 257 :host([no-label-float]) iron-icon { 258 margin-top: 0px; 259 } 260 261 .error { 262 display: inline-block; 263 visibility: hidden; 264 color: var(--paper-dropdown-error-color, --error-color); 265 @apply(--paper-font-caption); 266 position: absolute; 267 left:0; 268 right:0; 269 bottom: -12px; 270 } 271 272 :host([invalid]) .error { 273 visibility: visible; 274 } 275 </style> 276 277 <!-- this div fulfills an a11y requirement for combobox, do not remove --> 278 <span role="button"></span> 279 <paper-menu-button 280 id="menuButton" 281 vertical-align="[[verticalAlign]]" 282 horizontal-align="[[horizontalAlign]]" 283 vertical-offset="[[_computeMenuVerticalOffset(noLabelFloat)]]" 284 disabled="[[disabled]]" 285 no-animations="[[noAnimations]]" 286 on-iron-select="_onIronSelect" 287 on-iron-deselect="_onIronDeselect" 288 opened="{{opened}}" 289 close-on-activate 290 allow-outside-scroll="[[allowOutsideScroll]]"> 291 <div class="dropdown-trigger"> 292 <label class$="[[_computeLabelClass(noLabelFloat,alwaysFloatLabel,hasContent)]]"> 293 [[label]] 294 </label> 295 <div id="input" tabindex="-1"> </div> 296 <iron-icon icon="paper-dropdown-menu:arrow-drop-down"></iron-icon> 297 <span class="error">[[errorMessage]]</span> 298 </div> 299 <content id="content" select=".dropdown-content"></content> 300 </paper-menu-button> 301 </template> 302 303 <script> 304 (function() { 305 'use strict'; 306 307 Polymer({ 308 is: 'paper-dropdown-menu-light', 309 310 behaviors: [ 311 Polymer.IronButtonState, 312 Polymer.IronControlState, 313 Polymer.PaperRippleBehavior, 314 Polymer.IronFormElementBehavior, 315 Polymer.IronValidatableBehavior 316 ], 317 318 properties: { 319 /** 320 * The derived "label" of the currently selected item. This value 321 * is the `label` property on the selected item if set, or else the 322 * trimmed text content of the selected item. 323 */ 324 selectedItemLabel: { 325 type: String, 326 notify: true, 327 readOnly: true 328 }, 329 330 /** 331 * The last selected item. An item is selected if the dropdown menu has 332 * a child with class `dropdown-content`, and that child triggers an 333 * `iron-select` event with the selected `item` in the `detail`. 334 * 335 * @type {?Object} 336 */ 337 selectedItem: { 338 type: Object, 339 notify: true, 340 readOnly: true 341 }, 342 343 /** 344 * The value for this element that will be used when submitting in 345 * a form. It is read only, and will always have the same value 346 * as `selectedItemLabel`. 347 */ 348 value: { 349 type: String, 350 notify: true, 351 readOnly: true, 352 observer: '_valueChanged', 353 }, 354 355 /** 356 * The label for the dropdown. 357 */ 358 label: { 359 type: String 360 }, 361 362 /** 363 * The placeholder for the dropdown. 364 */ 365 placeholder: { 366 type: String 367 }, 368 369 /** 370 * True if the dropdown is open. Otherwise, false. 371 */ 372 opened: { 373 type: Boolean, 374 notify: true, 375 value: false, 376 observer: '_openedChanged' 377 }, 378 379 /** 380 * By default, the dropdown will constrain scrolling on the page 381 * to itself when opened. 382 * Set to true in order to prevent scroll from being constrained 383 * to the dropdown when it opens. 384 */ 385 allowOutsideScroll: { 386 type: Boolean, 387 value: false 388 }, 389 390 /** 391 * Set to true to disable the floating label. Bind this to the 392 * `<paper-input-container>`'s `noLabelFloat` property. 393 */ 394 noLabelFloat: { 395 type: Boolean, 396 value: false, 397 reflectToAttribute: true 398 }, 399 400 /** 401 * Set to true to always float the label. Bind this to the 402 * `<paper-input-container>`'s `alwaysFloatLabel` property. 403 */ 404 alwaysFloatLabel: { 405 type: Boolean, 406 value: false 407 }, 408 409 /** 410 * Set to true to disable animations when opening and closing the 411 * dropdown. 412 */ 413 noAnimations: { 414 type: Boolean, 415 value: false 416 }, 417 418 /** 419 * The orientation against which to align the menu dropdown 420 * horizontally relative to the dropdown trigger. 421 */ 422 horizontalAlign: { 423 type: String, 424 value: 'right' 425 }, 426 427 /** 428 * The orientation against which to align the menu dropdown 429 * vertically relative to the dropdown trigger. 430 */ 431 verticalAlign: { 432 type: String, 433 value: 'top' 434 }, 435 436 hasContent: { 437 type: Boolean, 438 readOnly: true 439 } 440 }, 441 442 listeners: { 443 'tap': '_onTap' 444 }, 445 446 keyBindings: { 447 'up down': 'open', 448 'esc': 'close' 449 }, 450 451 hostAttributes: { 452 tabindex: 0, 453 role: 'combobox', 454 'aria-autocomplete': 'none', 455 'aria-haspopup': 'true' 456 }, 457 458 observers: [ 459 '_selectedItemChanged(selectedItem)' 460 ], 461 462 attached: function() { 463 // NOTE(cdata): Due to timing, a preselected value in a `IronSelectable` 464 // child will cause an `iron-select` event to fire while the element is 465 // still in a `DocumentFragment`. This has the effect of causing 466 // handlers not to fire. So, we double check this value on attached: 467 var contentElement = this.contentElement; 468 if (contentElement && contentElement.selectedItem) { 469 this._setSelectedItem(contentElement.selectedItem); 470 } 471 }, 472 473 /** 474 * The content element that is contained by the dropdown menu, if any. 475 */ 476 get contentElement() { 477 return Polymer.dom(this.$.content).getDistributedNodes()[0]; 478 }, 479 480 /** 481 * Show the dropdown content. 482 */ 483 open: function() { 484 this.$.menuButton.open(); 485 }, 486 487 /** 488 * Hide the dropdown content. 489 */ 490 close: function() { 491 this.$.menuButton.close(); 492 }, 493 494 /** 495 * A handler that is called when `iron-select` is fired. 496 * 497 * @param {CustomEvent} event An `iron-select` event. 498 */ 499 _onIronSelect: function(event) { 500 this._setSelectedItem(event.detail.item); 501 }, 502 503 /** 504 * A handler that is called when `iron-deselect` is fired. 505 * 506 * @param {CustomEvent} event An `iron-deselect` event. 507 */ 508 _onIronDeselect: function(event) { 509 this._setSelectedItem(null); 510 }, 511 512 /** 513 * A handler that is called when the dropdown is tapped. 514 * 515 * @param {CustomEvent} event A tap event. 516 */ 517 _onTap: function(event) { 518 if (Polymer.Gestures.findOriginalTarget(event) === this) { 519 this.open(); 520 } 521 }, 522 523 /** 524 * Compute the label for the dropdown given a selected item. 525 * 526 * @param {Element} selectedItem A selected Element item, with an 527 * optional `label` property. 528 */ 529 _selectedItemChanged: function(selectedItem) { 530 var value = ''; 531 if (!selectedItem) { 532 value = ''; 533 } else { 534 value = selectedItem.label || selectedItem.getAttribute('label') || selectedItem.textContent.trim(); 535 } 536 537 this._setValue(value); 538 this._setSelectedItemLabel(value); 539 }, 540 541 /** 542 * Compute the vertical offset of the menu based on the value of 543 * `noLabelFloat`. 544 * 545 * @param {boolean} noLabelFloat True if the label should not float 546 * above the input, otherwise false. 547 */ 548 _computeMenuVerticalOffset: function(noLabelFloat) { 549 // NOTE(cdata): These numbers are somewhat magical because they are 550 // derived from the metrics of elements internal to `paper-input`'s 551 // template. The metrics will change depending on whether or not the 552 // input has a floating label. 553 return noLabelFloat ? -4 : 8; 554 }, 555 556 /** 557 * Returns false if the element is required and does not have a selection, 558 * and true otherwise. 559 * @param {*=} _value Ignored. 560 * @return {boolean} true if `required` is false, or if `required` is true 561 * and the element has a valid selection. 562 */ 563 _getValidity: function(_value) { 564 return this.disabled || !this.required || (this.required && !!this.value); 565 }, 566 567 _openedChanged: function() { 568 var openState = this.opened ? 'true' : 'false'; 569 var e = this.contentElement; 570 if (e) { 571 e.setAttribute('aria-expanded', openState); 572 } 573 }, 574 575 _computeLabelClass: function(noLabelFloat, alwaysFloatLabel, hasContent) { 576 var cls = ''; 577 if (noLabelFloat === true) { 578 return hasContent ? 'label-is-hidden' : ''; 579 } 580 581 if (hasContent || alwaysFloatLabel === true) { 582 cls += ' label-is-floating'; 583 } 584 return cls; 585 }, 586 587 _valueChanged: function() { 588 // Only update if it's actually different. 589 if (this.$.input && this.$.input.textContent !== this.value) { 590 this.$.input.textContent = this.value; 591 } 592 this._setHasContent(!!this.value); 593 }, 594 }); 595 })(); 596 </script> 597</dom-module> 598