| <!-- |
| @license |
| Copyright (c) 2015 The Polymer Project Authors. All rights reserved. |
| This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt |
| The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt |
| Code distributed by Google as part of the polymer project is also |
| subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt |
| --> |
| |
| <link rel="import" href="../polymer/polymer.html"> |
| <link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> |
| <link rel="import" href="../iron-behaviors/iron-button-state.html"> |
| <link rel="import" href="../iron-behaviors/iron-control-state.html"> |
| <link rel="import" href="../iron-form-element-behavior/iron-form-element-behavior.html"> |
| <link rel="import" href="../iron-icon/iron-icon.html"> |
| <link rel="import" href="../iron-validatable-behavior/iron-validatable-behavior.html"> |
| <link rel="import" href="../paper-input/paper-input.html"> |
| <link rel="import" href="../paper-menu-button/paper-menu-button.html"> |
| <link rel="import" href="../paper-ripple/paper-ripple.html"> |
| <link rel="import" href="../paper-styles/default-theme.html"> |
| |
| <link rel="import" href="paper-dropdown-menu-icons.html"> |
| <link rel="import" href="paper-dropdown-menu-shared-styles.html"> |
| |
| <!-- |
| Material design: [Dropdown menus](https://www.google.com/design/spec/components/buttons.html#buttons-dropdown-buttons) |
| |
| `paper-dropdown-menu` is similar to a native browser select element. |
| `paper-dropdown-menu` works with selectable content. The currently selected |
| item is displayed in the control. If no item is selected, the `label` is |
| displayed instead. |
| |
| Example: |
| |
| <paper-dropdown-menu label="Your favourite pastry"> |
| <paper-listbox class="dropdown-content"> |
| <paper-item>Croissant</paper-item> |
| <paper-item>Donut</paper-item> |
| <paper-item>Financier</paper-item> |
| <paper-item>Madeleine</paper-item> |
| </paper-listbox> |
| </paper-dropdown-menu> |
| |
| This example renders a dropdown menu with 4 options. |
| |
| The child element with the class `dropdown-content` is used as the dropdown |
| menu. This can be a [`paper-listbox`](paper-listbox), or any other or |
| element that acts like an [`iron-selector`](iron-selector). |
| |
| Specifically, the menu child must fire an |
| [`iron-select`](iron-selector#event-iron-select) event when one of its |
| children is selected, and an [`iron-deselect`](iron-selector#event-iron-deselect) |
| event when a child is deselected. The selected or deselected item must |
| be passed as the event's `detail.item` property. |
| |
| Applications can listen for the `iron-select` and `iron-deselect` events |
| to react when options are selected and deselected. |
| |
| ### Styling |
| |
| The following custom properties and mixins are also available for styling: |
| |
| Custom property | Description | Default |
| ----------------|-------------|---------- |
| `--paper-dropdown-menu` | A mixin that is applied to the element host | `{}` |
| `--paper-dropdown-menu-disabled` | A mixin that is applied to the element host when disabled | `{}` |
| `--paper-dropdown-menu-ripple` | A mixin that is applied to the internal ripple | `{}` |
| `--paper-dropdown-menu-button` | A mixin that is applied to the internal menu button | `{}` |
| `--paper-dropdown-menu-input` | A mixin that is applied to the internal paper input | `{}` |
| `--paper-dropdown-menu-icon` | A mixin that is applied to the internal icon | `{}` |
| |
| You can also use any of the `paper-input-container` and `paper-menu-button` |
| style mixins and custom properties to style the internal input and menu button |
| respectively. |
| |
| @group Paper Elements |
| @element paper-dropdown-menu |
| @hero hero.svg |
| @demo demo/index.html |
| --> |
| |
| <dom-module id="paper-dropdown-menu"> |
| <template> |
| <style include="paper-dropdown-menu-shared-styles"></style> |
| |
| <!-- this div fulfills an a11y requirement for combobox, do not remove --> |
| <span role="button"></span> |
| <paper-menu-button |
| id="menuButton" |
| vertical-align="[[verticalAlign]]" |
| horizontal-align="[[horizontalAlign]]" |
| dynamic-align="[[dynamicAlign]]" |
| vertical-offset="[[_computeMenuVerticalOffset(noLabelFloat)]]" |
| disabled="[[disabled]]" |
| no-animations="[[noAnimations]]" |
| on-iron-select="_onIronSelect" |
| on-iron-deselect="_onIronDeselect" |
| opened="{{opened}}" |
| close-on-activate |
| allow-outside-scroll="[[allowOutsideScroll]]" |
| restore-focus-on-close="[[restoreFocusOnClose]]"> |
| <div class="dropdown-trigger"> |
| <paper-ripple></paper-ripple> |
| <!-- paper-input has type="text" for a11y, do not remove --> |
| <paper-input |
| type="text" |
| invalid="[[invalid]]" |
| readonly |
| disabled="[[disabled]]" |
| value="[[selectedItemLabel]]" |
| placeholder="[[placeholder]]" |
| error-message="[[errorMessage]]" |
| always-float-label="[[alwaysFloatLabel]]" |
| no-label-float="[[noLabelFloat]]" |
| label="[[label]]"> |
| <iron-icon icon="paper-dropdown-menu:arrow-drop-down" suffix></iron-icon> |
| </paper-input> |
| </div> |
| <content id="content" select=".dropdown-content"></content> |
| </paper-menu-button> |
| </template> |
| |
| <script> |
| (function() { |
| 'use strict'; |
| |
| Polymer({ |
| is: 'paper-dropdown-menu', |
| |
| behaviors: [ |
| Polymer.IronButtonState, |
| Polymer.IronControlState, |
| Polymer.IronFormElementBehavior, |
| Polymer.IronValidatableBehavior |
| ], |
| |
| properties: { |
| /** |
| * The derived "label" of the currently selected item. This value |
| * is the `label` property on the selected item if set, or else the |
| * trimmed text content of the selected item. |
| */ |
| selectedItemLabel: { |
| type: String, |
| notify: true, |
| readOnly: true |
| }, |
| |
| /** |
| * The last selected item. An item is selected if the dropdown menu has |
| * a child with class `dropdown-content`, and that child triggers an |
| * `iron-select` event with the selected `item` in the `detail`. |
| * |
| * @type {?Object} |
| */ |
| selectedItem: { |
| type: Object, |
| notify: true, |
| readOnly: true |
| }, |
| |
| /** |
| * The value for this element that will be used when submitting in |
| * a form. It is read only, and will always have the same value |
| * as `selectedItemLabel`. |
| */ |
| value: { |
| type: String, |
| notify: true, |
| readOnly: true |
| }, |
| |
| /** |
| * The label for the dropdown. |
| */ |
| label: { |
| type: String |
| }, |
| |
| /** |
| * The placeholder for the dropdown. |
| */ |
| placeholder: { |
| type: String |
| }, |
| |
| /** |
| * The error message to display when invalid. |
| */ |
| errorMessage: { |
| type: String |
| }, |
| |
| /** |
| * True if the dropdown is open. Otherwise, false. |
| */ |
| opened: { |
| type: Boolean, |
| notify: true, |
| value: false, |
| observer: '_openedChanged' |
| }, |
| |
| /** |
| * By default, the dropdown will constrain scrolling on the page |
| * to itself when opened. |
| * Set to true in order to prevent scroll from being constrained |
| * to the dropdown when it opens. |
| */ |
| allowOutsideScroll: { |
| type: Boolean, |
| value: false |
| }, |
| |
| /** |
| * Set to true to disable the floating label. Bind this to the |
| * `<paper-input-container>`'s `noLabelFloat` property. |
| */ |
| noLabelFloat: { |
| type: Boolean, |
| value: false, |
| reflectToAttribute: true |
| }, |
| |
| /** |
| * Set to true to always float the label. Bind this to the |
| * `<paper-input-container>`'s `alwaysFloatLabel` property. |
| */ |
| alwaysFloatLabel: { |
| type: Boolean, |
| value: false |
| }, |
| |
| /** |
| * Set to true to disable animations when opening and closing the |
| * dropdown. |
| */ |
| noAnimations: { |
| type: Boolean, |
| value: false |
| }, |
| |
| /** |
| * The orientation against which to align the menu dropdown |
| * horizontally relative to the dropdown trigger. |
| */ |
| horizontalAlign: { |
| type: String, |
| value: 'right' |
| }, |
| |
| /** |
| * The orientation against which to align the menu dropdown |
| * vertically relative to the dropdown trigger. |
| */ |
| verticalAlign: { |
| type: String, |
| value: 'top' |
| }, |
| |
| /** |
| * If true, the `horizontalAlign` and `verticalAlign` properties will |
| * be considered preferences instead of strict requirements when |
| * positioning the dropdown and may be changed if doing so reduces |
| * the area of the dropdown falling outside of `fitInto`. |
| */ |
| dynamicAlign: { |
| type: Boolean |
| }, |
| |
| /** |
| * Whether focus should be restored to the dropdown when the menu closes. |
| */ |
| restoreFocusOnClose: { |
| type: Boolean, |
| value: true |
| }, |
| }, |
| |
| listeners: { |
| 'tap': '_onTap' |
| }, |
| |
| keyBindings: { |
| 'up down': 'open', |
| 'esc': 'close' |
| }, |
| |
| hostAttributes: { |
| role: 'combobox', |
| 'aria-autocomplete': 'none', |
| 'aria-haspopup': 'true' |
| }, |
| |
| observers: [ |
| '_selectedItemChanged(selectedItem)' |
| ], |
| |
| attached: function() { |
| // NOTE(cdata): Due to timing, a preselected value in a `IronSelectable` |
| // child will cause an `iron-select` event to fire while the element is |
| // still in a `DocumentFragment`. This has the effect of causing |
| // handlers not to fire. So, we double check this value on attached: |
| var contentElement = this.contentElement; |
| if (contentElement && contentElement.selectedItem) { |
| this._setSelectedItem(contentElement.selectedItem); |
| } |
| }, |
| |
| /** |
| * The content element that is contained by the dropdown menu, if any. |
| */ |
| get contentElement() { |
| return Polymer.dom(this.$.content).getDistributedNodes()[0]; |
| }, |
| |
| /** |
| * Show the dropdown content. |
| */ |
| open: function() { |
| this.$.menuButton.open(); |
| }, |
| |
| /** |
| * Hide the dropdown content. |
| */ |
| close: function() { |
| this.$.menuButton.close(); |
| }, |
| |
| /** |
| * A handler that is called when `iron-select` is fired. |
| * |
| * @param {CustomEvent} event An `iron-select` event. |
| */ |
| _onIronSelect: function(event) { |
| this._setSelectedItem(event.detail.item); |
| }, |
| |
| /** |
| * A handler that is called when `iron-deselect` is fired. |
| * |
| * @param {CustomEvent} event An `iron-deselect` event. |
| */ |
| _onIronDeselect: function(event) { |
| this._setSelectedItem(null); |
| }, |
| |
| /** |
| * A handler that is called when the dropdown is tapped. |
| * |
| * @param {CustomEvent} event A tap event. |
| */ |
| _onTap: function(event) { |
| if (Polymer.Gestures.findOriginalTarget(event) === this) { |
| this.open(); |
| } |
| }, |
| |
| /** |
| * Compute the label for the dropdown given a selected item. |
| * |
| * @param {Element} selectedItem A selected Element item, with an |
| * optional `label` property. |
| */ |
| _selectedItemChanged: function(selectedItem) { |
| var value = ''; |
| if (!selectedItem) { |
| value = ''; |
| } else { |
| value = selectedItem.label || selectedItem.getAttribute('label') || selectedItem.textContent.trim(); |
| } |
| |
| this._setValue(value); |
| this._setSelectedItemLabel(value); |
| }, |
| |
| /** |
| * Compute the vertical offset of the menu based on the value of |
| * `noLabelFloat`. |
| * |
| * @param {boolean} noLabelFloat True if the label should not float |
| * above the input, otherwise false. |
| */ |
| _computeMenuVerticalOffset: function(noLabelFloat) { |
| // NOTE(cdata): These numbers are somewhat magical because they are |
| // derived from the metrics of elements internal to `paper-input`'s |
| // template. The metrics will change depending on whether or not the |
| // input has a floating label. |
| return noLabelFloat ? -4 : 8; |
| }, |
| |
| /** |
| * Returns false if the element is required and does not have a selection, |
| * and true otherwise. |
| * @param {*=} _value Ignored. |
| * @return {boolean} true if `required` is false, or if `required` is true |
| * and the element has a valid selection. |
| */ |
| _getValidity: function(_value) { |
| return this.disabled || !this.required || (this.required && !!this.value); |
| }, |
| |
| _openedChanged: function() { |
| var openState = this.opened ? 'true' : 'false'; |
| var e = this.contentElement; |
| if (e) { |
| e.setAttribute('aria-expanded', openState); |
| } |
| } |
| }); |
| })(); |
| </script> |
| </dom-module> |