| Index: ui/webui/resources/cr_elements/cr_shared_menu/cr_shared_menu.js
|
| diff --git a/ui/webui/resources/cr_elements/cr_shared_menu/cr_shared_menu.js b/ui/webui/resources/cr_elements/cr_shared_menu/cr_shared_menu.js
|
| index ab746bdf9f4df1b0af4a6f2f285f95b5f4d39b22..e08fd0d10d43d197a45af4e9ad8909037107e120 100644
|
| --- a/ui/webui/resources/cr_elements/cr_shared_menu/cr_shared_menu.js
|
| +++ b/ui/webui/resources/cr_elements/cr_shared_menu/cr_shared_menu.js
|
| @@ -4,14 +4,16 @@
|
|
|
| /** Same as paper-menu-button's custom easing cubic-bezier param. */
|
| var SLIDE_CUBIC_BEZIER = 'cubic-bezier(0.3, 0.95, 0.5, 1)';
|
| -var FADE_CUBIC_BEZIER = 'cubic-bezier(0.4, 0, 0.2, 1)';
|
|
|
| Polymer({
|
| is: 'cr-shared-menu',
|
|
|
| + behaviors: [Polymer.IronA11yKeysBehavior],
|
| +
|
| properties: {
|
| menuOpen: {
|
| type: Boolean,
|
| + observer: 'menuOpenChanged_',
|
| value: false,
|
| },
|
|
|
| @@ -24,48 +26,112 @@ Polymer({
|
| type: Object,
|
| value: null,
|
| },
|
| +
|
| + /** @override */
|
| + keyEventTarget: {
|
| + type: Object,
|
| + value: function() {
|
| + return this.$.menu;
|
| + }
|
| + },
|
| +
|
| + openAnimationConfig: {
|
| + type: Object,
|
| + value: function() {
|
| + return [{
|
| + name: 'fade-in-animation',
|
| + timing: {
|
| + delay: 50,
|
| + duration: 200
|
| + }
|
| + }, {
|
| + name: 'paper-menu-grow-width-animation',
|
| + timing: {
|
| + delay: 50,
|
| + duration: 150,
|
| + easing: SLIDE_CUBIC_BEZIER
|
| + }
|
| + }, {
|
| + name: 'paper-menu-grow-height-animation',
|
| + timing: {
|
| + delay: 100,
|
| + duration: 275,
|
| + easing: SLIDE_CUBIC_BEZIER
|
| + }
|
| + }];
|
| + }
|
| + },
|
| +
|
| + closeAnimationConfig: {
|
| + type: Object,
|
| + value: function() {
|
| + return [{
|
| + name: 'fade-out-animation',
|
| + timing: {
|
| + duration: 150
|
| + }
|
| + }, {
|
| + name: 'paper-menu-shrink-width-animation',
|
| + timing: {
|
| + delay: 100,
|
| + duration: 50,
|
| + easing: SLIDE_CUBIC_BEZIER
|
| + }
|
| + }, {
|
| + name: 'paper-menu-shrink-height-animation',
|
| + timing: {
|
| + delay: 200,
|
| + easing: 'ease-in'
|
| + }
|
| + }];
|
| + }
|
| + }
|
| },
|
|
|
| - /**
|
| - * Current animation being played, or null if there is none.
|
| - * @type {?Animation}
|
| - * @private
|
| - */
|
| - animation_: null,
|
| + keyBindings: {
|
| + 'tab': 'onTabPressed_',
|
| + },
|
|
|
| /**
|
| * The last anchor that was used to open a menu. It's necessary for toggling.
|
| - * @type {?Element}
|
| + * @private {?Element}
|
| */
|
| lastAnchor_: null,
|
|
|
| /**
|
| - * Adds listeners to the window in order to dismiss the menu on resize and
|
| - * when escape is pressed.
|
| + * The first focusable child in the menu's light DOM.
|
| + * @private {?Element}
|
| + */
|
| + firstFocus_: null,
|
| +
|
| + /**
|
| + * The last focusable child in the menu's light DOM.
|
| + * @private {?Element}
|
| */
|
| + lastFocus_: null,
|
| +
|
| + /** @override */
|
| attached: function() {
|
| window.addEventListener('resize', this.closeMenu.bind(this));
|
| - window.addEventListener('keydown', function(e) {
|
| - // Escape button on keyboard
|
| - if (e.keyCode == 27)
|
| - this.closeMenu();
|
| - }.bind(this));
|
| +
|
| + var focusableChildren = Polymer.dom(this).querySelectorAll(
|
| + '[tabindex],button');
|
| + if (focusableChildren.length > 0) {
|
| + this.$.dropdown.focusTarget = focusableChildren[0];
|
| + this.firstFocus_ = focusableChildren[0];
|
| + this.lastFocus_ = focusableChildren[focusableChildren.length - 1];
|
| + }
|
| },
|
|
|
| /** Closes the menu. */
|
| closeMenu: function() {
|
| - if (!this.menuOpen)
|
| - return;
|
| - // If there is a open menu animation going, cancel it and start closing.
|
| - this.cancelAnimation_();
|
| + if (this.root.activeElement == null) {
|
| + // Something else has taken focus away from the menu. Do not attempt to
|
| + // restore focus to the button which opened the menu.
|
| + this.$.dropdown.restoreFocusOnClose = false;
|
| + }
|
| this.menuOpen = false;
|
| - this.itemData = null;
|
| - this.animation_ = this.animateClose_();
|
| - this.animation_.addEventListener('finish', function() {
|
| - this.style.display = 'none';
|
| - // Reset the animation for the next time the menu opens.
|
| - this.cancelAnimation_();
|
| - }.bind(this));
|
| + this.$.dropdown.restoreFocusOnClose = true;
|
| },
|
|
|
| /**
|
| @@ -74,30 +140,12 @@ Polymer({
|
| * @param {!Object} itemData The contextual item's data.
|
| */
|
| openMenu: function(anchor, itemData) {
|
| - this.menuOpen = true;
|
| - this.style.display = 'block';
|
| this.itemData = itemData;
|
| this.lastAnchor_ = anchor;
|
|
|
| // Move the menu to the anchor.
|
| - var anchorRect = anchor.getBoundingClientRect();
|
| - var parentRect = this.offsetParent.getBoundingClientRect();
|
| -
|
| - var left = (isRTL() ? anchorRect.left : anchorRect.right) - parentRect.left;
|
| - var top = anchorRect.top - parentRect.top;
|
| -
|
| - cr.ui.positionPopupAtPoint(left, top, this, cr.ui.AnchorType.BEFORE);
|
| -
|
| - // Handle the bottom of the screen.
|
| - if (this.getBoundingClientRect().top != anchorRect.top) {
|
| - var bottom = anchorRect.bottom - parentRect.top;
|
| - cr.ui.positionPopupAtPoint(left, bottom, this, cr.ui.AnchorType.BEFORE);
|
| - }
|
| -
|
| - this.$.menu.focus();
|
| -
|
| - this.cancelAnimation_();
|
| - this.animation_ = this.animateOpen_();
|
| + this.$.dropdown.positionTarget = anchor;
|
| + this.menuOpen = true;
|
| },
|
|
|
| /**
|
| @@ -106,128 +154,44 @@ Polymer({
|
| * @param {!Object} itemData The contextual item's data.
|
| */
|
| toggleMenu: function(anchor, itemData) {
|
| - // If there is an animation going (e.g. user clicks too fast), cancel it and
|
| - // start the new action.
|
| - this.cancelAnimation_();
|
| if (anchor == this.lastAnchor_ && this.menuOpen)
|
| this.closeMenu();
|
| else
|
| this.openMenu(anchor, itemData);
|
| },
|
|
|
| - /** @private */
|
| - cancelAnimation_: function() {
|
| - if (this.animation_) {
|
| - this.animation_.cancel();
|
| - this.animation_ = null;
|
| - }
|
| - },
|
| -
|
| /**
|
| - * @param {!Array<!KeyframeEffect>} effects
|
| - * @return {!Animation}
|
| + * Trap focus inside the menu. As a very basic heuristic, will wrap focus from
|
| + * the first element with a nonzero tabindex to the last such element.
|
| + * TODO(tsergeant): Use iron-focus-wrap-behavior once it is available
|
| + * (https://github.com/PolymerElements/iron-overlay-behavior/issues/179).
|
| + * @param {CustomEvent} e
|
| */
|
| - playEffects: function(effects) {
|
| - /** @type {function(new:Object, !Array<!KeyframeEffect>)} */
|
| - window.GroupEffect;
|
| + onTabPressed_: function(e) {
|
| + if (!this.firstFocus_ || !this.lastFocus_)
|
| + return;
|
|
|
| - /** @type {{play: function(Object): !Animation}} */
|
| - document.timeline;
|
| + var toFocus;
|
| + var keyEvent = e.detail.keyboardEvent;
|
| + if (keyEvent.shiftKey && keyEvent.target == this.firstFocus_)
|
| + toFocus = this.lastFocus_;
|
| + else if (keyEvent.target == this.lastFocus_)
|
| + toFocus = this.firstFocus_;
|
|
|
| - return document.timeline.play(new window.GroupEffect(effects));
|
| - },
|
| + if (!toFocus)
|
| + return;
|
|
|
| - /**
|
| - * Slide-in animation when opening the menu. The animation configuration is
|
| - * the same as paper-menu-button except for a shorter delay time.
|
| - * @private
|
| - * @return {!Animation}
|
| - */
|
| - animateOpen_: function() {
|
| - var rect = this.getBoundingClientRect();
|
| - var height = rect.height;
|
| - var width = rect.width;
|
| -
|
| - var fadeIn = new KeyframeEffect(/** @type {Animatable} */(this), [{
|
| - 'opacity': '0'
|
| - }, {
|
| - 'opacity': '1'
|
| - }], /** @type {!KeyframeEffectOptions} */({
|
| - delay: 50,
|
| - duration: 200,
|
| - easing: FADE_CUBIC_BEZIER,
|
| - fill: 'both'
|
| - }));
|
| -
|
| - var growHeight = new KeyframeEffect(/** @type {Animatable} */(this), [{
|
| - height: (height / 2) + 'px'
|
| - }, {
|
| - height: height + 'px'
|
| - }], /** @type {!KeyframeEffectOptions} */({
|
| - delay: 50,
|
| - duration: 275,
|
| - easing: SLIDE_CUBIC_BEZIER,
|
| - fill: 'both'
|
| - }));
|
| -
|
| - var growWidth = new KeyframeEffect(/** @type {Animatable} */(this), [{
|
| - width: (width / 2) + 'px'
|
| - }, {
|
| - width: width + 'px'
|
| - }], /** @type {!KeyframeEffectOptions} */({
|
| - delay: 50,
|
| - duration: 150,
|
| - easing: SLIDE_CUBIC_BEZIER,
|
| - fill: 'both'
|
| - }));
|
| -
|
| - return this.playEffects([fadeIn, growHeight, growWidth]);
|
| + e.preventDefault();
|
| + toFocus.focus();
|
| },
|
|
|
| /**
|
| - * Slide-out animation when closing the menu. The animation configuration is
|
| - * the same as paper-menu-button.
|
| + * Ensure the menu is reset properly when it is closed by the dropdown (eg,
|
| + * clicking outside).
|
| * @private
|
| - * @return {!Animation}
|
| */
|
| - animateClose_: function() {
|
| - var rect = this.getBoundingClientRect();
|
| - var height = rect.height;
|
| - var width = rect.width;
|
| -
|
| - var fadeOut = new KeyframeEffect(/** @type {Animatable} */(this), [{
|
| - 'opacity': '1'
|
| - }, {
|
| - 'opacity': '0'
|
| - }], /** @type {!KeyframeEffectOptions} */({
|
| - duration: 150,
|
| - easing: FADE_CUBIC_BEZIER,
|
| - fill: 'both'
|
| - }));
|
| -
|
| - var shrinkHeight = new KeyframeEffect(/** @type {Animatable} */(this), [{
|
| - height: height + 'px',
|
| - transform: 'translateY(0)'
|
| - }, {
|
| - height: height / 2 + 'px',
|
| - transform: 'translateY(-20px)'
|
| - }], /** @type {!KeyframeEffectOptions} */({
|
| - duration: 200,
|
| - easing: 'ease-in',
|
| - fill: 'both'
|
| - }));
|
| -
|
| - var shrinkWidth = new KeyframeEffect(/** @type {Animatable} */(this), [{
|
| - width: width + 'px'
|
| - }, {
|
| - width: width - (width / 20) + 'px'
|
| - }], /** @type {!KeyframeEffectOptions} */({
|
| - delay: 100,
|
| - duration: 50,
|
| - easing: SLIDE_CUBIC_BEZIER,
|
| - fill: 'both'
|
| - }));
|
| -
|
| - return this.playEffects([fadeOut, shrinkHeight, shrinkWidth]);
|
| + menuOpenChanged_: function() {
|
| + if (!this.menuOpen)
|
| + this.itemData = null;
|
| },
|
| });
|
|
|