Skip to content

Commit

Permalink
feat(timepicker): input filter to accept only numbers
Browse files Browse the repository at this point in the history
  • Loading branch information
fbasso committed Sep 2, 2019
1 parent e26edc1 commit 2581725
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 4 deletions.
2 changes: 2 additions & 0 deletions e2e-app/src/app/app.module.ts
Expand Up @@ -20,6 +20,7 @@ import {TooltipFocusComponent} from './tooltip/focus/tooltip-focus.component';
import {TooltipPositionComponent} from './tooltip/position/tooltip-position.component';
import {TypeaheadAutoCloseComponent} from './typeahead/autoclose/typeahead-autoclose.component';
import {TypeaheadFocusComponent} from './typeahead/focus/typeahead-focus.component';
import {TimepickerFilterComponent} from './timepicker/filter/timepicker-filter.component';
import {TimepickerNavigationComponent} from './timepicker/navigation/timepicker-navigation.component';
import {TypeaheadValidationComponent} from './typeahead/validation/typeahead-validation.component';

Expand All @@ -41,6 +42,7 @@ import {TypeaheadValidationComponent} from './typeahead/validation/typeahead-val
TypeaheadFocusComponent,
TypeaheadValidationComponent,
TypeaheadAutoCloseComponent,
TimepickerFilterComponent,
TimepickerNavigationComponent,
],
imports: [BrowserModule, FormsModule, ReactiveFormsModule, routing, NgbModule],
Expand Down
2 changes: 2 additions & 0 deletions e2e-app/src/app/app.routing.ts
Expand Up @@ -12,6 +12,7 @@ import {TooltipFocusComponent} from './tooltip/focus/tooltip-focus.component';
import {TooltipPositionComponent} from './tooltip/position/tooltip-position.component';
import {TypeaheadAutoCloseComponent} from './typeahead/autoclose/typeahead-autoclose.component';
import {TypeaheadFocusComponent} from './typeahead/focus/typeahead-focus.component';
import {TimepickerFilterComponent} from './timepicker/filter/timepicker-filter.component';
import {TimepickerNavigationComponent} from './timepicker/navigation/timepicker-navigation.component';
import {TypeaheadValidationComponent} from './typeahead/validation/typeahead-validation.component';
import {DropdownPositionComponent} from './dropdown/position/dropdown-position.component';
Expand Down Expand Up @@ -52,6 +53,7 @@ export const routes: Routes = [
path: 'timepicker',
children: [
{path: 'navigation', component: TimepickerNavigationComponent},
{path: 'filter', component: TimepickerFilterComponent},
]
},
];
Expand Down
@@ -0,0 +1,4 @@
<ngb-timepicker
[seconds]="true"
[(ngModel)]="time"
></ngb-timepicker>
@@ -0,0 +1,8 @@
import {Component} from '@angular/core';

@Component({
templateUrl: './timepicker-filter.component.html',
})
export class TimepickerFilterComponent {
time = {hour: null, minute: null, second: null};
}
69 changes: 69 additions & 0 deletions e2e-app/src/app/timepicker/filter/timepicker-filter.e2e-spec.ts
@@ -0,0 +1,69 @@
import {protractor} from 'protractor';

import {openUrl} from '../../tools.po';

import {TimepickerFilterPage} from './timepicker-filter.po';

describe('Timepicker', () => {
let page: TimepickerFilterPage;

beforeAll(() => page = new TimepickerFilterPage());
beforeEach(async() => await openUrl('timepicker/filter'));

async function expectValue(expectedValue) {
const inputs = page.getFields();

const values = [];
for (let i = 0; i < inputs.length; i++) {
values[i] = await inputs[i].getAttribute('value');
}
expect(values.join(':')).toBe(expectedValue);
}

describe('filter', async() => {
it(`should accept numbers`, async() => {
await expectValue('::'); // No starting values

const inputs = page.getFields();
await inputs[0].sendKeys('1');
await inputs[1].sendKeys('2');
await inputs[2].sendKeys('3');

await inputs[0].click();
await expectValue('01:02:03');
});

it(`shouldn't accept alpha`, async() => {
await expectValue('::'); // No starting values

const inputs = page.getFields();
await inputs[0].sendKeys('A');
await inputs[1].sendKeys('A');
await inputs[2].sendKeys('A');

await inputs[0].click();
await expectValue('::');
});

it(`shouldn accept special commands`, async() => {

const inputs = page.getFields();

await inputs[0].sendKeys('1');
await inputs[0].sendKeys(protractor.Key.chord(protractor.Key.CONTROL, 'a'));
await inputs[0].sendKeys(protractor.Key.chord(protractor.Key.CONTROL, 'c'));

await inputs[1].click();
await inputs[1].sendKeys(protractor.Key.chord(protractor.Key.CONTROL, 'v'));

await inputs[2].click();
await inputs[2].sendKeys(protractor.Key.chord(protractor.Key.CONTROL, 'v'));

await inputs[0].click();
await expectValue('01:01:01');

});

});

});
8 changes: 8 additions & 0 deletions e2e-app/src/app/timepicker/filter/timepicker-filter.po.ts
@@ -0,0 +1,8 @@
import {$} from 'protractor';

export type Field = 'hour' | 'minute' | 'second';

export class TimepickerFilterPage {
getField(field: Field) { return $(`.ngb-tp-${field} > input`); }
getFields() { return ['hour', 'minute', 'second'].map((field: Field) => this.getField(field)); }
}
28 changes: 24 additions & 4 deletions src/timepicker/timepicker.ts
Expand Up @@ -38,10 +38,12 @@ const NGB_TIMEPICKER_VALUE_ACCESSOR = {
<span class="chevron ngb-tp-chevron"></span>
<span class="sr-only" i18n="@@ngb.timepicker.increment-hours">Increment hours</span>
</button>
<input type="text" class="ngb-tp-input form-control" [class.form-control-sm]="isSmallSize" [class.form-control-lg]="isLargeSize"
maxlength="2" placeholder="HH" i18n-placeholder="@@ngb.timepicker.HH"
<input type="text" class="ngb-tp-input form-control" [class.form-control-sm]="isSmallSize"
[class.form-control-lg]="isLargeSize"
maxlength="2" inputmode="numeric" placeholder="HH" i18n-placeholder="@@ngb.timepicker.HH"
[value]="formatHour(model?.hour)" (change)="updateHour($event.target.value)"
[readOnly]="readonlyInputs" [disabled]="disabled" aria-label="Hours" i18n-aria-label="@@ngb.timepicker.hours"
(keydown)="_filter($event)"
(keydown.ArrowUp)="changeHour(hourStep); $event.preventDefault()"
(keydown.ArrowDown)="changeHour(-hourStep); $event.preventDefault()">
<button *ngIf="spinners" tabindex="-1" type="button" (click)="changeHour(-hourStep)"
Expand All @@ -60,9 +62,10 @@ const NGB_TIMEPICKER_VALUE_ACCESSOR = {
<span class="sr-only" i18n="@@ngb.timepicker.increment-minutes">Increment minutes</span>
</button>
<input type="text" class="ngb-tp-input form-control" [class.form-control-sm]="isSmallSize" [class.form-control-lg]="isLargeSize"
maxlength="2" placeholder="MM" i18n-placeholder="@@ngb.timepicker.MM"
maxlength="2" inputmode="numeric" placeholder="MM" i18n-placeholder="@@ngb.timepicker.MM"
[value]="formatMinSec(model?.minute)" (change)="updateMinute($event.target.value)"
[readOnly]="readonlyInputs" [disabled]="disabled" aria-label="Minutes" i18n-aria-label="@@ngb.timepicker.minutes"
(keydown)="_filter($event)"
(keydown.ArrowUp)="changeMinute(minuteStep); $event.preventDefault()"
(keydown.ArrowDown)="changeMinute(-minuteStep); $event.preventDefault()">
<button *ngIf="spinners" tabindex="-1" type="button" (click)="changeMinute(-minuteStep)"
Expand All @@ -81,9 +84,10 @@ const NGB_TIMEPICKER_VALUE_ACCESSOR = {
<span class="sr-only" i18n="@@ngb.timepicker.increment-seconds">Increment seconds</span>
</button>
<input type="text" class="ngb-tp-input form-control" [class.form-control-sm]="isSmallSize" [class.form-control-lg]="isLargeSize"
maxlength="2" placeholder="SS" i18n-placeholder="@@ngb.timepicker.SS"
maxlength="2" inputmode="numeric" placeholder="SS" i18n-placeholder="@@ngb.timepicker.SS"
[value]="formatMinSec(model?.second)" (change)="updateSecond($event.target.value)"
[readOnly]="readonlyInputs" [disabled]="disabled" aria-label="Seconds" i18n-aria-label="@@ngb.timepicker.seconds"
(keydown)="_filter($event)"
(keydown.ArrowUp)="changeSecond(secondStep); $event.preventDefault()"
(keydown.ArrowDown)="changeSecond(-secondStep); $event.preventDefault()">
<button *ngIf="spinners" tabindex="-1" type="button" (click)="changeSecond(-secondStep)"
Expand Down Expand Up @@ -270,6 +274,22 @@ export class NgbTimepicker implements ControlValueAccessor,
}
}

_filter(e: KeyboardEvent) {
// tslint:disable-next-line
const keyCode = e.keyCode;
if ([46, 8, 9, 27, 13].indexOf(keyCode) > -1 || // Allow: Delete, Backspace, Tab, Escape, Enter
((e.ctrlKey || e.metaKey) &&
[65, 67, 86, 88].indexOf(keyCode) > -1) || // Allow Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X (Window or Mac)
(keyCode >= 35 && keyCode <= 39) // Home, End, Left, Right
) {
return; // let it happen, don't do anything
}
// Ensure that it is a number and stop the keypress
if ((e.shiftKey || keyCode < 48 || keyCode > 57) && (keyCode < 96 || keyCode > 105)) {
e.preventDefault();
}
}

private propagateModelChange(touched = true) {
if (touched) {
this.onTouched();
Expand Down

0 comments on commit 2581725

Please sign in to comment.