From c5f6b058db35742700e1aa787f8caa082d8707ae Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 8 Mar 2022 18:00:41 +0100 Subject: [PATCH] MOBILE-3153 mainmenu: Implement User Menu tour --- scripts/langindex.json | 3 + src/assets/img/user-tours/user-menu.svg | 1 + src/core/directives/directives.module.ts | 3 + src/core/directives/on-appear.ts | 58 +++++++++++++++++++ .../mainmenu/components/components.module.ts | 3 + .../user-menu-button/user-menu-button.html | 3 +- .../user-menu-button/user-menu-button.ts | 22 ++++++- .../user-menu-tour/user-menu-tour.html | 6 ++ .../user-menu-tour/user-menu-tour.scss | 26 +++++++++ .../user-menu-tour/user-menu-tour.ts | 35 +++++++++++ src/core/features/mainmenu/lang.json | 4 +- src/core/lang.json | 1 + src/core/services/utils/dom.ts | 18 ++++++ 13 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 src/assets/img/user-tours/user-menu.svg create mode 100644 src/core/directives/on-appear.ts create mode 100644 src/core/features/mainmenu/components/user-menu-tour/user-menu-tour.html create mode 100644 src/core/features/mainmenu/components/user-menu-tour/user-menu-tour.scss create mode 100644 src/core/features/mainmenu/components/user-menu-tour/user-menu-tour.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index de6d427b4..85f7ebd97 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -2003,6 +2003,8 @@ "core.mainmenu.home": "moodle", "core.mainmenu.logout": "moodle", "core.mainmenu.switchaccount": "local_moodlemobileapp", + "core.mainmenu.usermenutourdescription": "local_moodlemobileapp", + "core.mainmenu.usermenutourtitle": "local_moodlemobileapp", "core.maxfilesize": "moodle", "core.maxsizeandattachments": "moodle", "core.min": "moodle", @@ -2335,6 +2337,7 @@ "core.usernotfullysetup": "error", "core.users": "moodle", "core.usersuspended": "tool_reportbuilder", + "core.endonesteptour": "tool_usertours", "core.view": "moodle", "core.viewcode": "local_moodlemobileapp", "core.vieweditor": "local_moodlemobileapp", diff --git a/src/assets/img/user-tours/user-menu.svg b/src/assets/img/user-tours/user-menu.svg new file mode 100644 index 000000000..33528d215 --- /dev/null +++ b/src/assets/img/user-tours/user-menu.svg @@ -0,0 +1 @@ + diff --git a/src/core/directives/directives.module.ts b/src/core/directives/directives.module.ts index 9350f14eb..2767f4973 100644 --- a/src/core/directives/directives.module.ts +++ b/src/core/directives/directives.module.ts @@ -32,6 +32,7 @@ import { CoreSwipeNavigationDirective } from './swipe-navigation'; import { CoreCollapsibleItemDirective } from './collapsible-item'; import { CoreCollapsibleFooterDirective } from './collapsible-footer'; import { CoreContentDirective } from './content'; +import { CoreOnAppearDirective } from './on-appear'; @NgModule({ declarations: [ @@ -46,6 +47,7 @@ import { CoreContentDirective } from './content'; CoreSupressEventsDirective, CoreUserLinkDirective, CoreAriaButtonClickDirective, + CoreOnAppearDirective, CoreOnResizeDirective, CoreDownloadFileDirective, CoreCollapsibleHeaderDirective, @@ -66,6 +68,7 @@ import { CoreContentDirective } from './content'; CoreSupressEventsDirective, CoreUserLinkDirective, CoreAriaButtonClickDirective, + CoreOnAppearDirective, CoreOnResizeDirective, CoreDownloadFileDirective, CoreCollapsibleHeaderDirective, diff --git a/src/core/directives/on-appear.ts b/src/core/directives/on-appear.ts new file mode 100644 index 000000000..52d7691b6 --- /dev/null +++ b/src/core/directives/on-appear.ts @@ -0,0 +1,58 @@ +// (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. + +import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; +import { CoreDomUtils } from '@services/utils/dom'; + +/** + * Directive to listen when an element becomes visible. + */ +@Directive({ + selector: '[onAppear]', +}) +export class CoreOnAppearDirective implements OnInit, OnDestroy { + + @Output() onAppear = new EventEmitter(); + + private element: HTMLElement; + private interval?: number; + + constructor(element: ElementRef) { + this.element = element.nativeElement; + } + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.interval = window.setInterval(() => { + if (!CoreDomUtils.isElementVisible(this.element)) { + return; + } + + this.onAppear.emit(); + window.clearInterval(this.interval); + + delete this.interval; + }, 50); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.interval && window.clearInterval(this.interval); + } + +} diff --git a/src/core/features/mainmenu/components/components.module.ts b/src/core/features/mainmenu/components/components.module.ts index 613c1c790..c81a44b1d 100644 --- a/src/core/features/mainmenu/components/components.module.ts +++ b/src/core/features/mainmenu/components/components.module.ts @@ -17,11 +17,13 @@ import { CoreSharedModule } from '@/core/shared.module'; import { CoreMainMenuUserButtonComponent } from './user-menu-button/user-menu-button'; import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu'; import { CoreLoginComponentsModule } from '@features/login/components/components.module'; +import { CoreMainMenuUserMenuTourComponent } from './user-menu-tour/user-menu-tour'; @NgModule({ declarations: [ CoreMainMenuUserButtonComponent, CoreMainMenuUserMenuComponent, + CoreMainMenuUserMenuTourComponent, ], imports: [ CoreSharedModule, @@ -30,6 +32,7 @@ import { CoreLoginComponentsModule } from '@features/login/components/components exports: [ CoreMainMenuUserButtonComponent, CoreMainMenuUserMenuComponent, + CoreMainMenuUserMenuTourComponent, ], }) export class CoreMainMenuComponentsModule {} diff --git a/src/core/features/mainmenu/components/user-menu-button/user-menu-button.html b/src/core/features/mainmenu/components/user-menu-button/user-menu-button.html index 187ad5ef7..bd0e01204 100644 --- a/src/core/features/mainmenu/components/user-menu-button/user-menu-button.html +++ b/src/core/features/mainmenu/components/user-menu-button/user-menu-button.html @@ -1,3 +1,4 @@ + (ariaButtonClick)="openUserMenu($event)" (onAppear)="showTour()" role="button" tabindex="0" + [attr.aria-label]="'core.user.useraccount' | translate" #avatar> diff --git a/src/core/features/mainmenu/components/user-menu-button/user-menu-button.ts b/src/core/features/mainmenu/components/user-menu-button/user-menu-button.ts index f1140b4ec..ebde9fb45 100644 --- a/src/core/features/mainmenu/components/user-menu-button/user-menu-button.ts +++ b/src/core/features/mainmenu/components/user-menu-button/user-menu-button.ts @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit } from '@angular/core'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { CoreSiteInfo } from '@classes/site'; +import { CoreUserTours, CoreUserToursStyle } from '@features/usertours/services/user-tours'; import { IonRouterOutlet } from '@ionic/angular'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; +import { CoreMainMenuUserMenuTourComponent } from '../user-menu-tour/user-menu-tour'; import { CoreMainMenuUserMenuComponent } from '../user-menu/user-menu'; /** @@ -34,6 +36,8 @@ export class CoreMainMenuUserButtonComponent implements OnInit { siteInfo?: CoreSiteInfo; isMainScreen = false; + @ViewChild('avatar', { read: ElementRef }) avatar?: ElementRef; + constructor(protected routerOutlet: IonRouterOutlet) { const currentSite = CoreSites.getRequiredCurrentSite(); @@ -61,4 +65,20 @@ export class CoreMainMenuUserButtonComponent implements OnInit { }); } + /** + * Show User Tour. + */ + async showTour(): Promise { + if (!this.avatar) { + return; + } + + await CoreUserTours.showIfPending({ + id: 'user-menu', + component: CoreMainMenuUserMenuTourComponent, + focus: this.avatar.nativeElement, + style: CoreUserToursStyle.Overlay, + }); + } + } diff --git a/src/core/features/mainmenu/components/user-menu-tour/user-menu-tour.html b/src/core/features/mainmenu/components/user-menu-tour/user-menu-tour.html new file mode 100644 index 000000000..697f56fcb --- /dev/null +++ b/src/core/features/mainmenu/components/user-menu-tour/user-menu-tour.html @@ -0,0 +1,6 @@ + +

{{ 'core.mainmenu.usermenutourtitle' | translate }}

+

{{ 'core.mainmenu.usermenutourdescription' | translate }}

+ + {{ 'core.endonesteptour' | translate }} + diff --git a/src/core/features/mainmenu/components/user-menu-tour/user-menu-tour.scss b/src/core/features/mainmenu/components/user-menu-tour/user-menu-tour.scss new file mode 100644 index 000000000..faecdd58f --- /dev/null +++ b/src/core/features/mainmenu/components/user-menu-tour/user-menu-tour.scss @@ -0,0 +1,26 @@ +:host { + width: 100%; + height: 100%; + display: flex; + max-width: 85vw; + align-items: center; + flex-direction: column; + + img { + width: calc(100vw - var(--core-avatar-size) * 2 - 16px); + margin-top: 12px; + } + + p { + text-align: center; + } + + ion-button { + width: 100%; + } + +} + +:host-context([dir=rtl]) img { + transform: scaleX(-1); +} diff --git a/src/core/features/mainmenu/components/user-menu-tour/user-menu-tour.ts b/src/core/features/mainmenu/components/user-menu-tour/user-menu-tour.ts new file mode 100644 index 000000000..4d7c1a3d7 --- /dev/null +++ b/src/core/features/mainmenu/components/user-menu-tour/user-menu-tour.ts @@ -0,0 +1,35 @@ +// (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. + +import { Component } from '@angular/core'; +import { CoreUserTours } from '@features/usertours/services/user-tours'; + +/** + * Component showing the User Tour for the User Menu feature. + */ +@Component({ + selector: 'core-mainmenu-user-menu-tour', + templateUrl: 'user-menu-tour.html', + styleUrls: ['user-menu-tour.scss'], +}) +export class CoreMainMenuUserMenuTourComponent { + + /** + * Dismiss the User Tour. + */ + async dismiss(): Promise { + await CoreUserTours.dismiss(); + } + +} diff --git a/src/core/features/mainmenu/lang.json b/src/core/features/mainmenu/lang.json index 9e9977545..b0ff77c61 100644 --- a/src/core/features/mainmenu/lang.json +++ b/src/core/features/mainmenu/lang.json @@ -1,5 +1,7 @@ { "home": "Home", "logout": "Log out", - "switchaccount": "Switch account" + "switchaccount": "Switch account", + "usermenutourdescription": "The place to check your grades, change your preferences or switch accounts.", + "usermenutourtitle": "Explore your personal area" } diff --git a/src/core/lang.json b/src/core/lang.json index 2a72d84e7..02ef670c7 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -329,6 +329,7 @@ "usernotfullysetup": "User not fully set-up", "usernologin": "Authentication has been revoked for this account", "usersuspended": "Registration suspended", + "endonesteptour": "Got it", "users": "Users", "view": "View", "viewcode": "View code", diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index a91e89caf..390fe0c53 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -806,6 +806,24 @@ export class CoreDomUtilsProvider { return elementPoint > window.innerHeight || elementPoint < scrollTopPos; } + /** + * Check whether an element is visible or not. + * + * @param element Element. + */ + isElementVisible(element: HTMLElement): boolean { + if (element.clientWidth === 0 || element.clientHeight === 0) { + return false; + } + + const style = getComputedStyle(element); + if (style.opacity === '0' || style.display === 'none' || style.visibility === 'hidden') { + return false; + } + + return element.offsetParent !== null; + } + /** * Check if rich text editor is enabled. *