Update Polymer and pull in iron-list
[chromium-blink-merge.git] / third_party / polymer / v1_0 / components-chromium / iron-dropdown / iron-dropdown-extracted.js
blob4423a426ed9730e2c3b5b4d54c8f695b1fc79a0b
2     (function() {
3       'use strict';
5       Polymer({
6         is: 'iron-dropdown',
8         behaviors: [
9           Polymer.IronControlState,
10           Polymer.IronA11yKeysBehavior,
11           Polymer.IronOverlayBehavior,
12           Polymer.NeonAnimationRunnerBehavior
13         ],
15         properties: {
16           /**
17            * The orientation against which to align the dropdown content
18            * horizontally relative to the dropdown trigger.
19            */
20           horizontalAlign: {
21             type: String,
22             value: 'left',
23             reflectToAttribute: true
24           },
26           /**
27            * The orientation against which to align the dropdown content
28            * vertically relative to the dropdown trigger.
29            */
30           verticalAlign: {
31             type: String,
32             value: 'top',
33             reflectToAttribute: true
34           },
36           /**
37            * A pixel value that will be added to the position calculated for the
38            * given `horizontalAlign`. Use a negative value to offset to the
39            * left, or a positive value to offset to the right.
40            */
41           horizontalOffset: {
42             type: Number,
43             value: 0,
44             notify: true
45           },
47           /**
48            * A pixel value that will be added to the position calculated for the
49            * given `verticalAlign`. Use a negative value to offset towards the
50            * top, or a positive value to offset towards the bottom.
51            */
52           verticalOffset: {
53             type: Number,
54             value: 0,
55             notify: true
56           },
58           /**
59            * The element that should be used to position the dropdown when
60            * it is opened.
61            */
62           positionTarget: {
63             type: Object,
64             observer: '_positionTargetChanged'
65           },
67           /**
68            * An animation config. If provided, this will be used to animate the
69            * opening of the dropdown.
70            */
71           openAnimationConfig: {
72             type: Object
73           },
75           /**
76            * An animation config. If provided, this will be used to animate the
77            * closing of the dropdown.
78            */
79           closeAnimationConfig: {
80             type: Object
81           },
83           /**
84            * If provided, this will be the element that will be focused when
85            * the dropdown opens.
86            */
87           focusTarget: {
88             type: Object
89           },
91           /**
92            * Set to true to disable animations when opening and closing the
93            * dropdown.
94            */
95           noAnimations: {
96             type: Boolean,
97             value: false
98           },
100           /**
101            * We memoize the positionTarget bounding rectangle so that we can
102            * limit the number of times it is queried per resize / relayout.
103            * @type {?Object}
104            */
105           _positionRectMemo: {
106             type: Object
107           }
108         },
110         listeners: {
111           'neon-animation-finish': '_onNeonAnimationFinish'
112         },
114         observers: [
115           '_updateOverlayPosition(verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
116         ],
118         attached: function() {
119           if (this.positionTarget === undefined) {
120             this.positionTarget = this._defaultPositionTarget;
121           }
122         },
124         /**
125          * The element that is contained by the dropdown, if any.
126          */
127         get containedElement() {
128           return Polymer.dom(this.$.content).getDistributedNodes()[0];
129         },
131         /**
132          * The element that should be focused when the dropdown opens.
133          */
134         get _focusTarget() {
135           return this.focusTarget || this.containedElement;
136         },
138         /**
139          * The element that should be used to position the dropdown when
140          * it opens, if no position target is configured.
141          */
142         get _defaultPositionTarget() {
143           var parent = Polymer.dom(this).parentNode;
145           if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
146             parent = parent.host;
147           }
149           return parent;
150         },
152         /**
153          * The bounding rect of the position target.
154          */
155         get _positionRect() {
156           if (!this._positionRectMemo && this.positionTarget) {
157             this._positionRectMemo = this.positionTarget.getBoundingClientRect();
158           }
160           return this._positionRectMemo;
161         },
163         /**
164          * The horizontal offset value used to position the dropdown.
165          */
166         get _horizontalAlignTargetValue() {
167           var target;
169           if (this.horizontalAlign === 'right') {
170             target = document.documentElement.clientWidth - this._positionRect.right;
171           } else {
172             target = this._positionRect.left;
173           }
175           target += this.horizontalOffset;
177           return Math.max(target, 0);
178         },
180         /**
181          * The vertical offset value used to position the dropdown.
182          */
183         get _verticalAlignTargetValue() {
184           var target;
186           if (this.verticalAlign === 'bottom') {
187             target = document.documentElement.clientHeight - this._positionRect.bottom;
188           } else {
189             target = this._positionRect.top;
190           }
192           target += this.verticalOffset;
194           return Math.max(target, 0);
195         },
197         /**
198          * Called when the value of `opened` changes.
199          *
200          * @param {boolean} opened True if the dropdown is opened.
201          */
202         _openedChanged: function(opened) {
203           if (opened && this.disabled) {
204             this.cancel();
205           } else {
206             this.cancelAnimation();
207             this._prepareDropdown();
208             Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
209           }
211           if (this.opened) {
212             this._focusContent();
213           }
214         },
216         /**
217          * Overridden from `IronOverlayBehavior`.
218          */
219         _renderOpened: function() {
220           Polymer.IronDropdownScrollManager.pushScrollLock(this);
221           if (!this.noAnimations && this.animationConfig && this.animationConfig.open) {
222             this.$.contentWrapper.classList.add('animating');
223             this.playAnimation('open');
224           } else {
225             Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments);
226           }
227         },
229         /**
230          * Overridden from `IronOverlayBehavior`.
231          */
232         _renderClosed: function() {
233           Polymer.IronDropdownScrollManager.removeScrollLock(this);
234           if (!this.noAnimations && this.animationConfig && this.animationConfig.close) {
235             this.$.contentWrapper.classList.add('animating');
236             this.playAnimation('close');
237           } else {
238             Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments);
239           }
240         },
242         /**
243          * Called when animation finishes on the dropdown (when opening or
244          * closing). Responsible for "completing" the process of opening or
245          * closing the dropdown by positioning it or setting its display to
246          * none.
247          */
248         _onNeonAnimationFinish: function() {
249           this.$.contentWrapper.classList.remove('animating');
250           if (this.opened) {
251             Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this);
252           } else {
253             Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this);
254           }
255         },
257         /**
258          * Called when an `iron-resize` event fires.
259          */
260         _onIronResize: function() {
261           var containedElement = this.containedElement;
262           var scrollTop;
263           var scrollLeft;
265           if (containedElement) {
266             scrollTop = containedElement.scrollTop;
267             scrollLeft = containedElement.scrollLeft;
268           }
270           if (this.opened) {
271             this._updateOverlayPosition();
272           }
274           Polymer.IronOverlayBehaviorImpl._onIronResize.apply(this, arguments);
276           if (containedElement) {
277             containedElement.scrollTop = scrollTop;
278             containedElement.scrollLeft = scrollLeft;
279           }
280         },
282         /**
283          * Called when the `positionTarget` property changes.
284          */
285         _positionTargetChanged: function() {
286           this._updateOverlayPosition();
287         },
289         /**
290          * Constructs the final animation config from different properties used
291          * to configure specific parts of the opening and closing animations.
292          */
293         _updateAnimationConfig: function() {
294           var animationConfig = {};
295           var animations = [];
297           if (this.openAnimationConfig) {
298             // NOTE(cdata): When making `display:none` elements visible in Safari,
299             // the element will paint once in a fully visible state, causing the
300             // dropdown to flash before it fades in. We prepend an
301             // `opaque-animation` to fix this problem:
302             animationConfig.open = [{
303               name: 'opaque-animation',
304             }].concat(this.openAnimationConfig);
305             animations = animations.concat(animationConfig.open);
306           }
308           if (this.closeAnimationConfig) {
309             animationConfig.close = this.closeAnimationConfig;
310             animations = animations.concat(animationConfig.close);
311           }
313           animations.forEach(function(animation) {
314             animation.node = this.containedElement;
315           }, this);
317           this.animationConfig = animationConfig;
318         },
320         /**
321          * Prepares the dropdown for opening by updating measured layout
322          * values.
323          */
324         _prepareDropdown: function() {
325           this.sizingTarget = this.containedElement || this.sizingTarget;
326           this._updateAnimationConfig();
327           this._updateOverlayPosition();
328         },
330         /**
331          * Updates the overlay position based on configured horizontal
332          * and vertical alignment, and re-memoizes these values for the sake
333          * of behavior in `IronFitBehavior`.
334          */
335         _updateOverlayPosition: function() {
336           this._positionRectMemo = null;
338           if (!this.positionTarget) {
339             return;
340           }
342           this.style[this.horizontalAlign] =
343             this._horizontalAlignTargetValue + 'px';
345           this.style[this.verticalAlign] =
346             this._verticalAlignTargetValue + 'px';
348           // NOTE(cdata): We re-memoize inline styles here, otherwise
349           // calling `refit` from `IronFitBehavior` will reset inline styles
350           // to whatever they were when the dropdown first opened.
351           if (this._fitInfo) {
352             this._fitInfo.inlineStyle[this.horizontalAlign] =
353               this.style[this.horizontalAlign];
355             this._fitInfo.inlineStyle[this.verticalAlign] =
356               this.style[this.verticalAlign];
357           }
358         },
360         /**
361          * Focuses the configured focus target.
362          */
363         _focusContent: function() {
364           // NOTE(cdata): This is async so that it can attempt the focus after
365           // `display: none` is removed from the element.
366           this.async(function() {
367             if (this._focusTarget) {
368               this._focusTarget.focus();
369             }
370           });
371         }
372       });
373     })();
374