Skip to content

Commit

Permalink
fix(components/lookup): add required label asterisk for template driv…
Browse files Browse the repository at this point in the history
…en forms (#2694)
  • Loading branch information
Blackbaud-ErikaMcVey committed Sep 9, 2024
1 parent e1d391b commit 2f25548
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
[disabled]="disabled"
>
<sky-lookup
required
formControlName="friends2"
idProperty="id"
[data]="people"
Expand Down Expand Up @@ -105,8 +106,11 @@

<div class="app-screenshot" id="lookup-single-visual">
<form novalidate [formGroup]="bestFriendsForm">
<sky-input-box class="sky-margin-stacked-lg" [disabled]="disabled">
<label class="sky-control-label"> Who is your best friend? </label>
<sky-input-box
class="sky-margin-stacked-lg"
[disabled]="disabled"
labelText="Who is your best friend?"
>
<sky-lookup
formControlName="bestFriend"
idProperty="id"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validators,
} from '@angular/forms';
import {
SkyAutocompleteSearchAsyncArgs,
Expand Down Expand Up @@ -177,7 +178,7 @@ export class LookupComponent implements OnInit {
});

this.bestFriendsForm = this.formBuilder.group({
bestFriend: new UntypedFormControl(this.bestFriend),
bestFriend: new UntypedFormControl(this.bestFriend, Validators.required),
bestFriendAsync: undefined,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,22 @@
</sky-input-box>
</div>

<div class="easy-input-host-service">
<sky-input-box
labelText="Input using host service"
[hintText]="easyModeHintText"
>
<sky-input-box-host-service-fixture>
<input
#hostServiceInput="skyId"
class="sky-form-control"
skyId
type="text"
/>
</sky-input-box-host-service-fixture>
</sky-input-box>
</div>

<div class="input-box-has-errors">
<sky-input-box class="sky-margin-stacked-lg" [hasErrors]="hasErrors">
<label class="sky-control-label" [for]="hasErrorsInput.id">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';

import { Observable } from 'rxjs';
import { BehaviorSubject, Observable } from 'rxjs';

import { SkyInputBoxPopulateArgs } from './input-box-populate-args';
import { SkyInputBoxComponent } from './input-box.component';
Expand All @@ -9,8 +9,11 @@ import { SkyInputBoxComponent } from './input-box.component';
* @internal
*/
@Injectable()
export class SkyInputBoxHostService {
export class SkyInputBoxHostService implements OnDestroy {
#host: SkyInputBoxComponent | undefined;
#requiredSubject = new BehaviorSubject<boolean>(false);

public required = this.#requiredSubject.asObservable();

public get controlId(): string {
return this.#host?.controlId ?? '';
Expand All @@ -35,6 +38,10 @@ export class SkyInputBoxHostService {
this.#ariaDescribedBy = host.ariaDescribedBy.asObservable();
}

public ngOnDestroy(): void {
this.#requiredSubject.complete();
}

public populate(args: SkyInputBoxPopulateArgs): void {
if (!this.#host) {
throw new Error(
Expand Down Expand Up @@ -74,4 +81,13 @@ export class SkyInputBoxHostService {

this.#host.setHintTextScreenReaderOnly(hide);
}

/**
* Set required so that input box displays the label correctly. When the input is supplied by the consumer it is a content
* child that input box can read required from and this is unnecessary. When the input is supplied internally by the
* component the input box does not have a ref to it, so the component needs to inform the input box of its required state.
*/
public setRequired(required: boolean): void {
this.#requiredSubject.next(required);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,28 @@ describe('Input box component', () => {
expect(els.characterCountEl).not.toExist();
});

it('should set required if set by the child via host service', () => {
const fixture = TestBed.createComponent(InputBoxFixtureComponent);
const hostServiceInputBox = fixture.debugElement
.query(By.css('.easy-input-host-service sky-input-box'))
.injector.get(SkyInputBoxHostService);

fixture.detectChanges();

let requiredLabel = fixture.nativeElement.querySelector(
'.easy-input-host-service .sky-control-label-required',
);
expect(requiredLabel).not.toExist();

hostServiceInputBox.setRequired(true);
fixture.detectChanges();

requiredLabel = fixture.nativeElement.querySelector(
'.easy-input-host-service .sky-control-label-required',
);
expect(requiredLabel).toExist();
});

it('should add hint text', () => {
const fixture = TestBed.createComponent(InputBoxFixtureComponent);
fixture.detectChanges();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
} from '@angular/forms';
import { SkyContentInfoProvider, SkyIdService } from '@skyux/core';

import { ReplaySubject } from 'rxjs';
import { ReplaySubject, Subject, takeUntil } from 'rxjs';

import { SKY_FORM_ERRORS_ENABLED } from '../form-error/form-errors-enabled-token';

Expand Down Expand Up @@ -196,6 +196,8 @@ export class SkyInputBoxComponent
public readonly hintTextId = this.#idSvc.generateId();
public readonly ariaDescribedBy = new ReplaySubject<string | undefined>(1);

#requiredByFormField: boolean | undefined;

@HostBinding('class')
public cssClass = '';

Expand Down Expand Up @@ -233,7 +235,9 @@ export class SkyInputBoxComponent

protected get required(): boolean {
return (
this.#hasRequiredValidator() || this.inputRef?.nativeElement.required
this.#hasRequiredValidator() ||
this.inputRef?.nativeElement.required ||
this.#requiredByFormField
);
}

Expand All @@ -245,9 +249,17 @@ export class SkyInputBoxComponent

#previousInputRef: ElementRef | undefined;
#previousMaxLengthValidator: ValidatorFn | undefined;
#ngUnsubscribe = new Subject<void>();

public ngOnInit(): void {
this.#inputBoxHostSvc.init(this);

this.#inputBoxHostSvc.required
.pipe(takeUntil(this.#ngUnsubscribe))
.subscribe((required) => {
this.#requiredByFormField = required;
this.#changeRef.markForCheck();
});
}

public ngAfterContentChecked(): void {
Expand All @@ -263,6 +275,8 @@ export class SkyInputBoxComponent

public ngOnDestroy(): void {
this.ariaDescribedBy.complete();
this.#ngUnsubscribe.next();
this.#ngUnsubscribe.complete();
}

public formControlFocusIn(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
[autocompleteAttribute]="autocompleteAttribute"
[data]="data"
[enableShowMore]="enableShowMore"
[required]="required"
[selectMode]="selectMode"
/>
</sky-input-box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export class SkyLookupInputBoxTestComponent {

public form: UntypedFormGroup;

public required = false;

public selectMode: SkyLookupSelectModeType | undefined;

constructor(formBuilder: UntypedFormBuilder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7481,6 +7481,23 @@ describe('Lookup component', function () {
expect(lookupComponent.isInputFocused).toEqual(false);
});

it('should add or remove the required label class if `required` is set on lookup element', async function () {
component.required = true;
fixture.detectChanges();

let requiredLabel = fixture.nativeElement.querySelector(
'.sky-control-label-required',
);
expect(requiredLabel).toExist();

component.required = false;
fixture.detectChanges();
requiredLabel = fixture.nativeElement.querySelector(
'.sky-control-label-required',
);
expect(requiredLabel).not.toExist();
});

describe('aria-describedby attribute', () => {
it('should be set when hint text is specified and select mode is single', () => {
validateDescribedBy('single');
Expand Down
18 changes: 18 additions & 0 deletions libs/components/lookup/src/lib/modules/lookup/lookup.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
TemplateRef,
ViewChild,
ViewEncapsulation,
booleanAttribute,
inject,
} from '@angular/core';
import { ControlValueAccessor, NgControl } from '@angular/forms';
Expand Down Expand Up @@ -124,6 +125,20 @@ export class SkyLookupComponent
return this.#_disabled;
}

/**
* Whether the lookup field is required.
* @default false
*/
@Input({ transform: booleanAttribute })
public set required(value: boolean) {
this.#_required = value;
this.inputBoxHostSvc?.setRequired(value);
}

public get required(): boolean {
return this.#_required;
}

/**
* Whether to enable users to open a picker where they can view all options.
* @default false
Expand Down Expand Up @@ -306,6 +321,7 @@ export class SkyLookupComponent
#ngUnsubscribe = new Subject<void>();
#openNativePicker: SkyModalInstance | undefined;
#openSelectionModal: SkySelectionModalInstance | undefined;
#_required = false;

#_autocompleteInputDirective: SkyAutocompleteInputDirective | undefined;
#_data: any[] | undefined;
Expand Down Expand Up @@ -352,6 +368,8 @@ export class SkyLookupComponent
? undefined
: this.searchIconTemplateRef,
});

this.inputBoxHostSvc?.setRequired(this.required);
} else {
this.controlId = this.#idService.generateId();
}
Expand Down

0 comments on commit 2f25548

Please sign in to comment.