MOBILE-3745 tabs: Add keyboard a11y to tabs
parent
43ed1d9917
commit
f108d0a8d8
|
@ -0,0 +1,137 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
export class CoreAriaRoleTab<T = unknown> {
|
||||||
|
|
||||||
|
componentInstance: T;
|
||||||
|
|
||||||
|
constructor(componentInstance: T) {
|
||||||
|
this.componentInstance = componentInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A11y key functionallity that prevents keyDown events.
|
||||||
|
*
|
||||||
|
* @param e Event.
|
||||||
|
*/
|
||||||
|
keyDown(e: KeyboardEvent): void {
|
||||||
|
if (e.key == ' ' ||
|
||||||
|
e.key == 'Enter' ||
|
||||||
|
e.key == 'Home' ||
|
||||||
|
e.key == 'End' ||
|
||||||
|
(this.isHorizontal() && (e.key == 'ArrowRight' || e.key == 'ArrowLeft')) ||
|
||||||
|
(!this.isHorizontal() && (e.key == 'ArrowUp' ||e.key == 'ArrowDown'))
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A11y key functionallity.
|
||||||
|
*
|
||||||
|
* Enter or Space: When a tab has focus, activates the tab, causing its associated panel to be displayed.
|
||||||
|
* Right Arrow: When a tab has focus: Moves focus to the next tab. If focus is on the last tab, moves focus to the first tab.
|
||||||
|
* Left Arrow: When a tab has focus: Moves focus to the previous tab. If focus is on the first tab, moves focus to the last tab.
|
||||||
|
* Home: When a tab has focus, moves focus to the first tab.
|
||||||
|
* End: When a tab has focus, moves focus to the last tab.
|
||||||
|
* https://www.w3.org/TR/wai-aria-practices-1.1/examples/tabs/tabs-2/tabs.html
|
||||||
|
*
|
||||||
|
* @param tabFindIndex Tab finable index.
|
||||||
|
* @param e Event.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
keyUp(tabFindIndex: string, e: KeyboardEvent): void {
|
||||||
|
if (e.key == ' ' || e.key == 'Enter') {
|
||||||
|
this.selectTab(tabFindIndex, e);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const tabs = this.getSelectableTabs();
|
||||||
|
|
||||||
|
let index = tabs.findIndex((tab) => tabFindIndex == tab.findIndex);
|
||||||
|
|
||||||
|
const previousKey = this.isHorizontal() ? 'ArrowLeft' : 'ArrowUp';
|
||||||
|
const nextKey = this.isHorizontal() ? 'ArrowRight' : 'ArrowDown';
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case nextKey:
|
||||||
|
index++;
|
||||||
|
if (index >= tabs.length) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
index = 0;
|
||||||
|
break;
|
||||||
|
case previousKey:
|
||||||
|
index--;
|
||||||
|
if (index < 0) {
|
||||||
|
index = tabs.length - 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
index = tabs.length - 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabId = tabs[index].id;
|
||||||
|
|
||||||
|
// @todo Pages should match aria-controls id.
|
||||||
|
const tabElement = document.querySelector<HTMLIonTabButtonElement>(`ion-tab-button[aria-controls=${tabId}]`);
|
||||||
|
|
||||||
|
tabElement?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the tab.
|
||||||
|
*
|
||||||
|
* @param tabId Tab identifier.
|
||||||
|
* @param e Event.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
selectTab(tabId: string, e: Event): void {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all the selectable tabs.
|
||||||
|
*
|
||||||
|
* @returns all the selectable tabs.
|
||||||
|
*/
|
||||||
|
getSelectableTabs(): CoreAriaRoleTabFindable[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if tabs are displayed horizontal or not.
|
||||||
|
*
|
||||||
|
* @returns Where the tabs are displayed horizontal.
|
||||||
|
*/
|
||||||
|
isHorizontal(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CoreAriaRoleTabFindable = {
|
||||||
|
id: string;
|
||||||
|
findIndex: string;
|
||||||
|
};
|
|
@ -31,6 +31,7 @@ import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { Platform, Translate } from '@singletons';
|
import { Platform, Translate } from '@singletons';
|
||||||
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
|
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
|
||||||
|
import { CoreAriaRoleTab, CoreAriaRoleTabFindable } from './aria-role-tab';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to abstract some common code for tabs.
|
* Class to abstract some common code for tabs.
|
||||||
|
@ -89,10 +90,13 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
|
||||||
protected slidesSwiperLoaded = false;
|
protected slidesSwiperLoaded = false;
|
||||||
protected scrollElements: Record<string | number, HTMLElement> = {}; // Scroll elements for each loaded tab.
|
protected scrollElements: Record<string | number, HTMLElement> = {}; // Scroll elements for each loaded tab.
|
||||||
|
|
||||||
|
tabAction: CoreTabsRoleTab<T>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected element: ElementRef,
|
protected element: ElementRef,
|
||||||
) {
|
) {
|
||||||
this.backButtonFunction = this.backButtonClicked.bind(this);
|
this.backButtonFunction = this.backButtonClicked.bind(this);
|
||||||
|
this.tabAction = new CoreTabsRoleTab(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -632,6 +636,30 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to manage rol tab.
|
||||||
|
*/
|
||||||
|
class CoreTabsRoleTab<T extends CoreTabBase> extends CoreAriaRoleTab<CoreTabsBaseComponent<T>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
selectTab(tabId: string, e: Event): void {
|
||||||
|
this.componentInstance.selectTab(tabId, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getSelectableTabs(): CoreAriaRoleTabFindable[] {
|
||||||
|
return this.componentInstance.tabs.filter((tab) => tab.enabled).map((tab) => ({
|
||||||
|
id: tab.id!,
|
||||||
|
findIndex: tab.id!,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data for each tab.
|
* Data for each tab.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -7,15 +7,28 @@
|
||||||
</ion-col>
|
</ion-col>
|
||||||
<ion-col class="ion-no-padding" size="10">
|
<ion-col class="ion-no-padding" size="10">
|
||||||
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slidesOpts" [dir]="direction" role="tablist"
|
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slidesOpts" [dir]="direction" role="tablist"
|
||||||
[attr.aria-label]="description" aria-hidden="false">
|
[attr.aria-label]="description">
|
||||||
<ng-container *ngFor="let tab of tabs">
|
<ng-container *ngFor="let tab of tabs">
|
||||||
<ion-slide [hidden]="!hideUntil" [attr.aria-selected]="selected == tab.id" class="tab-slide" role="tab"
|
<ion-slide
|
||||||
[attr.aria-label]="tab.title | translate" [attr.aria-controls]="tab.id" [id]="tab.id! + '-tab'"
|
role="presentation"
|
||||||
[tabindex]="selected == tab.id ? null : -1">
|
[hidden]="!hideUntil"
|
||||||
<ion-tab-button (ionTabButtonClick)="selectTab(tab.id, $event)" [tab]="tab.page" [layout]="layout"
|
[id]="tab.id! + '-tab'"
|
||||||
class="{{tab.class}}" [attr.aria-label]="tab.title | translate">
|
class="tab-slide"
|
||||||
<ion-icon *ngIf="tab.icon" aria-hidden="true"></ion-icon>
|
[class.selected]="selected == tab.id">
|
||||||
<ion-label aria-hidden="true">{{ tab.title | translate}}</ion-label>
|
<ion-tab-button
|
||||||
|
(ionTabButtonClick)="selectTab(tab.id, $event)"
|
||||||
|
(keydown)="tabAction.keyDown($event)"
|
||||||
|
(keyup)="tabAction.keyUp(tab.id, $event)"
|
||||||
|
[tab]="tab.page"
|
||||||
|
[layout]="layout"
|
||||||
|
class="{{tab.class}}"
|
||||||
|
role="tab"
|
||||||
|
[attr.aria-controls]="tab.id"
|
||||||
|
[attr.aria-selected]="selected == tab.id"
|
||||||
|
[tabindex]="selected == tab.id ? 0 : -1"
|
||||||
|
>
|
||||||
|
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
|
||||||
|
<ion-label>{{ tab.title | translate}}</ion-label>
|
||||||
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
|
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
</ion-slide>
|
</ion-slide>
|
||||||
|
|
|
@ -45,7 +45,8 @@ import { CoreTabBase, CoreTabsBaseComponent } from '@classes/tabs';
|
||||||
* Tab contents will only be shown if that tab is selected.
|
* Tab contents will only be shown if that tab is selected.
|
||||||
*
|
*
|
||||||
* @todo: Test RTL and tab history.
|
* @todo: Test RTL and tab history.
|
||||||
* @todo: This should behave like the split-view in relation to routing (maybe we could reuse some code from CoreItemsListManager).
|
* @todo: This should behave like the split-view in relation to routing (maybe we could reuse some code from
|
||||||
|
* CorePageItemsListManager).
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'core-tabs-outlet',
|
selector: 'core-tabs-outlet',
|
||||||
|
|
|
@ -6,15 +6,30 @@
|
||||||
</ion-col>
|
</ion-col>
|
||||||
<ion-col class="ion-no-padding" size="10">
|
<ion-col class="ion-no-padding" size="10">
|
||||||
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slidesOpts" [dir]="direction" role="tablist"
|
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slidesOpts" [dir]="direction" role="tablist"
|
||||||
[attr.aria-label]="description" aria-hidden="false">
|
[attr.aria-label]="description">
|
||||||
<ng-container *ngFor="let tab of tabs">
|
<ng-container *ngFor="let tab of tabs">
|
||||||
<ion-slide *ngIf="tab.enabled" [hidden]="!hideUntil" [attr.aria-selected]="selected == tab.id"
|
<ion-slide
|
||||||
class="tab-slide {{tab.class}}" role="tab" [attr.aria-label]="tab.title | translate"
|
*ngIf="tab.enabled"
|
||||||
[attr.aria-controls]="tab.id" [id]="tab.id! + '-tab'" [tabindex]="selected == tab.id ? null : -1"
|
role="presentation"
|
||||||
(click)="selectTab(tab.id, $event)" [attr.aria-label]="tab.title | translate">
|
[hidden]="!hideUntil"
|
||||||
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
|
class="tab-slide"
|
||||||
<ion-label aria-hidden="true">{{ tab.title | translate}}</ion-label>
|
[id]="tab.id! + '-tab'"
|
||||||
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
|
[class.selected]="selected == tab.id">
|
||||||
|
<ion-tab-button
|
||||||
|
(click)="selectTab(tab.id, $event)"
|
||||||
|
(keydown)="tabAction.keyDown($event)"
|
||||||
|
(keyup)="tabAction.keyUp(tab.id, $event)"
|
||||||
|
class="{{tab.class}}"
|
||||||
|
[layout]="layout"
|
||||||
|
role="tab"
|
||||||
|
[attr.aria-controls]="tab.id"
|
||||||
|
[attr.aria-selected]="selected == tab.id"
|
||||||
|
[tabindex]="selected == tab.id ? 0 : -1"
|
||||||
|
>
|
||||||
|
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
|
||||||
|
<ion-label>{{ tab.title | translate}}</ion-label>
|
||||||
|
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
|
||||||
|
</ion-tab-button>
|
||||||
</ion-slide>
|
</ion-slide>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-slides>
|
</ion-slides>
|
||||||
|
|
|
@ -84,6 +84,7 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
|
||||||
|
|
||||||
this.element.setAttribute('role', 'tabpanel');
|
this.element.setAttribute('role', 'tabpanel');
|
||||||
this.element.setAttribute('tabindex', '0');
|
this.element.setAttribute('tabindex', '0');
|
||||||
|
this.element.setAttribute('aria-hidden', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -113,6 +114,7 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
|
||||||
|
|
||||||
this.tabElement = this.tabElement || document.getElementById(this.id + '-tab');
|
this.tabElement = this.tabElement || document.getElementById(this.id + '-tab');
|
||||||
this.tabElement?.setAttribute('aria-selected', 'true');
|
this.tabElement?.setAttribute('aria-selected', 'true');
|
||||||
|
this.element.setAttribute('aria-hidden', 'false');
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.ionSelect.emit(this);
|
this.ionSelect.emit(this);
|
||||||
|
@ -128,6 +130,8 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
|
||||||
unselectTab(): void {
|
unselectTab(): void {
|
||||||
this.tabElement?.setAttribute('aria-selected', 'false');
|
this.tabElement?.setAttribute('aria-selected', 'false');
|
||||||
this.element.classList.remove('selected');
|
this.element.classList.remove('selected');
|
||||||
|
this.element.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
this.showHideNavBarButtons(false);
|
this.showHideNavBarButtons(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
ion-tab-button {
|
ion-tab-button {
|
||||||
|
max-width: 100%;
|
||||||
ion-label {
|
ion-label {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
@ -44,7 +45,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[aria-selected=true] {
|
&[aria-selected=true],
|
||||||
|
&.selected {
|
||||||
color: var(--color-active);
|
color: var(--color-active);
|
||||||
border-bottom-color: var(--border-color-active);
|
border-bottom-color: var(--border-color-active);
|
||||||
ion-tab-button {
|
ion-tab-button {
|
||||||
|
|
|
@ -45,6 +45,7 @@ import { CoreTabComponent } from './tab';
|
||||||
export class CoreTabsComponent extends CoreTabsBaseComponent<CoreTabComponent> implements AfterViewInit {
|
export class CoreTabsComponent extends CoreTabsBaseComponent<CoreTabComponent> implements AfterViewInit {
|
||||||
|
|
||||||
@Input() parentScrollable = false; // Determine if the scroll should be in the parent content or the tab itself.
|
@Input() parentScrollable = false; // Determine if the scroll should be in the parent content or the tab itself.
|
||||||
|
@Input() layout: 'icon-top' | 'icon-start' | 'icon-end' | 'icon-bottom' | 'icon-hide' | 'label-hide' = 'icon-hide';
|
||||||
|
|
||||||
@ViewChild('originalTabs') originalTabsRef?: ElementRef;
|
@ViewChild('originalTabs') originalTabsRef?: ElementRef;
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,38 @@
|
||||||
<ion-tab-bar slot="bottom" [hidden]="hidden">
|
<ion-tab-bar slot="bottom" [hidden]="hidden">
|
||||||
<ion-spinner *ngIf="!loaded"></ion-spinner>
|
<ion-spinner *ngIf="!loaded"></ion-spinner>
|
||||||
|
|
||||||
<ion-tab-button (ionTabButtonClick)="tabClicked($event, tab.page)" [hidden]="!loaded && tab.hide" *ngFor="let tab of tabs"
|
<ion-tab-button
|
||||||
[tab]="tab.page" [disabled]="tab.hide" layout="label-hide" class="{{tab.class}}"
|
*ngFor="let tab of tabs"
|
||||||
[attr.aria-label]="tab.title | translate">
|
(click)="tabClicked($event, tab.page)"
|
||||||
|
(keydown)="tabAction.keyDown($event)"
|
||||||
|
(keyup)="tabAction.keyUp(tab.page, $event)"
|
||||||
|
[hidden]="!loaded && tab.hide"
|
||||||
|
[tab]="tab.page"
|
||||||
|
[disabled]="tab.hide"
|
||||||
|
layout="label-hide"
|
||||||
|
class="{{tab.class}}"
|
||||||
|
[tabindex]="selectedTab == tab.page ? 0 : -1"
|
||||||
|
[attr.aria-controls]="tab.id"
|
||||||
|
[attr.aria-label]="tab.title | translate"
|
||||||
|
>
|
||||||
<ion-icon [name]="tab.icon" aria-hidden="true"></ion-icon>
|
<ion-icon [name]="tab.icon" aria-hidden="true"></ion-icon>
|
||||||
<ion-label aria-hidden="true">{{ tab.title | translate }}</ion-label>
|
<ion-label aria-hidden="true">{{ tab.title | translate }}</ion-label>
|
||||||
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
|
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
|
|
||||||
<ion-tab-button (ionTabButtonClick)="tabClicked($event, morePageName)" [hidden]="!loaded" [tab]="morePageName" layout="label-hide">
|
<ion-tab-button
|
||||||
<ion-icon name="fas-bars" [attr.aria-labelledby]="'mainmenu-tab-more'"></ion-icon>
|
(click)="tabClicked($event, morePageName)"
|
||||||
<ion-label id="mainmenu-tab-more">{{ 'core.more' | translate }}</ion-label>
|
(keydown)="tabAction.keyDown($event)"
|
||||||
|
(keyup)="tabAction.keyUp(morePageName, $event)"
|
||||||
|
[hidden]="!loaded"
|
||||||
|
[tab]="morePageName"
|
||||||
|
layout="label-hide"
|
||||||
|
[tabindex]="selectedTab == morePageName ? 0 : -1"
|
||||||
|
[attr.aria-controls]="morePageName"
|
||||||
|
[attr.aria-label]="'core.more' | translate"
|
||||||
|
>
|
||||||
|
<ion-icon name="fas-bars" aria-hidden="true"></ion-icon>
|
||||||
|
<ion-label aria-hidden="true">{{ 'core.more' | translate }}</ion-label>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
</ion-tab-bar>
|
</ion-tab-bar>
|
||||||
</ion-tabs>
|
</ion-tabs>
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
ion-tab-button {
|
ion-tab-button {
|
||||||
display: contents;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
ion-badge {
|
ion-badge {
|
||||||
top: calc(50% - 20px);
|
top: calc(50% - 20px);
|
||||||
|
|
|
@ -25,6 +25,8 @@ import { CoreMainMenu, CoreMainMenuProvider } from '../../services/mainmenu';
|
||||||
import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from '../../services/mainmenu-delegate';
|
import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from '../../services/mainmenu-delegate';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreAriaRoleTab, CoreAriaRoleTabFindable } from '@classes/aria-role-tab';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays the main menu of the app.
|
* Page that displays the main menu of the app.
|
||||||
|
@ -40,20 +42,22 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
||||||
allHandlers?: CoreMainMenuHandlerToDisplay[];
|
allHandlers?: CoreMainMenuHandlerToDisplay[];
|
||||||
loaded = false;
|
loaded = false;
|
||||||
showTabs = false;
|
showTabs = false;
|
||||||
tabsPlacement = 'bottom';
|
tabsPlacement: 'bottom' | 'side' = 'bottom';
|
||||||
hidden = false;
|
hidden = false;
|
||||||
morePageName = CoreMainMenuProvider.MORE_PAGE_NAME;
|
morePageName = CoreMainMenuProvider.MORE_PAGE_NAME;
|
||||||
|
selectedTab?: string;
|
||||||
|
|
||||||
protected subscription?: Subscription;
|
protected subscription?: Subscription;
|
||||||
protected keyboardObserver?: CoreEventObserver;
|
protected keyboardObserver?: CoreEventObserver;
|
||||||
protected resizeFunction: () => void;
|
protected resizeFunction: () => void;
|
||||||
protected backButtonFunction: (event: BackButtonEvent) => void;
|
protected backButtonFunction: (event: BackButtonEvent) => void;
|
||||||
protected selectHistory: string[] = [];
|
protected selectHistory: string[] = [];
|
||||||
protected selectedTab?: string;
|
|
||||||
protected firstSelectedTab?: string;
|
protected firstSelectedTab?: string;
|
||||||
|
|
||||||
@ViewChild('mainTabs') mainTabs?: IonTabs;
|
@ViewChild('mainTabs') mainTabs?: IonTabs;
|
||||||
|
|
||||||
|
tabAction: CoreMainMenuRoleTab;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected changeDetector: ChangeDetectorRef,
|
protected changeDetector: ChangeDetectorRef,
|
||||||
|
@ -61,6 +65,7 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
||||||
) {
|
) {
|
||||||
this.resizeFunction = this.initHandlers.bind(this);
|
this.resizeFunction = this.initHandlers.bind(this);
|
||||||
this.backButtonFunction = this.backButtonClicked.bind(this);
|
this.backButtonFunction = this.backButtonClicked.bind(this);
|
||||||
|
this.tabAction = new CoreMainMenuRoleTab(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -111,10 +116,11 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
||||||
const handler = handlers[i];
|
const handler = handlers[i];
|
||||||
|
|
||||||
// Check if the handler is already in the tabs list. If so, use it.
|
// Check if the handler is already in the tabs list. If so, use it.
|
||||||
const tab = this.tabs.find((tab) => tab.title == handler.title && tab.icon == handler.icon);
|
const tab = this.tabs.find((tab) => tab.page == handler.page);
|
||||||
|
|
||||||
tab ? tab.hide = false : null;
|
tab ? tab.hide = false : null;
|
||||||
handler.hide = false;
|
handler.hide = false;
|
||||||
|
handler.id = handler.id || 'core-mainmenu-' + CoreUtils.getUniqueId('CoreMainMenuPage');
|
||||||
|
|
||||||
newTabs.push(tab || handler);
|
newTabs.push(tab || handler);
|
||||||
}
|
}
|
||||||
|
@ -246,3 +252,42 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to manage rol tab.
|
||||||
|
*/
|
||||||
|
class CoreMainMenuRoleTab extends CoreAriaRoleTab<CoreMainMenuPage> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
selectTab(tabId: string, e: Event): void {
|
||||||
|
this.componentInstance.tabClicked(e, tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getSelectableTabs(): CoreAriaRoleTabFindable[] {
|
||||||
|
const allTabs: CoreAriaRoleTabFindable[] =
|
||||||
|
this.componentInstance.tabs.filter((tab) => !tab.hide).map((tab) => ({
|
||||||
|
id: tab.id || tab.page,
|
||||||
|
findIndex: tab.page,
|
||||||
|
}));
|
||||||
|
|
||||||
|
allTabs.push({
|
||||||
|
id: this.componentInstance.morePageName,
|
||||||
|
findIndex: this.componentInstance.morePageName,
|
||||||
|
});
|
||||||
|
|
||||||
|
return allTabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
isHorizontal(): boolean {
|
||||||
|
return this.componentInstance.tabsPlacement == 'bottom';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -89,6 +89,11 @@ export interface CoreMainMenuHandlerToDisplay extends CoreDelegateToDisplay, Cor
|
||||||
* Hide tab. Used then resizing.
|
* Hide tab. Used then resizing.
|
||||||
*/
|
*/
|
||||||
hide?: boolean;
|
hide?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to control tabs.
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -179,7 +179,7 @@ export class CoreMainMenuProvider {
|
||||||
*
|
*
|
||||||
* @return Tabs placement including side value.
|
* @return Tabs placement including side value.
|
||||||
*/
|
*/
|
||||||
getTabPlacement(): string {
|
getTabPlacement(): 'bottom' | 'side' {
|
||||||
const tablet = !!(window.innerWidth && window.innerWidth >= 576 && (window.innerHeight >= 576 ||
|
const tablet = !!(window.innerWidth && window.innerWidth >= 576 && (window.innerHeight >= 576 ||
|
||||||
((CoreApp.isKeyboardVisible() || CoreApp.isKeyboardOpening()) && window.innerHeight >= 200)));
|
((CoreApp.isKeyboardVisible() || CoreApp.isKeyboardOpening()) && window.innerHeight >= 200)));
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue