feat(ui): Added textarea component to ui-libs. (TV-849)

Squashed commit of the following:

commit 61ecaf467c63cdd10f4d66131b105d3b12d60e49
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Nov 3 11:14:19 2021 +0100

    Removed unused input

commit 2254c1dd547cbf3639e5232fe6d8e657491a369c
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Nov 3 11:12:38 2021 +0100

    Implemented textarea component inside ui-libs
This commit is contained in:
Erik Tiekstra
2021-11-03 12:04:15 +01:00
parent 37cdf9285a
commit e51e13dfa8
17 changed files with 256 additions and 63 deletions

View File

@@ -24,19 +24,16 @@
</li>
</ol>
</div>
<digi-ng-form-textarea
<ui-textarea
formControlName="emails"
afSize="m"
afDescription="För att skicka inbjudningar till flera e-postadresser samtidigt kan e-postadresser
uiLabel="E-postadresser"
uiDescription="För att skicka inbjudningar till flera e-postadresser samtidigt kan e-postadresser
separeras med bl.a. kommatecken eller mellanslag. Se fler exempel innanför textfältet nedan.
Antalet mellanslag spelar inte roll och en blandning av tecknen går också bra."
afPlaceholder="namn@foretag.se namn@foretag.se, namn@foretag.se; namn@foretag.se:namn@foretag.se,namn@foretag.se: namn@foretag.se"
[afRequired]="true"
[afDisableValidStyle]="true"
[afInvalidMessage]="emailsControl.errors?.required || emailsControl.errors?.invalid || 'Ogiltig e-postadress'"
[afInvalid]="emailsControl.invalid && emailsControl.dirty"
afLabel="E-postadresser"
></digi-ng-form-textarea>
[uiRequired]="true"
[uiInvalid]="emailsControl.invalid && emailsControl.dirty"
[uiValidationMessage]="emailsControl.errors?.required || emailsControl.errors?.invalid || 'Ogiltig e-postadress'"
></ui-textarea>
<digi-notification-alert
*ngIf="(lastInvites$ | async) && !emailsControl.dirty"

View File

@@ -1,4 +1,3 @@
import { DigiNgFormTextareaModule } from '@af/digi-ng/_form/form-textarea';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
@@ -6,6 +5,7 @@ import { RouterModule } from '@angular/router';
import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { UiLoaderModule } from '@ui/loader/loader.module';
import { UiTextareaModule } from '@ui/textarea/textarea.module';
import { EmployeeInviteComponent } from './employee-invite.component';
@NgModule({
@@ -17,7 +17,7 @@ import { EmployeeInviteComponent } from './employee-invite.component';
LayoutModule,
BackLinkModule,
ReactiveFormsModule,
DigiNgFormTextareaModule,
UiTextareaModule,
UiLoaderModule,
],
})

View File

@@ -58,16 +58,15 @@
class="avvikelse-report-form__form-item"
*ngFor="let question of questionsFormArray.controls; let i=index"
>
<digi-ng-form-textarea
<ui-textarea
[formControlName]="i"
[afLabel]="questions[i]?.name"
[afDisableValidStyle]="true"
[afRequired]="questionIsRequired(questions[i])"
[afInvalidMessage]="question.errors?.required || question.errors?.invalid"
[afAnnounceIfOptional]="true"
[afMaxLength]="2000"
[afInvalid]="formControlIsInvalid(question)"
></digi-ng-form-textarea>
[uiLabel]="questions[i]?.name"
[uiRequired]="questionIsRequired(questions[i])"
[uiValidationMessage]="question.errors?.required || question.errors?.invalid"
[uiAnnounceIfOptional]="true"
[uiMaxLength]="2000"
[uiInvalid]="formControlIsInvalid(question)"
></ui-textarea>
</div>
</div>

View File

@@ -3,7 +3,6 @@ import { DigiNgFormDatepickerModule } from '@af/digi-ng/_form/form-datepicker';
import { DigiNgFormInputModule } from '@af/digi-ng/_form/form-input';
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { DigiNgFormTextareaModule } from '@af/digi-ng/_form/form-textarea';
import { DigiNgProgressProgressbarModule } from '@af/digi-ng/_progress/progressbar';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
@@ -14,6 +13,7 @@ import { ConfirmDialogModule } from '@msfa-shared/components/confirm-dialog/conf
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { UiLoaderModule } from '@ui/loader/loader.module';
import { UiSkeletonModule } from '@ui/skeleton/skeleton.module';
import { UiTextareaModule } from '@ui/textarea/textarea.module';
import { ReportDescriptionListModule } from '../../../components/report-description-list/report-description-list.module';
import { ReportLayoutModule } from '../../../components/report-layout/report-layout.module';
import { AvvikelseReportFormComponent } from './avvikelse-report-form.component';
@@ -29,7 +29,6 @@ import { AvvikelseReportFormService } from './avvikelse-report-form.service';
ReactiveFormsModule,
DigiNgFormRadiobuttonGroupModule,
DigiNgFormDatepickerModule,
DigiNgFormTextareaModule,
DigiNgProgressProgressbarModule,
ReportLayoutModule,
ConfirmDialogModule,
@@ -40,6 +39,7 @@ import { AvvikelseReportFormService } from './avvikelse-report-form.service';
ReportDescriptionListModule,
DigiNgFormInputModule,
DigiNgDialogModule,
UiTextareaModule,
],
providers: [AvvikelseReportFormService],
exports: [AvvikelseReportFormComponent],

View File

@@ -87,24 +87,15 @@
</div>
<div class="franvaro-report-form__form-item" *ngIf="showKnownReasonTextArea">
<digi-ng-form-textarea
[afDisableValidStyle]="true"
[afInvalid]="formControlIsInvalid(['knownReasonComment'])"
[afMaxLength]="2000"
[afAnnounceIfOptional]="true"
[afRequired]="true"
afLabel="Beskriv frånvaro"
afSize="s"
<ui-textarea
[formControl]="knownReasonCommentFormControl"
></digi-ng-form-textarea>
<div aria-atomic="true" role="alert">
<digi-ng-form-validation-message
*ngIf="formControlIsInvalid(['knownReasonComment'])"
class="franvaro-report-form__validation-message"
[afPositive]="false"
[afValidationText]="formErrors.knownReasonComment"
></digi-ng-form-validation-message>
</div>
uiLabel="Beskriv frånvaro"
[uiInvalid]="formControlIsInvalid(['knownReasonComment'])"
[uiValidationMessage]="formErrors?.knownReasonComment"
[uiMaxLength]="2000"
[uiRequired]="true"
[uiAnnounceIfOptional]="true"
></ui-textarea>
</div>
</ng-container>

View File

@@ -3,7 +3,6 @@ import { DigiNgFormDatepickerModule } from '@af/digi-ng/_form/form-datepicker';
import { DigiNgFormInputModule } from '@af/digi-ng/_form/form-input';
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { DigiNgFormTextareaModule } from '@af/digi-ng/_form/form-textarea';
import { DigiNgFormValidationMessageModule } from '@af/digi-ng/_form/form-validation-message';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
@@ -13,6 +12,7 @@ import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.modu
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { UiLoaderModule } from '@ui/loader/loader.module';
import { UiSkeletonModule } from '@ui/skeleton/skeleton.module';
import { UiTextareaModule } from '@ui/textarea/textarea.module';
import { ReportDescriptionListModule } from '../../../components/report-description-list/report-description-list.module';
import { ReportLayoutModule } from '../../../components/report-layout/report-layout.module';
import { FranvaroReportFormComponent } from './franvaro-report-form.component';
@@ -34,10 +34,10 @@ import { FranvaroReportFormService } from './franvaro-report-form.service';
DigiNgFormDatepickerModule,
DigiNgFormRadiobuttonGroupModule,
UiSkeletonModule,
DigiNgFormTextareaModule,
DigiNgFormInputModule,
DigiNgFormValidationMessageModule,
DigiNgDialogModule,
UiTextareaModule,
],
providers: [FranvaroReportFormService],
exports: [FranvaroReportFormComponent],

View File

@@ -61,17 +61,16 @@
></digi-ng-form-select>
</div>
<div class="informativ-rapport-form__form-item">
<digi-ng-form-textarea
<ui-textarea
[formControl]="commentFormControl"
afLabel="Kompletterande information"
afDescription="Undvik att skriva in information som känsliga personuppgifter, skyddad identitet eller deltagarens mående. Skriv i sådant fall in det telefonnummer du vill bli kontaktad på, vi bedömer om återkoppling är relevant."
[afDisableValidStyle]="true"
[afRequired]="true"
[afInvalidMessage]="commentFormControl.errors?.required"
[afAnnounceIfOptional]="true"
[afMaxLength]="2000"
[afInvalid]="formControlIsInvalid('comment')"
></digi-ng-form-textarea>
uiLabel="Kompletterande information"
uiDescription="Undvik att skriva in information som känsliga personuppgifter, skyddad identitet eller deltagarens mående. Skriv i sådant fall in det telefonnummer du vill bli kontaktad på, vi bedömer om återkoppling är relevant."
[uiRequired]="true"
[uiInvalid]="formControlIsInvalid('comment')"
[uiValidationMessage]="commentFormControl.errors?.required"
[uiAnnounceIfOptional]="true"
[uiMaxLength]="2000"
></ui-textarea>
</div>
<footer class="informativ-rapport-form__footer">

View File

@@ -1,7 +1,5 @@
import { DigiNgDialogModule } from '@af/digi-ng/_dialog/dialog';
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { DigiNgFormTextareaModule } from '@af/digi-ng/_form/form-textarea';
import { UiSkeletonModule } from '@ui/skeleton/skeleton.module';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
@@ -9,6 +7,8 @@ import { RouterModule } from '@angular/router';
import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.module';
import { ConfirmDialogModule } from '@msfa-shared/components/confirm-dialog/confirm-dialog.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { UiSkeletonModule } from '@ui/skeleton/skeleton.module';
import { UiTextareaModule } from '@ui/textarea/textarea.module';
import { ReportDescriptionListModule } from '../../../components/report-description-list/report-description-list.module';
import { ReportLayoutModule } from '../../../components/report-layout/report-layout.module';
import { InformativRapportFormComponent } from './informativ-rapport-form.component';
@@ -29,7 +29,7 @@ import { InformativRapportFormService } from './informativ-rapport-form.service'
UiSkeletonModule,
DigiNgDialogModule,
DigiNgFormSelectModule,
DigiNgFormTextareaModule,
UiTextareaModule,
],
providers: [InformativRapportFormService],
exports: [InformativRapportFormComponent],

View File

@@ -1,5 +1,5 @@
/* tslint:disable:no-unused-variable */
import { FormCheckboxComponent } from './form-checkbox.component';
import { CheckboxComponent } from './checkbox.component';
export class MockInjector {
get = jest.fn();
@@ -14,10 +14,10 @@ export class MockChangeDetectorRef {
checkNoChanges = jest.fn();
}
describe('FormCheckboxComponent', () => {
let component: FormCheckboxComponent;
describe('CheckboxComponent', () => {
let component: CheckboxComponent;
it('should create', () => {
component = new FormCheckboxComponent(new MockInjector(), new MockChangeDetectorRef());
component = new CheckboxComponent(new MockInjector(), new MockChangeDetectorRef());
expect(component).toBeTruthy();
});
});

View File

@@ -8,7 +8,6 @@ import {
Input,
OnChanges,
Output,
SimpleChanges,
} from '@angular/core';
import { ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { uuid } from '@utils/uuid.util';
@@ -42,7 +41,7 @@ export class CheckboxComponent implements AfterViewInit, ControlValueAccessor, O
@Input() uiId: string = uuid();
@Input() uiName: string;
@Input() uiAnnounceIfOptional: boolean = false;
@Output() uiOnChange: EventEmitter<any> = new EventEmitter();
@Output() uiOnChange: EventEmitter<boolean> = new EventEmitter();
name: string | number;
@@ -79,7 +78,7 @@ export class CheckboxComponent implements AfterViewInit, ControlValueAccessor, O
}
}
ngOnChanges(changes: SimpleChanges): void {
ngOnChanges(): void {
const ngControl: NgControl = this.injector.get(NgControl, null);
if (ngControl) {
this.name = ngControl.name;
@@ -96,7 +95,7 @@ export class CheckboxComponent implements AfterViewInit, ControlValueAccessor, O
}
}
writeValue(value: any): void {
writeValue(value: boolean): void {
this._value = value;
this.changeDetectorRef.detectChanges();
}

View File

@@ -0,0 +1,6 @@
export enum UiTextAreaVariation {
AUTO = 'auto',
S = 's',
M = 'm',
L = 'l',
}

View File

@@ -0,0 +1,26 @@
<div
class="ui-textarea"
[ngClass]="{'ui-textarea--invalid': uiInvalid && uiValidationMessage, 'ui-textarea--with-counter': uiMaxLength}"
>
<digi-form-textarea
class="ui-textarea__textarea"
[afId]="uiId"
[afName]="uiName"
[afLabel]="uiLabel"
[afLabelDescription]="uiDescription"
[afVariation]="uiVariation"
[afRequired]="uiRequired"
[afAnnounceIfOptional]="uiAnnounceIfOptional"
[afMaxlength]="uiMaxLength"
[afMinlength]="uiMinLength"
[afValue]="currentValue"
[afValidation]="uiInvalid ? 'error' : 'neutral'"
(afOnInput)="checkForChange($event.detail.target.value)"
></digi-form-textarea>
<span class="ui-textarea__counter" *ngIf="uiMaxLength">{{charactersLeft}} tecken kvar</span>
<div class="ui-textarea__validation" aria-atomic="true" role="alert">
<digi-form-validation-message *ngIf="uiInvalid && uiValidationMessage" af-variation="error"
>{{uiValidationMessage}}</digi-form-validation-message
>
</div>
</div>

View File

@@ -0,0 +1,25 @@
.ui-textarea {
display: grid;
grid-template-columns: 1fr max-content;
grid-template-areas:
'textarea textarea'
'validation counter';
&--invalid,
&--with-counter {
gap: var(--digi--layout--gutter--s);
}
&__textarea {
grid-area: textarea;
}
&__counter {
grid-area: counter;
color: var(--digi--typography--color--text--dimmed);
font-size: var(--digi--typography--font-size--s);
}
&__validation {
grid-area: validation;
}
}

View File

@@ -0,0 +1,23 @@
/* tslint:disable:no-unused-variable */
import { TextareaComponent } from './textarea.component';
export class MockInjector {
get = jest.fn();
}
// tslint:disable-next-line: max-classes-per-file
export class MockChangeDetectorRef {
markForCheck = jest.fn();
detach = jest.fn();
detectChanges = jest.fn();
reattach = jest.fn();
checkNoChanges = jest.fn();
}
describe('TextareaComponent', () => {
let component: TextareaComponent;
it('should create', () => {
component = new TextareaComponent(new MockInjector(), new MockChangeDetectorRef());
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,16 @@
import { ReactiveFormsModule } from '@angular/forms';
import { TextareaComponent } from './textarea.component';
import { UiTextareaModule } from './textarea.module';
export default { title: 'Textarea', component: TextareaComponent };
const componentModule = {
moduleMetadata: {
imports: [ReactiveFormsModule, UiTextareaModule],
},
};
export const standard = () => ({
...componentModule,
template: '<ui-textarea></ui-textarea>',
});

View File

@@ -0,0 +1,101 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Injector,
Input,
OnChanges,
Output,
} from '@angular/core';
import { ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { uuid } from '@utils/uuid.util';
import { UiTextAreaVariation } from './textarea-variation.enum';
/**
* A textarea input. Implemented with control value accessor
*
* ## Usage
* ``import {UiTextareaModule} from '@ui/textarea/textarea.module';``
*/
@Component({
selector: 'ui-textarea',
templateUrl: './textarea.component.html',
styleUrls: ['./textarea.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: TextareaComponent,
multi: true,
},
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TextareaComponent implements AfterViewInit, ControlValueAccessor, OnChanges {
@Input() uiId: string = uuid();
@Input() uiName: string;
@Input() uiLabel: string = '';
@Input() uiDescription: string;
@Input() uiVariation: UiTextAreaVariation = UiTextAreaVariation.M;
@Input() uiRequired: boolean;
@Input() uiAnnounceIfOptional: boolean = false;
@Input() uiMaxLength: number;
@Input() uiMinLength: number;
@Input() uiInvalid: boolean;
@Input() uiValidationMessage: string;
@Output() uiOnChange: EventEmitter<string> = new EventEmitter();
name: string | number;
onTouched: () => {};
private onChange: (value: any) => {};
private _value: string = '';
constructor(private injector: Injector, private changeDetectorRef: ChangeDetectorRef) {}
get currentValue(): string {
return this._value;
}
get charactersLeft(): number {
return this.uiMaxLength - this.currentValue.length;
}
ngAfterViewInit(): void {
const ngControl: NgControl = this.injector.get(NgControl, null);
if (ngControl) {
this.name = ngControl.name;
}
}
ngOnChanges(): void {
const ngControl: NgControl = this.injector.get(NgControl, null);
if (ngControl) {
this.name = ngControl.name;
}
}
checkForChange(value: string): void {
if (this._value !== value) {
if (this.onChange) {
this.onChange(value);
}
this._value = value;
this.uiOnChange.emit(value);
}
}
writeValue(value: string): void {
this._value = value;
this.changeDetectorRef.detectChanges();
}
registerOnChange(fn: (value: string) => {}) {
this.onChange = fn;
}
registerOnTouched(fn: () => {}) {
this.onTouched = fn;
}
}

View File

@@ -0,0 +1,11 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { TextareaComponent } from './textarea.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [CommonModule],
declarations: [TextareaComponent],
exports: [TextareaComponent],
})
export class UiTextareaModule {}