MOBILE-3947 core: Slot core-show-password on ion-inputs

main
Pau Ferrer Ocaña 2023-12-12 11:38:34 +01:00
parent 243386232e
commit be7f86edd2
10 changed files with 119 additions and 101 deletions

View File

@ -28,11 +28,11 @@
<ion-card *ngIf="askPassword"> <ion-card *ngIf="askPassword">
<form (ngSubmit)="submitPassword($event, passwordinput)" #passwordForm> <form (ngSubmit)="submitPassword($event, passwordinput)" #passwordForm>
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label position="stacked">{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label> <ion-input labelPlacement="stacked" name="password" type="password"
<core-show-password name="password"> placeholder="{{ 'core.login.password' | translate }}" core-auto-focus #passwordinput [clearOnEdit]="false"
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" [label]="'addon.mod_lesson.enterpassword' | translate">
core-auto-focus #passwordinput [clearOnEdit]="false" /> <core-show-password slot="end" />
</core-show-password> </ion-input>
</ion-item> </ion-item>
<ion-button expand="block" type="submit"> <ion-button expand="block" type="submit">
{{ 'addon.mod_lesson.continue' | translate }} {{ 'addon.mod_lesson.continue' | translate }}

View File

@ -5,9 +5,9 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item [formGroup]="form"> <ion-item [formGroup]="form">
<ion-label class="sr-only">{{ 'addon.mod_quiz.quizpassword' | translate }}</ion-label> <ion-input id="addon-mod_quiz-accessrule-password-input" name="quizpassword" type="password"
<core-show-password name="quizpassword"> placeholder="{{ 'addon.mod_quiz.quizpassword' | translate }}" [formControlName]="'quizpassword'" [clearOnEdit]="false"
<ion-input id="addon-mod_quiz-accessrule-password-input" name="quizpassword" type="password" [attr.aria-label]="'addon.mod_quiz.quizpassword' | translate">
placeholder="{{ 'addon.mod_quiz.quizpassword' | translate }}" [formControlName]="'quizpassword'" [clearOnEdit]="false" /> <core-show-password slot="end" />
</core-show-password> </ion-input>
</ion-item> </ion-item>

View File

@ -14,11 +14,11 @@
<form (ngSubmit)="submitPassword($event)" #passwordForm> <form (ngSubmit)="submitPassword($event)" #passwordForm>
<div> <div>
<ion-item> <ion-item>
<core-show-password name="password"> <ion-input [attr.aria-label]="placeholder | translate" class="ion-text-wrap core-ioninput-password" name="password"
<ion-input [attr.aria-label]="placeholder | translate" class="ion-text-wrap core-ioninput-password" name="password" type="password" placeholder="{{ placeholder | translate }}" [(ngModel)]="password" core-auto-focus
type="password" placeholder="{{ placeholder | translate }}" [(ngModel)]="password" core-auto-focus [clearOnEdit]="false">
[clearOnEdit]="false" /> <core-show-password slot="end" />
</core-show-password> </ion-input>
</ion-item> </ion-item>
<ion-item *ngIf="error" class="ion-text-wrap ion-padding-top text-danger"> <ion-item *ngIf="error" class="ion-text-wrap ion-padding-top text-danger">
<core-format-text [text]="error | translate" /> <core-format-text [text]="error | translate" />

View File

@ -1,4 +1,4 @@
<ng-content></ng-content> <ng-content />
<ion-button fill="clear" [attr.aria-label]="(shown ? 'core.hide' : 'core.show') | translate" core-suppress-events (onClick)="toggle($event)" <ion-button fill="clear" [attr.aria-label]="(shown ? 'core.hide' : 'core.show') | translate" core-suppress-events (onClick)="toggle($event)"
(mousedown)="doNotBlur($event)" (keydown)="doNotBlur($event)" (keyup)="toggle($event)"> (mousedown)="doNotBlur($event)" (keydown)="doNotBlur($event)" (keyup)="toggle($event)">
<ion-icon [name]="shown ? 'fas-eye-slash' : 'fas-eye'" slot="icon-only" aria-hidden="true" /> <ion-icon [name]="shown ? 'fas-eye-slash' : 'fas-eye'" slot="icon-only" aria-hidden="true" />

View File

@ -3,30 +3,29 @@
:host { :host {
display: contents; display: contents;
ion-button { // Only applies to deprecated way (surrounding).
::ng-deep ion-input + ion-button {
background: transparent; background: transparent;
padding: 0 calc(var(--padding-start) / 2); padding: 0 var(--inner-padding-end) 0 4px;
position: absolute;
@include safe-area-position(null, 0px, null, null);
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
z-index: 3; position: absolute;
bottom: 0; @include safe-area-position(null, 0px, null, null);
top: 0;
} }
// Only applies to deprecated way (surrounding).
::ng-deep ion-input {
--padding-end: 56px !important;
}
::ng-deep ion-input.input-label-placement-stacked + ion-button {
top: 14px;
}
} }
::ng-deep ion-input { ion-button {
--padding-end: 47px !important; z-index: 5;
} pointer-events: visible;
:host-context(.md.item-label.stacked) ion-button {
bottom: 0;
}
:host-context(.iositem-label.stacked) ion-button {
bottom: -5px;
}
:host-context(.ios) ion-button {
bottom: 0;
} }

View File

@ -18,18 +18,31 @@ import { IonInput } from '@ionic/angular';
import { CorePlatform } from '@services/platform'; import { CorePlatform } from '@services/platform';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreLogger } from '@singletons/logger';
/** /**
* Component to allow showing and hiding a password. The affected input MUST have a name to identify it. * This component allows to show/hide a password.
* It's meant to be used with ion-input.
* It's recommended to use it as a slot of the input.
* *
* @description * @description
* This directive needs to surround the input with the password.
* *
* You need to supply the name of the input. * There are 2 ways to use ths component:
* - Slot it to start or end on the ion-input element.
* - Surround the ion-input with the password with this component. This is deprecated.
* *
* Example: * In order to help finding the input you can specify the name of the input or the ion-input element.
* *
* <core-show-password name="password"> *
* Example of new usage:
*
* <ion-input type="password" name="password">
* <core-show-password slot="end" />
* </ion-input>
*
* Example deprecated usage:
*
* <core-show-password>
* <ion-input type="password" name="password"></ion-input> * <ion-input type="password" name="password"></ion-input>
* </core-show-password> * </core-show-password>
*/ */
@ -40,17 +53,30 @@ import { CoreUtils } from '@services/utils/utils';
}) })
export class CoreShowPasswordComponent implements OnInit, AfterViewInit { export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
@Input() name?: string; // Name of the input affected.
@Input() initialShown?: boolean | string; // Whether the password should be shown at start. @Input() initialShown?: boolean | string; // Whether the password should be shown at start.
@ContentChild(IonInput) ionInput?: IonInput;
shown = false; // Whether the password is shown. @Input() name = ''; // Deprecated. Not used anymore.
@ContentChild(IonInput) ionInput?: IonInput | HTMLIonInputElement; // Deprecated. Use slot instead.
protected input?: HTMLInputElement; // Input affected. protected input?: HTMLInputElement;
protected element: HTMLElement; // Current element. protected hostElement: HTMLElement;
protected logger: CoreLogger;
constructor(element: ElementRef) { constructor(element: ElementRef) {
this.element = element.nativeElement; this.hostElement = element.nativeElement;
this.logger = CoreLogger.getInstance('CoreShowPasswordComponent');
}
get shown(): boolean {
return this.input?.type === 'text';
}
set shown(shown: boolean) {
if (!this.input) {
return;
}
this.input.type = shown ? 'text' : 'password';
} }
/** /**
@ -64,28 +90,12 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
* @inheritdoc * @inheritdoc
*/ */
async ngAfterViewInit(): Promise<void> { async ngAfterViewInit(): Promise<void> {
if (this.ionInput) { await this.setInputElement();
try {
// It's an ion-input, use it to get the native element.
this.input = await this.ionInput.getInputElement();
this.setData(this.input);
} catch (error) {
// This should never fail, but it does in some testing environment because Ionic elements are not
// rendered properly. So in case this fails, we'll just ignore the error.
}
return;
}
// Search the input.
this.input = this.element.querySelector<HTMLInputElement>('input[name="' + this.name + '"]') ?? undefined;
if (!this.input) { if (!this.input) {
return; return;
} }
this.setData(this.input);
// By default, don't autocapitalize and autocorrect. // By default, don't autocapitalize and autocorrect.
if (!this.input.getAttribute('autocorrect')) { if (!this.input.getAttribute('autocorrect')) {
this.input.setAttribute('autocorrect', 'off'); this.input.setAttribute('autocorrect', 'off');
@ -96,12 +106,33 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
} }
/** /**
* Set label, icon name and input type. * Set the input element to affect.
*
* @param input The input element.
*/ */
protected setData(input: HTMLInputElement): void { protected async setInputElement(): Promise<void> {
input.type = this.shown ? 'text' : 'password'; if (!this.ionInput) {
this.ionInput = this.hostElement.closest('ion-input') ?? undefined;
this.hostElement.setAttribute('slot', 'end');
} else {
// It's outside ion-input, warn devs.
this.logger.warn('Deprecated CoreShowPasswordComponent usage, it\'s not needed to surround ion-input anymore.');
}
if (!this.ionInput) {
return;
}
try {
this.input = await this.ionInput.getInputElement();
} catch {
// This should never fail, but it does in some testing environment because Ionic elements are not
// rendered properly. So in case this fails it will try to find through the name and ignore the error.
const name = this.ionInput.name;
if (!name) {
return;
}
this.input = this.hostElement.querySelector<HTMLInputElement>('input[name="' + name + '"]') ?? undefined;
}
} }
/** /**
@ -110,7 +141,7 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
* @param event The mouse event. * @param event The mouse event.
*/ */
toggle(event: Event): void { toggle(event: Event): void {
if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
return; return;
} }
@ -120,13 +151,8 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
const isFocused = document.activeElement === this.input; const isFocused = document.activeElement === this.input;
this.shown = !this.shown; this.shown = !this.shown;
if (!this.input) {
return;
}
this.setData(this.input);
// In Android, the keyboard is closed when the input type changes. Focus it again. // In Android, the keyboard is closed when the input type changes. Focus it again.
if (isFocused && CorePlatform.isAndroid()) { if (this.input && isFocused && CorePlatform.isAndroid()) {
CoreDomUtils.focusElement(this.input); CoreDomUtils.focusElement(this.input);
} }
} }

View File

@ -18,15 +18,9 @@
ion-button.core-button-as-link { ion-button.core-button-as-link {
--color: var(--core-login-text-color); --color: var(--core-login-text-color);
text-decoration-color: var(--core-login-text-color); text-decoration-color: var(--color);
ion-label {
color: var(--core-login-text-color);
}
} }
.core-login-reconnect-warning { .core-login-reconnect-warning {
margin: 0px 0px 32px 0px; margin: 0px 0px 32px 0px;
} }

View File

@ -47,11 +47,11 @@
required="true" [attr.aria-label]="'core.login.username' | translate " /> required="true" [attr.aria-label]="'core.login.username' | translate " />
</ion-item> </ion-item>
<ion-item class="ion-margin-bottom" lines="inset"> <ion-item class="ion-margin-bottom" lines="inset">
<core-show-password name="password"> <ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false" autocomplete="current-password" enterkeyhint="go"
formControlName="password" [clearOnEdit]="false" autocomplete="current-password" enterkeyhint="go" required="true" [attr.aria-label]="'core.login.password' | translate ">
required="true" [attr.aria-label]="'core.login.password' | translate " /> <core-show-password slot="end" />
</core-show-password> </ion-input>
</ion-item> </ion-item>
<ion-button expand="block" type="submit" [disabled]="!credForm.valid" <ion-button expand="block" type="submit" [disabled]="!credForm.valid"
class="ion-margin core-login-login-button ion-text-wrap"> class="ion-margin core-login-login-button ion-text-wrap">

View File

@ -104,13 +104,12 @@
<core-input-errors [control]="signupForm.controls.username" [errorMessages]="usernameErrors" /> <core-input-errors [control]="signupForm.controls.username" [errorMessages]="usernameErrors" />
</ion-item> </ion-item>
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label position="stacked"> <ion-input labelPlacement="stacked" name="password" type="password"
<p [core-mark-required]="true">{{ 'core.login.password' | translate }}</p> placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"
</ion-label> autocomplete="new-password" required="true">
<core-show-password name="password"> <div slot="label" [core-mark-required]="true">{{ 'core.login.password' | translate }}</div>
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" <core-show-password slot="end" />
formControlName="password" [clearOnEdit]="false" autocomplete="new-password" required="true" /> </ion-input>
</core-show-password>
<p *ngIf="settings.passwordpolicy" class="core-input-footnote"> <p *ngIf="settings.passwordpolicy" class="core-input-footnote">
{{settings.passwordpolicy}} {{settings.passwordpolicy}}
</p> </p>

View File

@ -58,12 +58,12 @@
<div class="core-login-methods"> <div class="core-login-methods">
<form *ngIf="!isBrowserSSO" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #reconnectForm> <form *ngIf="!isBrowserSSO" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #reconnectForm>
<ion-item class="ion-margin-bottom" lines="inset"> <ion-item class="ion-margin-bottom" lines="inset">
<core-show-password name="password"> <ion-input class="core-ioninput-password" name="password" type="password"
<ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"
placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false" autocomplete="current-password" enterkeyhint="go" required="true"
autocomplete="current-password" enterkeyhint="go" required="true" [attr.aria-label]="'core.login.password' | translate">
[attr.aria-label]="'core.login.password' | translate" /> <core-show-password slot="end" />
</core-show-password> </ion-input>
</ion-item> </ion-item>
<ion-button type="submit" expand="block" [disabled]="!credForm.valid" <ion-button type="submit" expand="block" [disabled]="!credForm.valid"
class="ion-margin core-login-login-button ion-text-wrap"> class="ion-margin core-login-login-button ion-text-wrap">