1 <!-- 2 @license 3 Copyright (c) 2015 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-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 <!-- 27 Material 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 31 item is displayed in the control. If no item is selected, the `label` is 32 displayed instead. 33 34 Example: 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 45 This example renders a dropdown menu with 4 options. 46 47 The child element with the class `dropdown-content` is used as the dropdown 48 menu. This can be a [`paper-listbox`](paper-listbox), or any other or 49 element that acts like an [`iron-selector`](iron-selector). 50 51 Specifically, the menu child must fire an 52 [`iron-select`](iron-selector#event-iron-select) event when one of its 53 children is selected, and an [`iron-deselect`](iron-selector#event-iron-deselect) 54 event when a child is deselected. The selected or deselected item must 55 be passed as the event's `detail.item` property. 56 57 Applications can listen for the `iron-select` and `iron-deselect` events 58 to react when options are selected and deselected. 59 60 ### Styling 61 62 The following custom properties and mixins are also available for styling: 63 64 Custom 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 73 You can also use any of the `paper-input-container` and `paper-menu-button` 74 style mixins and custom properties to style the internal input and menu button 75 respectively. 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 <div role="button"></div> 89 <paper-menu-button 90 id="menuButton" 91 vertical-align="[[verticalAlign]]" 92 horizontal-align="[[horizontalAlign]]" 93 vertical-offset="[[_computeMenuVerticalOffset(noLabelFloat)]]" 94 disabled="[[disabled]]" 95 no-animations="[[noAnimations]]" 96 on-iron-select="_onIronSelect" 97 on-iron-deselect="_onIronDeselect" 98 opened="{{opened}}"> 99 <div class="dropdown-trigger"> 100 <paper-ripple></paper-ripple> 101 <!-- paper-input has type="text" for a11y, do not remove --> 102 <paper-input 103 type="text" 104 invalid="[[invalid]]" 105 readonly 106 disabled="[[disabled]]" 107 value="[[selectedItemLabel]]" 108 placeholder="[[placeholder]]" 109 error-message="[[errorMessage]]" 110 always-float-label="[[alwaysFloatLabel]]" 111 no-label-float="[[noLabelFloat]]" 112 label="[[label]]"> 113 <iron-icon icon="paper-dropdown-menu:arrow-drop-down" suffix></iron-icon> 114 </paper-input> 115 </div> 116 <content id="content" select=".dropdown-content"></content> 117 </paper-menu-button> 118 </template> 119 120 <script> 121 (function() { 122 'use strict'; 123 124 Polymer({ 125 is: 'paper-dropdown-menu', 126 127 behaviors: [ 128 Polymer.IronButtonState, 129 Polymer.IronControlState, 130 Polymer.IronFormElementBehavior, 131 Polymer.IronValidatableBehavior 132 ], 133 134 properties: { 135 /** 136 * The derived "label" of the currently selected item. This value 137 * is the `label` property on the selected item if set, or else the 138 * trimmed text content of the selected item. 139 */ 140 selectedItemLabel: { 141 type: String, 142 notify: true, 143 readOnly: true 144 }, 145 146 /** 147 * The last selected item. An item is selected if the dropdown menu has 148 * a child with class `dropdown-content`, and that child triggers an 149 * `iron-select` event with the selected `item` in the `detail`. 150 * 151 * @type {?Object} 152 */ 153 selectedItem: { 154 type: Object, 155 notify: true, 156 readOnly: true 157 }, 158 159 /** 160 * The value for this element that will be used when submitting in 161 * a form. It is read only, and will always have the same value 162 * as `selectedItemLabel`. 163 */ 164 value: { 165 type: String, 166 notify: true, 167 readOnly: true 168 }, 169 170 /** 171 * The label for the dropdown. 172 */ 173 label: { 174 type: String 175 }, 176 177 /** 178 * The placeholder for the dropdown. 179 */ 180 placeholder: { 181 type: String 182 }, 183 184 /** 185 * The error message to display when invalid. 186 */ 187 errorMessage: { 188 type: String 189 }, 190 191 /** 192 * True if the dropdown is open. Otherwise, false. 193 */ 194 opened: { 195 type: Boolean, 196 notify: true, 197 value: false, 198 observer: '_openedChanged' 199 }, 200 201 /** 202 * Set to true to disable the floating label. Bind this to the 203 * `<paper-input-container>`'s `noLabelFloat` property. 204 */ 205 noLabelFloat: { 206 type: Boolean, 207 value: false, 208 reflectToAttribute: true 209 }, 210 211 /** 212 * Set to true to always float the label. Bind this to the 213 * `<paper-input-container>`'s `alwaysFloatLabel` property. 214 */ 215 alwaysFloatLabel: { 216 type: Boolean, 217 value: false 218 }, 219 220 /** 221 * Set to true to disable animations when opening and closing the 222 * dropdown. 223 */ 224 noAnimations: { 225 type: Boolean, 226 value: false 227 }, 228 229 /** 230 * The orientation against which to align the menu dropdown 231 * horizontally relative to the dropdown trigger. 232 */ 233 horizontalAlign: { 234 type: String, 235 value: 'right' 236 }, 237 238 /** 239 * The orientation against which to align the menu dropdown 240 * vertically relative to the dropdown trigger. 241 */ 242 verticalAlign: { 243 type: String, 244 value: 'top' 245 } 246 }, 247 248 listeners: { 249 'tap': '_onTap' 250 }, 251 252 keyBindings: { 253 'up down': 'open', 254 'esc': 'close' 255 }, 256 257 hostAttributes: { 258 role: 'combobox', 259 'aria-autocomplete': 'none', 260 'aria-haspopup': 'true' 261 }, 262 263 observers: [ 264 '_selectedItemChanged(selectedItem)' 265 ], 266 267 attached: function() { 268 // NOTE(cdata): Due to timing, a preselected value in a `IronSelectable` 269 // child will cause an `iron-select` event to fire while the element is 270 // still in a `DocumentFragment`. This has the effect of causing 271 // handlers not to fire. So, we double check this value on attached: 272 var contentElement = this.contentElement; 273 if (contentElement && contentElement.selectedItem) { 274 this._setSelectedItem(contentElement.selectedItem); 275 } 276 }, 277 278 /** 279 * The content element that is contained by the dropdown menu, if any. 280 */ 281 get contentElement() { 282 return Polymer.dom(this.$.content).getDistributedNodes()[0]; 283 }, 284 285 /** 286 * Show the dropdown content. 287 */ 288 open: function() { 289 this.$.menuButton.open(); 290 }, 291 292 /** 293 * Hide the dropdown content. 294 */ 295 close: function() { 296 this.$.menuButton.close(); 297 }, 298 299 /** 300 * A handler that is called when `iron-select` is fired. 301 * 302 * @param {CustomEvent} event An `iron-select` event. 303 */ 304 _onIronSelect: function(event) { 305 this._setSelectedItem(event.detail.item); 306 }, 307 308 /** 309 * A handler that is called when `iron-deselect` is fired. 310 * 311 * @param {CustomEvent} event An `iron-deselect` event. 312 */ 313 _onIronDeselect: function(event) { 314 this._setSelectedItem(null); 315 }, 316 317 /** 318 * A handler that is called when the dropdown is tapped. 319 * 320 * @param {CustomEvent} event A tap event. 321 */ 322 _onTap: function(event) { 323 if (Polymer.Gestures.findOriginalTarget(event) === this) { 324 this.open(); 325 } 326 }, 327 328 /** 329 * Compute the label for the dropdown given a selected item. 330 * 331 * @param {Element} selectedItem A selected Element item, with an 332 * optional `label` property. 333 */ 334 _selectedItemChanged: function(selectedItem) { 335 var value = ''; 336 if (!selectedItem) { 337 value = ''; 338 } else { 339 value = selectedItem.label || selectedItem.textContent.trim(); 340 } 341 342 this._setValue(value); 343 this._setSelectedItemLabel(value); 344 }, 345 346 /** 347 * Compute the vertical offset of the menu based on the value of 348 * `noLabelFloat`. 349 * 350 * @param {boolean} noLabelFloat True if the label should not float 351 * above the input, otherwise false. 352 */ 353 _computeMenuVerticalOffset: function(noLabelFloat) { 354 // NOTE(cdata): These numbers are somewhat magical because they are 355 // derived from the metrics of elements internal to `paper-input`'s 356 // template. The metrics will change depending on whether or not the 357 // input has a floating label. 358 return noLabelFloat ? -4 : 8; 359 }, 360 361 /** 362 * Returns false if the element is required and does not have a selection, 363 * and true otherwise. 364 * @param {*=} _value Ignored. 365 * @return {boolean} true if `required` is false, or if `required` is true 366 * and the element has a valid selection. 367 */ 368 _getValidity: function(_value) { 369 return this.disabled || !this.required || (this.required && !!this.value); 370 }, 371 372 _openedChanged: function() { 373 var openState = this.opened ? 'true' : 'false'; 374 var e = this.contentElement; 375 if (e) { 376 e.setAttribute('aria-expanded', openState); 377 } 378 } 379 }); 380 })(); 381 </script> 382 </dom-module> 383