Path: blob/trunk/third_party/closure/goog/ui/menubutton.js
4523 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview A menu button control.8*9* @see ../demos/menubutton.html10*/1112goog.provide('goog.ui.MenuButton');1314goog.require('goog.Timer');15goog.require('goog.a11y.aria');16goog.require('goog.a11y.aria.State');17goog.require('goog.asserts');18goog.require('goog.dom');19goog.require('goog.events.EventType');20goog.require('goog.events.KeyCodes');21goog.require('goog.events.KeyHandler');22goog.require('goog.math.Box');23goog.require('goog.math.Coordinate');24goog.require('goog.math.Rect');25goog.require('goog.positioning');26goog.require('goog.positioning.Corner');27goog.require('goog.positioning.MenuAnchoredPosition');28goog.require('goog.positioning.Overflow');29goog.require('goog.style');30goog.require('goog.ui.Button');31goog.require('goog.ui.Component');32goog.require('goog.ui.IdGenerator');33goog.require('goog.ui.Menu');34goog.require('goog.ui.MenuButtonRenderer');35goog.require('goog.ui.MenuItem');36goog.require('goog.ui.MenuRenderer');37goog.require('goog.ui.SubMenu');38goog.require('goog.ui.registry');39goog.requireType('goog.events.BrowserEvent');40goog.requireType('goog.events.Event');41goog.requireType('goog.events.EventTarget');42goog.requireType('goog.math.Size');43goog.requireType('goog.positioning.AnchoredPosition');44goog.requireType('goog.ui.ButtonRenderer');45goog.requireType('goog.ui.Control');46goog.requireType('goog.ui.ControlContent');47goog.requireType('goog.ui.MenuSeparator');48495051/**52* A menu button control. Extends {@link goog.ui.Button} by composing a button53* with a dropdown arrow and a popup menu.54*55* @param {goog.ui.ControlContent=} opt_content Text caption or existing DOM56* structure to display as the button's caption (if any).57* @param {goog.ui.Menu=} opt_menu Menu to render under the button when clicked.58* @param {goog.ui.ButtonRenderer=} opt_renderer Renderer used to render or59* decorate the menu button; defaults to {@link goog.ui.MenuButtonRenderer}.60* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for61* document interaction.62* @param {!goog.ui.MenuRenderer=} opt_menuRenderer Renderer used to render or63* decorate the menu; defaults to {@link goog.ui.MenuRenderer}.64* @constructor65* @extends {goog.ui.Button}66*/67goog.ui.MenuButton = function(68opt_content, opt_menu, opt_renderer, opt_domHelper, opt_menuRenderer) {69'use strict';70goog.ui.Button.call(71this, opt_content,72opt_renderer || goog.ui.MenuButtonRenderer.getInstance(), opt_domHelper);7374// Menu buttons support the OPENED state.75this.setSupportedState(goog.ui.Component.State.OPENED, true);7677/**78* The menu position on this button.79* @type {!goog.positioning.AnchoredPosition}80* @private81*/82this.menuPosition_ = new goog.positioning.MenuAnchoredPosition(83null, goog.positioning.Corner.BOTTOM_START);8485if (opt_menu) {86this.setMenu(opt_menu);87}88this.menuMargin_ = null;89this.timer_ = new goog.Timer(500); // 0.5 sec9091/**92* Whether the enter or space key should close the menu, if it is already93* open. This should be true for accessibility reasons, but is provided as an94* option for backward compatibility.95* @private {boolean}96*/97this.closeOnEnterOrSpace_ = true;9899/** @private {!goog.ui.MenuRenderer} */100this.menuRenderer_ = opt_menuRenderer || goog.ui.MenuRenderer.getInstance();101};102goog.inherits(goog.ui.MenuButton, goog.ui.Button);103104105/**106* The menu.107* @type {goog.ui.Menu|undefined}108* @private109*/110goog.ui.MenuButton.prototype.menu_;111112113/**114* The position element. If set, use positionElement_ to position the115* popup menu instead of the default which is to use the menu button element.116* @type {Element|undefined}117* @private118*/119goog.ui.MenuButton.prototype.positionElement_;120121122/**123* The margin to apply to the menu's position when it is shown. If null, no124* margin will be applied.125* @type {goog.math.Box}126* @private127*/128goog.ui.MenuButton.prototype.menuMargin_;129130131/**132* Whether the attached popup menu is focusable or not (defaults to false).133* Popup menus attached to menu buttons usually don't need to be focusable,134* i.e. the button retains keyboard focus, and forwards key events to the135* menu for processing. However, menus like {@link goog.ui.FilteredMenu}136* need to be focusable.137* @type {boolean}138* @private139*/140goog.ui.MenuButton.prototype.isFocusablePopupMenu_ = false;141142143/**144* A Timer to correct menu position.145* @type {goog.Timer}146* @private147*/148goog.ui.MenuButton.prototype.timer_;149150151/**152* The bounding rectangle of the button element.153* @type {goog.math.Rect}154* @private155*/156goog.ui.MenuButton.prototype.buttonRect_;157158159/**160* The viewport rectangle.161* @type {goog.math.Box}162* @private163*/164goog.ui.MenuButton.prototype.viewportBox_;165166167/**168* The original size.169* @type {goog.math.Size|undefined}170* @private171*/172goog.ui.MenuButton.prototype.originalSize_;173174175/**176* Do we render the drop down menu as a sibling to the label, or at the end177* of the current dom?178* @type {boolean}179* @private180*/181goog.ui.MenuButton.prototype.renderMenuAsSibling_ = false;182183184/**185* Whether to select the first item in the menu when it is opened using186* enter or space. By default, the first item is selected only when187* opened by a key up or down event. When this is on, the first item will188* be selected due to any of the four events.189* @private190*/191goog.ui.MenuButton.prototype.selectFirstOnEnterOrSpace_ = false;192193194/**195* Sets up event handlers specific to menu buttons.196* @override197*/198goog.ui.MenuButton.prototype.enterDocument = function() {199'use strict';200goog.ui.MenuButton.superClass_.enterDocument.call(this);201this.attachKeyDownEventListener_(true);202if (this.menu_) {203this.attachMenuEventListeners_(this.menu_, true);204}205goog.a11y.aria.setState(206this.getElementStrict(), goog.a11y.aria.State.HASPOPUP, !!this.menu_);207};208209210/**211* Removes event handlers specific to menu buttons, and ensures that the212* attached menu also exits the document.213* @override214*/215goog.ui.MenuButton.prototype.exitDocument = function() {216'use strict';217goog.ui.MenuButton.superClass_.exitDocument.call(this);218this.attachKeyDownEventListener_(false);219if (this.menu_) {220this.setOpen(false);221this.menu_.exitDocument();222this.attachMenuEventListeners_(this.menu_, false);223224var menuElement = this.menu_.getElement();225if (menuElement) {226goog.dom.removeNode(menuElement);227}228}229};230231232/** @override */233goog.ui.MenuButton.prototype.disposeInternal = function() {234'use strict';235goog.ui.MenuButton.superClass_.disposeInternal.call(this);236if (this.menu_) {237this.menu_.dispose();238delete this.menu_;239}240delete this.positionElement_;241this.timer_.dispose();242};243244245/**246* Handles mousedown events. Invokes the superclass implementation to dispatch247* an ACTIVATE event and activate the button. Also toggles the visibility of248* the attached menu.249* @param {goog.events.Event} e Mouse event to handle.250* @override251* @protected252*/253goog.ui.MenuButton.prototype.handleMouseDown = function(e) {254'use strict';255goog.ui.MenuButton.superClass_.handleMouseDown.call(this, e);256if (this.isActive()) {257// The component was allowed to activate; toggle menu visibility.258this.setOpen(!this.isOpen(), e);259if (this.menu_) {260this.menu_.setMouseButtonPressed(this.isOpen());261}262}263};264265266/**267* Handles mouseup events. Invokes the superclass implementation to dispatch268* an ACTION event and deactivate the button.269* @param {goog.events.Event} e Mouse event to handle.270* @override271* @protected272*/273goog.ui.MenuButton.prototype.handleMouseUp = function(e) {274'use strict';275goog.ui.MenuButton.superClass_.handleMouseUp.call(this, e);276if (this.menu_ && !this.isActive()) {277this.menu_.setMouseButtonPressed(false);278}279};280281282/**283* Performs the appropriate action when the menu button is activated by the284* user. Overrides the superclass implementation by not dispatching an285* `ACTION` event, because menu buttons exist only to reveal menus, not to286* perform actions themselves. Calls {@link #setActive} to deactivate the287* button.288* @param {goog.events.Event} e Mouse or key event that triggered the action.289* @return {boolean} Whether the action was allowed to proceed.290* @override291* @protected292*/293goog.ui.MenuButton.prototype.performActionInternal = function(e) {294'use strict';295this.setActive(false);296return true;297};298299300/**301* Handles mousedown events over the document. If the mousedown happens over302* an element unrelated to the component, hides the menu.303* TODO(attila): Reconcile this with goog.ui.Popup (and handle frames/windows).304* @param {goog.events.BrowserEvent} e Mouse event to handle.305* @protected306*/307goog.ui.MenuButton.prototype.handleDocumentMouseDown = function(e) {308'use strict';309if (this.menu_ && this.menu_.isVisible() &&310!this.containsElement(/** @type {Element} */ (e.target))) {311// User clicked somewhere else in the document while the menu was visible;312// dismiss menu.313this.setOpen(false);314}315};316317318/**319* Returns true if the given element is to be considered part of the component,320* even if it isn't a DOM descendant of the component's root element.321* @param {Element} element Element to test (if any).322* @return {boolean} Whether the element is considered part of the component.323* @protected324*/325goog.ui.MenuButton.prototype.containsElement = function(element) {326'use strict';327return element && goog.dom.contains(this.getElement(), element) ||328this.menu_ && this.menu_.containsElement(element) || false;329};330331332/** @override */333goog.ui.MenuButton.prototype.handleKeyEventInternal = function(e) {334'use strict';335// Handle SPACE on keyup and all other keys on keypress.336if (e.keyCode == goog.events.KeyCodes.SPACE) {337// Prevent page scrolling in Chrome.338e.preventDefault();339if (e.type != goog.events.EventType.KEYUP) {340// Ignore events because KeyCodes.SPACE is handled further down.341return true;342}343} else if (e.type != goog.events.KeyHandler.EventType.KEY) {344return false;345}346347if (this.menu_ && this.menu_.isVisible()) {348// Menu is open.349const isEnterOrSpace = e.keyCode == goog.events.KeyCodes.ENTER ||350e.keyCode == goog.events.KeyCodes.SPACE;351const handledByMenu = this.menu_.handleKeyEvent(e);352// If the submenu has handled the key event, then defer to it to close the353// menu if necessary and do not close it here. This is needed because the354// enter key should keep the submenu open, but should close other types of355// menu items.356// Check for this.menu_ again here because some widgets set this.dispose357// after handleKeyEvent. Example: go/widget-dispose-ex358const handledBySubMenu = handledByMenu && this.menu_ &&359this.menu_.getOpenItem() instanceof goog.ui.SubMenu;360if (!handledBySubMenu &&361(e.keyCode == goog.events.KeyCodes.ESC ||362(isEnterOrSpace && this.closeOnEnterOrSpace_))) {363// Dismiss the menu.364this.setOpen(false);365return true;366}367return handledByMenu;368}369370if (e.keyCode == goog.events.KeyCodes.DOWN ||371e.keyCode == goog.events.KeyCodes.UP ||372e.keyCode == goog.events.KeyCodes.SPACE ||373e.keyCode == goog.events.KeyCodes.ENTER) {374// Menu is closed, and the user hit the down/up/space/enter key; open menu.375this.setOpen(true, e);376return true;377}378379// Key event wasn't handled by the component.380return false;381};382383384/**385* Handles `ACTION` events dispatched by an activated menu item.386* @param {goog.events.Event} e Action event to handle.387* @protected388*/389goog.ui.MenuButton.prototype.handleMenuAction = function(e) {390'use strict';391// Close the menu on click.392this.setOpen(false);393};394395396/**397* Handles `BLUR` events dispatched by the popup menu by closing it.398* Only registered if the menu is focusable.399* @param {goog.events.Event} e Blur event dispatched by a focusable menu.400*/401goog.ui.MenuButton.prototype.handleMenuBlur = function(e) {402'use strict';403// Close the menu when it reports that it lost focus, unless the button is404// pressed (active).405if (!this.isActive()) {406this.setOpen(false);407}408};409410411/**412* Handles blur events dispatched by the button's key event target when it413* loses keyboard focus by closing the popup menu (unless it is focusable).414* Only registered if the button is focusable.415* @param {goog.events.Event} e Blur event dispatched by the menu button.416* @override417* @protected418*/419goog.ui.MenuButton.prototype.handleBlur = function(e) {420'use strict';421if (!this.isFocusablePopupMenu()) {422this.setOpen(false);423}424goog.ui.MenuButton.superClass_.handleBlur.call(this, e);425};426427428/**429* Returns the menu attached to the button. If no menu is attached, creates a430* new empty menu.431* @return {goog.ui.Menu} Popup menu attached to the menu button.432*/433goog.ui.MenuButton.prototype.getMenu = function() {434'use strict';435if (!this.menu_) {436this.setMenu(new goog.ui.Menu(this.getDomHelper(), this.menuRenderer_));437}438return this.menu_ || null;439};440441442/**443* Replaces the menu attached to the button with the argument, and returns the444* previous menu (if any).445* @param {goog.ui.Menu?} menu New menu to be attached to the menu button (null446* to remove the menu).447* @return {goog.ui.Menu|undefined} Previous menu (undefined if none).448*/449goog.ui.MenuButton.prototype.setMenu = function(menu) {450'use strict';451var oldMenu = this.menu_;452453// Do nothing unless the new menu is different from the current one.454if (menu != oldMenu) {455if (oldMenu) {456this.setOpen(false);457if (this.isInDocument()) {458this.attachMenuEventListeners_(oldMenu, false);459}460delete this.menu_;461}462if (this.isInDocument()) {463goog.a11y.aria.setState(464this.getElementStrict(), goog.a11y.aria.State.HASPOPUP, !!menu);465}466if (menu) {467this.menu_ = menu;468menu.setParent(this);469menu.setVisible(false);470menu.setAllowAutoFocus(this.isFocusablePopupMenu());471if (this.isInDocument()) {472this.attachMenuEventListeners_(menu, true);473}474}475}476477return oldMenu;478};479480481/**482* Specify which positioning algorithm to use.483*484* This method is preferred over the fine-grained positioning methods like485* setPositionElement, setAlignMenuToStart, and setScrollOnOverflow. Calling486* this method will override settings by those methods.487*488* @param {goog.positioning.AnchoredPosition} position The position of the489* Menu the button. If the position has a null anchor, we will use the490* menubutton element as the anchor.491*/492goog.ui.MenuButton.prototype.setMenuPosition = function(position) {493'use strict';494if (position) {495this.menuPosition_ = position;496this.positionElement_ = position.element;497}498};499500501/**502* Sets an element for anchoring the menu.503* @param {Element} positionElement New element to use for504* positioning the dropdown menu. Null to use the default behavior505* of positioning to this menu button.506*/507goog.ui.MenuButton.prototype.setPositionElement = function(positionElement) {508'use strict';509this.positionElement_ = positionElement;510this.positionMenu();511};512513514/**515* Sets a margin that will be applied to the menu's position when it is shown.516* If null, no margin will be applied.517* @param {goog.math.Box} margin Margin to apply.518*/519goog.ui.MenuButton.prototype.setMenuMargin = function(margin) {520'use strict';521this.menuMargin_ = margin;522};523524/**525* Sets whether the enter or space key should close the menu, if it is already526* open. By default, only the ESC key will close an open menu.527* @param {boolean} close Whether pressing Enter or Space when the button has528* focus will close the menu if it is already open.529*/530goog.ui.MenuButton.prototype.setCloseOnEnterOrSpace = function(close) {531this.closeOnEnterOrSpace_ = close;532};533534/**535* Sets whether to select the first item in the menu when it is opened using536* enter or space. By default, the first item is selected only when537* opened by a key up or down event. When this is on, the first item will538* be selected due to any of the four events.539* @param {boolean} select540*/541goog.ui.MenuButton.prototype.setSelectFirstOnEnterOrSpace = function(select) {542'use strict';543this.selectFirstOnEnterOrSpace_ = select;544};545546547/**548* Adds a new menu item at the end of the menu.549* @param {goog.ui.MenuItem|goog.ui.MenuSeparator|goog.ui.Control} item Menu550* item to add to the menu.551*/552goog.ui.MenuButton.prototype.addItem = function(item) {553'use strict';554this.getMenu().addChild(item, true);555};556557558/**559* Adds a new menu item at the specific index in the menu.560* @param {goog.ui.MenuItem|goog.ui.MenuSeparator} item Menu item to add to the561* menu.562* @param {number} index Index at which to insert the menu item.563*/564goog.ui.MenuButton.prototype.addItemAt = function(item, index) {565'use strict';566this.getMenu().addChildAt(item, index, true);567};568569570/**571* Removes the item from the menu and disposes of it.572* @param {goog.ui.MenuItem|goog.ui.MenuSeparator} item The menu item to remove.573*/574goog.ui.MenuButton.prototype.removeItem = function(item) {575'use strict';576var child = this.getMenu().removeChild(item, true);577if (child) {578child.dispose();579}580};581582583/**584* Removes the menu item at a given index in the menu and disposes of it.585* @param {number} index Index of item.586*/587goog.ui.MenuButton.prototype.removeItemAt = function(index) {588'use strict';589var child = this.getMenu().removeChildAt(index, true);590if (child) {591child.dispose();592}593};594595596/**597* Returns the menu item at a given index.598* @param {number} index Index of menu item.599* @return {goog.ui.MenuItem?} Menu item (null if not found).600*/601goog.ui.MenuButton.prototype.getItemAt = function(index) {602'use strict';603return this.menu_ ?604/** @type {goog.ui.MenuItem} */ (this.menu_.getChildAt(index)) :605null;606};607608609/**610* Returns the number of items in the menu (including separators).611* @return {number} The number of items in the menu.612*/613goog.ui.MenuButton.prototype.getItemCount = function() {614'use strict';615return this.menu_ ? this.menu_.getChildCount() : 0;616};617618619/**620* Shows/hides the menu button based on the value of the argument. Also hides621* the popup menu if the button is being hidden.622* @param {boolean} visible Whether to show or hide the button.623* @param {boolean=} opt_force If true, doesn't check whether the component624* already has the requested visibility, and doesn't dispatch any events.625* @return {boolean} Whether the visibility was changed.626* @override627*/628goog.ui.MenuButton.prototype.setVisible = function(visible, opt_force) {629'use strict';630var visibilityChanged =631goog.ui.MenuButton.superClass_.setVisible.call(this, visible, opt_force);632if (visibilityChanged && !this.isVisible()) {633this.setOpen(false);634}635return visibilityChanged;636};637638639/**640* Enables/disables the menu button based on the value of the argument, and641* updates its CSS styling. Also hides the popup menu if the button is being642* disabled.643* @param {boolean} enable Whether to enable or disable the button.644* @override645*/646goog.ui.MenuButton.prototype.setEnabled = function(enable) {647'use strict';648goog.ui.MenuButton.superClass_.setEnabled.call(this, enable);649if (!this.isEnabled()) {650this.setOpen(false);651}652};653654655// TODO(nicksantos): AlignMenuToStart and ScrollOnOverflow and PositionElement656// should all be deprecated, in favor of people setting their own657// AnchoredPosition with the parameters they need. Right now, we try658// to be backwards-compatible as possible, but this is incomplete because659// the APIs are non-orthogonal.660661662/**663* @return {boolean} Whether the menu is aligned to the start of the button664* (left if the render direction is left-to-right, right if the render665* direction is right-to-left).666*/667goog.ui.MenuButton.prototype.isAlignMenuToStart = function() {668'use strict';669var corner = this.menuPosition_.corner;670return corner == goog.positioning.Corner.BOTTOM_START ||671corner == goog.positioning.Corner.TOP_START;672};673674675/**676* Sets whether the menu is aligned to the start or the end of the button.677* @param {boolean} alignToStart Whether the menu is to be aligned to the start678* of the button (left if the render direction is left-to-right, right if679* the render direction is right-to-left).680*/681goog.ui.MenuButton.prototype.setAlignMenuToStart = function(alignToStart) {682'use strict';683this.menuPosition_.corner = alignToStart ?684goog.positioning.Corner.BOTTOM_START :685goog.positioning.Corner.BOTTOM_END;686};687688689/**690* Sets whether the menu should scroll when it's too big to fix vertically on691* the screen. The css of the menu element should have overflow set to auto.692* Note: Adding or removing items while the menu is open will not work correctly693* if scrollOnOverflow is on.694* @param {boolean} scrollOnOverflow Whether the menu should scroll when too big695* to fit on the screen. If false, adjust logic will be used to try and696* reposition the menu to fit.697* @suppress {strictMissingProperties} Added to tighten compiler checks698*/699goog.ui.MenuButton.prototype.setScrollOnOverflow = function(scrollOnOverflow) {700'use strict';701if (this.menuPosition_.setLastResortOverflow) {702var overflowX = goog.positioning.Overflow.ADJUST_X;703var overflowY = scrollOnOverflow ? goog.positioning.Overflow.RESIZE_HEIGHT :704goog.positioning.Overflow.ADJUST_Y;705this.menuPosition_.setLastResortOverflow(overflowX | overflowY);706}707};708709710/**711* @return {boolean} Whether the menu will scroll when it's to big to fit712* vertically on the screen.713* @suppress {strictMissingProperties} Added to tighten compiler checks714*/715goog.ui.MenuButton.prototype.isScrollOnOverflow = function() {716'use strict';717return this.menuPosition_.getLastResortOverflow &&718!!(this.menuPosition_.getLastResortOverflow() &719goog.positioning.Overflow.RESIZE_HEIGHT);720};721722723/**724* @return {boolean} Whether the attached menu is focusable.725*/726goog.ui.MenuButton.prototype.isFocusablePopupMenu = function() {727'use strict';728return this.isFocusablePopupMenu_;729};730731732/**733* Sets whether the attached popup menu is focusable. If the popup menu is734* focusable, it may steal keyboard focus from the menu button, so the button735* will not hide the menu on blur.736* @param {boolean} focusable Whether the attached menu is focusable.737*/738goog.ui.MenuButton.prototype.setFocusablePopupMenu = function(focusable) {739'use strict';740// TODO(attila): The menu itself should advertise whether it is focusable.741this.isFocusablePopupMenu_ = focusable;742};743744745/**746* Sets whether to render the menu as a sibling element of the button.747* Normally, the menu is a child of document.body. This option is useful if748* you need the menu to inherit styles from a common parent element, or if you749* otherwise need it to share a parent element for desired event handling. One750* example of the latter is if the parent is in a goog.ui.Popup, to ensure that751* clicks on the menu are considered being within the popup.752* @param {boolean} renderMenuAsSibling Whether we render the menu at the end753* of the dom or as a sibling to the button/label that renders the drop754* down.755*/756goog.ui.MenuButton.prototype.setRenderMenuAsSibling = function(757renderMenuAsSibling) {758'use strict';759this.renderMenuAsSibling_ = renderMenuAsSibling;760};761762763/**764* Reveals the menu and hooks up menu-specific event handling.765* @deprecated Use {@link #setOpen} instead.766*/767goog.ui.MenuButton.prototype.showMenu = function() {768'use strict';769this.setOpen(true);770};771772773/**774* Hides the menu and cleans up menu-specific event handling.775* @deprecated Use {@link #setOpen} instead.776*/777goog.ui.MenuButton.prototype.hideMenu = function() {778'use strict';779this.setOpen(false);780};781782783/**784* Opens or closes the attached popup menu.785* @param {boolean} open Whether to open or close the menu.786* @param {goog.events.Event=} opt_e Event that caused the menu to be opened.787* @override788*/789goog.ui.MenuButton.prototype.setOpen = function(open, opt_e) {790'use strict';791goog.ui.MenuButton.superClass_.setOpen.call(this, open);792if (this.menu_ && this.hasState(goog.ui.Component.State.OPENED) == open) {793if (open) {794if (!this.menu_.isInDocument()) {795if (this.renderMenuAsSibling_) {796// When we render the menu in the same parent as this button, we797// prefer to add it immediately after the button. This way, the screen798// readers will go to the menu on the very next element after the799// button is read.800var nextElementSibling =801goog.dom.getNextElementSibling(this.getElement());802if (nextElementSibling) {803this.menu_.renderBefore(nextElementSibling);804} else {805this.menu_.render(806/** @type {Element} */ (this.getElement().parentNode));807}808} else {809this.menu_.render();810}811}812this.viewportBox_ =813goog.style.getVisibleRectForElement(this.getElement());814this.buttonRect_ = goog.style.getBounds(this.getElement());815this.positionMenu();816817// As per aria spec, highlight the first element in the menu when818// keyboarding up or down. Thus, the first menu item will be announced819// for screen reader users. If selectFirstOnEnterOrSpace is set, do this820// for enter or space as well.821/**822* @suppress {strictMissingProperties} Added to tighten compiler checks823*/824var isEnterOrSpace = !!opt_e &&825(opt_e.keyCode == goog.events.KeyCodes.ENTER ||826opt_e.keyCode == goog.events.KeyCodes.SPACE);827/**828* @suppress {strictMissingProperties} Added to tighten compiler checks829*/830var isUpOrDown = !!opt_e &&831(opt_e.keyCode == goog.events.KeyCodes.DOWN ||832opt_e.keyCode == goog.events.KeyCodes.UP);833var focus =834isUpOrDown || (isEnterOrSpace && this.selectFirstOnEnterOrSpace_);835if (focus) {836this.menu_.highlightFirst();837} else {838this.menu_.setHighlightedIndex(-1);839}840} else {841this.setActive(false);842this.menu_.setMouseButtonPressed(false);843844var element = this.getElement();845// Clear any remaining a11y state.846if (element) {847goog.a11y.aria.setState(848element, goog.a11y.aria.State.ACTIVEDESCENDANT, '');849goog.a11y.aria.setState(element, goog.a11y.aria.State.OWNS, '');850}851852// Clear any sizes that might have been stored.853if (this.originalSize_ != null) {854this.originalSize_ = undefined;855var elem = this.menu_.getElement();856if (elem) {857goog.style.setSize(elem, '', '');858}859}860}861this.menu_.setVisible(open, false, opt_e);862// In Pivot Tables the menu button somehow gets disposed of during the863// setVisible call, causing attachPopupListeners_ to fail.864// TODO(user): Debug what happens.865if (!this.isDisposed()) {866this.attachPopupListeners_(open);867}868}869if (this.menu_ && this.menu_.getElement()) {870// Remove the aria-hidden state on the menu element so that it won't be871// hidden to screen readers if it's inside a dialog (see b/17610491).872goog.a11y.aria.removeState(873this.menu_.getElementStrict(), goog.a11y.aria.State.HIDDEN);874}875};876877878/**879* Resets the MenuButton's size. This is useful for cases where items are added880* or removed from the menu and scrollOnOverflow is on. In those cases the881* menu will not behave correctly and resize itself unless this is called882* (usually followed by positionMenu()).883*/884goog.ui.MenuButton.prototype.invalidateMenuSize = function() {885'use strict';886this.originalSize_ = undefined;887};888889890/**891* Positions the menu under the button. May be called directly in cases when892* the menu size is known to change.893*/894goog.ui.MenuButton.prototype.positionMenu = function() {895'use strict';896if (!this.menu_.isInDocument()) {897return;898}899900var positionElement = this.positionElement_ || this.getElement();901var position = this.menuPosition_;902this.menuPosition_.element = positionElement;903904var elem = this.menu_.getElement();905if (!this.menu_.isVisible()) {906elem.style.visibility = 'hidden';907goog.style.setElementShown(elem, true);908}909910if (!this.originalSize_ && this.isScrollOnOverflow()) {911this.originalSize_ = goog.style.getSize(elem);912}913var popupCorner = goog.positioning.flipCornerVertical(position.corner);914position.reposition(elem, popupCorner, this.menuMargin_, this.originalSize_);915916if (!this.menu_.isVisible()) {917goog.style.setElementShown(elem, false);918elem.style.visibility = 'visible';919}920};921922923/**924* Periodically repositions the menu while it is visible.925*926* @param {goog.events.Event} e An event object.927* @private928*/929goog.ui.MenuButton.prototype.onTick_ = function(e) {930'use strict';931// Call positionMenu() only if the button position or size was932// changed, or if the window's viewport was changed.933var currentButtonRect = goog.style.getBounds(this.getElement());934var currentViewport = goog.style.getVisibleRectForElement(this.getElement());935if (goog.math.Rect.equals(this.buttonRect_, currentButtonRect) &&936goog.math.Box.equals(this.viewportBox_, currentViewport)) {937return;938}939940// Reduction in the viewport width (e.g. due to increasing the zoom) can941// cause the menu to get squashed against the right edge, distorting its942// shape. When we move the menu back where it belongs, we risk using the943// distorted size, causing mispositioning. To be safe, start by moving the944// menu to the top left to let it reassume its true shape.945if (this.menu_.isInDocument() && currentViewport && this.viewportBox_ &&946(currentViewport.getWidth() < this.viewportBox_.getWidth())) {947var elem = this.menu_.getElement();948if (!this.menu_.isVisible()) {949elem.style.visibility = 'hidden';950goog.style.setElementShown(elem, true);951}952953goog.style.setPosition(elem, new goog.math.Coordinate(0, 0));954}955956this.buttonRect_ = currentButtonRect;957this.viewportBox_ = currentViewport;958this.positionMenu();959};960961962/**963* Attaches or detaches menu event listeners to/from the given menu.964* Called each time a menu is attached to or detached from the button.965* @param {goog.ui.Menu} menu Menu on which to listen for events.966* @param {boolean} attach Whether to attach or detach event listeners.967* @private968*/969goog.ui.MenuButton.prototype.attachMenuEventListeners_ = function(970menu, attach) {971'use strict';972var handler = this.getHandler();973var method = attach ? handler.listen : handler.unlisten;974975// Handle events dispatched by menu items.976method.call(977handler, menu, goog.ui.Component.EventType.ACTION, this.handleMenuAction);978method.call(979handler, menu, goog.ui.Component.EventType.CLOSE, this.handleCloseItem);980method.call(981handler, menu, goog.ui.Component.EventType.HIGHLIGHT,982this.handleHighlightItem);983method.call(984handler, menu, goog.ui.Component.EventType.UNHIGHLIGHT,985this.handleUnHighlightItem);986};987988989/**990* Attaches or detaches a keydown event listener to/from the given element.991* Called each time the button enters or exits the document.992* @param {boolean} attach Whether to attach or detach the event listener.993* @private994*/995goog.ui.MenuButton.prototype.attachKeyDownEventListener_ = function(attach) {996'use strict';997var handler = this.getHandler();998var method = attach ? handler.listen : handler.unlisten;9991000// Handle keydown events dispatched by the button.1001method.call(1002handler, this.getElement(), goog.events.EventType.KEYDOWN,1003this.handleKeyDownEvent_);1004};100510061007/**1008* Handles `HIGHLIGHT` events dispatched by the attached menu.1009* @param {goog.events.Event} e Highlight event to handle.1010*/1011goog.ui.MenuButton.prototype.handleHighlightItem = function(e) {1012'use strict';1013/** @suppress {strictMissingProperties} Added to tighten compiler checks */1014var targetEl = e.target.getElement();1015if (targetEl) {1016this.setAriaActiveDescendant_(targetEl);1017}1018};101910201021/**1022* Handles `KEYDOWN` events dispatched by the button element. When the1023* button is focusable and the menu is present and visible, prevents the event1024* from propagating since the desired behavior is only to close the menu.1025* @param {goog.events.Event} e KeyDown event to handle.1026* @private1027*/1028goog.ui.MenuButton.prototype.handleKeyDownEvent_ = function(e) {1029'use strict';1030if (this.isSupportedState(goog.ui.Component.State.FOCUSED) &&1031this.getKeyEventTarget() && this.menu_ && this.menu_.isVisible()) {1032e.stopPropagation();1033}1034};103510361037/**1038* Handles UNHIGHLIGHT events dispatched by the associated menu.1039* @param {goog.events.Event} e Unhighlight event to handle.1040*/1041goog.ui.MenuButton.prototype.handleUnHighlightItem = function(e) {1042'use strict';1043if (!this.menu_.getHighlighted()) {1044var element = this.getElement();1045goog.asserts.assert(element, 'The menu button DOM element cannot be null.');1046goog.a11y.aria.setState(element, goog.a11y.aria.State.ACTIVEDESCENDANT, '');1047goog.a11y.aria.setState(element, goog.a11y.aria.State.OWNS, '');1048}1049};105010511052/**1053* Handles `CLOSE` events dispatched by the associated menu.1054* @param {goog.events.Event} e Close event to handle.1055*/1056goog.ui.MenuButton.prototype.handleCloseItem = function(e) {1057'use strict';1058// When a submenu is closed by pressing left arrow, no highlight event is1059// dispatched because the newly focused item was already highlighted, so this1060// scenario is handled by listening for the submenu close event instead.1061if (this.isOpen() && e.target instanceof goog.ui.MenuItem) {1062var menuItem = /** @type {!goog.ui.MenuItem} */ (e.target);1063var menuItemEl = menuItem.getElement();1064if (menuItem.isVisible() && menuItem.isHighlighted() &&1065menuItemEl != null) {1066this.setAriaActiveDescendant_(menuItemEl);1067}1068}1069};107010711072/**1073* Updates the aria-activedescendant attribute to the given target element.1074* @param {!Element} targetEl The target element.1075* @private1076*/1077goog.ui.MenuButton.prototype.setAriaActiveDescendant_ = function(targetEl) {1078'use strict';1079var element = this.getElement();1080goog.asserts.assert(element, 'The menu button DOM element cannot be null.');10811082// If target element has an activedescendant, then set this control's1083// activedescendant to that, otherwise set it to the target element. This is1084// a workaround for some screen readers which do not handle1085// aria-activedescendant redirection properly.1086var targetActiveDescendant = goog.a11y.aria.getActiveDescendant(targetEl);1087var activeDescendant = targetActiveDescendant || targetEl;10881089if (!activeDescendant.id) {1090// Create an id if there isn't one already.1091var idGenerator = goog.ui.IdGenerator.getInstance();1092activeDescendant.id = idGenerator.getNextUniqueId();1093}10941095goog.a11y.aria.setActiveDescendant(element, activeDescendant);1096goog.a11y.aria.setState(1097element, goog.a11y.aria.State.OWNS, activeDescendant.id);1098};109911001101/**1102* Attaches or detaches event listeners depending on whether the popup menu1103* is being shown or hidden. Starts listening for document mousedown events1104* and for menu blur events when the menu is shown, and stops listening for1105* these events when it is hidden. Called from {@link #setOpen}.1106* @param {boolean} attach Whether to attach or detach event listeners.1107* @private1108*/1109goog.ui.MenuButton.prototype.attachPopupListeners_ = function(attach) {1110'use strict';1111var handler = this.getHandler();1112var method = attach ? handler.listen : handler.unlisten;11131114// Listen for document mousedown events in the capture phase, because1115// the target may stop propagation of the event in the bubble phase.1116method.call(1117handler, this.getDomHelper().getDocument(),1118goog.events.EventType.MOUSEDOWN, this.handleDocumentMouseDown, true);11191120// Only listen for blur events dispatched by the menu if it is focusable.1121if (this.isFocusablePopupMenu()) {1122method.call(1123handler, /** @type {!goog.events.EventTarget} */ (this.menu_),1124goog.ui.Component.EventType.BLUR, this.handleMenuBlur);1125}11261127method.call(handler, this.timer_, goog.Timer.TICK, this.onTick_);1128if (attach) {1129this.timer_.start();1130} else {1131this.timer_.stop();1132}1133};113411351136// Register a decorator factory function for goog.ui.MenuButtons.1137goog.ui.registry.setDecoratorByClassName(1138goog.ui.MenuButtonRenderer.CSS_CLASS, function() {1139'use strict';1140// MenuButton defaults to using MenuButtonRenderer.1141return new goog.ui.MenuButton(null);1142});114311441145