();
- public showMultiple = false;
public newValues = {
colorModel: '#000',
colorModel2: '#111',
- colorModel3: '#222',
- colorModel4: '#333',
};
- public colorControl = new UntypedFormControl('#00f');
- public colorControl2 = new UntypedFormControl('#aaa');
- public colorControl3 = new UntypedFormControl('#bbb');
- public colorControl4 = new UntypedFormControl('#ccc');
+ public colorControl = new UntypedFormControl('#00f', [
+ (control: AbstractControl): ValidationErrors | null => {
+ if (control.value?.rgba?.alpha < 0.8) {
+ return { opaque: true };
+ }
+
+ return null;
+ },
+ ]);
+
+ public colorControl2 = new UntypedFormControl('#00f', [
+ (control: AbstractControl): ValidationErrors | null => {
+ if (control.value?.rgba?.alpha < 0.8) {
+ return { opaque: true };
+ }
+
+ return null;
+ },
+ ]);
+
public colorForm = new UntypedFormGroup({
colorModel: this.colorControl,
colorModel2: this.colorControl2,
- colorModel3: this.colorControl3,
- colorModel4: this.colorControl4,
});
public sendMessage(type: SkyColorpickerMessageType) {
diff --git a/libs/components/forms/src/assets/locales/resources_en_US.json b/libs/components/forms/src/assets/locales/resources_en_US.json
index 0558fe7047..84c971b1c3 100644
--- a/libs/components/forms/src/assets/locales/resources_en_US.json
+++ b/libs/components/forms/src/assets/locales/resources_en_US.json
@@ -7,10 +7,6 @@
"_description": "Screen reader only label for character count over limit symbol",
"message": "You are over the character limit."
},
- "skyux_form_error_character_count": {
- "_description": "Error message for a field with a value that exceeds the character count limit",
- "message": "Limit {0} to {1} character(s)."
- },
"skyux_form_error_date": {
"_description": "Error message for a field with an invalid date value",
"message": "Select or enter a valid date."
@@ -150,5 +146,9 @@
"skyux_input_box_help_inline_aria_label": {
"_description": "The accessible label for an input box help inline button",
"message": "Show help content for {0}"
+ },
+ "skyux_checkbox_required_label_text": {
+ "_description": "The label text portion of the required validation message",
+ "message": "This selection"
}
}
diff --git a/libs/components/forms/src/index.ts b/libs/components/forms/src/index.ts
index 5f047eecc3..f4aa170c35 100644
--- a/libs/components/forms/src/index.ts
+++ b/libs/components/forms/src/index.ts
@@ -29,6 +29,9 @@ export { SkySelectionBoxGridAlignItemsType } from './lib/modules/selection-box/t
export { SkyToggleSwitchModule } from './lib/modules/toggle-switch/toggle-switch.module';
export { SkyToggleSwitchChange } from './lib/modules/toggle-switch/types/toggle-switch-change';
+export { SkyFormErrorComponent } from './lib/modules/form-error/form-error.component';
+export { FORM_ERRORS } from './lib/modules/form-error/form-errors-token';
+
// Components and directives must be exported to support Angular's "partial" Ivy compiler.
// Obscure names are used to indicate types are not part of the public API.
diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.html b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.html
index 1c91c06109..318e50cb1c 100644
--- a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.html
+++ b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.html
@@ -17,6 +17,10 @@
[attr.aria-label]="label"
[attr.aria-labelledby]="labelledBy"
[attr.aria-required]="required ? true : null"
+ [attr.aria-invalid]="!!ngControl?.errors"
+ [attr.aria-errormessage]="
+ labelText && ngControl?.errors ? errorId : undefined
+ "
(blur)="onInputBlur()"
(change)="onInteractionEvent($event)"
#inputEl
@@ -51,5 +55,20 @@
/>
-
+
+ {{ labelText }}
+
+
+
+
+
+
+
+
+
diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.spec.ts b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.spec.ts
index 58781e3f1f..ba27f796b4 100644
--- a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.spec.ts
+++ b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.spec.ts
@@ -36,6 +36,7 @@ import { SkyCheckboxModule } from './checkbox.module';
[disabled]="isDisabled"
[icon]="icon"
[id]="id"
+ [labelText]="labelText"
[(indeterminate)]="indeterminate"
(change)="checkboxChange($event)"
>
@@ -54,6 +55,7 @@ class SingleCheckboxComponent implements AfterViewInit {
public isChecked: boolean | undefined = false;
public isDisabled = false;
public showInlineHelp = false;
+ public labelText: string | undefined;
@ViewChild(SkyCheckboxComponent)
public checkboxComponent: SkyCheckboxComponent | undefined;
@@ -92,7 +94,12 @@ class CheckboxWithFormDirectivesComponent {
template: `
+
+
Help content from template
diff --git a/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.component.ts b/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.component.ts
index baa02a6c51..3f9b9beaa7 100644
--- a/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.component.ts
+++ b/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.component.ts
@@ -11,6 +11,7 @@ import {
})
export class InputBoxHarnessTestComponent {
public myForm: UntypedFormGroup;
+ public directiveErrorForm: UntypedFormGroup;
@ViewChild('helpContentTemplate', {
read: TemplateRef,
@@ -32,5 +33,10 @@ export class InputBoxHarnessTestComponent {
firstName: new UntypedFormControl('John'),
lastName: new UntypedFormControl('Doe'),
});
+ this.directiveErrorForm = formBuilder.group({
+ easyModeDatepicker: new UntypedFormControl('123'),
+ easyModeTimepicker: new UntypedFormControl('abc'),
+ easyModePhoneField: new UntypedFormControl('abc'),
+ });
}
}
diff --git a/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.module.ts b/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.module.ts
index 26b4933f3d..08ca311bf2 100644
--- a/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.module.ts
+++ b/libs/components/forms/testing/src/input-box/fixtures/input-box-harness-test.module.ts
@@ -1,8 +1,10 @@
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { SkyIdModule } from '@skyux/core';
+import { SkyDatepickerModule, SkyTimepickerModule } from '@skyux/datetime';
import { SkyInputBoxModule } from '@skyux/forms';
import { SkyStatusIndicatorModule } from '@skyux/indicators';
+import { SkyPhoneFieldModule } from '@skyux/phone-field';
import { InputBoxHarnessTestComponent } from './input-box-harness-test.component';
@@ -13,6 +15,9 @@ import { InputBoxHarnessTestComponent } from './input-box-harness-test.component
SkyIdModule,
SkyInputBoxModule,
SkyStatusIndicatorModule,
+ SkyDatepickerModule,
+ SkyTimepickerModule,
+ SkyPhoneFieldModule,
],
declarations: [InputBoxHarnessTestComponent],
})
diff --git a/libs/components/forms/testing/src/input-box/input-box-harness.spec.ts b/libs/components/forms/testing/src/input-box/input-box-harness.spec.ts
index b349563143..8539749f63 100644
--- a/libs/components/forms/testing/src/input-box/input-box-harness.spec.ts
+++ b/libs/components/forms/testing/src/input-box/input-box-harness.spec.ts
@@ -2,6 +2,7 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Validators } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { SkyValidators } from '@skyux/validation';
import { InputBoxHarnessTestComponent } from './fixtures/input-box-harness-test.component';
import { InputBoxHarnessTestModule } from './fixtures/input-box-harness-test.module';
@@ -138,6 +139,127 @@ describe('Input box harness', () => {
await expectAsync(customError.getText()).toBeResolvedTo('Test error');
});
+ it('should return whether custom form error has fired', async () => {
+ const { inputBoxHarness } = await setupTest({
+ dataSkyId: 'custom-error-easy-mode',
+ });
+
+ await expectAsync(
+ inputBoxHarness.hasCustomFormError('custom'),
+ ).toBeResolvedTo(true);
+ });
+
+ it('should return whether required error has fired', async () => {
+ const { component, fixture, inputBoxHarness } = await setupTest({
+ dataSkyId: 'my-input-box-last-name-easy-mode',
+ });
+
+ const control = component.myForm.controls['lastName'];
+ control.addValidators(Validators.required);
+ control.setValue('');
+ control.markAsDirty();
+
+ fixture.detectChanges();
+
+ await expectAsync(inputBoxHarness.hasRequiredError()).toBeResolvedTo(true);
+ });
+
+ it('should return whether minimum length error has fired', async () => {
+ const { component, fixture, inputBoxHarness } = await setupTest({
+ dataSkyId: 'my-input-box-last-name-easy-mode',
+ });
+
+ const control = component.myForm.controls['lastName'];
+ control.addValidators(Validators.minLength(2));
+ control.setValue('a');
+ control.markAsDirty();
+
+ fixture.detectChanges();
+
+ await expectAsync(inputBoxHarness.hasMinLengthError()).toBeResolvedTo(true);
+ });
+
+ it('should return whether maximum length error has fired', async () => {
+ const { component, fixture, inputBoxHarness } = await setupTest({
+ dataSkyId: 'my-input-box-last-name-easy-mode',
+ });
+
+ const control = component.myForm.controls['lastName'];
+ control.addValidators(Validators.maxLength(1));
+ control.setValue('abc');
+ control.markAsDirty();
+
+ fixture.detectChanges();
+
+ await expectAsync(inputBoxHarness.hasMaxLengthError()).toBeResolvedTo(true);
+ });
+
+ it('should return whether email validator error has fired', async () => {
+ const { component, fixture, inputBoxHarness } = await setupTest({
+ dataSkyId: 'my-input-box-last-name-easy-mode',
+ });
+
+ const control = component.myForm.controls['lastName'];
+ control.addValidators(SkyValidators.email);
+ control.setValue('abc');
+ control.markAsDirty();
+
+ fixture.detectChanges();
+
+ await expectAsync(inputBoxHarness.hasEmailError()).toBeResolvedTo(true);
+ });
+
+ it('should return whether url validator error has fired', async () => {
+ const { component, fixture, inputBoxHarness } = await setupTest({
+ dataSkyId: 'my-input-box-last-name-easy-mode',
+ });
+
+ const control = component.myForm.controls['lastName'];
+ control.addValidators(SkyValidators.url);
+ control.setValue('abc');
+ control.markAsDirty();
+
+ fixture.detectChanges();
+
+ await expectAsync(inputBoxHarness.hasUrlError()).toBeResolvedTo(true);
+ });
+
+ it('should return whether date picker validator error has fired', async () => {
+ const { fixture, inputBoxHarness } = await setupTest({
+ dataSkyId: 'datepicker-easy-mode',
+ });
+
+ fixture.detectChanges();
+
+ await expectAsync(inputBoxHarness.hasDateError()).toBeResolvedTo(true);
+ });
+
+ it('should return whether time picker validator error has fired', async () => {
+ const { component, fixture, inputBoxHarness } = await setupTest({
+ dataSkyId: 'timepicker-easy-mode',
+ });
+
+ const control = component.directiveErrorForm.controls['easyModeTimepicker'];
+ control.markAsDirty();
+ fixture.detectChanges();
+
+ await expectAsync(inputBoxHarness.hasTimeError()).toBeResolvedTo(true);
+ });
+
+ it('should return whether phone field validator error has fired', async () => {
+ const { component, fixture, inputBoxHarness } = await setupTest({
+ dataSkyId: 'phone-field-easy-mode',
+ });
+
+ const control = component.directiveErrorForm.controls['easyModePhoneField'];
+ control.markAsDirty();
+ fixture.detectChanges();
+
+ await expectAsync(inputBoxHarness.hasPhoneFieldError()).toBeResolvedTo(
+ true,
+ );
+ });
+
it('should return character counter indicator', async () => {
const { component, fixture, inputBoxHarness } = await setupTest({
dataSkyId: DATA_SKY_ID_EASY_MODE,
diff --git a/libs/components/forms/testing/src/input-box/input-box-harness.ts b/libs/components/forms/testing/src/input-box/input-box-harness.ts
index 01aefb6fcc..dc00ac57c8 100644
--- a/libs/components/forms/testing/src/input-box/input-box-harness.ts
+++ b/libs/components/forms/testing/src/input-box/input-box-harness.ts
@@ -9,6 +9,7 @@ import { SkyStatusIndicatorHarness } from '@skyux/indicators/testing';
import { SkyPopoverHarness } from '@skyux/popovers/testing';
import { SkyCharacterCounterIndicatorHarness } from '../character-counter/character-counter-indicator-harness';
+import { SkyFormErrorsHarness } from '../public-api';
import { SkyInputBoxHarnessFilters } from './input-box-harness-filters';
@@ -25,6 +26,10 @@ export class SkyInputBoxHarness extends SkyComponentHarness {
#getLabel = this.locatorForOptional('.sky-control-label');
#getWrapper = this.locatorFor('.sky-input-box');
+ async #getFormError(): Promise {
+ return this.locatorFor(SkyFormErrorsHarness)();
+ }
+
/**
* Gets a `HarnessPredicate` that can be used to search for a
* `SkyInputBoxHarness` that meets certain criteria.
@@ -79,6 +84,69 @@ export class SkyInputBoxHarness extends SkyComponentHarness {
return errors;
}
+ /**
+ * Whether the custom error is triggered.
+ */
+ public async hasCustomFormError(errorName: string): Promise {
+ return (await this.#getFormError()).hasError(errorName);
+ }
+
+ /**
+ * Whether the required field is empty.
+ */
+ public async hasRequiredError(): Promise {
+ return (await this.#getFormError()).hasError('required');
+ }
+
+ /**
+ * Whether the field has more characters than allowed.
+ */
+ public async hasMaxLengthError(): Promise {
+ return (await this.#getFormError()).hasError('maxlength');
+ }
+
+ /**
+ * Whether the field has fewer characters than allowed.
+ */
+ public async hasMinLengthError(): Promise {
+ return (await this.#getFormError()).hasError('minlength');
+ }
+
+ /*
+ * Whether the field is set to an invalid email address.
+ */
+ public async hasEmailError(): Promise {
+ return (await this.#getFormError()).hasError('email');
+ }
+
+ /*
+ * Whether the field is set to an invalid URL.
+ */
+ public async hasUrlError(): Promise {
+ return (await this.#getFormError()).hasError('url');
+ }
+
+ /*
+ * Whether the field is set to an invalid date.
+ */
+ public async hasDateError(): Promise {
+ return (await this.#getFormError()).hasError('date');
+ }
+
+ /*
+ * Whether the field is set to an invalid phone number.
+ */
+ public async hasPhoneFieldError(): Promise {
+ return (await this.#getFormError()).hasError('phone');
+ }
+
+ /*
+ * Whether the field is set to an invalid time.
+ */
+ public async hasTimeError(): Promise {
+ return (await this.#getFormError()).hasError('time');
+ }
+
/**
* Indicates whether the input box has disabled styles applied.
*/