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-control-state.html">
14<link rel="import" href="../iron-dropdown/iron-dropdown.html">
15<link rel="import" href="../neon-animation/animations/fade-in-animation.html">
16<link rel="import" href="../neon-animation/animations/fade-out-animation.html">
17<link rel="import" href="../paper-styles/default-theme.html">
18<link rel="import" href="../paper-styles/shadow.html">
19<link rel="import" href="paper-menu-button-animations.html">
20
21<!--
22Material design: [Dropdown buttons](https://www.google.com/design/spec/components/buttons.html#buttons-dropdown-buttons)
23
24`paper-menu-button` allows one to compose a designated "trigger" element with
25another element that represents "content", to create a dropdown menu that
26displays the "content" when the "trigger" is clicked.
27
28The child element with the class `dropdown-trigger` will be used as the
29"trigger" element. The child element with the class `dropdown-content` will be
30used as the "content" element.
31
32The `paper-menu-button` is sensitive to its content's `iron-select` events. If
33the "content" element triggers an `iron-select` event, the `paper-menu-button`
34will close automatically.
35
36Example:
37
38    <paper-menu-button>
39      <paper-icon-button icon="menu" class="dropdown-trigger"></paper-icon-button>
40      <paper-menu class="dropdown-content">
41        <paper-item>Share</paper-item>
42        <paper-item>Settings</paper-item>
43        <paper-item>Help</paper-item>
44      </paper-menu>
45    </paper-menu-button>
46
47### Styling
48
49The following custom properties and mixins are also available for styling:
50
51Custom property | Description | Default
52----------------|-------------|----------
53`--paper-menu-button-dropdown-background` | Background color of the paper-menu-button dropdown | `--primary-background-color`
54`--paper-menu-button` | Mixin applied to the paper-menu-button | `{}`
55`--paper-menu-button-disabled` | Mixin applied to the paper-menu-button when disabled | `{}`
56`--paper-menu-button-dropdown` | Mixin applied to the paper-menu-button dropdown | `{}`
57`--paper-menu-button-content` | Mixin applied to the paper-menu-button content | `{}`
58
59@hero hero.svg
60@demo demo/index.html
61-->
62
63<dom-module id="paper-menu-button">
64  <template>
65    <style>
66      :host {
67        display: inline-block;
68        position: relative;
69        padding: 8px;
70        outline: none;
71
72        @apply(--paper-menu-button);
73      }
74
75      :host([disabled]) {
76        cursor: auto;
77        color: var(--disabled-text-color);
78
79        @apply(--paper-menu-button-disabled);
80      }
81
82      iron-dropdown {
83        @apply(--paper-menu-button-dropdown);
84      }
85
86      .dropdown-content {
87        @apply(--shadow-elevation-2dp);
88
89        position: relative;
90        border-radius: 2px;
91        background-color: var(--paper-menu-button-dropdown-background, --primary-background-color);
92
93        @apply(--paper-menu-button-content);
94      }
95
96      :host([vertical-align="top"]) .dropdown-content {
97        margin-bottom: 20px;
98        margin-top: -10px;
99        top: 10px;
100      }
101
102      :host([vertical-align="bottom"]) .dropdown-content {
103        bottom: 10px;
104        margin-bottom: -10px;
105        margin-top: 20px;
106      }
107
108      #trigger {
109        cursor: pointer;
110      }
111    </style>
112
113    <div id="trigger" on-tap="toggle">
114      <content select=".dropdown-trigger"></content>
115    </div>
116
117    <iron-dropdown
118      id="dropdown"
119      opened="{{opened}}"
120      horizontal-align="[[horizontalAlign]]"
121      vertical-align="[[verticalAlign]]"
122      dynamic-align="[[dynamicAlign]]"
123      horizontal-offset="[[horizontalOffset]]"
124      vertical-offset="[[verticalOffset]]"
125      no-overlap="[[noOverlap]]"
126      open-animation-config="[[openAnimationConfig]]"
127      close-animation-config="[[closeAnimationConfig]]"
128      no-animations="[[noAnimations]]"
129      focus-target="[[_dropdownContent]]"
130      allow-outside-scroll="[[allowOutsideScroll]]"
131      restore-focus-on-close="[[restoreFocusOnClose]]"
132      on-iron-overlay-canceled="__onIronOverlayCanceled">
133      <div class="dropdown-content">
134        <content id="content" select=".dropdown-content"></content>
135      </div>
136    </iron-dropdown>
137  </template>
138
139  <script>
140    (function() {
141      'use strict';
142
143      var config = {
144        ANIMATION_CUBIC_BEZIER: 'cubic-bezier(.3,.95,.5,1)',
145        MAX_ANIMATION_TIME_MS: 400
146      };
147
148      var PaperMenuButton = Polymer({
149        is: 'paper-menu-button',
150
151        /**
152         * Fired when the dropdown opens.
153         *
154         * @event paper-dropdown-open
155         */
156
157        /**
158         * Fired when the dropdown closes.
159         *
160         * @event paper-dropdown-close
161         */
162
163        behaviors: [
164          Polymer.IronA11yKeysBehavior,
165          Polymer.IronControlState
166        ],
167
168        properties: {
169          /**
170           * True if the content is currently displayed.
171           */
172          opened: {
173            type: Boolean,
174            value: false,
175            notify: true,
176            observer: '_openedChanged'
177          },
178
179          /**
180           * The orientation against which to align the menu dropdown
181           * horizontally relative to the dropdown trigger.
182           */
183          horizontalAlign: {
184            type: String,
185            value: 'left',
186            reflectToAttribute: true
187          },
188
189          /**
190           * The orientation against which to align the menu dropdown
191           * vertically relative to the dropdown trigger.
192           */
193          verticalAlign: {
194            type: String,
195            value: 'top',
196            reflectToAttribute: true
197          },
198
199          /**
200           * If true, the `horizontalAlign` and `verticalAlign` properties will
201           * be considered preferences instead of strict requirements when
202           * positioning the dropdown and may be changed if doing so reduces
203           * the area of the dropdown falling outside of `fitInto`.
204           */
205          dynamicAlign: {
206            type: Boolean
207          },
208
209          /**
210           * A pixel value that will be added to the position calculated for the
211           * given `horizontalAlign`. Use a negative value to offset to the
212           * left, or a positive value to offset to the right.
213           */
214          horizontalOffset: {
215            type: Number,
216            value: 0,
217            notify: true
218          },
219
220          /**
221           * A pixel value that will be added to the position calculated for the
222           * given `verticalAlign`. Use a negative value to offset towards the
223           * top, or a positive value to offset towards the bottom.
224           */
225          verticalOffset: {
226            type: Number,
227            value: 0,
228            notify: true
229          },
230
231          /**
232           * If true, the dropdown will be positioned so that it doesn't overlap
233           * the button.
234           */
235          noOverlap: {
236            type: Boolean
237          },
238
239          /**
240           * Set to true to disable animations when opening and closing the
241           * dropdown.
242           */
243          noAnimations: {
244            type: Boolean,
245            value: false
246          },
247
248          /**
249           * Set to true to disable automatically closing the dropdown after
250           * a selection has been made.
251           */
252          ignoreSelect: {
253            type: Boolean,
254            value: false
255          },
256
257          /**
258           * Set to true to enable automatically closing the dropdown after an
259           * item has been activated, even if the selection did not change.
260           */
261          closeOnActivate: {
262            type: Boolean,
263            value: false
264          },
265
266          /**
267           * An animation config. If provided, this will be used to animate the
268           * opening of the dropdown.
269           */
270          openAnimationConfig: {
271            type: Object,
272            value: function() {
273              return [{
274                name: 'fade-in-animation',
275                timing: {
276                  delay: 100,
277                  duration: 200
278                }
279              }, {
280                name: 'paper-menu-grow-width-animation',
281                timing: {
282                  delay: 100,
283                  duration: 150,
284                  easing: config.ANIMATION_CUBIC_BEZIER
285                }
286              }, {
287                name: 'paper-menu-grow-height-animation',
288                timing: {
289                  delay: 100,
290                  duration: 275,
291                  easing: config.ANIMATION_CUBIC_BEZIER
292                }
293              }];
294            }
295          },
296
297          /**
298           * An animation config. If provided, this will be used to animate the
299           * closing of the dropdown.
300           */
301          closeAnimationConfig: {
302            type: Object,
303            value: function() {
304              return [{
305                name: 'fade-out-animation',
306                timing: {
307                  duration: 150
308                }
309              }, {
310                name: 'paper-menu-shrink-width-animation',
311                timing: {
312                  delay: 100,
313                  duration: 50,
314                  easing: config.ANIMATION_CUBIC_BEZIER
315                }
316              }, {
317                name: 'paper-menu-shrink-height-animation',
318                timing: {
319                  duration: 200,
320                  easing: 'ease-in'
321                }
322              }];
323            }
324          },
325
326          /**
327           * By default, the dropdown will constrain scrolling on the page
328           * to itself when opened.
329           * Set to true in order to prevent scroll from being constrained
330           * to the dropdown when it opens.
331           */
332          allowOutsideScroll: {
333            type: Boolean,
334            value: false
335          },
336
337          /**
338           * Whether focus should be restored to the button when the menu closes.
339           */
340          restoreFocusOnClose: {
341            type: Boolean,
342            value: true
343          },
344
345          /**
346           * This is the element intended to be bound as the focus target
347           * for the `iron-dropdown` contained by `paper-menu-button`.
348           */
349          _dropdownContent: {
350            type: Object
351          }
352        },
353
354        hostAttributes: {
355          role: 'group',
356          'aria-haspopup': 'true'
357        },
358
359        listeners: {
360          'iron-activate': '_onIronActivate',
361          'iron-select': '_onIronSelect'
362        },
363
364        /**
365         * The content element that is contained by the menu button, if any.
366         */
367        get contentElement() {
368          return Polymer.dom(this.$.content).getDistributedNodes()[0];
369        },
370
371        /**
372         * Toggles the drowpdown content between opened and closed.
373         */
374        toggle: function() {
375          if (this.opened) {
376            this.close();
377          } else {
378            this.open();
379          }
380        },
381
382        /**
383         * Make the dropdown content appear as an overlay positioned relative
384         * to the dropdown trigger.
385         */
386        open: function() {
387          if (this.disabled) {
388            return;
389          }
390
391          this.$.dropdown.open();
392        },
393
394        /**
395         * Hide the dropdown content.
396         */
397        close: function() {
398          this.$.dropdown.close();
399        },
400
401        /**
402         * When an `iron-select` event is received, the dropdown should
403         * automatically close on the assumption that a value has been chosen.
404         *
405         * @param {CustomEvent} event A CustomEvent instance with type
406         * set to `"iron-select"`.
407         */
408        _onIronSelect: function(event) {
409          if (!this.ignoreSelect) {
410            this.close();
411          }
412        },
413
414        /**
415         * Closes the dropdown when an `iron-activate` event is received if
416         * `closeOnActivate` is true.
417         *
418         * @param {CustomEvent} event A CustomEvent of type 'iron-activate'.
419         */
420        _onIronActivate: function(event) {
421          if (this.closeOnActivate) {
422            this.close();
423          }
424        },
425
426        /**
427         * When the dropdown opens, the `paper-menu-button` fires `paper-open`.
428         * When the dropdown closes, the `paper-menu-button` fires `paper-close`.
429         *
430         * @param {boolean} opened True if the dropdown is opened, otherwise false.
431         * @param {boolean} oldOpened The previous value of `opened`.
432         */
433        _openedChanged: function(opened, oldOpened) {
434          if (opened) {
435            // TODO(cdata): Update this when we can measure changes in distributed
436            // children in an idiomatic way.
437            // We poke this property in case the element has changed. This will
438            // cause the focus target for the `iron-dropdown` to be updated as
439            // necessary:
440            this._dropdownContent = this.contentElement;
441            this.fire('paper-dropdown-open');
442          } else if (oldOpened != null) {
443            this.fire('paper-dropdown-close');
444          }
445        },
446
447        /**
448         * If the dropdown is open when disabled becomes true, close the
449         * dropdown.
450         *
451         * @param {boolean} disabled True if disabled, otherwise false.
452         */
453        _disabledChanged: function(disabled) {
454          Polymer.IronControlState._disabledChanged.apply(this, arguments);
455          if (disabled && this.opened) {
456            this.close();
457          }
458        },
459
460        __onIronOverlayCanceled: function(event) {
461          var uiEvent = event.detail;
462          var target = Polymer.dom(uiEvent).rootTarget;
463          var trigger = this.$.trigger;
464          var path = Polymer.dom(uiEvent).path;
465
466          if (path.indexOf(trigger) > -1) {
467            event.preventDefault();
468          }
469        }
470      });
471
472      Object.keys(config).forEach(function (key) {
473        PaperMenuButton[key] = config[key];
474      });
475
476      Polymer.PaperMenuButton = PaperMenuButton;
477    })();
478  </script>
479</dom-module>
480