MOBILE-3947 core: Avoid item overflow on input errors

main
Pau Ferrer Ocaña 2023-12-12 16:23:14 +01:00
parent be7f86edd2
commit bea73940ed
9 changed files with 72 additions and 101 deletions

View File

@ -21,7 +21,7 @@
formControlName="name">
<div slot="label" [core-mark-required]="true">{{ 'addon.calendar.eventname' | translate }}</div>
</ion-input>
<core-input-errors [control]="form.controls.name" [errorMessages]="errors" />
<core-input-errors [control]="form.controls.name" />
</ion-item>
<!-- Date. -->
@ -37,7 +37,7 @@
</ion-datetime>
</ng-template>
</ion-modal>
<core-input-errors [control]="form.controls.timestart" [errorMessages]="errors" />
<core-input-errors [control]="form.controls.timestart" />
</ion-item>
<!-- Type. -->

View File

@ -69,7 +69,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
groups: CoreGroup[] = [];
loadingGroups = false;
courseGroupSet = false;
errors: Record<string, string>;
error = false;
eventRepeatId?: number;
otherEventsCount = 0;
@ -100,9 +99,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
) {
this.currentSite = CoreSites.getRequiredCurrentSite();
this.remindersEnabled = CoreReminders.isEnabled();
this.errors = {
required: Translate.instant('core.required'),
};
this.form = new FormGroup({});

View File

@ -1,7 +1,7 @@
<ng-container *ngIf="control && control.dirty && !control.valid">
<ng-container *ngFor="let error of errorKeys">
<div *ngIf="control.hasError(error)" class="core-input-error">
<span *ngIf="errorMessages && errorMessages[error]">{{errorMessages[error]}}</span>
<span *ngIf="errorMessages && errorMessages[error]">{{ errorMessages[error] | translate }}</span>
<span *ngIf="(!errorMessages || !errorMessages[error]) && error === 'max' && control.errors?.max">
{{ 'core.login.invalidvaluemax' | translate:{$a: control.errors!.max.max} }}
</span>

View File

@ -14,7 +14,6 @@
import { Component, ElementRef, HostBinding, Input, OnChanges, OnInit, SimpleChange } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Translate } from '@singletons';
/**
* Component to show errors if an input isn't valid.
@ -30,8 +29,7 @@ import { Translate } from '@singletons';
* Example usage:
*
* <ion-item class="ion-text-wrap">
* <ion-label stacked [core-mark-required]="true">{{ 'core.login.username' | translate }}</ion-label>
* <ion-input type="text" name="username" formControlName="username"></ion-input>
* <ion-input type="text" name="username" formControlName="username" required="true"></ion-input>
* <core-input-errors [control]="myForm.controls.username" [errorMessages]="usernameErrors"></core-input-errors>
* </ion-item>
*/
@ -42,12 +40,12 @@ import { Translate } from '@singletons';
})
export class CoreInputErrorsComponent implements OnInit, OnChanges {
@Input() control?: FormControl;
@Input() errorMessages: Record<string, string> = {};
@Input() control?: FormControl; // Needed to be able to check the validity of the input.
@Input() errorMessages: Record<string, string> = {}; // Error messages to show. Keys must be the name of the error.
@Input() errorText = ''; // Set other non automatic errors.
errorKeys: string[] = [];
protected element: HTMLElement;
protected hostElement: HTMLElement;
@HostBinding('class.has-errors')
get hasErrors(): boolean {
@ -59,31 +57,60 @@ export class CoreInputErrorsComponent implements OnInit, OnChanges {
constructor(
element: ElementRef,
) {
this.element = element.nativeElement;
this.hostElement = element.nativeElement;
}
/**
* Initialize some common errors if they aren't set.
*/
protected initErrorMessages(): void {
this.errorMessages.required = this.errorMessages.required || Translate.instant('core.required');
this.errorMessages.email = this.errorMessages.email || Translate.instant('core.login.invalidemail');
this.errorMessages.date = this.errorMessages.date || Translate.instant('core.login.invaliddate');
this.errorMessages.datetime = this.errorMessages.datetime || Translate.instant('core.login.invaliddate');
this.errorMessages.datetimelocal = this.errorMessages.datetimelocal || Translate.instant('core.login.invaliddate');
this.errorMessages.time = this.errorMessages.time || Translate.instant('core.login.invalidtime');
this.errorMessages.url = this.errorMessages.url || Translate.instant('core.login.invalidurl');
this.errorMessages = {
required: this.errorMessages.required || 'core.required',
email: this.errorMessages.email || 'core.login.invalidemail',
date: this.errorMessages.date || 'core.login.invaliddate',
datetime: this.errorMessages.datetime || 'core.login.invaliddate',
datetimelocal: this.errorMessages.datetimelocal || 'core.login.invaliddate',
time: this.errorMessages.time || 'core.login.invalidtime',
url: this.errorMessages.url || 'core.login.invalidurl',
// Set empty values by default, the default error messages will be built in the template when needed.
max: this.errorMessages.max || '',
min: this.errorMessages.min || '',
};
// Set empty values by default, the default error messages will be built in the template when needed.
this.errorMessages.max = this.errorMessages.max || '';
this.errorMessages.min = this.errorMessages.min || '';
this.errorMessages.requiredTrue = this.errorMessages.required;
this.errorKeys = Object.keys(this.errorMessages);
}
/**
* @inheritdoc
*/
ngOnInit(): void {
this.element.closest('ion-item')?.classList.add('has-core-input-errors');
const parent = this.hostElement.parentElement;
let item: HTMLElement | null = null;
if (parent?.tagName === 'ION-ITEM') {
item = parent;
// Get all elements on the parent and wrap them with a div.
// This is needed because otherwise the error message will be shown on the right of the input. Or overflowing the item.
const wrapper = document.createElement('div');
wrapper.classList.add('core-input-errors-wrapper');
Array.from(parent.children).forEach((child) => {
if (!child.slot) {
wrapper.appendChild(child);
}
});
parent.appendChild(wrapper);
} else {
item = this.hostElement.closest('ion-item');
}
item?.classList.add('has-core-input-errors');
}
/**
@ -92,11 +119,6 @@ export class CoreInputErrorsComponent implements OnInit, OnChanges {
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if ((changes.control || changes.errorMessages) && this.control) {
this.initErrorMessages();
this.errorKeys = this.errorMessages ? Object.keys(this.errorMessages) : [];
}
if (changes.errorText) {
this.errorText = changes.errorText.currentValue;
}
}

View File

@ -37,13 +37,13 @@ export class CoreMarkRequiredComponent implements OnInit, AfterViewInit {
@Input('core-mark-required') coreMarkRequired: boolean | string = true;
protected element: HTMLElement;
protected hostElement: HTMLElement;
requiredLabel = Translate.instant('core.required');
constructor(
element: ElementRef,
) {
this.element = element.nativeElement;
this.hostElement = element.nativeElement;
}
/**
@ -59,18 +59,21 @@ export class CoreMarkRequiredComponent implements OnInit, AfterViewInit {
ngAfterViewInit(): void {
if (this.coreMarkRequired) {
// Add the "required" to the aria-label.
const ariaLabel = this.element.getAttribute('aria-label') ||
CoreTextUtils.cleanTags(this.element.innerHTML, { singleLine: true });
const ariaLabel = this.hostElement.getAttribute('aria-label') ||
CoreTextUtils.cleanTags(this.hostElement.innerHTML, { singleLine: true });
if (ariaLabel) {
this.element.setAttribute('aria-label', ariaLabel + ' ' + this.requiredLabel);
this.hostElement.setAttribute('aria-label', ariaLabel + '. ' + this.requiredLabel);
}
} else {
// Remove the "required" from the aria-label.
const ariaLabel = this.element.getAttribute('aria-label');
const ariaLabel = this.hostElement.getAttribute('aria-label');
if (ariaLabel) {
this.element.setAttribute('aria-label', ariaLabel.replace(' ' + this.requiredLabel, ''));
this.hostElement.setAttribute('aria-label', ariaLabel.replace('. ' + this.requiredLabel, ''));
}
}
const input = this.hostElement.closest('ion-input, ion-textarea');
input?.setAttribute('required', this.coreMarkRequired ? 'true' : 'false');
}
}

View File

@ -105,15 +105,14 @@ export class CoreLoginEmailSignupPage implements OnInit {
});
// Setup validation errors.
this.usernameErrors = CoreLoginHelper.getErrorMessages('core.login.usernamerequired');
this.passwordErrors = CoreLoginHelper.getErrorMessages('core.login.passwordrequired');
this.emailErrors = CoreLoginHelper.getErrorMessages('core.login.missingemail');
this.policyErrors = CoreLoginHelper.getErrorMessages('core.login.policyagree');
this.email2Errors = CoreLoginHelper.getErrorMessages(
'core.login.missingemail',
undefined,
'core.login.emailnotmatch',
);
this.usernameErrors = { required: 'core.login.usernamerequired' };
this.passwordErrors = { required: 'core.login.passwordrequired' };
this.emailErrors = { required: 'core.login.missingemail' };
this.policyErrors = { required: 'core.login.policyagree' };
this.email2Errors = {
required: 'core.login.missingemail',
pattern: 'core.login.emailnotmatch',
};
}
/**
@ -224,7 +223,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
const namefieldsErrors = {};
if (this.settings.namefields) {
this.settings.namefields.forEach((field) => {
namefieldsErrors[field] = CoreLoginHelper.getErrorMessages('core.login.missing' + field);
namefieldsErrors[field] = { required: 'core.login.missing' + field };
});
}
this.namefieldsErrors = namefieldsErrors;

View File

@ -252,60 +252,6 @@ export class CoreLoginHelperProvider {
return CoreTextUtils.treatDisabledFeatures(disabledFeatures);
}
/**
* Builds an object with error messages for some common errors.
* Please notice that this function doesn't support all possible error types.
*
* @param requiredMsg Code of the string for required error.
* @param emailMsg Code of the string for invalid email error.
* @param patternMsg Code of the string for pattern not match error.
* @param urlMsg Code of the string for invalid url error.
* @param minlengthMsg Code of the string for "too short" error.
* @param maxlengthMsg Code of the string for "too long" error.
* @param minMsg Code of the string for min value error.
* @param maxMsg Code of the string for max value error.
* @returns Object with the errors.
*/
getErrorMessages(
requiredMsg?: string,
emailMsg?: string,
patternMsg?: string,
urlMsg?: string,
minlengthMsg?: string,
maxlengthMsg?: string,
minMsg?: string,
maxMsg?: string,
): Record<string, string> {
const errors: Record<string, string> = {};
if (requiredMsg) {
errors.required = errors.requiredTrue = Translate.instant(requiredMsg);
}
if (emailMsg) {
errors.email = Translate.instant(emailMsg);
}
if (patternMsg) {
errors.pattern = Translate.instant(patternMsg);
}
if (urlMsg) {
errors.url = Translate.instant(urlMsg);
}
if (minlengthMsg) {
errors.minlength = Translate.instant(minlengthMsg);
}
if (maxlengthMsg) {
errors.maxlength = Translate.instant(maxlengthMsg);
}
if (minMsg) {
errors.min = Translate.instant(minMsg);
}
if (maxMsg) {
errors.max = Translate.instant(maxMsg);
}
return errors;
}
/**
* Get logo URL from a site public config.
*

View File

@ -126,6 +126,10 @@ ion-item > .in-item,
white-space: normal !important;
}
ion-item .core-input-errors-wrapper {
width: 100%;
}
@each $color-name, $unused in $colors {
.text-#{$color-name},
p.text-#{$color-name} {

View File

@ -10,6 +10,7 @@ For more information about upgrading, read the official documentation: https://m
- Removed CoreToLocaleStringPipe deprecated since 3.6.0
- With the upgrade to Ionic 7 ion-slides is no longer supported and now you need to use swiper-container and swiper-slide. More info here: https://ionicframework.com/docs/angular/slides
- With the upgrade to Ionic7 ion-datetime has changed its usage. We recommend using ion-datetime-button. More info here: https://ionicframework.com/docs/updating/6-0#datetime
- CoreLoginHelper.getErrorMessages has been removed. Please create the messages object yourself.
=== 4.3.0 ===