Project import generated by Copybara.
GitOrigin-RevId: 63746295f1a5ab5a619056791995793d65529e62
diff --git a/node_modules/mdl-ext/src/menu-button/menu-button.js b/node_modules/mdl-ext/src/menu-button/menu-button.js
new file mode 100644
index 0000000..e7e37d8
--- /dev/null
+++ b/node_modules/mdl-ext/src/menu-button/menu-button.js
@@ -0,0 +1,758 @@
+/**
+ * @license
+ * Copyright 2016 Leif Olsen. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * This code is built with Google Material Design Lite,
+ * which is Licensed under the Apache License, Version 2.0
+ */
+
+
+/**
+ * A menu button is a button that opens a menu. It is often styled as a
+ * typical push button with a downward pointing arrow or triangle to hint
+ * that activating the button will display a menu.
+ */
+import { randomString } from '../utils/string-utils';
+import fullThrottle from '../utils/full-throttle';
+import {
+ VK_TAB,
+ VK_ENTER,
+ VK_ESC,
+ VK_SPACE,
+ VK_END,
+ VK_HOME,
+ VK_ARROW_LEFT,
+ VK_ARROW_UP,
+ VK_ARROW_RIGHT,
+ VK_ARROW_DOWN,
+ IS_UPGRADED,
+} from '../utils/constants';
+
+import { getScrollParents, tether } from '../utils/dom-utils';
+
+const JS_MENU_BUTTON = 'mdlext-js-menu-button';
+const MENU_BUTTON_MENU = 'mdlext-menu';
+const MENU_BUTTON_MENU_ITEM = 'mdlext-menu__item';
+const MENU_BUTTON_MENU_ITEM_SEPARATOR = 'mdlext-menu__item-separator';
+//const MDL_LAYOUT_CONTENT = 'mdl-layout__content';
+
+/**
+ * Creates the menu controlled by the menu button
+ * @param element
+ * @return {{element: Element, selected: Element, open: (function(*=)), removeListeners: (function()), downgrade: (function())}}
+ */
+
+const menuFactory = element => {
+
+ let ariaControls = null;
+ let parentNode = null;
+
+ const removeAllSelected = () => {
+ [...element.querySelectorAll(`.${MENU_BUTTON_MENU_ITEM}[aria-selected="true"]`)]
+ .forEach(selectedItem => selectedItem.removeAttribute('aria-selected'));
+ };
+
+ const setSelected = (item, force=false) => {
+ if(force || (item && !item.hasAttribute('aria-selected'))) {
+ removeAllSelected();
+ if(item) {
+ item.setAttribute('aria-selected', 'true');
+ }
+ }
+ };
+
+ const getSelected = () => {
+ return element.querySelector(`.${MENU_BUTTON_MENU_ITEM}[aria-selected="true"]`);
+ };
+
+ const isDisabled = item => item && item.hasAttribute('disabled');
+
+ const isSeparator = item => item && item.classList.contains(MENU_BUTTON_MENU_ITEM_SEPARATOR);
+
+ const focus = item => {
+ if(item) {
+ item = item.closest(`.${MENU_BUTTON_MENU_ITEM}`);
+ }
+ if(item) {
+ item.focus();
+ }
+ };
+
+ const nextItem = current => {
+ let n = current.nextElementSibling;
+ if(!n) {
+ n = element.firstElementChild;
+ }
+ if(!isDisabled(n) && !isSeparator(n)) {
+ focus(n);
+ }
+ else {
+ let i = element.children.length;
+ while(n && i-- > 0) {
+ if(isDisabled(n) || isSeparator(n)) {
+ n = n.nextElementSibling;
+ if(!n) {
+ n = element.firstElementChild;
+ }
+ }
+ else {
+ focus(n);
+ break;
+ }
+ }
+ }
+ };
+
+ const previousItem = current => {
+ let p = current.previousElementSibling;
+ if(!p) {
+ p = element.lastElementChild;
+ }
+ if(!isDisabled(p) && !isSeparator(p)) {
+ focus(p);
+ }
+ else {
+ let i = element.children.length;
+ while(p && i-- > 0) {
+ if(isDisabled(p) || isSeparator(p)) {
+ p = p.previousElementSibling;
+ if(!p) {
+ p = element.lastElementChild;
+ }
+ }
+ else {
+ focus(p);
+ break;
+ }
+ }
+ }
+ };
+
+ const firstItem = () => {
+ const item = element.firstElementChild;
+ if(isDisabled(item) || isSeparator(item) ) {
+ nextItem(item);
+ }
+ else {
+ focus(item);
+ }
+ };
+
+ const lastItem = () => {
+ const item = element.lastElementChild;
+ if(isDisabled(item) || isSeparator(item)) {
+ previousItem(item);
+ }
+ else {
+ focus(item);
+ }
+ };
+
+ const selectItem = item => {
+ if(item && !isDisabled(item) && !isSeparator(item)) {
+ setSelected(item);
+ close(true, item);
+ }
+ };
+
+ const keyDownHandler = event => {
+
+ const item = event.target.closest(`.${MENU_BUTTON_MENU_ITEM}`);
+
+ switch (event.keyCode) {
+ case VK_ARROW_UP:
+ case VK_ARROW_LEFT:
+ if(item) {
+ previousItem(item);
+ }
+ else {
+ firstItem();
+ }
+ break;
+
+ case VK_ARROW_DOWN:
+ case VK_ARROW_RIGHT:
+ if(item) {
+ nextItem(item);
+ }
+ else {
+ lastItem();
+ }
+ break;
+
+ case VK_HOME:
+ firstItem();
+ break;
+
+ case VK_END:
+ lastItem();
+ break;
+
+ case VK_SPACE:
+ case VK_ENTER:
+ selectItem(item);
+ break;
+
+ case VK_ESC:
+ close(true);
+ break;
+
+ case VK_TAB:
+ // We do not have a "natural" tab order from menu, so the best we can do is to set focus back to the button
+ close(true);
+ break;
+
+ default:
+ return;
+ }
+ event.preventDefault();
+ };
+
+
+ const blurHandler = event => {
+
+ // See: https://github.com/facebook/react/issues/2011
+ const t = event.relatedTarget ||
+ event.explicitOriginalTarget || // FF
+ document.activeElement; // IE11
+
+ //console.log('***** blur, target, relatedTarget', event.target, t);
+
+ try {
+ if (t) {
+ if (t.closest(`.${MENU_BUTTON_MENU}`) !== element && shouldClose(t)) {
+ close();
+ }
+ }
+ else {
+ close();
+ }
+ }
+ catch(err) {
+ // FF throws error: "TypeError: n.closest is not a function" if related target is a text node
+ close();
+ }
+ };
+
+ const clickHandler = event => {
+ //console.log('***** click, target', event.target);
+
+ event.preventDefault();
+ const t = event.target;
+ if (t && t.closest(`.${MENU_BUTTON_MENU}`) === element) {
+ const item = t.closest(`.${MENU_BUTTON_MENU_ITEM}`);
+ if (item) {
+ selectItem(item);
+ }
+ }
+ else {
+ if (shouldClose(t)) {
+ close();
+ }
+ }
+ };
+
+ const touchStartHandler = event => {
+ //console.log('***** touchStart, target', event.target);
+
+ const t = event.target;
+ if(!(t && t.closest(`.${MENU_BUTTON_MENU}`) === element)) {
+ if (event.type === 'touchstart') {
+ event.preventDefault();
+ }
+ close();
+ }
+ };
+
+ const addListeners = () => {
+ element.addEventListener('keydown', keyDownHandler, false);
+ element.addEventListener('blur', blurHandler, true);
+ element.addEventListener('click', clickHandler, true);
+ document.documentElement.addEventListener('touchstart', touchStartHandler, true);
+ };
+
+ const removeListeners = () => {
+ element.removeEventListener('keydown', keyDownHandler, false);
+ element.removeEventListener('blur', blurHandler, true);
+ element.removeEventListener('click', clickHandler, true);
+ document.documentElement.removeEventListener('touchstart', touchStartHandler, true);
+ };
+
+ const open = (controlElement, position='first') => {
+
+ ariaControls = controlElement.closest(`.${JS_MENU_BUTTON}`);
+
+ element.style['min-width'] = `${Math.max(124, controlElement.getBoundingClientRect().width)}px`;
+ element.removeAttribute('hidden');
+ tether(controlElement, element);
+
+ let item;
+ switch (position.toLowerCase()) {
+ case 'first':
+ firstItem();
+ break;
+
+ case 'last':
+ lastItem();
+ break;
+
+ case 'selected':
+ item = getSelected();
+ if(item && !item.hasAttribute('disabled')) {
+ focus(item);
+ }
+ else {
+ firstItem();
+ }
+ break;
+ }
+
+ addListeners();
+ };
+
+
+ const shouldClose = target => {
+ //console.log('***** shouldClose');
+
+ let result = false;
+ const btn = (target && target.closest(`.${JS_MENU_BUTTON}`)) || null;
+ if(!btn) {
+ result = true;
+ }
+ else if(btn.getAttribute('aria-controls') === element.id) {
+ if(btn !== ariaControls) {
+ result = true;
+ }
+ }
+ else {
+ result = true;
+ }
+ return result;
+ };
+
+ const close = (forceFocus = false, item = null) => {
+ removeListeners();
+
+ element.dispatchEvent(
+ new CustomEvent('_closemenu', {
+ bubbles: true,
+ cancelable: true,
+ detail: { forceFocus: forceFocus, item: item }
+ })
+ );
+ };
+
+ const addWaiAria = () => {
+ if (!element.hasAttribute('id')) {
+ // Generate a random id
+ element.id = `menu-button-${randomString()}`;
+ }
+ element.setAttribute('tabindex', '-1');
+ element.setAttribute('role', 'menu');
+ element.setAttribute('hidden', '');
+
+ [...element.querySelectorAll(`.${MENU_BUTTON_MENU_ITEM}`)].forEach( menuitem => {
+ menuitem.setAttribute('tabindex', '-1');
+ menuitem.setAttribute('role', 'menuitem');
+ });
+
+ [...element.querySelectorAll(`.${MENU_BUTTON_MENU_ITEM_SEPARATOR}`)].forEach( menuitem => {
+ menuitem.setAttribute('role', 'separator');
+ });
+ };
+
+ const init = () => {
+ addWaiAria();
+ parentNode = element.parentNode;
+ element.classList.add('is-upgraded');
+ };
+
+ const downgrade = () => {
+ removeListeners();
+ if(element.parentNode !== parentNode) {
+ parentNode.appendChild(element);
+ }
+ element.classList.remove('is-upgraded');
+ };
+
+ init();
+
+ return {
+ /**
+ * Get the menu element
+ * @returns {Element} the menu element
+ */
+ get element() {
+ return element;
+ },
+
+ /**
+ * Set selected menu item
+ * @param item
+ */
+ set selected(item) {
+ setSelected(item, true);
+ },
+
+ /**
+ * Open menu
+ * @param {Element} controlElement the element where the menu should be aligned to
+ * @param {String} position menuElement item to receive focus after menu element is opened
+ */
+ open: (controlElement, position='first') => open(controlElement, position),
+
+ /**
+ * Remove event listeners.
+ */
+ removeListeners: () => removeListeners(),
+
+ /**
+ * Downgrade menu
+ */
+ downgrade: () => downgrade(),
+ };
+};
+
+
+/**
+ * The menubutton component
+ */
+
+class MenuButton {
+
+ constructor(element) {
+ this.element = element;
+ this.focusElement = undefined;
+ this.focusElementLastScrollPosition = undefined;
+ this.scrollElements = [];
+ this.menu = undefined;
+ this.selectedItem = null;
+ this.init();
+ }
+
+ keyDownHandler = event => {
+ if(!this.isDisabled()) {
+ switch (event.keyCode) {
+ case VK_ARROW_UP:
+ this.openMenu('last');
+ break;
+
+ case VK_ARROW_DOWN:
+ this.openMenu();
+ break;
+
+ case VK_SPACE:
+ case VK_ENTER:
+ this.openMenu('selected');
+ break;
+
+ case VK_ESC:
+ this.closeMenu();
+ break;
+
+ case VK_TAB:
+ this.closeMenu();
+ return;
+
+ default:
+ return;
+ }
+ }
+ //event.stopPropagation();
+ event.preventDefault();
+ };
+
+ clickHandler = () => {
+ if(!this.isDisabled()) {
+ if(this.element.getAttribute('aria-expanded').toLowerCase() === 'true') {
+ this.closeMenu(true);
+ }
+ else {
+ this.openMenu('selected');
+ }
+ }
+ };
+
+ /**
+ * Re-position menu if content is scrolled, window is resized or orientation change
+ * @see https://javascriptweblog.wordpress.com/2015/11/02/of-classes-and-arrow-functions-a-cautionary-tale/
+ */
+ recalcMenuPosition = fullThrottle( () => {
+ const c = this.focusElement.getBoundingClientRect();
+ const dx = this.focusElementLastScrollPosition.left - c.left;
+ const dy = this.focusElementLastScrollPosition.top - c.top;
+ const left = (parseFloat(this.menu.element.style.left) || 0) - dx;
+ const top = (parseFloat(this.menu.element.style.top) || 0) - dy;
+
+ this.menu.element.style.left = `${left}px`;
+ this.menu.element.style.top = `${top}px`;
+ this.focusElementLastScrollPosition = c;
+ });
+
+
+ positionChangeHandler = () => {
+ this.recalcMenuPosition(this);
+ };
+
+ closeMenuHandler = event => {
+ if(event && event.detail) {
+ if(event.detail.item && event.detail.item !== this.selectedItem) {
+ this.selectedItem = event.detail.item;
+ this.dispatchMenuSelect();
+ }
+ this.closeMenu(event.detail.forceFocus);
+ }
+ };
+
+ dispatchMenuSelect() {
+ this.element.dispatchEvent(
+ new CustomEvent('menuselect', {
+ bubbles: true,
+ cancelable: true,
+ detail: { source: this.selectedItem }
+ })
+ );
+ }
+
+ isDisabled() {
+ return this.element.hasAttribute('disabled');
+ }
+
+ removeListeners() {
+ this.element.removeEventListener('keydown', this.keyDownHandler);
+ this.element.removeEventListener('click', this.clickHandler);
+ }
+
+ openMenu(position='first') {
+
+ if(!this.isDisabled() && this.menu) {
+
+ // Close the menu if button position change
+ this.scrollElements = getScrollParents(this.element);
+ this.scrollElements.forEach(el => el.addEventListener('scroll', this.positionChangeHandler));
+
+ window.addEventListener('resize', this.positionChangeHandler);
+ window.addEventListener('orientationchange', this.positionChangeHandler);
+ this.menu.element.addEventListener('_closemenu', this.closeMenuHandler);
+
+ this.menu.selected = this.selectedItem;
+ this.menu.open(this.focusElement, position);
+ this.element.setAttribute('aria-expanded', 'true');
+
+ this.focusElementLastScrollPosition = this.focusElement.getBoundingClientRect();
+ }
+ }
+
+ closeMenu(forceFocus = false) {
+ if(this.menu) {
+ this.menu.removeListeners();
+ this.scrollElements.forEach(el => el.removeEventListener('scroll', this.positionChangeHandler));
+ window.removeEventListener('resize', this.positionChangeHandler);
+ window.removeEventListener('orientationchange', this.positionChangeHandler);
+ this.menu.element.removeEventListener('_closemenu', this.closeMenuHandler);
+
+ if (forceFocus) {
+ this.focus();
+ }
+ this.element.setAttribute('aria-expanded', 'false');
+ this.menu.element.setAttribute('hidden', '');
+ }
+ }
+
+ focus() {
+ if(!this.isDisabled()) {
+ this.focusElement.focus();
+ }
+ }
+
+ init() {
+ const addListeners = () => {
+ this.element.addEventListener('keydown', this.keyDownHandler);
+ this.element.addEventListener('click', this.clickHandler);
+ };
+
+ const addWaiAria = () => {
+ this.element.setAttribute('role', 'button');
+ this.element.setAttribute('aria-expanded', 'false');
+ this.element.setAttribute('aria-haspopup', 'true');
+ };
+
+ const addFocusElement = () => {
+ this.focusElement = this.element.querySelector('input[type="text"]');
+ if(!this.focusElement) {
+ this.focusElement = this.element;
+
+ if(!(this.focusElement.tagName.toLowerCase() === 'button' || this.focusElement.tagName.toLowerCase() === 'input')) {
+ if (!this.focusElement.hasAttribute('tabindex')) {
+ this.focusElement.setAttribute('tabindex', '0');
+ }
+ }
+ }
+ };
+
+ const moveElementToDocumentBody = (element) => {
+ // To position an element on top of all other z-indexed elements, the element should be moved to document.body
+ // See: https://philipwalton.com/articles/what-no-one-told-you-about-z-index/
+
+ if(element.parentNode !== document.body) {
+ return document.body.appendChild(element);
+ }
+ return element;
+ };
+
+ const findMenuElement = () => {
+ let menuElement;
+ const menuElementId = this.element.getAttribute('aria-controls');
+ if(menuElementId !== null) {
+ menuElement = document.querySelector(`#${menuElementId }`);
+ }
+ else {
+ menuElement = this.element.parentNode.querySelector(`.${MENU_BUTTON_MENU}`);
+ }
+ return menuElement;
+ };
+
+ const addMenu = () => {
+ const menuElement = findMenuElement();
+ if(menuElement) {
+ if(menuElement.componentInstance) {
+ this.menu = menuElement.componentInstance;
+ }
+ else {
+ this.menu = menuFactory(menuElement);
+ menuElement.componentInstance = this.menu;
+ moveElementToDocumentBody(menuElement);
+ }
+ this.element.setAttribute('aria-controls', this.menu.element.id);
+ }
+ };
+
+ addFocusElement();
+ addWaiAria();
+ addMenu();
+ this.removeListeners();
+ addListeners();
+ }
+
+ downgrade() {
+ if(this.menu) {
+ // Do not downgrade menu if there are other buttons sharing this menu
+ const related = [...document.querySelectorAll(`.${JS_MENU_BUTTON}[aria-controls="${this.element.getAttribute('aria-controls')}"]`)];
+ if(related.filter( c => c !== this.element && c.getAttribute('data-upgraded').indexOf('MaterialExtMenuButton') >= 0).length === 0) {
+ this.menu.downgrade();
+ }
+ }
+ this.removeListeners();
+ }
+
+}
+
+(function() {
+ 'use strict';
+
+ /**
+ * https://github.com/google/material-design-lite/issues/4205
+ * @constructor
+ * @param {Element} element The element that will be upgraded.
+ */
+ const MaterialExtMenuButton = function MaterialExtMenuButton(element) {
+ this.element_ = element;
+ this.menuButton_ = null;
+
+ // Initialize instance.
+ this.init();
+ };
+ window['MaterialExtMenuButton'] = MaterialExtMenuButton;
+
+
+ // Public methods.
+
+ /**
+ * Get the menu element controlled by this button, null if no menu is controlled by this button
+ * @public
+ */
+ MaterialExtMenuButton.prototype.getMenuElement = function() {
+ return this.menuButton_.menu ? this.menuButton_.menu.element : null;
+ };
+ MaterialExtMenuButton.prototype['getMenuElement'] = MaterialExtMenuButton.prototype.getMenuElement;
+
+ /**
+ * Open menu
+ * @public
+ * @param {String} position one of "first", "last" or "selected"
+ */
+ MaterialExtMenuButton.prototype.openMenu = function(position) {
+ this.menuButton_.openMenu(position);
+ };
+ MaterialExtMenuButton.prototype['openMenu'] = MaterialExtMenuButton.prototype.openMenu;
+
+ /**
+ * Close menu
+ * @public
+ */
+ MaterialExtMenuButton.prototype.closeMenu = function() {
+ this.menuButton_.closeMenu(true);
+ };
+ MaterialExtMenuButton.prototype['closeMenu'] = MaterialExtMenuButton.prototype.closeMenu;
+
+ /**
+ * Get selected menu item
+ * @public
+ * @returns {Element} The selected menu item or null if no item selected
+ */
+ MaterialExtMenuButton.prototype.getSelectedMenuItem = function() {
+ return this.menuButton_.selectedItem;
+ };
+ MaterialExtMenuButton.prototype['getSelectedMenuItem'] = MaterialExtMenuButton.prototype.getSelectedMenuItem;
+
+
+ /**
+ * Set (default) selected menu item
+ * @param {Element} item
+ */
+ MaterialExtMenuButton.prototype.setSelectedMenuItem = function(item) {
+ this.menuButton_.selectedItem = item;
+ };
+ MaterialExtMenuButton.prototype['setSelectedMenuItem'] = MaterialExtMenuButton.prototype.setSelectedMenuItem;
+
+ /**
+ * Initialize component
+ */
+ MaterialExtMenuButton.prototype.init = function() {
+ if (this.element_) {
+ this.menuButton_ = new MenuButton(this.element_);
+ this.element_.addEventListener('mdl-componentdowngraded', this.mdlDowngrade_.bind(this));
+ this.element_.classList.add(IS_UPGRADED);
+ }
+ };
+
+ /**
+ * Downgrade component
+ * E.g remove listeners and clean up resources
+ */
+ MaterialExtMenuButton.prototype.mdlDowngrade_ = function() {
+ this.menuButton_.downgrade();
+ };
+
+ // The component registers itself. It can assume componentHandler is available
+ // in the global scope.
+ /* eslint no-undef: 0 */
+ componentHandler.register({
+ constructor: MaterialExtMenuButton,
+ classAsString: 'MaterialExtMenuButton',
+ cssClass: JS_MENU_BUTTON,
+ widget: true
+ });
+})();