Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(components/colorpicker): add form errors to colorpicker #1999

Merged
merged 6 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
[presetColors]="swatches"
[skyColorpickerInput]="colorPicker"
/>
<sky-form-error
*ngIf="favoriteColor.errors?.['opaque']"
errorName="opaque"
errorText="Color must have at least 80% opacity."
/>
</sky-colorpicker>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormControl,
FormGroup,
ReactiveFormsModule,
ValidationErrors,
} from '@angular/forms';
import { SkyColorpickerModule, SkyColorpickerOutput } from '@skyux/colorpicker';

@Component({
standalone: true,
selector: 'app-demo',
templateUrl: './demo.component.html',
imports: [ReactiveFormsModule, SkyColorpickerModule],
imports: [CommonModule, ReactiveFormsModule, SkyColorpickerModule],
})
export class DemoComponent {
protected formGroup: FormGroup;
protected favoriteColor: FormControl<string | null>;

protected swatches: string[] = [
'#BD4040',
Expand All @@ -26,8 +30,18 @@ export class DemoComponent {
];

constructor() {
this.favoriteColor = new FormControl('#f00', [
(control: AbstractControl): ValidationErrors | null => {
if (control.value?.rgba?.alpha < 0.8) {
return { opaque: true };
}

return null;
},
]);

this.formGroup = inject(FormBuilder).group({
favoriteColor: new FormControl('#f00'),
favoriteColor: this.favoriteColor,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,29 @@
[presetColors]="swatches"
[skyColorpickerInput]="colorPicker"
/>

<sky-form-error
*ngIf="favoriteColor.errors?.['opaque']"
errorName="opaque"
errorText="Color must have at least 80% opacity."
/>
</sky-colorpicker>
</div>

<table>
<tr>
<th>Touched</th>
<td>{{ favoriteColor.touched }}</td>
</tr>
<tr>
<th>Pristine</th>
<td>{{ favoriteColor.pristine }}</td>
</tr>
<tr>
<th>Valid</th>
<td>{{ favoriteColor.valid }}</td>
</tr>
</table>

<button class="sky-btn sky-btn-primary" type="submit">Submit</button>
</form>
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Component } from '@angular/core';
import {
AbstractControl,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors,
} from '@angular/forms';
import { SkyColorpickerOutput } from '@skyux/colorpicker';

Expand All @@ -12,6 +14,7 @@ import { SkyColorpickerOutput } from '@skyux/colorpicker';
})
export class ColorpickerComponent {
public reactiveForm: UntypedFormGroup;
public favoriteColor: UntypedFormControl;

public swatches: string[] = [
'#BD4040',
Expand All @@ -23,8 +26,18 @@ export class ColorpickerComponent {
];

constructor(formBuilder: UntypedFormBuilder) {
this.favoriteColor = new UntypedFormControl('#f00', [
(control: AbstractControl): ValidationErrors | null => {
if (control.value?.rgba?.alpha < 0.8) {
return { opaque: true };
}

return null;
},
]);

this.reactiveForm = formBuilder.group({
favoriteColor: new UntypedFormControl('#f00'),
favoriteColor: this.favoriteColor,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from '@angular/forms';
import { SkyLibResourcesService } from '@skyux/i18n';

import { Subject, Subscription, takeUntil } from 'rxjs';
import { Subject, Subscription, distinctUntilChanged, takeUntil } from 'rxjs';

import { SkyColorpickerInputService } from './colorpicker-input.service';
import { SkyColorpickerComponent } from './colorpicker.component';
Expand Down Expand Up @@ -223,6 +223,27 @@ export class SkyColorpickerInputDirective
}
});

this.#colorpickerInputSvc.ariaError
.pipe(
distinctUntilChanged((a, b) => {
return a.hasError === b.hasError && a.errorId === b.errorId;
}),
takeUntil(this.#ngUnsubscribe),
)
.subscribe((errorState) => {
if (errorState.hasError) {
this.#renderer.setAttribute(element, 'aria-invalid', 'true');
this.#renderer.setAttribute(
element,
'aria-errormessage',
errorState.errorId,
);
} else {
this.#renderer.removeAttribute(element, 'aria-invalid');
this.#renderer.removeAttribute(element, 'aria-errormessage');
}
});

this.skyColorpickerInput.updatePickerValues(this.initialColor);

/* Sanity check */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';

import { ReplaySubject } from 'rxjs';

/**
* @internal
*/
@Injectable()
export class SkyColorpickerInputService {
export class SkyColorpickerInputService implements OnDestroy {
public inputId = new ReplaySubject<string>(1);
public labelText = new ReplaySubject<string | undefined>(1);
public ariaError = new ReplaySubject<{ hasError: boolean; errorId: string }>(
1,
);

public ngOnDestroy(): void {
this.inputId.complete();
this.labelText.complete();
this.ariaError.complete();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,13 @@
<sky-icon *skyThemeIf="'modern'" icon="trash" size="lg" />
</button>
</div>

<sky-form-errors
*ngIf="labelText && ngControl?.errors"
[id]="errorId"
[errors]="ngControl?.errors"
[labelText]="labelText"
[showErrors]="ngControl?.touched || ngControl?.dirty"
>
<ng-content select="sky-form-error" />
</sky-form-errors>
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ describe('Colorpicker Component', () => {
return document.querySelector('.sky-colorpicker-container') as HTMLElement;
}

function openColorpicker(element: HTMLElement): void {
function openColorpicker(element: HTMLElement, className?: string): void {
tick();
fixture.detectChanges();
verifyMenuVisibility(false);

const buttonElem = element.querySelector(
'.sky-colorpicker-button',
) as HTMLElement;
const buttonSelector = className
? `.${className} .sky-colorpicker-button`
: '.sky-colorpicker-button';
const buttonElem = element.querySelector(buttonSelector) as HTMLElement;

buttonElem.click();
tick();
fixture.detectChanges();
Expand Down Expand Up @@ -1210,20 +1212,21 @@ describe('Colorpicker Component', () => {
verifyColorpicker(nativeElement, 'rgba(40,137,229,1)', '40, 137, 229');
}));

it('should toggle reset button via messageStream.', fakeAsync(() => {
it('should toggle reset button via messageStream', fakeAsync(() => {
fixture.detectChanges();
tick();
expect(getResetButton().length).toEqual(1);
expect(getResetButton().length).toEqual(2);
component.sendMessage(SkyColorpickerMessageType.ToggleResetButton);
tick();
fixture.detectChanges();
tick();
expect(getResetButton().length).toEqual(0);
// There are 2 colorpicker components and only one is using the message stream
expect(getResetButton().length).toEqual(1);
component.sendMessage(SkyColorpickerMessageType.ToggleResetButton);
tick();
fixture.detectChanges();
tick();
expect(getResetButton().length).toEqual(1);
expect(getResetButton().length).toEqual(2);
}));

it('should only emit the form control valueChanged event once per change', (done) => {
Expand Down Expand Up @@ -1318,6 +1321,67 @@ describe('Colorpicker Component', () => {

expect(outermostDiv).not.toHaveCssClass('sky-colorpicker-disabled');
});

it('should render an error message if the form control set via name has an error', fakeAsync(() => {
component.labelText = 'Label Text';

fixture.detectChanges();

let inputElement: HTMLInputElement | null =
nativeElement.querySelector('input');

expect(inputElement?.getAttribute('aria-invalid')).toBeNull();
expect(inputElement?.getAttribute('aria-errormessage')).toBeNull();

openColorpicker(nativeElement);
setInputElementValue(nativeElement, 'red', '163');
setInputElementValue(nativeElement, 'green', '19');
setInputElementValue(nativeElement, 'blue', '84');
setInputElementValue(nativeElement, 'alpha', '0.5');
applyColorpicker();

fixture.detectChanges();

inputElement = nativeElement.querySelector('input');

expect(inputElement?.getAttribute('aria-invalid')).toBe('true');
expect(inputElement?.getAttribute('aria-errormessage')).toBeDefined();

const errorMessage = nativeElement.querySelector('sky-form-error');

expect(errorMessage).toBeVisible();
}));

it('should render an error message if the form control has an error set via form control', fakeAsync(() => {
fixture.detectChanges();

let inputElement: HTMLInputElement | null = nativeElement.querySelector(
'.colorpicker-form-control input',
);

expect(inputElement?.getAttribute('aria-invalid')).toBeNull();
expect(inputElement?.getAttribute('aria-errormessage')).toBeNull();

openColorpicker(nativeElement, 'colorpicker-form-control');
setInputElementValue(nativeElement, 'red', '163');
setInputElementValue(nativeElement, 'green', '19');
setInputElementValue(nativeElement, 'blue', '84');
setInputElementValue(nativeElement, 'alpha', '0.5');
applyColorpicker();

fixture.detectChanges();

inputElement = nativeElement.querySelector(
'.colorpicker-form-control input',
);

expect(inputElement?.getAttribute('aria-invalid')).toBe('true');
expect(inputElement?.getAttribute('aria-errormessage')).toBeDefined();

const errorMessage = nativeElement.querySelector('sky-form-error');

expect(errorMessage).toBeVisible();
}));
});

describe('accessibility', () => {
Expand Down
Loading
Loading