8 Polymer.IronControlState,
9 Polymer.IronA11yKeysBehavior,
10 Polymer.IronOverlayBehavior,
11 Polymer.NeonAnimationRunnerBehavior
16 * The orientation against which to align the dropdown content
17 * horizontally relative to the dropdown trigger.
22 reflectToAttribute: true
26 * The orientation against which to align the dropdown content
27 * vertically relative to the dropdown trigger.
32 reflectToAttribute: true
36 * A pixel value that will be added to the position calculated for the
37 * given `horizontalAlign`. Use a negative value to offset to the
38 * left, or a positive value to offset to the right.
47 * A pixel value that will be added to the position calculated for the
48 * given `verticalAlign`. Use a negative value to offset towards the
49 * top, or a positive value to offset towards the bottom.
58 * The element that should be used to position the dropdown when
63 observer: '_positionTargetChanged'
67 * An animation config. If provided, this will be used to animate the
68 * opening of the dropdown.
70 openAnimationConfig: {
75 * An animation config. If provided, this will be used to animate the
76 * closing of the dropdown.
78 closeAnimationConfig: {
83 * If provided, this will be the element that will be focused when
91 * Set to true to disable animations when opening and closing the
100 * We memoize the positionTarget bounding rectangle so that we can
101 * limit the number of times it is queried per resize / relayout.
110 'neon-animation-finish': '_onNeonAnimationFinish'
114 '_updateOverlayPosition(verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
117 attached: function() {
118 if (this.positionTarget === undefined) {
119 this.positionTarget = this._defaultPositionTarget;
124 * The element that is contained by the dropdown, if any.
126 get containedElement() {
127 return Polymer.dom(this.$.content).getDistributedNodes()[0];
131 * The element that should be focused when the dropdown opens.
134 return this.focusTarget || this.containedElement;
138 * The element that should be used to position the dropdown when
139 * it opens, if no position target is configured.
141 get _defaultPositionTarget() {
142 var parent = Polymer.dom(this).parentNode;
144 if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
145 parent = parent.host;
152 * The bounding rect of the position target.
154 get _positionRect() {
155 if (!this._positionRectMemo && this.positionTarget) {
156 this._positionRectMemo = this.positionTarget.getBoundingClientRect();
159 return this._positionRectMemo;
163 * The horizontal offset value used to position the dropdown.
165 get _horizontalAlignTargetValue() {
168 if (this.horizontalAlign === 'right') {
169 target = document.documentElement.clientWidth - this._positionRect.right;
171 target = this._positionRect.left;
174 target += this.horizontalOffset;
176 return Math.max(target, 0);
180 * The vertical offset value used to position the dropdown.
182 get _verticalAlignTargetValue() {
185 if (this.verticalAlign === 'bottom') {
186 target = document.documentElement.clientHeight - this._positionRect.bottom;
188 target = this._positionRect.top;
191 target += this.verticalOffset;
193 return Math.max(target, 0);
197 * Called when the value of `opened` changes.
199 * @param {boolean} opened True if the dropdown is opened.
201 _openedChanged: function(opened) {
202 if (opened && this.disabled) {
205 this.cancelAnimation();
206 this._prepareDropdown();
207 Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
211 this._focusContent();
216 * Overridden from `IronOverlayBehavior`.
218 _renderOpened: function() {
219 Polymer.IronDropdownScrollManager.pushScrollLock(this);
220 if (!this.noAnimations && this.animationConfig && this.animationConfig.open) {
221 this.$.contentWrapper.classList.add('animating');
222 this.playAnimation('open');
224 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments);
229 * Overridden from `IronOverlayBehavior`.
231 _renderClosed: function() {
232 Polymer.IronDropdownScrollManager.removeScrollLock(this);
233 if (!this.noAnimations && this.animationConfig && this.animationConfig.close) {
234 this.$.contentWrapper.classList.add('animating');
235 this.playAnimation('close');
237 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments);
242 * Called when animation finishes on the dropdown (when opening or
243 * closing). Responsible for "completing" the process of opening or
244 * closing the dropdown by positioning it or setting its display to
247 _onNeonAnimationFinish: function() {
248 this.$.contentWrapper.classList.remove('animating');
250 Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this);
252 Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this);
257 * Called when an `iron-resize` event fires.
259 _onIronResize: function() {
260 var containedElement = this.containedElement;
264 if (containedElement) {
265 scrollTop = containedElement.scrollTop;
266 scrollLeft = containedElement.scrollLeft;
270 this._updateOverlayPosition();
273 Polymer.IronOverlayBehaviorImpl._onIronResize.apply(this, arguments);
275 if (containedElement) {
276 containedElement.scrollTop = scrollTop;
277 containedElement.scrollLeft = scrollLeft;
282 * Called when the `positionTarget` property changes.
284 _positionTargetChanged: function() {
285 this._updateOverlayPosition();
289 * Constructs the final animation config from different properties used
290 * to configure specific parts of the opening and closing animations.
292 _updateAnimationConfig: function() {
293 var animationConfig = {};
296 if (this.openAnimationConfig) {
297 // NOTE(cdata): When making `display:none` elements visible in Safari,
298 // the element will paint once in a fully visible state, causing the
299 // dropdown to flash before it fades in. We prepend an
300 // `opaque-animation` to fix this problem:
301 animationConfig.open = [{
302 name: 'opaque-animation',
303 }].concat(this.openAnimationConfig);
304 animations = animations.concat(animationConfig.open);
307 if (this.closeAnimationConfig) {
308 animationConfig.close = this.closeAnimationConfig;
309 animations = animations.concat(animationConfig.close);
312 animations.forEach(function(animation) {
313 animation.node = this.containedElement;
316 this.animationConfig = animationConfig;
320 * Prepares the dropdown for opening by updating measured layout
323 _prepareDropdown: function() {
324 this.sizingTarget = this.containedElement || this.sizingTarget;
325 this._updateAnimationConfig();
326 this._updateOverlayPosition();
330 * Updates the overlay position based on configured horizontal
331 * and vertical alignment, and re-memoizes these values for the sake
332 * of behavior in `IronFitBehavior`.
334 _updateOverlayPosition: function() {
335 this._positionRectMemo = null;
337 if (!this.positionTarget) {
341 this.style[this.horizontalAlign] =
342 this._horizontalAlignTargetValue + 'px';
344 this.style[this.verticalAlign] =
345 this._verticalAlignTargetValue + 'px';
347 // NOTE(cdata): We re-memoize inline styles here, otherwise
348 // calling `refit` from `IronFitBehavior` will reset inline styles
349 // to whatever they were when the dropdown first opened.
351 this._fitInfo.inlineStyle[this.horizontalAlign] =
352 this.style[this.horizontalAlign];
354 this._fitInfo.inlineStyle[this.verticalAlign] =
355 this.style[this.verticalAlign];
360 * Focuses the configured focus target.
362 _focusContent: function() {
363 // NOTE(cdata): This is async so that it can attempt the focus after
364 // `display: none` is removed from the element.
365 this.async(function() {
366 if (this._focusTarget) {
367 this._focusTarget.focus();