MOBILE-4290 login: Collapse site help questions

main
Noel De Martin 2023-04-19 13:05:18 +02:00
parent bfe9100879
commit b382486580
4 changed files with 259 additions and 75 deletions

View File

@ -4,7 +4,7 @@
<h1>{{ 'core.login.help' | translate }}</h1> <h1>{{ 'core.login.help' | translate }}</h1>
</ion-title> </ion-title>
<ion-buttons slot="end"> <ion-buttons slot="end">
<ion-button fill="clear" (click)="closeHelp()" [attr.aria-label]="'core.close' | translate"> <ion-button fill="clear" (click)="close()" [attr.aria-label]="'core.close' | translate">
<ion-icon slot="icon-only" name="fas-xmark" aria-hidden="true"></ion-icon> <ion-icon slot="icon-only" name="fas-xmark" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
@ -12,58 +12,26 @@
</ion-header> </ion-header>
<ion-content> <ion-content>
<ion-list> <ion-list>
<ion-item class="ion-text-wrap"> <ng-container *ngFor="let question of questions">
<ion-label> <ion-item button class="ion-text-wrap divider" (click)="toggle(question)" sticky="true" [attr.aria-expanded]="isOpen(question)"
<h2>{{ 'core.login.faqwhatisurlquestion' | translate }}</h2> [attr.aria-controls]="'question-' + question.id + '-answer'" role="heading" detail="false">
</ion-label> <ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon"
</ion-item> [class.expandable-status-icon-expanded]="isOpen(question)">
<ion-item class="ion-text-wrap"> </ion-icon>
<ion-label> <ion-label>
<p [innerHTML]="'core.login.faqwhatisurlanswer' | translate: {$image: urlImageHtml}"></p> <h2>{{ question.text }}</h2>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item class="ion-text-wrap"> <ion-item [id]="'question-' + question.id + '-answer'"
<ion-label> [class]="question.answer.class + 'ion-text-wrap core-login-site-help--answer'" [class.open]="isOpen(question)"
<h2>{{ 'core.login.faqcannotfindmysitequestion' | translate }}</h2> [tabindex]="isOpen(question) ? null : -1" [attr.inert]="isOpen(question) ? null : 'true'">
</ion-label> <ion-label>
</ion-item> <p *ngIf="question.answer.format === 'text'">{{ question.answer.text }}</p>
<ion-item class="ion-text-wrap"> <p *ngIf="question.answer.format === 'safe-html'" [innerHTML]="question.answer.text"></p>
<ion-label> <core-format-text *ngIf="question.answer.format === 'unsafe-html'" [text]="question.answer.text" [filter]="false">
<p>{{ 'core.login.faqcannotfindmysiteanswer' | translate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'core.login.faqsetupsitequestion' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<p>
<core-format-text [text]="'core.login.faqsetupsiteanswer' | translate:{$link: setupLinkHtml}" [filter]="false">
</core-format-text> </core-format-text>
</p> </ion-label>
</ion-label> </ion-item>
</ion-item> </ng-container>
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ 'core.login.faqtestappquestion' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<p [innerHTML]="'core.login.faqtestappanswer' | translate"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="canScanQR">
<ion-label>
<h2>{{ 'core.login.faqwhereisqrcode' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap core-login-faqwhereisqrcodeanswer" *ngIf="canScanQR">
<ion-label>
<p [innerHTML]="'core.login.faqwhereisqrcodeanswer' | translate: {$image: qrCodeImageHtml}"></p>
</ion-label>
</ion-item>
</ion-list> </ion-list>
</ion-content> </ion-content>

View File

@ -1,13 +1,38 @@
.core-login-faqwhatisurlanswer img { :host {
max-height: 50px;
}
.core-login-faqwhereisqrcodeanswer img { .core-login-faqwhatisurlanswer img {
max-height: 220px; max-height: 50px;
margin-top: 5px; }
margin-bottom: 5px;
} .core-login-faqwhereisqrcodeanswer img {
max-height: 220px;
margin-top: 5px;
margin-bottom: 5px;
}
&:not(.hydrated) {
.core-login-site-help--answer {
opacity: 0;
max-width: 100%;
position: absolute;
pointer-events: none;
}
}
&.hydrated {
.core-login-site-help--answer {
height: 0;
transition: height 200ms ease-in-out;
&.open {
height: var(--height);
}
}
}
h2 {
font-weight: bold;
} }

View File

@ -12,11 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { AfterViewInit, Component, ElementRef, HostBinding, OnDestroy } from '@angular/core';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { ModalController, Translate } from '@singletons'; import { ModalController, Translate } from '@singletons';
import { CoreLoginHelperProvider, GET_STARTED_URL } from '@features/login/services/login-helper'; import { CoreLoginHelperProvider, GET_STARTED_URL } from '@features/login/services/login-helper';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
/** /**
* Component that displays help to connect to a site. * Component that displays help to connect to a site.
@ -26,27 +28,187 @@ import { CoreLoginHelperProvider, GET_STARTED_URL } from '@features/login/servic
templateUrl: 'site-help.html', templateUrl: 'site-help.html',
styleUrls: ['site-help.scss'], styleUrls: ['site-help.scss'],
}) })
export class CoreLoginSiteHelpComponent { export class CoreLoginSiteHelpComponent implements AfterViewInit, OnDestroy {
urlImageHtml: string; openQuestion?: number;
setupLinkHtml: string; questions: Question[] = [];
qrCodeImageHtml: string; @HostBinding('class.hydrated') hydrated = false;
canScanQR: boolean;
constructor() { private promises: CoreCancellablePromise[] = [];
constructor(protected el: ElementRef<HTMLElement>) {
const getStartedTitle = Translate.instant('core.login.faqsetupsitelinktitle'); const getStartedTitle = Translate.instant('core.login.faqsetupsitelinktitle');
const canScanQR = !CoreUtils.canScanQR();
const urlImageHtml = CoreLoginHelperProvider.FAQ_URL_IMAGE_HTML;
const qrCodeImageHtml = CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML;
const setupLinkHtml = `<a href="${GET_STARTED_URL}" title="${getStartedTitle}">${GET_STARTED_URL}</a>`;
const questions: Array<QuestionDefinition | false> = [
{
text: Translate.instant('core.login.faqwhatisurlquestion'),
answer: {
text: Translate.instant('core.login.faqwhatisurlanswer', { $image: urlImageHtml }),
format: AnswerFormat.SafeHTML,
class: 'core-login-faqwhatisurlanswer',
},
},
{
text: Translate.instant('core.login.faqcannotfindmysitequestion'),
answer: {
text: Translate.instant('core.login.faqcannotfindmysiteanswer'),
format: AnswerFormat.Text,
},
},
{
text: Translate.instant('core.login.faqsetupsitequestion'),
answer: {
text: Translate.instant('core.login.faqsetupsiteanswer', { $link: setupLinkHtml }),
format: AnswerFormat.UnsafeHTML,
},
},
{
text: Translate.instant('core.login.faqtestappquestion'),
answer: {
text: Translate.instant('core.login.faqtestappanswer'),
format: AnswerFormat.SafeHTML,
},
},
canScanQR && {
text: Translate.instant('core.login.faqwhereisqrcode'),
answer: {
text: Translate.instant('core.login.faqwhereisqrcodeanswer', { $image: qrCodeImageHtml }),
format: AnswerFormat.SafeHTML,
class: 'core-login-faqwhereisqrcodeanswer',
},
},
];
this.canScanQR = CoreUtils.canScanQR(); for (const question of questions) {
this.urlImageHtml = CoreLoginHelperProvider.FAQ_URL_IMAGE_HTML; if (!question) {
this.qrCodeImageHtml = CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML; continue;
this.setupLinkHtml = `<a href="${GET_STARTED_URL}" title="${getStartedTitle}">${GET_STARTED_URL}</a>`; }
this.questions.push({
...question,
id: this.questions.length + 1,
answer: {
...question.answer,
class: question.answer.class ?? '',
},
});
}
}
/**
* @inheritdoc
*/
async ngAfterViewInit(): Promise<void> {
const answers = Array.from(this.el.nativeElement.querySelectorAll<HTMLElement>('.core-login-site-help--answer'));
await Promise.all(answers.map(async answer => {
await this.track(CoreUtils.waitFor(() => answer.clientHeight !== 0));
await this.track(CoreDomUtils.waitForImages(answer));
answer.style.setProperty('--height', `${answer.clientHeight}px`);
}));
this.hydrated = true;
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.promises.forEach(promise => promise.cancel());
}
/**
* Check whether the given question is open or not.
*
* @param question Question.
* @returns Whether the given question is open.
*/
isOpen(question: Question): boolean {
return this.openQuestion === question.id;
}
/**
* Toggle question.
*
* @param question Question to toggle.
*/
toggle(question: Question): void {
if (question.id === this.openQuestion) {
delete this.openQuestion;
return;
}
this.openQuestion = question.id;
} }
/** /**
* Close help modal. * Close help modal.
*/ */
closeHelp(): void { close(): void {
ModalController.dismiss(); ModalController.dismiss();
} }
/**
* Track a promise for cleanup.
*
* @param promise Cancellable promise.
* @returns The promise.
*/
protected track<T>(promise: CoreCancellablePromise<T>): Promise<T> {
const remove = () => {
const index = this.promises.indexOf(promise);
if (index === -1) {
return;
}
this.promises.splice(index, 1);
};
this.promises.push(promise);
promise.then(remove).catch(remove);
return promise;
}
} }
/**
* Question data.
*/
interface Question {
id: number;
text: string;
answer: Answer;
}
/**
* Question answer.
*/
interface Answer {
text: string;
class: string;
format: AnswerFormat;
}
/**
* Question answer format.
*/
enum AnswerFormat {
Text = 'text',
SafeHTML = 'safe-html',
UnsafeHTML = 'unsafe-html',
}
/**
* Question definition.
*/
type QuestionDefinition = Omit<Question, 'id' | 'answer'> & {
answer: Omit<Answer, 'class'> & Partial<Pick<Answer, 'class'>>;
};

View File

@ -38,6 +38,7 @@ import { CorePlatform } from '@services/platform';
import { CoreErrorWithOptions } from '@classes/errors/errorwithtitle'; import { CoreErrorWithOptions } from '@classes/errors/errorwithtitle';
import { CoreFilepool } from '@services/filepool'; import { CoreFilepool } from '@services/filepool';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
export type TreeNode<T> = T & { children: TreeNode<T>[] }; export type TreeNode<T> = T & { children: TreeNode<T>[] };
@ -1795,6 +1796,34 @@ export class CoreUtilsProvider {
return new Promise(resolve => setTimeout(resolve, milliseconds)); return new Promise(resolve => setTimeout(resolve, milliseconds));
} }
/**
* Wait until a given condition is met.
*
* @param condition Condition.
* @returns Cancellable promise.
*/
waitFor(condition: () => boolean, interval: number = 50): CoreCancellablePromise<void> {
if (condition()) {
return CoreCancellablePromise.resolve();
}
let intervalId: number | undefined;
return new CoreCancellablePromise<void>(
async (resolve) => {
intervalId = window.setInterval(() => {
if (!condition()) {
return;
}
resolve();
window.clearInterval(intervalId);
}, interval);
},
() => window.clearInterval(intervalId),
);
}
/** /**
* Wait until the next tick. * Wait until the next tick.
* *