diff --git a/apps/code-examples/src/app/code-examples/forms/checkbox/basic/demo.component.html b/apps/code-examples/src/app/code-examples/forms/checkbox/basic/demo.component.html index d75615aa1e..da210ed334 100644 --- a/apps/code-examples/src/app/code-examples/forms/checkbox/basic/demo.component.html +++ b/apps/code-examples/src/app/code-examples/forms/checkbox/basic/demo.component.html @@ -13,5 +13,12 @@ +
+ +
diff --git a/apps/code-examples/src/app/code-examples/forms/checkbox/basic/demo.component.ts b/apps/code-examples/src/app/code-examples/forms/checkbox/basic/demo.component.ts index 47a8452783..bb0471a9c5 100644 --- a/apps/code-examples/src/app/code-examples/forms/checkbox/basic/demo.component.ts +++ b/apps/code-examples/src/app/code-examples/forms/checkbox/basic/demo.component.ts @@ -22,10 +22,13 @@ export class DemoComponent { email: new FormControl(false), phone: new FormControl(false), text: new FormControl(false), + terms: new FormControl(false), }); } protected onSubmit(): void { + this.formGroup.markAllAsTouched(); + console.log(this.formGroup.value); } } diff --git a/apps/e2e/forms-storybook-e2e/src/e2e/checkbox.component.cy.ts b/apps/e2e/forms-storybook-e2e/src/e2e/checkbox.component.cy.ts index dc9709ca2b..4f07c25794 100644 --- a/apps/e2e/forms-storybook-e2e/src/e2e/checkbox.component.cy.ts +++ b/apps/e2e/forms-storybook-e2e/src/e2e/checkbox.component.cy.ts @@ -13,6 +13,8 @@ describe('forms-storybook - checkbox', () => { cy.get('app-checkbox') .should('exist') .should('be.visible') + .get('#touched-required-checkbox') + .dblclick() .get('#standard-checkboxes') .should('exist') .should('be.visible') diff --git a/apps/e2e/forms-storybook/src/app/checkbox/checkbox.component.html b/apps/e2e/forms-storybook/src/app/checkbox/checkbox.component.html index 608834aa42..b319ad2437 100644 --- a/apps/e2e/forms-storybook/src/app/checkbox/checkbox.component.html +++ b/apps/e2e/forms-storybook/src/app/checkbox/checkbox.component.html @@ -48,6 +48,17 @@ + + + + + diff --git a/apps/e2e/forms-storybook/src/app/checkbox/checkbox.module.ts b/apps/e2e/forms-storybook/src/app/checkbox/checkbox.module.ts index d6dd5a7b69..6758965448 100644 --- a/apps/e2e/forms-storybook/src/app/checkbox/checkbox.module.ts +++ b/apps/e2e/forms-storybook/src/app/checkbox/checkbox.module.ts @@ -1,5 +1,6 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; import { SkyCheckboxModule } from '@skyux/forms'; import { SkyHelpInlineModule } from '@skyux/indicators'; @@ -13,6 +14,7 @@ const routes: Routes = [{ path: '', component: CheckboxComponent }]; declarations: [CheckboxComponent], imports: [ CommonModule, + FormsModule, SkyCheckboxModule, SkyFluidGridModule, SkyHelpInlineModule, diff --git a/apps/playground/src/app/components/forms/checkbox/checkbox.component.ts b/apps/playground/src/app/components/forms/checkbox/checkbox.component.ts index ce44f26b4f..16339e6270 100644 --- a/apps/playground/src/app/components/forms/checkbox/checkbox.component.ts +++ b/apps/playground/src/app/components/forms/checkbox/checkbox.component.ts @@ -1,5 +1,9 @@ import { Component, OnInit } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { + FormControl, + UntypedFormBuilder, + UntypedFormGroup, +} from '@angular/forms'; @Component({ selector: 'app-checkbox', @@ -28,7 +32,7 @@ export class CheckboxComponent implements OnInit { public ngOnInit(): void { this.reactiveFormGroup = this.#formBuilder.group({ - reactiveCheckbox: [undefined], + reactiveCheckbox: new FormControl(undefined), }); } 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 f581bd7466..2edd7af960 100644 --- a/libs/components/forms/src/assets/locales/resources_en_US.json +++ b/libs/components/forms/src/assets/locales/resources_en_US.json @@ -150,5 +150,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/lib/modules/checkbox/checkbox.component.html b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.html index 0edf694ea9..a0e041fc58 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 @@ -58,3 +62,12 @@ + + + diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts index 3faa747fb9..829bc189b8 100644 --- a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts @@ -14,6 +14,7 @@ import { SkyIdService, SkyLogService } from '@skyux/core'; import { BehaviorSubject, Observable } from 'rxjs'; +import { SKY_FORM_ERRORS_ENABLED } from '../form-error/form-errors-enabled-token'; import { SkyFormsUtility } from '../shared/forms-utility'; import { SkyCheckboxChange } from './checkbox-change'; @@ -26,6 +27,7 @@ import { SkyCheckboxChange } from './checkbox-change'; selector: 'sky-checkbox', templateUrl: './checkbox.component.html', styleUrls: ['./checkbox.component.scss'], + providers: [{ provide: SKY_FORM_ERRORS_ENABLED, useValue: true }], }) export class SkyCheckboxComponent implements ControlValueAccessor, OnInit { /** @@ -260,13 +262,19 @@ export class SkyCheckboxComponent implements ControlValueAccessor, OnInit { #_required = false; #changeDetector = inject(ChangeDetectorRef); - #defaultId = inject(SkyIdService).generateId(); + #idSvc = inject(SkyIdService); + #defaultId = this.#idSvc.generateId(); #logger = inject(SkyLogService); - #ngControl = inject(NgControl, { optional: true, self: true }); + + protected readonly ngControl = inject(NgControl, { + optional: true, + self: true, + }); + protected readonly errorId = this.#idSvc.generateId(); constructor() { - if (this.#ngControl) { - this.#ngControl.valueAccessor = this; + if (this.ngControl) { + this.ngControl.valueAccessor = this; } this.#checkedChange = new BehaviorSubject(this.checked); @@ -282,10 +290,10 @@ export class SkyCheckboxComponent implements ControlValueAccessor, OnInit { } public ngOnInit(): void { - if (this.#ngControl) { + if (this.ngControl) { // Backwards compatibility support for anyone still using Validators.Required. this.required = - this.required || SkyFormsUtility.hasRequiredValidation(this.#ngControl); + this.required || SkyFormsUtility.hasRequiredValidation(this.ngControl); } } @@ -361,16 +369,16 @@ export class SkyCheckboxComponent implements ControlValueAccessor, OnInit { #setValidators(): void { if ( this.required && - !this.#ngControl?.control?.hasValidator(Validators.requiredTrue) + !this.ngControl?.control?.hasValidator(Validators.requiredTrue) ) { - this.#ngControl?.control?.addValidators(Validators.requiredTrue); - this.#ngControl?.control?.updateValueAndValidity(); + this.ngControl?.control?.addValidators(Validators.requiredTrue); + this.ngControl?.control?.updateValueAndValidity(); } else if ( !this.required && - this.#ngControl?.control?.hasValidator(Validators.requiredTrue) + this.ngControl?.control?.hasValidator(Validators.requiredTrue) ) { - this.#ngControl.control.removeValidators(Validators.requiredTrue); - this.#ngControl.control?.updateValueAndValidity(); + this.ngControl.control.removeValidators(Validators.requiredTrue); + this.ngControl.control?.updateValueAndValidity(); } } } diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox.module.ts b/libs/components/forms/src/lib/modules/checkbox/checkbox.module.ts index 9eef50c8f1..9d98a73d8e 100644 --- a/libs/components/forms/src/lib/modules/checkbox/checkbox.module.ts +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox.module.ts @@ -4,12 +4,28 @@ import { FormsModule } from '@angular/forms'; import { SkyTrimModule } from '@skyux/core'; import { SkyIconModule } from '@skyux/indicators'; +import { SkyFormErrorModule } from '../form-error/form-error.module'; +import { SkyFormErrorsModule } from '../form-error/form-errors.module'; +import { SkyFormsResourcesModule } from '../shared/sky-forms-resources.module'; + import { SkyCheckboxLabelComponent } from './checkbox-label.component'; import { SkyCheckboxComponent } from './checkbox.component'; @NgModule({ declarations: [SkyCheckboxComponent, SkyCheckboxLabelComponent], - imports: [CommonModule, FormsModule, SkyIconModule, SkyTrimModule], - exports: [SkyCheckboxComponent, SkyCheckboxLabelComponent], + imports: [ + CommonModule, + FormsModule, + SkyFormErrorModule, + SkyFormErrorsModule, + SkyFormsResourcesModule, + SkyIconModule, + SkyTrimModule, + ], + exports: [ + SkyCheckboxComponent, + SkyCheckboxLabelComponent, + SkyFormErrorModule, + ], }) export class SkyCheckboxModule {} diff --git a/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts b/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts index 3dd8c03b1b..19c6992432 100644 --- a/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts +++ b/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts @@ -96,6 +96,7 @@ const RESOURCES: { [locale: string]: SkyLibResources } = { skyux_input_box_help_inline_aria_label: { message: 'Show help content for {0}', }, + skyux_checkbox_required_label_text: { message: 'This selection' }, }, }; diff --git a/libs/components/forms/testing/src/checkbox/checkbox-harness.spec.ts b/libs/components/forms/testing/src/checkbox/checkbox-harness.spec.ts index abf0c51218..6bf50da55d 100644 --- a/libs/components/forms/testing/src/checkbox/checkbox-harness.spec.ts +++ b/libs/components/forms/testing/src/checkbox/checkbox-harness.spec.ts @@ -107,6 +107,15 @@ describe('Checkbox harness', () => { await expectAsync(checkboxHarness.getLabelText()).toBeResolvedTo(undefined); }); + it('should get the label when specified via labelText input', async () => { + const { checkboxHarness } = await setupTest({ + dataSkyId: 'my-phone-checkbox', + hideEmailLabel: true, + }); + + await expectAsync(checkboxHarness.getLabelText()).toBeResolvedTo('Phone'); + }); + it('should get the checkbox name and value', async () => { const { checkboxHarness } = await setupTest({ dataSkyId: 'my-email-checkbox', @@ -131,4 +140,39 @@ describe('Checkbox harness', () => { 'Could not toggle the checkbox because it is disabled.', ); }); + + it('should display a required error message when there is an error', async () => { + const { checkboxHarness } = await setupTest({ + dataSkyId: 'my-phone-checkbox', + }); + + await checkboxHarness.check(); + await checkboxHarness.uncheck(); + + await expectAsync(checkboxHarness.hasRequiredError()).toBeResolvedTo(true); + }); + + it('should display a custom error message when there is a custom validation error', async () => { + const { checkboxHarness } = await setupTest({ + dataSkyId: 'my-mail-checkbox', + }); + + await checkboxHarness.check(); + await checkboxHarness.uncheck(); + await checkboxHarness.check(); + + await expectAsync( + checkboxHarness.hasCustomError('requiredFalse'), + ).toBeResolvedTo(true); + }); + + it('should throw an error if no form error is found', async () => { + const { checkboxHarness } = await setupTest({ + dataSkyId: 'my-email-checkbox', + }); + + await expectAsync(checkboxHarness.hasRequiredError()).toBeRejectedWithError( + 'No form errors found.', + ); + }); }); diff --git a/libs/components/forms/testing/src/checkbox/checkbox-harness.ts b/libs/components/forms/testing/src/checkbox/checkbox-harness.ts index 6f9eb6fd02..e356204035 100644 --- a/libs/components/forms/testing/src/checkbox/checkbox-harness.ts +++ b/libs/components/forms/testing/src/checkbox/checkbox-harness.ts @@ -1,6 +1,8 @@ import { HarnessPredicate } from '@angular/cdk/testing'; import { SkyComponentHarness } from '@skyux/core/testing'; +import { SkyFormErrorsHarness } from '../form-error/form-errors-harness'; + import { SkyCheckboxHarnessFilters } from './checkbox-harness-filters'; import { SkyCheckboxLabelHarness } from './checkbox-label-harness'; @@ -18,6 +20,16 @@ export class SkyCheckboxHarness extends SkyComponentHarness { #getLabel = this.locatorForOptional(SkyCheckboxLabelHarness); + async #getFormErrors(): Promise { + const harness = await this.locatorForOptional(SkyFormErrorsHarness)(); + + if (harness) { + return harness; + } + + throw Error('No form errors found.'); + } + /** * Gets a `HarnessPredicate` that can be used to search for a * `SkyCheckboxHarness` that meets certain criteria. @@ -129,6 +141,14 @@ export class SkyCheckboxHarness extends SkyComponentHarness { } } + public async hasRequiredError(): Promise { + return (await this.#getFormErrors()).hasError('required'); + } + + public async hasCustomError(errorName: string): Promise { + return (await this.#getFormErrors()).hasError(errorName); + } + async #toggle(): Promise { if (await this.isDisabled()) { throw new Error('Could not toggle the checkbox because it is disabled.'); diff --git a/libs/components/forms/testing/src/checkbox/fixtures/checkbox-harness-test.component.html b/libs/components/forms/testing/src/checkbox/fixtures/checkbox-harness-test.component.html index 889ad13aff..bebc14e57b 100644 --- a/libs/components/forms/testing/src/checkbox/fixtures/checkbox-harness-test.component.html +++ b/libs/components/forms/testing/src/checkbox/fixtures/checkbox-harness-test.component.html @@ -15,12 +15,25 @@
  • +
  • +
  • + - Phone +
  • diff --git a/libs/components/forms/testing/src/checkbox/fixtures/checkbox-harness-test.component.ts b/libs/components/forms/testing/src/checkbox/fixtures/checkbox-harness-test.component.ts index 15e3741616..b8840529c9 100644 --- a/libs/components/forms/testing/src/checkbox/fixtures/checkbox-harness-test.component.ts +++ b/libs/components/forms/testing/src/checkbox/fixtures/checkbox-harness-test.component.ts @@ -1,9 +1,10 @@ import { Component } from '@angular/core'; import { + AbstractControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, - Validators, + ValidationErrors, } from '@angular/forms'; @Component({ @@ -13,15 +14,27 @@ import { export class CheckboxHarnessTestComponent { public myForm: UntypedFormGroup; public hideEmailLabel = false; + public mailControl: UntypedFormControl; #formBuilder: UntypedFormBuilder; constructor(formBuilder: UntypedFormBuilder) { this.#formBuilder = formBuilder; + this.mailControl = new UntypedFormControl(false, [ + (control: AbstractControl): ValidationErrors | null => { + if (control.value) { + return { requiredFalse: true }; + } + + return null; + }, + ]); + this.myForm = this.#formBuilder.group({ email: new UntypedFormControl(false), - phone: new UntypedFormControl(false, [Validators.required]), + phone: new UntypedFormControl(false), + mail: this.mailControl, }); }