fix(menu): setOpen must be coordinated by the controller

This commit is contained in:
Manu Mtz.-Almeida
2018-02-12 15:00:05 +01:00
parent dfb4fe2134
commit d83d8eed12
3 changed files with 75 additions and 65 deletions

View File

@ -1,5 +1,5 @@
import { Animation, AnimationBuilder, AnimationController, Menu } from '../../index';
import { Component, Method, Prop } from '@stencil/core'; import { Component, Method, Prop } from '@stencil/core';
import { Animation, AnimationBuilder, Menu } from '../../index';
import MenuOverlayAnimation from './animations/overlay'; import MenuOverlayAnimation from './animations/overlay';
import MenuPushAnimation from './animations/push'; import MenuPushAnimation from './animations/push';
@ -11,9 +11,9 @@ import MenuRevealAnimation from './animations/reveal';
export class MenuController { export class MenuController {
private menus: Menu[] = []; private menus: Menu[] = [];
private menuAnimations: { [name: string]: AnimationBuilder } = {}; private menuAnimations = new Map<string, AnimationBuilder>();
@Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController; @Prop({ connect: 'ion-animation-controller' }) animationCtrl: HTMLIonAnimationControllerElement;
constructor() { constructor() {
this.registerAnimation('reveal', MenuRevealAnimation); this.registerAnimation('reveal', MenuRevealAnimation);
@ -29,11 +29,7 @@ export class MenuController {
@Method() @Method()
open(menuId?: string): Promise<boolean> { open(menuId?: string): Promise<boolean> {
const menu = this.get(menuId); const menu = this.get(menuId);
if (menu && !this.isAnimating()) { if (menu) {
const openedMenu = this.getOpen();
if (openedMenu && menu !== openedMenu) {
openedMenu.setOpen(false, false);
}
return menu.open(); return menu.open();
} }
return Promise.resolve(false); return Promise.resolve(false);
@ -58,7 +54,6 @@ export class MenuController {
return Promise.resolve(false); return Promise.resolve(false);
} }
/** /**
* Toggle the menu. If it's closed, it will open, and if opened, it * Toggle the menu. If it's closed, it will open, and if opened, it
* will close. * will close.
@ -68,11 +63,7 @@ export class MenuController {
@Method() @Method()
toggle(menuId?: string): Promise<boolean> { toggle(menuId?: string): Promise<boolean> {
const menu = this.get(menuId); const menu = this.get(menuId);
if (menu && !this.isAnimating()) { if (menu) {
const openedMenu = this.getOpen();
if (openedMenu && menu !== openedMenu) {
openedMenu.setOpen(false, false);
}
return menu.toggle(); return menu.toggle();
} }
return Promise.resolve(false); return Promise.resolve(false);
@ -148,14 +139,12 @@ export class MenuController {
*/ */
@Method() @Method()
get(menuId?: string): HTMLIonMenuElement { get(menuId?: string): HTMLIonMenuElement {
let menu: Menu;
if (menuId === 'left' || menuId === 'right') { if (menuId === 'left' || menuId === 'right') {
// there could be more than one menu on the same side // there could be more than one menu on the same side
// so first try to get the enabled one // so first try to get the enabled one
menu = this.menus.find(m => m.side === menuId && !m.disabled); const menu = this.find(m => m.side === menuId && !m.disabled);
if (menu) { if (menu) {
return menu.getElement(); return menu;
} }
// didn't find a menu side that is enabled // didn't find a menu side that is enabled
@ -169,13 +158,13 @@ export class MenuController {
} }
// return the first enabled menu // return the first enabled menu
menu = this.menus.find(m => !m.disabled); const menu = this.find(m => !m.disabled);
if (menu) { if (menu) {
return menu.getElement(); return menu;
} }
// get the first menu in the array, if one exists // get the first menu in the array, if one exists
return (this.menus.length > 0 ? this.menus[0].getElement() : null); return (this.menus.length > 0 ? this.menus[0].el : null);
} }
/** /**
@ -191,7 +180,7 @@ export class MenuController {
*/ */
@Method() @Method()
getMenus(): HTMLIonMenuElement[] { getMenus(): HTMLIonMenuElement[] {
return this.menus.map(menu => menu.getElement()); return this.menus.map(menu => menu.el);
} }
/** /**
@ -238,23 +227,43 @@ export class MenuController {
.map(m => m.disabled = true); .map(m => m.disabled = true);
} }
/**
* @hidden
*/
@Method()
_setOpen(menu: Menu, shouldOpen: boolean, animated: boolean): Promise<boolean> {
if (this.isAnimating()) {
return Promise.resolve(false);
}
if (shouldOpen) {
const openedMenu = this.getOpen();
if (openedMenu && menu !== openedMenu) {
openedMenu.setOpen(false, false);
}
}
return menu._setOpen(shouldOpen, animated);
}
/** /**
* @hidden * @hidden
*/ */
@Method() @Method()
createAnimation(type: string, menuCmp: Menu): Promise<Animation> { createAnimation(type: string, menuCmp: Menu): Promise<Animation> {
const animationBuilder = this.menuAnimations[type]; const animationBuilder = this.menuAnimations.get(type);
if (!animationBuilder) {
return Promise.reject('animation not registered');
}
return this.animationCtrl.create(animationBuilder, null, menuCmp); return this.animationCtrl.create(animationBuilder, null, menuCmp);
} }
private registerAnimation(name: string, cls: AnimationBuilder) { private registerAnimation(name: string, animation: AnimationBuilder) {
this.menuAnimations[name] = cls; this.menuAnimations.set(name, animation);
} }
private find(predicate: (menu: Menu) => boolean): HTMLIonMenuElement { private find(predicate: (menu: Menu) => boolean): HTMLIonMenuElement {
const instance = this.menus.find(predicate); const instance = this.menus.find(predicate);
if (instance) { if (instance) {
return instance.getElement(); return instance.el;
} }
return null; return null;
} }

View File

@ -30,7 +30,7 @@ export class Menu {
contentEl: HTMLElement; contentEl: HTMLElement;
menuCtrl: HTMLIonMenuControllerElement; menuCtrl: HTMLIonMenuControllerElement;
@Element() private el: HTMLIonMenuElement; @Element() el: HTMLIonMenuElement;
@State() isRightSide = false; @State() isRightSide = false;
@ -41,7 +41,7 @@ export class Menu {
/** /**
* The content's id the menu should use. * The content's id the menu should use.
*/ */
@Prop() content: string; @Prop() contentId: string;
/** /**
* An id for the menu. * An id for the menu.
@ -56,11 +56,12 @@ export class Menu {
@Prop({ mutable: true }) type = 'overlay'; @Prop({ mutable: true }) type = 'overlay';
@Watch('type') @Watch('type')
typeChanged(type: string) { typeChanged(type: string, oldType: string | null) {
if (this.contentEl) { const contentEl = this.contentEl;
this.contentEl.classList.remove('menu-content-' + this.type); if (contentEl && oldType) {
this.contentEl.classList.add('menu-content-' + type); contentEl.classList.remove(`menu-content-${oldType}`);
this.contentEl.removeAttribute('style'); contentEl.classList.add(`menu-content-${type}`);
contentEl.removeAttribute('style');
} }
if (this.menuInnerEl) { if (this.menuInnerEl) {
// Remove effects of previous animations // Remove effects of previous animations
@ -132,8 +133,8 @@ export class Menu {
assert(!!this.menuCtrl, 'menucontroller was not initialized'); assert(!!this.menuCtrl, 'menucontroller was not initialized');
const el = this.el; const el = this.el;
const contentQuery = (this.content) const contentQuery = (this.contentId)
? '#' + this.content ? '#' + this.contentId
: '[main]'; : '[main]';
const parent = el.parentElement; const parent = el.parentElement;
const content = this.contentEl = parent.querySelector(contentQuery) as HTMLElement; const content = this.contentEl = parent.querySelector(contentQuery) as HTMLElement;
@ -141,13 +142,10 @@ export class Menu {
// requires content element // requires content element
return console.error('Menu: must have a "content" element to listen for drag events on.'); return console.error('Menu: must have a "content" element to listen for drag events on.');
} }
this.menuInnerEl = el.querySelector('.menu-inner') as HTMLElement;
this.backdropEl = el.querySelector('.menu-backdrop') as HTMLElement;
// add menu's content classes // add menu's content classes
content.classList.add('menu-content'); content.classList.add('menu-content');
this.typeChanged(this.type); this.typeChanged(this.type, null);
this.sideChanged(); this.sideChanged();
let isEnabled = !this.disabled; let isEnabled = !this.disabled;
@ -188,17 +186,32 @@ export class Menu {
} }
} }
getElement(): HTMLIonMenuElement {
return this.el;
}
@Method() @Method()
isOpen(): boolean { isOpen(): boolean {
return this._isOpen; return this._isOpen;
} }
@Method()
open(animated = true): Promise<boolean> {
return this.setOpen(true, animated);
}
@Method()
close(animated = true): Promise<boolean> {
return this.setOpen(false, animated);
}
@Method()
toggle(animated = true): Promise<boolean> {
return this.setOpen(!this._isOpen, animated);
}
@Method() @Method()
setOpen(shouldOpen: boolean, animated = true): Promise<boolean> { setOpen(shouldOpen: boolean, animated = true): Promise<boolean> {
return this.menuCtrl._setOpen(this, shouldOpen, animated);
}
_setOpen(shouldOpen: boolean, animated = true): Promise<boolean> {
// If the menu is disabled or it is currenly being animated, let's do nothing // If the menu is disabled or it is currenly being animated, let's do nothing
if (!this.isActive() || this.isAnimating || (shouldOpen === this._isOpen)) { if (!this.isActive() || this.isAnimating || (shouldOpen === this._isOpen)) {
return Promise.resolve(this._isOpen); return Promise.resolve(this._isOpen);
@ -211,18 +224,8 @@ export class Menu {
} }
@Method() @Method()
open(): Promise<boolean> { isActive(): boolean {
return this.setOpen(true); return !this.disabled && !this.isPane;
}
@Method()
close(): Promise<boolean> {
return this.setOpen(false);
}
@Method()
toggle(): Promise<boolean> {
return this.setOpen(!this._isOpen);
} }
private loadAnimation(): Promise<void> { private loadAnimation(): Promise<void> {
@ -259,10 +262,6 @@ export class Menu {
return promise; return promise;
} }
private isActive(): boolean {
return !this.disabled && !this.isPane;
}
private canSwipe(): boolean { private canSwipe(): boolean {
return this.swipeEnabled && return this.swipeEnabled &&
!this.isAnimating && !this.isAnimating &&
@ -428,24 +427,23 @@ export class Menu {
hostData() { hostData() {
const isRightSide = this.isRightSide; const isRightSide = this.isRightSide;
const typeClass = 'menu-type-' + this.type;
return { return {
role: 'complementary', role: 'complementary',
class: { class: {
[`menu-type-${this.type}`]: true,
'menu-enabled': !this.disabled, 'menu-enabled': !this.disabled,
'menu-side-right': isRightSide, 'menu-side-right': isRightSide,
'menu-side-left': !isRightSide, 'menu-side-left': !isRightSide,
[typeClass]: true,
} }
}; };
} }
render() { render() {
return ([ return ([
<div class='menu-inner page-inner'> <div class='menu-inner page-inner' ref={el => this.menuInnerEl = el}>
<slot></slot> <slot></slot>
</div>, </div>,
<ion-backdrop class='menu-backdrop'></ion-backdrop> , <ion-backdrop class='menu-backdrop' ref={el => this.backdropEl = el}></ion-backdrop> ,
<ion-gesture {...{ <ion-gesture {...{
'canStart': this.canStart.bind(this), 'canStart': this.canStart.bind(this),
'onWillStart': this.onWillStart.bind(this), 'onWillStart': this.onWillStart.bind(this),

View File

@ -7,7 +7,7 @@
## Properties ## Properties
#### content #### contentId
string string
@ -65,7 +65,7 @@ see the `menuType` in the [config](../../config/Config). Available options:
## Attributes ## Attributes
#### content #### content-id
string string
@ -144,6 +144,9 @@ Emitted when the menu is open.
#### close() #### close()
#### isActive()
#### isOpen() #### isOpen()