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

View File

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

View File

@ -14,11 +14,11 @@
<form (ngSubmit)="submitPassword($event)" #passwordForm>
<div>
<ion-item>
<core-show-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
[clearOnEdit]="false" />
</core-show-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
[clearOnEdit]="false">
<core-show-password slot="end" />
</ion-input>
</ion-item>
<ion-item *ngIf="error" class="ion-text-wrap ion-padding-top text-danger">
<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)"
(mousedown)="doNotBlur($event)" (keydown)="doNotBlur($event)" (keyup)="toggle($event)">
<ion-icon [name]="shown ? 'fas-eye-slash' : 'fas-eye'" slot="icon-only" aria-hidden="true" />

View File

@ -3,30 +3,29 @@
:host {
display: contents;
ion-button {
// Only applies to deprecated way (surrounding).
::ng-deep ion-input + ion-button {
background: transparent;
padding: 0 calc(var(--padding-start) / 2);
position: absolute;
@include safe-area-position(null, 0px, null, null);
padding: 0 var(--inner-padding-end) 0 4px;
margin-top: 0;
margin-bottom: 0;
z-index: 3;
bottom: 0;
position: absolute;
@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 {
--padding-end: 47px !important;
}
: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;
ion-button {
z-index: 5;
pointer-events: visible;
}

View File

@ -18,18 +18,31 @@ import { IonInput } from '@ionic/angular';
import { CorePlatform } from '@services/platform';
import { CoreDomUtils } from '@services/utils/dom';
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
* 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>
* </core-show-password>
*/
@ -40,17 +53,30 @@ import { CoreUtils } from '@services/utils/utils';
})
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.
@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 element: HTMLElement; // Current element.
protected input?: HTMLInputElement;
protected hostElement: HTMLElement;
protected logger: CoreLogger;
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
*/
async ngAfterViewInit(): Promise<void> {
if (this.ionInput) {
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;
await this.setInputElement();
if (!this.input) {
return;
}
this.setData(this.input);
// By default, don't autocapitalize and autocorrect.
if (!this.input.getAttribute('autocorrect')) {
this.input.setAttribute('autocorrect', 'off');
@ -96,12 +106,33 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
}
/**
* Set label, icon name and input type.
*
* @param input The input element.
* Set the input element to affect.
*/
protected setData(input: HTMLInputElement): void {
input.type = this.shown ? 'text' : 'password';
protected async setInputElement(): Promise<void> {
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.
*/
toggle(event: Event): void {
if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
return;
}
@ -120,13 +151,8 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
const isFocused = document.activeElement === this.input;
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.
if (isFocused && CorePlatform.isAndroid()) {
if (this.input && isFocused && CorePlatform.isAndroid()) {
CoreDomUtils.focusElement(this.input);
}
}

View File

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

View File

@ -47,11 +47,11 @@
required="true" [attr.aria-label]="'core.login.username' | translate " />
</ion-item>
<ion-item class="ion-margin-bottom" lines="inset">
<core-show-password name="password">
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
formControlName="password" [clearOnEdit]="false" autocomplete="current-password" enterkeyhint="go"
required="true" [attr.aria-label]="'core.login.password' | translate " />
</core-show-password>
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
formControlName="password" [clearOnEdit]="false" autocomplete="current-password" enterkeyhint="go"
required="true" [attr.aria-label]="'core.login.password' | translate ">
<core-show-password slot="end" />
</ion-input>
</ion-item>
<ion-button expand="block" type="submit" [disabled]="!credForm.valid"
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" />
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label position="stacked">
<p [core-mark-required]="true">{{ 'core.login.password' | translate }}</p>
</ion-label>
<core-show-password name="password">
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
formControlName="password" [clearOnEdit]="false" autocomplete="new-password" required="true" />
</core-show-password>
<ion-input labelPlacement="stacked" name="password" type="password"
placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"
autocomplete="new-password" required="true">
<div slot="label" [core-mark-required]="true">{{ 'core.login.password' | translate }}</div>
<core-show-password slot="end" />
</ion-input>
<p *ngIf="settings.passwordpolicy" class="core-input-footnote">
{{settings.passwordpolicy}}
</p>

View File

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