• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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">&nbsp;</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