• 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-icon/iron-icon.html">
17<link rel="import" href="../iron-validatable-behavior/iron-validatable-behavior.html">
18<link rel="import" href="../paper-behaviors/paper-ripple-behavior.html">
19<link rel="import" href="../paper-menu-button/paper-menu-button.html">
20<link rel="import" href="../paper-styles/default-theme.html">
21
22<link rel="import" href="paper-dropdown-menu-icons.html">
23<link rel="import" href="paper-dropdown-menu-shared-styles.html">
24
25<!--
26Material design: [Dropdown menus](https://www.google.com/design/spec/components/buttons.html#buttons-dropdown-buttons)
27
28This is a faster, lighter version of `paper-dropdown-menu`, that does not
29use a `<paper-input>` internally. Use this element if you're concerned about
30the performance of this element, i.e., if you plan on using many dropdowns on
31the same page. Note that this element has a slightly different styling API
32than `paper-dropdown-menu`.
33
34`paper-dropdown-menu-light` is similar to a native browser select element.
35`paper-dropdown-menu-light` works with selectable content. The currently selected
36item is displayed in the control. If no item is selected, the `label` is
37displayed instead.
38
39Example:
40
41    <paper-dropdown-menu-light label="Your favourite pastry">
42      <paper-listbox class="dropdown-content">
43        <paper-item>Croissant</paper-item>
44        <paper-item>Donut</paper-item>
45        <paper-item>Financier</paper-item>
46        <paper-item>Madeleine</paper-item>
47      </paper-listbox>
48    </paper-dropdown-menu-light>
49
50This example renders a dropdown menu with 4 options.
51
52The child element with the class `dropdown-content` is used as the dropdown
53menu. This can be a [`paper-listbox`](paper-listbox), or any other or
54element that acts like an [`iron-selector`](iron-selector).
55
56Specifically, the menu child must fire an
57[`iron-select`](iron-selector#event-iron-select) event when one of its
58children is selected, and an [`iron-deselect`](iron-selector#event-iron-deselect)
59event when a child is deselected. The selected or deselected item must
60be passed as the event's `detail.item` property.
61
62Applications can listen for the `iron-select` and `iron-deselect` events
63to react when options are selected and deselected.
64
65### Styling
66
67The following custom properties and mixins are also available for styling:
68
69Custom property | Description | Default
70----------------|-------------|----------
71`--paper-dropdown-menu` | A mixin that is applied to the element host | `{}`
72`--paper-dropdown-menu-disabled` | A mixin that is applied to the element host when disabled | `{}`
73`--paper-dropdown-menu-ripple` | A mixin that is applied to the internal ripple | `{}`
74`--paper-dropdown-menu-button` | A mixin that is applied to the internal menu button | `{}`
75`--paper-dropdown-menu-icon` | A mixin that is applied to the internal icon | `{}`
76`--paper-dropdown-menu-disabled-opacity` | The opacity of the dropdown when disabled  | `0.33`
77`--paper-dropdown-menu-color` | The color of the input/label/underline when the dropdown is unfocused | `--primary-text-color`
78`--paper-dropdown-menu-focus-color` | The color of the label/underline when the dropdown is focused  | `--primary-color`
79`--paper-dropdown-error-color` | The color of the label/underline when the dropdown is invalid  | `--error-color`
80`--paper-dropdown-menu-label` | Mixin applied to the label | `{}`
81`--paper-dropdown-menu-input` | Mixin appled to the input | `{}`
82
83Note that in this element, the underline is just the bottom border of the "input".
84To style it:
85
86    <style is=custom-style>
87      paper-dropdown-menu-light.custom {
88        --paper-dropdown-menu-input: {
89          border-bottom: 2px dashed lavender;
90        };
91    </style>
92
93@group Paper Elements
94@element paper-dropdown-menu-light
95@hero hero.svg
96@demo demo/index.html
97-->
98
99<dom-module id="paper-dropdown-menu-light">
100  <template>
101    <style include="paper-dropdown-menu-shared-styles">
102      :host(:focus) {
103        outline: none;
104      }
105
106      :host {
107        width: 200px;  /* Default size of an <input> */
108      }
109
110      /**
111       * All of these styles below are for styling the fake-input display
112       */
113      .dropdown-trigger {
114        box-sizing: border-box;
115        position: relative;
116        width: 100%;
117        padding: 16px 0 8px 0;
118      }
119
120      :host([disabled]) .dropdown-trigger {
121        pointer-events: none;
122        opacity: var(--paper-dropdown-menu-disabled-opacity, 0.33);
123      }
124
125      :host([no-label-float]) .dropdown-trigger {
126        padding-top: 8px;   /* If there's no label, we need less space up top. */
127      }
128
129      #input {
130        @apply(--paper-font-subhead);
131        @apply(--paper-font-common-nowrap);
132        line-height: 1.5;
133        border-bottom: 1px solid var(--paper-dropdown-menu-color, --secondary-text-color);
134        color: var(--paper-dropdown-menu-color, --primary-text-color);
135        width: 100%;
136        box-sizing: border-box;
137        padding: 12px 20px 0 0;   /* Right padding so that text doesn't overlap the icon */
138        outline: none;
139        @apply(--paper-dropdown-menu-input);
140      }
141
142      :host-context([dir="rtl"]) #input {
143        padding-right: 0px;
144        padding-left: 20px;
145      }
146
147      :host([disabled]) #input {
148        border-bottom: 1px dashed var(--paper-dropdown-menu-color, --secondary-text-color);
149      }
150
151      :host([invalid]) #input {
152        border-bottom: 2px solid var(--paper-dropdown-error-color, --error-color);
153      }
154
155      :host([no-label-float]) #input {
156        padding-top: 0;   /* If there's no label, we need less space up top. */
157      }
158
159      label {
160        @apply(--paper-font-subhead);
161        @apply(--paper-font-common-nowrap);
162        display: block;
163        position: absolute;
164        bottom: 0;
165        left: 0;
166        right: 0;
167        /**
168         * The container has a 16px top padding, and there's 12px of padding
169         * between the input and the label (from the input's padding-top)
170         */
171        top: 28px;
172        box-sizing: border-box;
173        width: 100%;
174        padding-right: 20px;    /* Right padding so that text doesn't overlap the icon */
175        text-align: left;
176        transition-duration: .2s;
177        transition-timing-function: cubic-bezier(.4,0,.2,1);
178        color: var(--paper-dropdown-menu-color, --secondary-text-color);
179        @apply(--paper-dropdown-menu-label);
180      }
181
182      :host-context([dir="rtl"]) label {
183        padding-right: 0px;
184        padding-left: 20px;
185      }
186
187      :host([no-label-float]) label {
188        top: 8px;
189        /* Since the label doesn't need to float, remove the animation duration
190        which slows down visibility changes (i.e. when a selection is made) */
191        transition-duration: 0s;
192      }
193
194      label.label-is-floating {
195        font-size: 12px;
196        top: 8px;
197      }
198
199      label.label-is-hidden {
200        visibility: hidden;
201      }
202
203      :host([focused]) label.label-is-floating {
204        color: var(--paper-dropdown-menu-focus-color, --primary-color);
205      }
206
207      :host([invalid]) label.label-is-floating {
208        color: var(--paper-dropdown-error-color, --error-color);
209      }
210
211      /**
212       * Sets up the focused underline. It's initially hidden, and becomes
213       * visible when it's focused.
214       */
215      label:after {
216        background-color: var(--paper-dropdown-menu-focus-color, --primary-color);
217        bottom: 7px;    /* The container has an 8px bottom padding */
218        content: '';
219        height: 2px;
220        left: 45%;
221        position: absolute;
222        transition-duration: .2s;
223        transition-timing-function: cubic-bezier(.4,0,.2,1);
224        visibility: hidden;
225        width: 8px;
226        z-index: 10;
227      }
228
229      :host([invalid]) label:after {
230        background-color: var(--paper-dropdown-error-color, --error-color);
231      }
232
233      :host([no-label-float]) label:after {
234        bottom: 7px;    /* The container has a 8px bottom padding */
235      }
236
237      :host([focused]:not([disabled])) label:after {
238        left: 0;
239        visibility: visible;
240        width: 100%;
241      }
242
243      iron-icon {
244        position: absolute;
245        right: 0px;
246        bottom: 8px;    /* The container has an 8px bottom padding */
247        @apply(--paper-font-subhead);
248        color: var(--disabled-text-color);
249        @apply(--paper-dropdown-menu-icon);
250      }
251
252      :host-context([dir="rtl"]) iron-icon {
253        left: 0;
254        right: auto;
255      }
256
257      :host([no-label-float]) iron-icon {
258        margin-top: 0px;
259      }
260
261      .error {
262        display: inline-block;
263        visibility: hidden;
264        color: var(--paper-dropdown-error-color, --error-color);
265        @apply(--paper-font-caption);
266        position: absolute;
267        left:0;
268        right:0;
269        bottom: -12px;
270      }
271
272      :host([invalid]) .error {
273        visibility: visible;
274      }
275    </style>
276
277    <!-- this div fulfills an a11y requirement for combobox, do not remove -->
278    <span role="button"></span>
279    <paper-menu-button
280      id="menuButton"
281      vertical-align="[[verticalAlign]]"
282      horizontal-align="[[horizontalAlign]]"
283      vertical-offset="[[_computeMenuVerticalOffset(noLabelFloat)]]"
284      disabled="[[disabled]]"
285      no-animations="[[noAnimations]]"
286      on-iron-select="_onIronSelect"
287      on-iron-deselect="_onIronDeselect"
288      opened="{{opened}}"
289      close-on-activate
290      allow-outside-scroll="[[allowOutsideScroll]]">
291      <div class="dropdown-trigger">
292        <label class$="[[_computeLabelClass(noLabelFloat,alwaysFloatLabel,hasContent)]]">
293          [[label]]
294        </label>
295        <div id="input" tabindex="-1">&nbsp;</div>
296        <iron-icon icon="paper-dropdown-menu:arrow-drop-down"></iron-icon>
297        <span class="error">[[errorMessage]]</span>
298      </div>
299      <content id="content" select=".dropdown-content"></content>
300    </paper-menu-button>
301  </template>
302
303  <script>
304    (function() {
305      'use strict';
306
307      Polymer({
308        is: 'paper-dropdown-menu-light',
309
310        behaviors: [
311          Polymer.IronButtonState,
312          Polymer.IronControlState,
313          Polymer.PaperRippleBehavior,
314          Polymer.IronFormElementBehavior,
315          Polymer.IronValidatableBehavior
316        ],
317
318        properties: {
319          /**
320           * The derived "label" of the currently selected item. This value
321           * is the `label` property on the selected item if set, or else the
322           * trimmed text content of the selected item.
323           */
324          selectedItemLabel: {
325            type: String,
326            notify: true,
327            readOnly: true
328          },
329
330          /**
331           * The last selected item. An item is selected if the dropdown menu has
332           * a child with class `dropdown-content`, and that child triggers an
333           * `iron-select` event with the selected `item` in the `detail`.
334           *
335           * @type {?Object}
336           */
337          selectedItem: {
338            type: Object,
339            notify: true,
340            readOnly: true
341          },
342
343          /**
344           * The value for this element that will be used when submitting in
345           * a form. It is read only, and will always have the same value
346           * as `selectedItemLabel`.
347           */
348          value: {
349            type: String,
350            notify: true,
351            readOnly: true,
352            observer: '_valueChanged',
353          },
354
355          /**
356           * The label for the dropdown.
357           */
358          label: {
359            type: String
360          },
361
362          /**
363           * The placeholder for the dropdown.
364           */
365          placeholder: {
366            type: String
367          },
368
369          /**
370           * True if the dropdown is open. Otherwise, false.
371           */
372          opened: {
373            type: Boolean,
374            notify: true,
375            value: false,
376            observer: '_openedChanged'
377          },
378
379          /**
380           * By default, the dropdown will constrain scrolling on the page
381           * to itself when opened.
382           * Set to true in order to prevent scroll from being constrained
383           * to the dropdown when it opens.
384           */
385          allowOutsideScroll: {
386            type: Boolean,
387            value: false
388          },
389
390          /**
391           * Set to true to disable the floating label. Bind this to the
392           * `<paper-input-container>`'s `noLabelFloat` property.
393           */
394          noLabelFloat: {
395              type: Boolean,
396              value: false,
397              reflectToAttribute: true
398          },
399
400          /**
401           * Set to true to always float the label. Bind this to the
402           * `<paper-input-container>`'s `alwaysFloatLabel` property.
403           */
404          alwaysFloatLabel: {
405            type: Boolean,
406            value: false
407          },
408
409          /**
410           * Set to true to disable animations when opening and closing the
411           * dropdown.
412           */
413          noAnimations: {
414            type: Boolean,
415            value: false
416          },
417
418          /**
419           * The orientation against which to align the menu dropdown
420           * horizontally relative to the dropdown trigger.
421           */
422          horizontalAlign: {
423            type: String,
424            value: 'right'
425          },
426
427          /**
428           * The orientation against which to align the menu dropdown
429           * vertically relative to the dropdown trigger.
430           */
431          verticalAlign: {
432            type: String,
433            value: 'top'
434          },
435
436          hasContent: {
437            type: Boolean,
438            readOnly: true
439          }
440        },
441
442        listeners: {
443          'tap': '_onTap'
444        },
445
446        keyBindings: {
447          'up down': 'open',
448          'esc': 'close'
449        },
450
451        hostAttributes: {
452          tabindex: 0,
453          role: 'combobox',
454          'aria-autocomplete': 'none',
455          'aria-haspopup': 'true'
456        },
457
458        observers: [
459          '_selectedItemChanged(selectedItem)'
460        ],
461
462        attached: function() {
463          // NOTE(cdata): Due to timing, a preselected value in a `IronSelectable`
464          // child will cause an `iron-select` event to fire while the element is
465          // still in a `DocumentFragment`. This has the effect of causing
466          // handlers not to fire. So, we double check this value on attached:
467          var contentElement = this.contentElement;
468          if (contentElement && contentElement.selectedItem) {
469            this._setSelectedItem(contentElement.selectedItem);
470          }
471        },
472
473        /**
474         * The content element that is contained by the dropdown menu, if any.
475         */
476        get contentElement() {
477          return Polymer.dom(this.$.content).getDistributedNodes()[0];
478        },
479
480        /**
481         * Show the dropdown content.
482         */
483        open: function() {
484          this.$.menuButton.open();
485        },
486
487        /**
488         * Hide the dropdown content.
489         */
490        close: function() {
491          this.$.menuButton.close();
492        },
493
494        /**
495         * A handler that is called when `iron-select` is fired.
496         *
497         * @param {CustomEvent} event An `iron-select` event.
498         */
499        _onIronSelect: function(event) {
500          this._setSelectedItem(event.detail.item);
501        },
502
503        /**
504         * A handler that is called when `iron-deselect` is fired.
505         *
506         * @param {CustomEvent} event An `iron-deselect` event.
507         */
508        _onIronDeselect: function(event) {
509          this._setSelectedItem(null);
510        },
511
512        /**
513         * A handler that is called when the dropdown is tapped.
514         *
515         * @param {CustomEvent} event A tap event.
516         */
517        _onTap: function(event) {
518          if (Polymer.Gestures.findOriginalTarget(event) === this) {
519            this.open();
520          }
521        },
522
523        /**
524         * Compute the label for the dropdown given a selected item.
525         *
526         * @param {Element} selectedItem A selected Element item, with an
527         * optional `label` property.
528         */
529        _selectedItemChanged: function(selectedItem) {
530          var value = '';
531          if (!selectedItem) {
532            value = '';
533          } else {
534            value = selectedItem.label || selectedItem.getAttribute('label') || selectedItem.textContent.trim();
535          }
536
537          this._setValue(value);
538          this._setSelectedItemLabel(value);
539        },
540
541        /**
542         * Compute the vertical offset of the menu based on the value of
543         * `noLabelFloat`.
544         *
545         * @param {boolean} noLabelFloat True if the label should not float
546         * above the input, otherwise false.
547         */
548        _computeMenuVerticalOffset: function(noLabelFloat) {
549          // NOTE(cdata): These numbers are somewhat magical because they are
550          // derived from the metrics of elements internal to `paper-input`'s
551          // template. The metrics will change depending on whether or not the
552          // input has a floating label.
553          return noLabelFloat ? -4 : 8;
554        },
555
556        /**
557         * Returns false if the element is required and does not have a selection,
558         * and true otherwise.
559         * @param {*=} _value Ignored.
560         * @return {boolean} true if `required` is false, or if `required` is true
561         * and the element has a valid selection.
562         */
563        _getValidity: function(_value) {
564          return this.disabled || !this.required || (this.required && !!this.value);
565        },
566
567        _openedChanged: function() {
568          var openState = this.opened ? 'true' : 'false';
569          var e = this.contentElement;
570          if (e) {
571            e.setAttribute('aria-expanded', openState);
572          }
573        },
574
575        _computeLabelClass: function(noLabelFloat, alwaysFloatLabel, hasContent) {
576          var cls = '';
577          if (noLabelFloat === true) {
578            return hasContent ? 'label-is-hidden' : '';
579          }
580
581          if (hasContent || alwaysFloatLabel === true) {
582            cls += ' label-is-floating';
583          }
584          return cls;
585        },
586
587        _valueChanged: function() {
588          // Only update if it's actually different.
589          if (this.$.input && this.$.input.textContent !== this.value) {
590            this.$.input.textContent = this.value;
591          }
592          this._setHasContent(!!this.value);
593        },
594      });
595    })();
596  </script>
597</dom-module>
598