1 <!-- 2 @license 3 Copyright (c) 2016 The Polymer Project Authors. All rights reserved. 4 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7 Code distributed by Google as part of the polymer project is also 8 subject 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 <!-- 25 Material design: [Dropdown menus](https://www.google.com/design/spec/components/buttons.html#buttons-dropdown-buttons) 26 27 This is a faster, lighter version of `paper-dropdown-menu`, that does not 28 use a `<paper-input>` internally. Use this element if you're concerned about 29 the performance of this element, i.e., if you plan on using many dropdowns on 30 the same page. Note that this element has a slightly different styling API 31 than `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 35 item is displayed in the control. If no item is selected, the `label` is 36 displayed instead. 37 38 Example: 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 49 This example renders a dropdown menu with 4 options. 50 51 The child element with the class `dropdown-content` is used as the dropdown 52 menu. This can be a [`paper-listbox`](paper-listbox), or any other or 53 element that acts like an [`iron-selector`](iron-selector). 54 55 Specifically, the menu child must fire an 56 [`iron-select`](iron-selector#event-iron-select) event when one of its 57 children is selected, and an [`iron-deselect`](iron-selector#event-iron-deselect) 58 event when a child is deselected. The selected or deselected item must 59 be passed as the event's `detail.item` property. 60 61 Applications can listen for the `iron-select` and `iron-deselect` events 62 to react when options are selected and deselected. 63 64 ### Styling 65 66 The following custom properties and mixins are also available for styling: 67 68 Custom 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 82 Note that in this element, the underline is just the bottom border of the "input". 83 To 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