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 Jun 24, 2019
1 parent e682ee3 commit 9765ded
Show file tree
Hide file tree
Showing 7 changed files with 126 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)); }
}
37 changes: 33 additions & 4 deletions src/timepicker/timepicker.ts
Expand Up @@ -37,10 +37,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 @@ -59,9 +61,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 @@ -80,9 +83,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 @@ -269,6 +273,31 @@ export class NgbTimepicker implements ControlValueAccessor,
}
}

_filter(e: KeyboardEvent) {
// tslint:disable-next-line
const keyCode = e.keyCode;
const ctrlKey = e.ctrlKey;
const metaKey = e.metaKey;
const shiftKey = e.shiftKey;
if ([46, 8, 9, 27, 13].indexOf(keyCode) !== -1 || // Allow: Delete, Backspace, Tab, Escape, Enter
(keyCode === 65 && ctrlKey === true) || // Allow: Ctrl+A
(keyCode === 67 && ctrlKey === true) || // Allow: Ctrl+C
(keyCode === 86 && ctrlKey === true) || // Allow: Ctrl+V
(keyCode === 88 && ctrlKey === true) || // Allow: Ctrl+X
(keyCode === 65 && metaKey === true) || // Cmd+A (Mac)
(keyCode === 67 && metaKey === true) || // Cmd+C (Mac)
(keyCode === 86 && metaKey === true) || // Cmd+V (Mac)
(keyCode === 88 && metaKey === true) || // Cmd+X (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 ((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 9765ded

Please sign in to comment.