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-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-input/paper-input.html"> 19<link rel="import" href="../paper-menu-button/paper-menu-button.html"> 20<link rel="import" href="../paper-ripple/paper-ripple.html"> 21<link rel="import" href="../paper-styles/default-theme.html"> 22 23<link rel="import" href="paper-dropdown-menu-icons.html"> 24<link rel="import" href="paper-dropdown-menu-shared-styles.html"> 25 26<!-- 27Material design: [Dropdown menus](https://www.google.com/design/spec/components/buttons.html#buttons-dropdown-buttons) 28 29`paper-dropdown-menu` is similar to a native browser select element. 30`paper-dropdown-menu` works with selectable content. The currently selected 31item is displayed in the control. If no item is selected, the `label` is 32displayed instead. 33 34Example: 35 36 <paper-dropdown-menu label="Your favourite pastry"> 37 <paper-listbox class="dropdown-content"> 38 <paper-item>Croissant</paper-item> 39 <paper-item>Donut</paper-item> 40 <paper-item>Financier</paper-item> 41 <paper-item>Madeleine</paper-item> 42 </paper-listbox> 43 </paper-dropdown-menu> 44 45This example renders a dropdown menu with 4 options. 46 47The child element with the class `dropdown-content` is used as the dropdown 48menu. This can be a [`paper-listbox`](paper-listbox), or any other or 49element that acts like an [`iron-selector`](iron-selector). 50 51Specifically, the menu child must fire an 52[`iron-select`](iron-selector#event-iron-select) event when one of its 53children is selected, and an [`iron-deselect`](iron-selector#event-iron-deselect) 54event when a child is deselected. The selected or deselected item must 55be passed as the event's `detail.item` property. 56 57Applications can listen for the `iron-select` and `iron-deselect` events 58to react when options are selected and deselected. 59 60### Styling 61 62The following custom properties and mixins are also available for styling: 63 64Custom property | Description | Default 65----------------|-------------|---------- 66`--paper-dropdown-menu` | A mixin that is applied to the element host | `{}` 67`--paper-dropdown-menu-disabled` | A mixin that is applied to the element host when disabled | `{}` 68`--paper-dropdown-menu-ripple` | A mixin that is applied to the internal ripple | `{}` 69`--paper-dropdown-menu-button` | A mixin that is applied to the internal menu button | `{}` 70`--paper-dropdown-menu-input` | A mixin that is applied to the internal paper input | `{}` 71`--paper-dropdown-menu-icon` | A mixin that is applied to the internal icon | `{}` 72 73You can also use any of the `paper-input-container` and `paper-menu-button` 74style mixins and custom properties to style the internal input and menu button 75respectively. 76 77@group Paper Elements 78@element paper-dropdown-menu 79@hero hero.svg 80@demo demo/index.html 81--> 82 83<dom-module id="paper-dropdown-menu"> 84 <template> 85 <style include="paper-dropdown-menu-shared-styles"></style> 86 87 <!-- this div fulfills an a11y requirement for combobox, do not remove --> 88 <span role="button"></span> 89 <paper-menu-button 90 id="menuButton" 91 vertical-align="[[verticalAlign]]" 92 horizontal-align="[[horizontalAlign]]" 93 dynamic-align="[[dynamicAlign]]" 94 vertical-offset="[[_computeMenuVerticalOffset(noLabelFloat)]]" 95 disabled="[[disabled]]" 96 no-animations="[[noAnimations]]" 97 on-iron-select="_onIronSelect" 98 on-iron-deselect="_onIronDeselect" 99 opened="{{opened}}" 100 close-on-activate 101 allow-outside-scroll="[[allowOutsideScroll]]" 102 restore-focus-on-close="[[restoreFocusOnClose]]"> 103 <div class="dropdown-trigger"> 104 <paper-ripple></paper-ripple> 105 <!-- paper-input has type="text" for a11y, do not remove --> 106 <paper-input 107 type="text" 108 invalid="[[invalid]]" 109 readonly 110 disabled="[[disabled]]" 111 value="[[selectedItemLabel]]" 112 placeholder="[[placeholder]]" 113 error-message="[[errorMessage]]" 114 always-float-label="[[alwaysFloatLabel]]" 115 no-label-float="[[noLabelFloat]]" 116 label="[[label]]"> 117 <iron-icon icon="paper-dropdown-menu:arrow-drop-down" suffix></iron-icon> 118 </paper-input> 119 </div> 120 <content id="content" select=".dropdown-content"></content> 121 </paper-menu-button> 122 </template> 123 124 <script> 125 (function() { 126 'use strict'; 127 128 Polymer({ 129 is: 'paper-dropdown-menu', 130 131 behaviors: [ 132 Polymer.IronButtonState, 133 Polymer.IronControlState, 134 Polymer.IronFormElementBehavior, 135 Polymer.IronValidatableBehavior 136 ], 137 138 properties: { 139 /** 140 * The derived "label" of the currently selected item. This value 141 * is the `label` property on the selected item if set, or else the 142 * trimmed text content of the selected item. 143 */ 144 selectedItemLabel: { 145 type: String, 146 notify: true, 147 readOnly: true 148 }, 149 150 /** 151 * The last selected item. An item is selected if the dropdown menu has 152 * a child with class `dropdown-content`, and that child triggers an 153 * `iron-select` event with the selected `item` in the `detail`. 154 * 155 * @type {?Object} 156 */ 157 selectedItem: { 158 type: Object, 159 notify: true, 160 readOnly: true 161 }, 162 163 /** 164 * The value for this element that will be used when submitting in 165 * a form. It is read only, and will always have the same value 166 * as `selectedItemLabel`. 167 */ 168 value: { 169 type: String, 170 notify: true, 171 readOnly: true 172 }, 173 174 /** 175 * The label for the dropdown. 176 */ 177 label: { 178 type: String 179 }, 180 181 /** 182 * The placeholder for the dropdown. 183 */ 184 placeholder: { 185 type: String 186 }, 187 188 /** 189 * The error message to display when invalid. 190 */ 191 errorMessage: { 192 type: String 193 }, 194 195 /** 196 * True if the dropdown is open. Otherwise, false. 197 */ 198 opened: { 199 type: Boolean, 200 notify: true, 201 value: false, 202 observer: '_openedChanged' 203 }, 204 205 /** 206 * By default, the dropdown will constrain scrolling on the page 207 * to itself when opened. 208 * Set to true in order to prevent scroll from being constrained 209 * to the dropdown when it opens. 210 */ 211 allowOutsideScroll: { 212 type: Boolean, 213 value: false 214 }, 215 216 /** 217 * Set to true to disable the floating label. Bind this to the 218 * `<paper-input-container>`'s `noLabelFloat` property. 219 */ 220 noLabelFloat: { 221 type: Boolean, 222 value: false, 223 reflectToAttribute: true 224 }, 225 226 /** 227 * Set to true to always float the label. Bind this to the 228 * `<paper-input-container>`'s `alwaysFloatLabel` property. 229 */ 230 alwaysFloatLabel: { 231 type: Boolean, 232 value: false 233 }, 234 235 /** 236 * Set to true to disable animations when opening and closing the 237 * dropdown. 238 */ 239 noAnimations: { 240 type: Boolean, 241 value: false 242 }, 243 244 /** 245 * The orientation against which to align the menu dropdown 246 * horizontally relative to the dropdown trigger. 247 */ 248 horizontalAlign: { 249 type: String, 250 value: 'right' 251 }, 252 253 /** 254 * The orientation against which to align the menu dropdown 255 * vertically relative to the dropdown trigger. 256 */ 257 verticalAlign: { 258 type: String, 259 value: 'top' 260 }, 261 262 /** 263 * If true, the `horizontalAlign` and `verticalAlign` properties will 264 * be considered preferences instead of strict requirements when 265 * positioning the dropdown and may be changed if doing so reduces 266 * the area of the dropdown falling outside of `fitInto`. 267 */ 268 dynamicAlign: { 269 type: Boolean 270 }, 271 272 /** 273 * Whether focus should be restored to the dropdown when the menu closes. 274 */ 275 restoreFocusOnClose: { 276 type: Boolean, 277 value: true 278 }, 279 }, 280 281 listeners: { 282 'tap': '_onTap' 283 }, 284 285 keyBindings: { 286 'up down': 'open', 287 'esc': 'close' 288 }, 289 290 hostAttributes: { 291 role: 'combobox', 292 'aria-autocomplete': 'none', 293 'aria-haspopup': 'true' 294 }, 295 296 observers: [ 297 '_selectedItemChanged(selectedItem)' 298 ], 299 300 attached: function() { 301 // NOTE(cdata): Due to timing, a preselected value in a `IronSelectable` 302 // child will cause an `iron-select` event to fire while the element is 303 // still in a `DocumentFragment`. This has the effect of causing 304 // handlers not to fire. So, we double check this value on attached: 305 var contentElement = this.contentElement; 306 if (contentElement && contentElement.selectedItem) { 307 this._setSelectedItem(contentElement.selectedItem); 308 } 309 }, 310 311 /** 312 * The content element that is contained by the dropdown menu, if any. 313 */ 314 get contentElement() { 315 return Polymer.dom(this.$.content).getDistributedNodes()[0]; 316 }, 317 318 /** 319 * Show the dropdown content. 320 */ 321 open: function() { 322 this.$.menuButton.open(); 323 }, 324 325 /** 326 * Hide the dropdown content. 327 */ 328 close: function() { 329 this.$.menuButton.close(); 330 }, 331 332 /** 333 * A handler that is called when `iron-select` is fired. 334 * 335 * @param {CustomEvent} event An `iron-select` event. 336 */ 337 _onIronSelect: function(event) { 338 this._setSelectedItem(event.detail.item); 339 }, 340 341 /** 342 * A handler that is called when `iron-deselect` is fired. 343 * 344 * @param {CustomEvent} event An `iron-deselect` event. 345 */ 346 _onIronDeselect: function(event) { 347 this._setSelectedItem(null); 348 }, 349 350 /** 351 * A handler that is called when the dropdown is tapped. 352 * 353 * @param {CustomEvent} event A tap event. 354 */ 355 _onTap: function(event) { 356 if (Polymer.Gestures.findOriginalTarget(event) === this) { 357 this.open(); 358 } 359 }, 360 361 /** 362 * Compute the label for the dropdown given a selected item. 363 * 364 * @param {Element} selectedItem A selected Element item, with an 365 * optional `label` property. 366 */ 367 _selectedItemChanged: function(selectedItem) { 368 var value = ''; 369 if (!selectedItem) { 370 value = ''; 371 } else { 372 value = selectedItem.label || selectedItem.getAttribute('label') || selectedItem.textContent.trim(); 373 } 374 375 this._setValue(value); 376 this._setSelectedItemLabel(value); 377 }, 378 379 /** 380 * Compute the vertical offset of the menu based on the value of 381 * `noLabelFloat`. 382 * 383 * @param {boolean} noLabelFloat True if the label should not float 384 * above the input, otherwise false. 385 */ 386 _computeMenuVerticalOffset: function(noLabelFloat) { 387 // NOTE(cdata): These numbers are somewhat magical because they are 388 // derived from the metrics of elements internal to `paper-input`'s 389 // template. The metrics will change depending on whether or not the 390 // input has a floating label. 391 return noLabelFloat ? -4 : 8; 392 }, 393 394 /** 395 * Returns false if the element is required and does not have a selection, 396 * and true otherwise. 397 * @param {*=} _value Ignored. 398 * @return {boolean} true if `required` is false, or if `required` is true 399 * and the element has a valid selection. 400 */ 401 _getValidity: function(_value) { 402 return this.disabled || !this.required || (this.required && !!this.value); 403 }, 404 405 _openedChanged: function() { 406 var openState = this.opened ? 'true' : 'false'; 407 var e = this.contentElement; 408 if (e) { 409 e.setAttribute('aria-expanded', openState); 410 } 411 } 412 }); 413 })(); 414 </script> 415</dom-module> 416