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) Improved field validation UX #118

Merged
merged 2 commits into from
Jan 18, 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
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ testem.log
# System Files
.DS_Store
Thumbs.db
./package-lock.json
./package-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { SelectOption } from '../../form-entry/question-models/interfaces/select

import { DataSource } from '../../form-entry/question-models/interfaces/data-source';
import * as _ from 'lodash';
import { TranslateService } from '@ngx-translate/core';

@Component({
selector: 'ofe-remote-select',
templateUrl: 'remote-select.component.html',
Expand All @@ -40,11 +42,12 @@ export class RemoteSelectComponent implements OnInit, ControlValueAccessor {
value = [];
loading = false;
searchText = '';
notFoundMsg = 'match no found';
@Input() placeholder = 'Search...';
notFoundMsg = this.translate.instant('matchNotFound');
@Input() placeholder = this.translate.instant('search');
@Input() componentID: string;
@Input() disabled = false;
@Input() theme = 'dark';
@Input() invalid = 'false';
@Output() done: EventEmitter<any> = new EventEmitter<any>();

private _dataSource: DataSource;
Expand All @@ -59,7 +62,10 @@ export class RemoteSelectComponent implements OnInit, ControlValueAccessor {
}
}

constructor(private renderer: Renderer2) {}
constructor(
private renderer: Renderer2,
private translate: TranslateService
) {}

ngOnInit() {
this.loadOptions();
Expand All @@ -71,7 +77,7 @@ export class RemoteSelectComponent implements OnInit, ControlValueAccessor {
this.items = results;
this.notFoundMsg = '';
} else {
this.notFoundMsg = 'Not found';
this.notFoundMsg = 'Match not found';
this.items = [];
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
<div>
<ng-select
[ngClass]="{ 'afe-custom': theme === 'light' }"
[disabled]="disabled"
[items]="remoteOptions$ | async"
bindLabel="label"
bindValue="value"
[multiple]="false"
[hideSelected]="true"
[compareWith]="compareItems"
[trackByFn]="trackByFn"
[loading]="loading"
typeToSearchText="{{ 'enterMoreCharacters' | translate }}"
[typeahead]="remoteOptionInput$"
[(ngModel)]="selectedRemoteOptions"
[appendTo]="'form'"
(ngModelChange)="selected($event)"
>
</ng-select>
</div>
<ng-select
[ngClass]="{
'afe-custom': theme === 'light',
'invalid': invalid ? true : null,
denniskigen marked this conversation as resolved.
Show resolved Hide resolved
}"
[disabled]="disabled"
[items]="remoteOptions$ | async"
bindLabel="label"
bindValue="value"
[multiple]="false"
[hideSelected]="true"
[compareWith]="compareItems"
[trackByFn]="trackByFn"
[loading]="loading"
typeToSearchText="{{ 'enterMoreCharacters' | translate }}"
[typeahead]="remoteOptionInput$"
[(ngModel)]="selectedRemoteOptions"
[appendTo]="'form'"
(ngModelChange)="selected($event)"
>
</ng-select>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
'cds--number--helpertext': helperText,
'cds--skeleton': skeleton,
'cds--number--sm': size === 'sm',
'cds--number--xl': size === 'xl'
'cds--number--md': size === 'md',
'cds--number--lg': size === 'lg'
}"
>
<div
Expand All @@ -18,9 +19,9 @@
}"
>
<input
ofeNumberScroll
type="number"
[id]="id"
ofeNumberScroll
[value]="value"
[attr.min]="min"
[attr.max]="max"
Expand All @@ -29,8 +30,22 @@
[attr.step]="step"
[disabled]="disabled"
[required]="required"
[attr.data-invalid]="invalid ? invalid : null"
[placeholder]="placeholder"
(input)="onNumberInputChange($event)"
/>
<svg
denniskigen marked this conversation as resolved.
Show resolved Hide resolved
*ngIf="!skeleton && !warn && invalid"
cdsIcon="warning--filled"
size="16"
class="cds--number__invalid"
></svg>
<svg
*ngIf="!skeleton && !invalid && warn"
cdsIcon="warning--alt--filled"
size="16"
class="cds--number__invalid cds--number__invalid--warning"
></svg>
<div class="cds--number__controls">
<button
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ describe('NumberInputComponent', () => {
let fixture: ComponentFixture<NumberInputComponent>;
let debugEl: DebugElement;
let nativeEl: HTMLElement;
let numberField: HTMLElement;
let containerElement: HTMLElement;
let inputElement: HTMLInputElement;
let incrementButton: HTMLButtonElement;
let decrementButton: HTMLButtonElement;

beforeEach(() => {
TestBed.configureTestingModule({
Expand All @@ -18,151 +21,151 @@ describe('NumberInputComponent', () => {
component = fixture.componentInstance;
debugEl = fixture.debugElement;
nativeEl = debugEl.nativeElement;
numberField = nativeEl.querySelector('div.cds--number');

containerElement = nativeEl.querySelector('div.cds--number');
inputElement = containerElement.querySelector('input[type="number"]');
incrementButton = containerElement.querySelector('button.up-icon');
decrementButton = containerElement.querySelector('button.down-icon');
component.id = 'test-id';
component.theme = 'light';
component.disabled = false;
component.skeleton = false;
component.invalid = false;
component.size = 'md';
component.required = false;
component.value = null;

fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
expect(numberField).toBeTruthy();
expect(component instanceof NumberInputComponent).toBe(true);
expect(inputElement).toBeDefined();
});

it('should render a number input with the correct attributes', () => {
component.label = 'Pill count';
component.value = 10;
component.label = 'Pill count';
component.placeholder = 'Enter pill count';

const input = numberField.querySelector('input[type="number"]');
expect(input.getAttribute('id')).toBe('test-id');
expect(input.getAttribute('type')).toBe('number');
expect(input.getAttribute('min')).toBe(null);
expect(input.getAttribute('max')).toBe(null);
fixture.detectChanges();

expect(inputElement.getAttribute('id')).toBe('test-id');
expect(inputElement.getAttribute('type')).toBe('number');
expect(inputElement.getAttribute('min')).toBe(null);
expect(inputElement.getAttribute('max')).toBe(null);
expect(inputElement.getAttribute('placeholder')).toBe('Enter pill count');
});

it('should decrement and increment the value when the buttons are clicked', () => {
component.value = 10;
const buttons = numberField.querySelectorAll('button');
const decrementBtn = buttons[0];
const incrementBtn = buttons[1];

decrementBtn.click();
decrementButton.click();
fixture.detectChanges();

expect(component.value).toBe(9);

decrementBtn.click();
decrementButton.click();
fixture.detectChanges();

expect(component.value).toBe(8);

incrementBtn.click();
incrementButton.click();
fixture.detectChanges();

expect(component.value).toBe(9);

incrementBtn.click();
incrementButton.click();
fixture.detectChanges();

expect(component.value).toBe(10);
});

it('should not decrement the value below the min value', () => {
component.min = 10;
component.value = 10;
const buttons = numberField.querySelectorAll('button');
const decrementBtn = buttons[0];
component.min = 10;

decrementBtn.click();
decrementButton.click();
fixture.detectChanges();

expect(component.value).toBe(10);

decrementBtn.click();
decrementButton.click();
fixture.detectChanges();

expect(component.value).not.toBe(9);
expect(component.value).toBe(10);
});

it('should not increment the value above the max value', () => {
component.max = 10;
component.value = 10;
const buttons = numberField.querySelectorAll('button');
const incrementBtn = buttons[1];
component.max = 10;

incrementBtn.click();
incrementButton.click();
fixture.detectChanges();

expect(component.value).toBe(10);

incrementBtn.click();
incrementButton.click();
fixture.detectChanges();

expect(component.value).not.toBe(11);
expect(component.value).toBe(10);
});

it('should decrement or increment the value by the step count value', () => {
component.step = 5;
it('should decrement or increment the value by the provided step count', () => {
component.value = 10;
const buttons = numberField.querySelectorAll('button');
const decrementBtn = buttons[0];
const incrementBtn = buttons[1];
component.step = 5;

decrementBtn.click();
decrementButton.click();
fixture.detectChanges();

expect(component.value).toBe(5);

incrementBtn.click();
incrementButton.click();
fixture.detectChanges();

expect(component.value).toBe(10);
});

it('should render helperText below the input when provided', () => {
const helperText = 'Max of 50';
component.value = 49;
component.max = 50;
component.helperText = 'Max of 50';
component.helperText = helperText;

fixture.detectChanges();

const helperText = numberField.querySelector('div.cds--form__helper-text');
expect(helperText.textContent).toBe('Max of 50');
const helperTextElement = containerElement.querySelector(
'div.cds--form__helper-text'
);
expect(containerElement.className.includes('cds--number--helpertext'));
expect(helperTextElement.textContent).toBe(helperText);
});

it('should render the supplied warning text when the warn input is true', () => {
it('should render the supplied warning text when the warn property is truthy', () => {
component.value = 11;
component.max = 10;
component.warn = true;
component.warnText = 'Min value should be 10';

numberField.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
containerElement.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Tab' })
);
fixture.detectChanges();

const warnText = numberField.querySelector('div.cds--form-requirement');
const warnText = containerElement.querySelector(
'div.cds--form-requirement'
);
expect(warnText.textContent).toBe('Min value should be 10');
});

it('should render the supplied invalid text when the invalid input is true', () => {
it('should render the supplied invalid text when the invalid property is truthy', () => {
component.value = 11;
component.max = 10;
component.invalid = true;
component.invalidText = 'Min value should be 10';

numberField.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
containerElement.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Tab' })
);
fixture.detectChanges();

const invalidText = numberField.querySelector('div.cds--form-requirement');
const invalidText = containerElement.querySelector(
'div.cds--form-requirement'
);
expect(invalidText.textContent).toBe('Min value should be 10');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,14 @@ export class NumberInputComponent implements ControlValueAccessor {
* The unique id for the number component.
*/
@Input() id = `number-${NumberInputComponent.numberCount}`;
/**
* Sets the placeholder attribute on the `input` element.
*/
@Input() placeholder = '';
/**
* Number input field render size
*/
@Input() size: 'sm' | 'md' | 'xl' = 'md';
@Input() size: 'sm' | 'md' | 'lg' = 'md';
/**
* Reflects the required attribute of the `input` element.
*/
Expand Down Expand Up @@ -133,11 +137,11 @@ export class NumberInputComponent implements ControlValueAccessor {
/**
* Sets the decrement label text
*/
@Input() decrementLabel = 'Decrease';
@Input() decrementLabel = 'Decrement';
/**
* Sets the increment label text
*/
@Input() incrementLabel = 'Increase';
@Input() incrementLabel = 'Increment';

protected _value = 0;

Expand Down
Loading