<!-- @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-resizable-behavior/iron-resizable-behavior.html"> <link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> <link rel="import" href="../iron-behaviors/iron-control-state.html"> <link rel="import" href="../iron-overlay-behavior/iron-overlay-behavior.html"> <link rel="import" href="../neon-animation/neon-animation-runner-behavior.html"> <link rel="import" href="../neon-animation/animations/opaque-animation.html"> <link rel="import" href="iron-dropdown-scroll-manager.html"> <!-- `<iron-dropdown>` is a generalized element that is useful when you have hidden content (`.dropdown-content`) that is revealed due to some change in state that should cause it to do so. Note that this is a low-level element intended to be used as part of other composite elements that cause dropdowns to be revealed. Examples of elements that might be implemented using an `iron-dropdown` include comboboxes, menubuttons, selects. The list goes on. The `<iron-dropdown>` element exposes attributes that allow the position of the `.dropdown-content` relative to the `.dropdown-trigger` to be configured. <iron-dropdown horizontal-align="right" vertical-align="top"> <div class="dropdown-content">Hello!</div> </iron-dropdown> In the above example, the `<div>` with class `.dropdown-content` will be hidden until the dropdown element has `opened` set to true, or when the `open` method is called on the element. @demo demo/index.html --> <dom-module id="iron-dropdown"> <style> :host { position: fixed; } #contentWrapper ::content > * { overflow: auto; } #contentWrapper.animating ::content > * { overflow: hidden; } </style> <template> <div id="contentWrapper"> <content id="content" select=".dropdown-content"></content> </div> </template> <script> (function() { 'use strict'; Polymer({ is: 'iron-dropdown', behaviors: [ Polymer.IronControlState, Polymer.IronA11yKeysBehavior, Polymer.IronOverlayBehavior, Polymer.NeonAnimationRunnerBehavior ], properties: { /** * The orientation against which to align the dropdown content * horizontally relative to the dropdown trigger. */ horizontalAlign: { type: String, value: 'left', reflectToAttribute: true }, /** * The orientation against which to align the dropdown content * vertically relative to the dropdown trigger. */ verticalAlign: { type: String, value: 'top', reflectToAttribute: true }, /** * A pixel value that will be added to the position calculated for the * given `horizontalAlign`. Use a negative value to offset to the * left, or a positive value to offset to the right. */ horizontalOffset: { type: Number, value: 0, notify: true }, /** * A pixel value that will be added to the position calculated for the * given `verticalAlign`. Use a negative value to offset towards the * top, or a positive value to offset towards the bottom. */ verticalOffset: { type: Number, value: 0, notify: true }, /** * The element that should be used to position the dropdown when * it is opened. */ positionTarget: { type: Object, observer: '_positionTargetChanged' }, /** * An animation config. If provided, this will be used to animate the * opening of the dropdown. */ openAnimationConfig: { type: Object }, /** * An animation config. If provided, this will be used to animate the * closing of the dropdown. */ closeAnimationConfig: { type: Object }, /** * If provided, this will be the element that will be focused when * the dropdown opens. */ focusTarget: { type: Object }, /** * Set to true to disable animations when opening and closing the * dropdown. */ noAnimations: { type: Boolean, value: false }, /** * 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 }, /** * We memoize the positionTarget bounding rectangle so that we can * limit the number of times it is queried per resize / relayout. * @type {?Object} */ _positionRectMemo: { type: Object } }, listeners: { 'neon-animation-finish': '_onNeonAnimationFinish' }, observers: [ '_updateOverlayPosition(verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)' ], attached: function() { if (this.positionTarget === undefined) { this.positionTarget = this._defaultPositionTarget; } }, /** * The element that is contained by the dropdown, if any. */ get containedElement() { return Polymer.dom(this.$.content).getDistributedNodes()[0]; }, /** * The element that should be focused when the dropdown opens. */ get _focusTarget() { return this.focusTarget || this.containedElement; }, /** * The element that should be used to position the dropdown when * it opens, if no position target is configured. */ get _defaultPositionTarget() { var parent = Polymer.dom(this).parentNode; if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { parent = parent.host; } return parent; }, /** * The bounding rect of the position target. */ get _positionRect() { if (!this._positionRectMemo && this.positionTarget) { this._positionRectMemo = this.positionTarget.getBoundingClientRect(); } return this._positionRectMemo; }, /** * The horizontal offset value used to position the dropdown. */ get _horizontalAlignTargetValue() { var target; if (this.horizontalAlign === 'right') { target = document.documentElement.clientWidth - this._positionRect.right; } else { target = this._positionRect.left; } target += this.horizontalOffset; return Math.max(target, 0); }, /** * The vertical offset value used to position the dropdown. */ get _verticalAlignTargetValue() { var target; if (this.verticalAlign === 'bottom') { target = document.documentElement.clientHeight - this._positionRect.bottom; } else { target = this._positionRect.top; } target += this.verticalOffset; return Math.max(target, 0); }, /** * Called when the value of `opened` changes. * * @param {boolean} opened True if the dropdown is opened. */ _openedChanged: function(opened) { if (opened && this.disabled) { this.cancel(); } else { this.cancelAnimation(); this._prepareDropdown(); Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments); } if (this.opened) { this._focusContent(); } }, /** * Overridden from `IronOverlayBehavior`. */ _renderOpened: function() { if (!this.allowOutsideScroll) { Polymer.IronDropdownScrollManager.pushScrollLock(this); } if (!this.noAnimations && this.animationConfig && this.animationConfig.open) { this.$.contentWrapper.classList.add('animating'); this.playAnimation('open'); } else { Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments); } }, /** * Overridden from `IronOverlayBehavior`. */ _renderClosed: function() { Polymer.IronDropdownScrollManager.removeScrollLock(this); if (!this.noAnimations && this.animationConfig && this.animationConfig.close) { this.$.contentWrapper.classList.add('animating'); this.playAnimation('close'); } else { Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments); } }, /** * Called when animation finishes on the dropdown (when opening or * closing). Responsible for "completing" the process of opening or * closing the dropdown by positioning it or setting its display to * none. */ _onNeonAnimationFinish: function() { this.$.contentWrapper.classList.remove('animating'); if (this.opened) { Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this); } else { Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this); } }, /** * Called when an `iron-resize` event fires. */ _onIronResize: function() { var containedElement = this.containedElement; var scrollTop; var scrollLeft; if (containedElement) { scrollTop = containedElement.scrollTop; scrollLeft = containedElement.scrollLeft; } if (this.opened) { this._updateOverlayPosition(); } Polymer.IronOverlayBehaviorImpl._onIronResize.apply(this, arguments); if (containedElement) { containedElement.scrollTop = scrollTop; containedElement.scrollLeft = scrollLeft; } }, /** * Called when the `positionTarget` property changes. */ _positionTargetChanged: function() { this._updateOverlayPosition(); }, /** * Constructs the final animation config from different properties used * to configure specific parts of the opening and closing animations. */ _updateAnimationConfig: function() { var animationConfig = {}; var animations = []; if (this.openAnimationConfig) { // NOTE(cdata): When making `display:none` elements visible in Safari, // the element will paint once in a fully visible state, causing the // dropdown to flash before it fades in. We prepend an // `opaque-animation` to fix this problem: animationConfig.open = [{ name: 'opaque-animation', }].concat(this.openAnimationConfig); animations = animations.concat(animationConfig.open); } if (this.closeAnimationConfig) { animationConfig.close = this.closeAnimationConfig; animations = animations.concat(animationConfig.close); } animations.forEach(function(animation) { animation.node = this.containedElement; }, this); this.animationConfig = animationConfig; }, /** * Prepares the dropdown for opening by updating measured layout * values. */ _prepareDropdown: function() { this.sizingTarget = this.containedElement || this.sizingTarget; this._updateAnimationConfig(); this._updateOverlayPosition(); }, /** * Updates the overlay position based on configured horizontal * and vertical alignment, and re-memoizes these values for the sake * of behavior in `IronFitBehavior`. */ _updateOverlayPosition: function() { this._positionRectMemo = null; if (!this.positionTarget) { return; } this.style[this.horizontalAlign] = this._horizontalAlignTargetValue + 'px'; this.style[this.verticalAlign] = this._verticalAlignTargetValue + 'px'; // NOTE(cdata): We re-memoize inline styles here, otherwise // calling `refit` from `IronFitBehavior` will reset inline styles // to whatever they were when the dropdown first opened. if (this._fitInfo) { this._fitInfo.inlineStyle[this.horizontalAlign] = this.style[this.horizontalAlign]; this._fitInfo.inlineStyle[this.verticalAlign] = this.style[this.verticalAlign]; } }, /** * Focuses the configured focus target. */ _focusContent: function() { // NOTE(cdata): This is async so that it can attempt the focus after // `display: none` is removed from the element. this.async(function() { if (this._focusTarget) { this._focusTarget.focus(); } }); } }); })(); </script> </dom-module>