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