• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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    <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