Home | History | Annotate | Download | only in paper-dropdown-menu
      1 <!--
      2 @license
      3 Copyright (c) 2016 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-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 <!--
     25 Material design: [Dropdown menus](https://www.google.com/design/spec/components/buttons.html#buttons-dropdown-buttons)
     26 
     27 This is a faster, lighter version of `paper-dropdown-menu`, that does not
     28 use a `<paper-input>` internally. Use this element if you're concerned about
     29 the performance of this element, i.e., if you plan on using many dropdowns on
     30 the same page. Note that this element has a slightly different styling API
     31 than `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
     35 item is displayed in the control. If no item is selected, the `label` is
     36 displayed instead.
     37 
     38 Example:
     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 
     49 This example renders a dropdown menu with 4 options.
     50 
     51 The child element with the class `dropdown-content` is used as the dropdown
     52 menu. This can be a [`paper-listbox`](paper-listbox), or any other or
     53 element that acts like an [`iron-selector`](iron-selector).
     54 
     55 Specifically, the menu child must fire an
     56 [`iron-select`](iron-selector#event-iron-select) event when one of its
     57 children is selected, and an [`iron-deselect`](iron-selector#event-iron-deselect)
     58 event when a child is deselected. The selected or deselected item must
     59 be passed as the event's `detail.item` property.
     60 
     61 Applications can listen for the `iron-select` and `iron-deselect` events
     62 to react when options are selected and deselected.
     63 
     64 ### Styling
     65 
     66 The following custom properties and mixins are also available for styling:
     67 
     68 Custom 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 
     82 Note that in this element, the underline is just the bottom border of the "input".
     83 To 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