AAE-46057 Stabilize form field status layout (#12165)

This commit is contained in:
Alex Molodyh
2026-08-20 09:55:29 -07:00
committed by GitHub
parent d0368ba9c8
commit 83b5769fbc
29 changed files with 348 additions and 252 deletions
@@ -20,6 +20,12 @@
.mat-mdc-form-field-infix {
width: auto;
}
.adf-form-field-input:not(.adf-inplace-input-mat-form-field, .adf-people-cloud, .adf-cloud-group) {
#{ms.$mat-form-field-subscript-wrapper} {
height: 40px;
}
}
}
.alfresco-tabs-widget {
@@ -40,7 +46,7 @@
.adf-container-widget {
.adf-form-field-input:not(.adf-inplace-input-mat-form-field, .adf-people-cloud, .adf-cloud-group) {
margin-bottom: 35px;
margin-bottom: 0;
}
.adf-grid-list {
@@ -265,7 +271,7 @@
}
&-error-messages-container {
min-height: 35px;
height: 40px;
}
&-error-messages-container-visible {
@@ -5,7 +5,7 @@
&-single-column {
display: flex;
flex-wrap: inherit;
align-items: center;
align-items: flex-start;
gap: 1%;
@include flex.layout-bp(lt-md) {
@@ -16,10 +16,9 @@
<span class="adf-asterisk" [style.visibility]="isRequired() ? 'visible' : 'hidden'">*</span>
</mat-checkbox>
<div class="adf-error-messages-container">
<error-widget [error]="field.validationSummary" />
<error-widget
*ngIf="isInvalidFieldRequired() && isTouched()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
[error]="field.validationSummary"
[required]="isInvalidFieldRequired() && isTouched() ? ('FORM.FIELD.REQUIRED' | translate) : ''"
/>
</div>
</div>
@@ -17,7 +17,7 @@
/* eslint-disable @angular-eslint/component-selector */
import { NgClass, NgIf } from '@angular/common';
import { NgClass } from '@angular/common';
import { Component, ViewEncapsulation } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -47,7 +47,7 @@ import { WidgetComponent } from '../widget.component';
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
imports: [NgClass, MatCheckboxModule, FormsModule, TranslatePipe, ErrorWidgetComponent, NgIf],
imports: [NgClass, MatCheckboxModule, FormsModule, TranslatePipe, ErrorWidgetComponent],
encapsulation: ViewEncapsulation.None
})
export class CheckboxWidgetComponent extends WidgetComponent {}
@@ -10,12 +10,16 @@
}
}
error-widget {
display: block;
}
.adf-error {
display: flex;
align-items: center;
&-widget-container {
height: auto;
height: 40px;
}
&-animate {
@@ -1,6 +1,7 @@
.adf-hyperlink-widget {
padding: 0.4375em 0;
border-top: 0.8438em solid transparent;
margin-bottom: 20px;
a {
color: var(--mat-sys-primary);
@@ -43,6 +43,6 @@
@include mixins.adf-error-icon;
}
.adf-container-widget .adf-multiline-text-widget .adf-form-field-input.adf-has-counter {
margin-bottom: 44px;
.adf-container-widget .adf-multiline-text-widget mat-form-field.adf-form-field-input.adf-has-counter {
margin-bottom: 20px;
}
@@ -9,7 +9,8 @@
}
&-row-action {
margin-left: 10px;
margin-inline-start: 10px;
margin-block-end: 35px;
}
&-row-limit {
@@ -17,6 +17,7 @@ $mat-button: '.mat-mdc-button';
$mat-button-label: '.mdc-button__label';
$mat-form-field: '.mat-mdc-form-field';
$mat-form-field-wrapper: '.mat-mdc-text-field-wrapper';
$mat-form-field-subscript-wrapper: '.mat-mdc-form-field-subscript-wrapper';
$mat-line-ripple: '.mdc-line-ripple';
$mat-form-field-prefix: '.mat-mdc-form-field-text-prefix';
$mat-form-field-suffix: '.mat-mdc-form-field-text-suffix';
@@ -1,21 +1,26 @@
<div class="adf-attach-file-widget-container">
<div class="adf-attach-widget {{field.className}}"
[class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id + '-label'">{{field.name}}
<span class="adf-asterisk" *ngIf="isRequired()">*</span>
<div class="adf-attach-widget {{ field.className }}" [class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id + '-label'"
>{{ field.name }}
@if (isRequired()) {
<span class="adf-asterisk">*</span>
}
</label>
<div class="adf-attach-widget-container" (focusout)="markAsTouched()">
<div class="adf-attach-widget__menu-upload" *ngIf="isUploadButtonVisible()">
@if (isUploadButtonVisible()) {
<div class="adf-attach-widget__menu-upload">
<button
(click)="openSelectDialog()"
mat-raised-button
class="adf-attach-widget__menu-upload__button"
[id]="field.id"
[title]="field.tooltip">
[title]="field.tooltip"
>
{{ 'FORM.FIELD.ATTACH' | translate }}
<mat-icon class="adf-attach-widget__menu-upload__button__icon" [adf-icon]="getWidgetIcon()" />
</button>
</div>
}
</div>
</div>
@@ -34,12 +39,15 @@
(contentModelFileHandler)="contentModelFormFileHandler($event)"
(removeAttachFile)="onRemoveAttachFile($event)"
/>
<div *ngIf="!hasFile && field.readOnly" id="{{'adf-attach-empty-list-'+field.id}}">
@if (!hasFile && field.readOnly) {
<div id="{{ 'adf-attach-empty-list-' + field.id }}">
{{ 'FORM.FIELD.NO_FILE_ATTACHED' | translate }}
</div>
}
</div>
<error-widget [error]="field.validationSummary" />
<error-widget *ngIf="!field.isValid && isTouched() && !isSelected()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
<error-widget
[error]="field.validationSummary"
[required]="!field.isValid && isTouched() && !isSelected() ? ('FORM.FIELD.REQUIRED' | translate) : ''"
/>
</div>
@@ -1,31 +1,23 @@
<div class="adf-data-table-widget-container">
<div class="adf-data-table-widget-label">
<label
class="adf-label"
[class.adf-left-label]="field.leftLabels"
[attr.for]="field.id">
{{field.name | translate }}
</label>
<label class="adf-label" [class.adf-left-label]="field.leftLabels" [attr.for]="field.id"> {{field.name | translate }} </label>
</div>
<ng-container *ngIf="!previewState; else previewTemplate">
@if (!previewState) {
<adf-datatable data-automation-id="adf-data-table-widget" [data]="dataSource">
<adf-no-content-template>
<ng-template>
<adf-empty-content
icon="border_all"
[title]="'FORM.FIELD.DATA_TABLE_EMPTY_CONTENT' | translate" />
<adf-empty-content icon="border_all" [title]="'FORM.FIELD.DATA_TABLE_EMPTY_CONTENT' | translate" />
</ng-template>
</adf-no-content-template>
</adf-datatable>
<error-widget *ngIf="dataTableLoadFailed"
<error-widget
class="adf-data-table-widget-failed-message"
[required]="'FORM.FIELD.DATA_TABLE_LOAD_FAILED' | translate" />
</ng-container>
<ng-template #previewTemplate>
[required]="dataTableLoadFailed ? ('FORM.FIELD.DATA_TABLE_LOAD_FAILED' | translate) : ''"
/>
} @else {
<adf-datatable data-automation-id="adf-data-table-widget-preview" />
<div class="adf-preview-placeholder"></div>
</ng-template>
}
</div>
@@ -1,4 +1,5 @@
.adf-data-table-widget-failed-message {
display: block;
margin: 10px;
}
@@ -283,7 +283,8 @@ describe('DataTableWidgetComponent', () => {
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-data-table-widget-failed-message'));
assertData(mockCountryColumns, []);
expect(failedErrorMsgElement).toBeNull();
expect(failedErrorMsgElement).toBeTruthy();
expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe('');
});
it('path points to single object with appropriate schema definition', () => {
@@ -294,7 +295,8 @@ describe('DataTableWidgetComponent', () => {
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-data-table-widget-failed-message'));
assertData(mockCountryColumns, [mockEuropeCountriesRows[1]]);
expect(failedErrorMsgElement).toBeNull();
expect(failedErrorMsgElement).toBeTruthy();
expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe('');
});
});
@@ -27,7 +27,6 @@ import {
NoContentTemplateDirective,
EmptyContentComponent
} from '@alfresco/adf-core';
import { NgIf } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
import { FormCloudService } from '../../../services/form-cloud.service';
import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
@@ -36,7 +35,7 @@ import { DataTablePathParserHelper } from './helpers/data-table-path-parser.help
@Component({
standalone: true,
imports: [NgIf, TranslatePipe, FormBaseModule, DataTableComponent, NoContentTemplateDirective, EmptyContentComponent],
imports: [TranslatePipe, FormBaseModule, DataTableComponent, NoContentTemplateDirective, EmptyContentComponent],
selector: 'data-table',
templateUrl: './data-table.widget.html',
styleUrls: ['./data-table.widget.scss'],
@@ -4,11 +4,13 @@
[class.adf-readonly]="field.readOnly"
[class.adf-left-label-input-container]="field.leftLabels"
>
<div *ngIf="field.leftLabels">
@if (field.leftLabels) {
<div>
<label class="adf-label adf-left-label" [attr.for]="field.id"
>{{field.name | translate }}<span class="adf-asterisk" [style.visibility]="isRequired() ? 'visible' : 'hidden'">*</span></label
>
</div>
}
<div>
<adf-cloud-group
[mode]="mode"
@@ -22,13 +24,7 @@
[preSelectGroups]="preSelectGroup"
(blur)="markAsTouched()"
[attr.title]="field.tooltip"
[label] = "field.name | translate"
/>
<error-widget [error]="field.validationSummary" />
<error-widget
class="adf-dropdown-required-message"
*ngIf="isInvalidFieldRequired() && isTouched()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
[label]="field.name | translate"
/>
</div>
</div>
@@ -141,8 +141,9 @@ describe('GroupCloudWidgetComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-error-text')).toBeTruthy();
expect(element.querySelector('.adf-error-text').textContent).toContain('ADF_CLOUD_GROUPS.ERROR.NOT_FOUND');
const errorMessages = element.querySelectorAll('.adf-error-text');
expect(errorMessages.length).toBe(1);
expect(errorMessages[0].textContent).toContain('ADF_CLOUD_GROUPS.ERROR.NOT_FOUND');
});
});
@@ -16,7 +16,7 @@
*/
import { Component, DestroyRef, inject, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ErrorWidgetComponent, WidgetComponent } from '@alfresco/adf-core';
import { WidgetComponent } from '@alfresco/adf-core';
import { UntypedFormControl } from '@angular/forms';
import { filter } from 'rxjs/operators';
import { ComponentSelectionMode } from '../../../../types';
@@ -31,7 +31,7 @@ import { GroupCloudComponent } from '../../../../group/components/group-cloud.co
@Component({
selector: 'group-cloud-widget',
imports: [CommonModule, TranslatePipe, ErrorWidgetComponent, GroupCloudComponent],
imports: [CommonModule, TranslatePipe, GroupCloudComponent],
templateUrl: './group-cloud.widget.html',
host: {
'(click)': 'event($event)',
@@ -1,10 +1,16 @@
<div class="adf-dropdown-widget {{field.className}}"
<div
class="adf-dropdown-widget {{field.className}}"
[class.adf-invalid]="!field.isValid && isTouched()"
[class.adf-readonly]="field.readOnly"
[class.adf-left-label-input-container]="field.leftLabels">
<div *ngIf="field.leftLabels">
<label class="adf-label adf-left-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" [style.visibility]="isRequired() ? 'visible' : 'hidden'">*</span></label>
[class.adf-left-label-input-container]="field.leftLabels"
>
@if (field.leftLabels) {
<div>
<label class="adf-label adf-left-label" [attr.for]="field.id"
>{{field.name | translate }}<span class="adf-asterisk" [style.visibility]="isRequired() ? 'visible' : 'hidden'">*</span></label
>
</div>
}
<div>
<adf-cloud-people
[preSelectUsers]="preSelectUsers"
@@ -21,11 +27,5 @@
[attr.title]="field.tooltip"
[label]="field.name | translate"
/>
<error-widget [error]="field.validationSummary" />
<error-widget
class="adf-dropdown-required-message"
*ngIf="isInvalidFieldRequired() && isTouched()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
</div>
</div>
@@ -171,8 +171,9 @@ describe('PeopleCloudWidgetComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-error-text')).toBeTruthy();
expect(element.querySelector('.adf-error-text').textContent).toContain('ADF_CLOUD_USERS.ERROR.NOT_FOUND');
const errorMessages = element.querySelectorAll('.adf-error-text');
expect(errorMessages.length).toBe(1);
expect(errorMessages[0].textContent).toContain('ADF_CLOUD_USERS.ERROR.NOT_FOUND');
});
});
@@ -16,7 +16,7 @@
*/
import { Component, DestroyRef, inject, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ErrorWidgetComponent, WidgetComponent } from '@alfresco/adf-core';
import { WidgetComponent } from '@alfresco/adf-core';
import { UntypedFormControl } from '@angular/forms';
import { filter } from 'rxjs/operators';
import { ComponentSelectionMode } from '../../../../types';
@@ -27,13 +27,12 @@ import { ReactivePreselectionService } from '../reactive-preselection.service';
import { CommonModule } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
import { PeopleCloudComponent } from '../../../../people/components/people-cloud.component';
import { MatFormFieldModule } from '@angular/material/form-field';
/* eslint-disable @angular-eslint/component-selector */
@Component({
selector: 'people-cloud-widget',
imports: [CommonModule, TranslatePipe, ErrorWidgetComponent, PeopleCloudComponent, MatFormFieldModule],
imports: [CommonModule, TranslatePipe, PeopleCloudComponent],
templateUrl: './people-cloud.widget.html',
host: {
'(click)': 'event($event)',
@@ -11,7 +11,6 @@
}
&-radio-button-container-horizontal {
margin-bottom: 15px;
display: flex;
flex-flow: column wrap;
align-items: flex-start;
@@ -4,7 +4,8 @@
>
<div class="adf-cloud-upload-widget-container">
<div>
<mat-list *ngIf="hasFile">
@if (hasFile) {
<mat-list>
<mat-list-item class="adf-upload-files-row" *ngFor="let file of uploadedFiles">
<img
matListItemLine
@@ -27,28 +28,26 @@
tabindex="0"
>{{file.name}}</span
>
<button
*ngIf="!field.readOnly"
mat-icon-button
[id]="'file-'+file.id+'-remove'"
(click)="removeFile(file);"
(keyup.enter)="removeFile(file);"
>
@if (!field.readOnly) {
<button mat-icon-button [id]="'file-'+file.id+'-remove'" (click)="removeFile(file);" (keyup.enter)="removeFile(file);">
<mat-icon class="mat-24" adf-icon="highlight_off" />
</button>
}
</mat-list-item>
</mat-list>
}
</div>
<div *ngIf="(!hasFile || multipleOption) && !field.readOnly">
@if ((!hasFile || multipleOption) && !field.readOnly) {
<div>
<button mat-raised-button (click)="uploadFiles.click()" [title]="field.tooltip">
{{ 'FORM.FIELD.UPLOAD' | translate }}<mat-icon adf-icon="file_upload" />
<input #uploadFiles [multiple]="multipleOption" type="file" [id]="field.form.nodeId" (change)="onFileChanged($event)" />
</button>
</div>
<div *ngIf="!hasFile && field.readOnly">{{ 'FORM.FIELD.NO_FILE_ATTACHED' | translate }}</div>
} @if (!hasFile && field.readOnly) {
<div>{{ 'FORM.FIELD.NO_FILE_ATTACHED' | translate }}</div>
}
</div>
<error-widget [error]="field.validationSummary" />
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
<error-widget [error]="field.validationSummary" [required]="isInvalidFieldRequired() ? ('FORM.FIELD.REQUIRED' | translate) : ''" />
</div>
@@ -55,4 +55,12 @@ describe('UploadCloudWidgetComponent', () => {
expect(eventSpy).toHaveBeenCalledWith(clickEvent);
});
});
it('should render one reserved form field status area', () => {
widget.field = new FormFieldModel(new FormModel(), {});
fixture.detectChanges();
const statusAreas = fixture.nativeElement.querySelectorAll('error-widget');
expect(statusAreas.length).toBe(1);
});
});
@@ -1,18 +1,26 @@
<form>
<mat-form-field class="adf-cloud-group adf-form-field-input" [class.adf-invalid]="hasError() && isDirty()">
@if (label || required) { <mat-label><span>{{label}}</span></mat-label> }
<mat-form-field subscriptSizing="dynamic" class="adf-cloud-group adf-form-field-input" [class.adf-invalid]="hasError() && isDirty()">
@if (label || required) {
<mat-label
><span>{{ label }}</span></mat-label
>
}
<mat-chip-grid [required]="required" [disabled]="isReadonly()" #groupChipList data-automation-id="adf-cloud-group-chip-list">
<mat-chip-row
*ngFor="let group of selectedGroups"
[removable]="!(group.readonly)"
[removable]="!group.readonly"
[attr.data-automation-id]="'adf-cloud-group-chip-' + group.name"
(removed)="onRemove(group)"
[disabled]="readOnly || isValidationLoading()"
title="{{ (group.readonly ? 'ADF_CLOUD_GROUPS.MANDATORY' : '') | translate }}">
{{group.name}}
<mat-icon *ngIf="!(group.readonly || readOnly)" matChipRemove [attr.data-automation-id]="'adf-cloud-group-chip-remove-icon-' + group.name" adf-icon="cancel" />
title="{{ (group.readonly ? 'ADF_CLOUD_GROUPS.MANDATORY' : '') | translate }}"
>
{{ group.name }}
@if (!(group.readonly || readOnly)) {
<mat-icon matChipRemove [attr.data-automation-id]="'adf-cloud-group-chip-remove-icon-' + group.name" adf-icon="cancel" />
}
</mat-chip-row>
<input matInput
<input
matInput
[formControl]="searchGroupsControl"
[matAutocomplete]="auto"
[matChipInputFor]="groupChipList"
@@ -21,7 +29,9 @@
(focus)="setFocus(true)"
(blur)="setFocus(false); markAsTouched()"
class="adf-group-input"
data-automation-id="adf-cloud-group-search-input" #groupInput>
data-automation-id="adf-cloud-group-search-input"
#groupInput
/>
</mat-chip-grid>
<mat-autocomplete
@@ -30,57 +40,77 @@
class="adf-cloud-group-list"
(optionSelected)="onSelect($event.option.value)"
[displayWith]="getDisplayName"
data-automation-id="adf-cloud-group-autocomplete">
<ng-container *ngIf="(searchGroups$ | async)?.length else noResults">
<mat-option *ngFor="let group of searchGroups$ | async; let i = index" [value]="group"
data-automation-id="adf-cloud-group-autocomplete"
>
@if ((searchGroups$ | async)?.length) {
<mat-option
*ngFor="let group of searchGroups$ | async; let i = index"
[value]="group"
[attr.data-automation-id]="'adf-cloud-group-chip-' + group.name"
class="adf-cloud-group-option-active">
<div
class="adf-cloud-group-row"
id="adf-group-{{i}}"
data-automation-id="adf-cloud-group-row">
<button class="adf-group-short-name" mat-fab>{{getGroupNameInitials(group)}}</button>
<span>{{group.name}}</span>
class="adf-cloud-group-option-active"
>
<div class="adf-cloud-group-row" id="adf-group-{{ i }}" data-automation-id="adf-cloud-group-row">
<button class="adf-group-short-name" mat-fab>{{ getGroupNameInitials(group) }}</button>
<span>{{ group.name }}</span>
</div>
</mat-option>
</ng-container>
} @else {
<ng-container [ngTemplateOutlet]="noResults" />
}
<ng-template #noResults>
<mat-option *ngIf="searchGroupsControl.hasError('searchTypingError') && !searchLoading" disabled
class="adf-cloud-group-option-not-active"
data-automation-id="adf-cloud-group-no-results">
@if (searchGroupsControl.hasError('searchTypingError') && !searchLoading) {
<mat-option disabled class="adf-cloud-group-option-not-active" data-automation-id="adf-cloud-group-no-results">
<span> {{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate }}</span>
</mat-option>
}
</ng-template>
</mat-autocomplete>
</mat-form-field>
<mat-progress-bar *ngIf="validationLoading" mode="indeterminate" />
<div class="adf-error-container adf-error-messages-container">
<mat-error *ngIf="hasPreselectError() && !isValidationLoading()" class="adf-error">
@if (validationLoading) {
<mat-progress-bar mode="indeterminate" />
}
@if (hasPreselectError() && !isValidationLoading()) {
<mat-error class="adf-error">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate }}</div>
</mat-error>
<mat-error *ngIf="searchGroupsControl.hasError('pattern')" class="adf-error">
}
@if (searchGroupsControl.hasError('pattern')) {
<mat-error class="adf-error">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}</div>
<div class="adf-error-text">
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}
</div>
</mat-error>
<mat-error *ngIf="searchGroupsControl.hasError('maxlength')" class="adf-error">
}
@if (searchGroupsControl.hasError('maxlength')) {
<mat-error class="adf-error">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}</div>
<div class="adf-error-text">
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}
</div>
</mat-error>
<mat-error *ngIf="searchGroupsControl.hasError('minlength')" class="adf-error">
}
@if (searchGroupsControl.hasError('minlength')) {
<mat-error class="adf-error">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}</div>
<div class="adf-error-text">
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}
</div>
</mat-error>
<mat-error *ngIf="(searchGroupsControl.hasError('required') || groupChipsCtrl.hasError('required')) && isDirty()"
class="adf-error">
}
@if ((searchGroupsControl.hasError('required') || groupChipsCtrl.hasError('required')) && isDirty()) {
<mat-error class="adf-error">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }} </div>
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }}</div>
</mat-error>
<mat-error *ngIf="searchGroupsControl.hasError('searchTypingError') && !this.isFocused"
data-automation-id="invalid-groups-typing-error" class="adf-error">
}
@if (searchGroupsControl.hasError('searchTypingError') && !this.isFocused) {
<mat-error data-automation-id="invalid-groups-typing-error" class="adf-error">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate }}</div>
</mat-error>
}
</div>
</form>
@@ -57,10 +57,12 @@
}
}
.adf-error-messages-container .adf-error-icon {
.adf-error-messages-container {
.adf-error-icon {
@include mixins.adf-error-icon;
}
}
.adf-error-messages-container .adf-error {
.adf-error {
animation: slide-down-fade-in 300ms cubic-bezier(0.55, 0, 0.55, 0.2);
}
}
@@ -27,6 +27,8 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatChipHarness } from '@angular/material/chips/testing';
import { MatIconHarness } from '@angular/material/icon/testing';
import { MatInputHarness } from '@angular/material/input/testing';
import { MatFormField } from '@angular/material/form-field';
import { MatProgressBar } from '@angular/material/progress-bar';
describe('GroupCloudComponent', () => {
let loader: HarnessLoader;
@@ -98,6 +100,22 @@ describe('GroupCloudComponent', () => {
expect(await inputElement.getPlaceholder()).toEqual('');
});
it('should use dynamic form field subscript sizing', () => {
fixture.detectChanges();
const formField = fixture.debugElement.query(By.directive(MatFormField)).componentInstance as MatFormField;
expect(formField.subscriptSizing).toBe('dynamic');
});
it('should render validation progress inside the reserved status area', () => {
component.validationLoading = true;
fixture.detectChanges();
const progressBar = fixture.debugElement.query(By.directive(MatProgressBar));
expect(progressBar.parent.classes['adf-error-messages-container']).toBeTrue();
});
describe('Search group', () => {
beforeEach(() => {
fixture.detectChanges();
@@ -4,10 +4,14 @@
class="adf-people-cloud adf-form-field-input"
[class.adf-invalid]="hasError() && isDirty()"
>
<mat-label *ngIf="!title">
<span>{{label}}</span>
@if (!title) {
<mat-label>
<span>{{ label }}</span>
</mat-label>
<mat-label *ngIf="title">{{ title | translate }}</mat-label>
}
@if (title) {
<mat-label>{{ title | translate }}</mat-label>
}
<mat-chip-grid [required]="required" [disabled]="isReadonly()" #userMultipleChipList data-automation-id="adf-cloud-people-chip-list">
<mat-chip-row
@@ -17,10 +21,12 @@
(removed)="onRemove(user)"
[disabled]="isReadonly() || isValidationLoading()"
title="{{ (user.readonly ? 'ADF_CLOUD_GROUPS.MANDATORY' : '') | translate }}"
[matTooltip]="showFullNameOnHover ? (user | fullName : true) : user.email"
[matTooltip]="showFullNameOnHover ? (user | fullName: true) : user.email"
>
{{ user | fullName }}
<mat-icon matChipRemove *ngIf="!(user.readonly || readOnly)" [attr.data-automation-id]="'adf-people-cloud-chip-remove-icon-' + user.username" adf-icon="cancel" />
@if (!(user.readonly || readOnly)) {
<mat-icon matChipRemove [attr.data-automation-id]="'adf-people-cloud-chip-remove-icon-' + user.username" adf-icon="cancel" />
}
</mat-chip-row>
<input
matInput
@@ -44,64 +50,73 @@
(optionSelected)="onSelect($event.option.value)"
[displayWith]="getDisplayName"
>
<ng-container *ngIf="(searchUsers$ | async)?.length; else noResults">
@if ((searchUsers$ | async)?.length) {
<mat-option *ngFor="let user of searchUsers$ | async; let i = index" [value]="user" class="adf-people-cloud-option-active">
<div class="adf-people-cloud-row" id="adf-people-cloud-user-{{ user.username }}" data-automation-id="adf-people-cloud-row">
<div [outerHTML]="user | usernameInitials : 'adf-people-cloud-pic'"></div>
<span class="adf-people-label-name"> {{ user | fullName : true }}</span>
<div [outerHTML]="user | usernameInitials: 'adf-people-cloud-pic'"></div>
<span class="adf-people-label-name"> {{ user | fullName: true }}</span>
</div>
</mat-option>
</ng-container>
} @else {
<ng-container [ngTemplateOutlet]="noResults" />
}
<ng-template #noResults>
<mat-option
*ngIf="searchUserCtrl.hasError('searchTypingError') && !searchLoading"
disabled
class="adf-people-cloud-option-not-active"
data-automation-id="adf-people-cloud-no-results"
>
<span> {{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName: searchedValue } }}</span>
@if (searchUserCtrl.hasError('searchTypingError') && !searchLoading) {
<mat-option disabled class="adf-people-cloud-option-not-active" data-automation-id="adf-people-cloud-no-results">
<span> {{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate: { userName: searchedValue } }}</span>
</mat-option>
}
</ng-template>
</mat-autocomplete>
</mat-form-field>
<mat-progress-bar *ngIf="validationLoading" mode="indeterminate" />
<div class="adf-error-container adf-error-messages-container" *ngIf="showErrors">
<mat-error *ngIf="hasPreselectError() && !isValidationLoading()" class="adf-error adf-error-animate">
<div class="adf-error-container adf-error-messages-container">
@if (validationLoading) {
<mat-progress-bar mode="indeterminate" />
}
@if (showErrors) {
@if (hasPreselectError() && !isValidationLoading()) {
<mat-error class="adf-error adf-error-animate">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName: validateUsersMessage } }}</div>
<div class="adf-error-text">{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate: { userName: validateUsersMessage } }}</div>
</mat-error>
<mat-error *ngIf="searchUserCtrl.hasError('pattern')" class="adf-error adf-error-animate">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate : { pattern: getValidationPattern() } }}</div>
</mat-error>
<mat-error *ngIf="searchUserCtrl.hasError('maxlength')" class="adf-error adf-error-animate">
}
@if (searchUserCtrl.hasError('pattern')) {
<mat-error class="adf-error adf-error-animate">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate : { requiredLength: getValidationMaxLength() } }}
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}
</div>
</mat-error>
<mat-error *ngIf="searchUserCtrl.hasError('minlength')" class="adf-error adf-error-animate">
}
@if (searchUserCtrl.hasError('maxlength')) {
<mat-error class="adf-error adf-error-animate">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate : { requiredLength: getValidationMinLength() } }}
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}
</div>
</mat-error>
<mat-error
*ngIf="(searchUserCtrl.hasError('required') || userChipsCtrl.hasError('required')) && isDirty()"
class="adf-error adf-error-animate"
>
}
@if (searchUserCtrl.hasError('minlength')) {
<mat-error class="adf-error adf-error-animate">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}
</div>
</mat-error>
}
@if ((searchUserCtrl.hasError('required') || userChipsCtrl.hasError('required')) && isDirty()) {
<mat-error class="adf-error adf-error-animate">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }}</div>
</mat-error>
<mat-error
*ngIf="searchUserCtrl.hasError('searchTypingError') && !this.isFocused"
data-automation-id="invalid-users-typing-error"
class="adf-error adf-error-animate"
>
}
@if (searchUserCtrl.hasError('searchTypingError') && !this.isFocused) {
<mat-error data-automation-id="invalid-users-typing-error" class="adf-error adf-error-animate">
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
<div class="adf-error-text">{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName: searchedValue } }}</div>
<div class="adf-error-text">{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate: { userName: searchedValue } }}</div>
</mat-error>
}
}
</div>
</form>
@@ -27,6 +27,7 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatChipHarness } from '@angular/material/chips/testing';
import { MatInputHarness } from '@angular/material/input/testing';
import { MatFormFieldHarness } from '@angular/material/form-field/testing';
import { MatProgressBar } from '@angular/material/progress-bar';
import { IdentityUserService } from '../services/identity-user.service';
describe('PeopleCloudComponent', () => {
@@ -99,6 +100,19 @@ describe('PeopleCloudComponent', () => {
expect(await inputField.getLabel()).toEqual('TITLE_KEY');
});
it('should use dynamic form field subscript sizing by default', () => {
expect(component.formFieldSubscriptSizing).toBe('dynamic');
});
it('should render validation progress inside the reserved status area', () => {
component.validationLoading = true;
fixture.detectChanges();
const progressBar = fixture.debugElement.query(By.directive(MatProgressBar));
expect(progressBar.parent.classes['adf-error-messages-container']).toBeTrue();
});
describe('Search user', () => {
beforeEach(() => {
fixture.detectChanges();
@@ -169,7 +169,7 @@ export class PeopleCloudComponent implements OnInit, OnChanges, AfterViewInit {
* Material form field subscript sizing (fixed / dynamic)
*/
@Input()
formFieldSubscriptSizing: SubscriptSizing = 'fixed';
formFieldSubscriptSizing: SubscriptSizing = 'dynamic';
/**
* Show errors under the form field