• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atinc / ngx-tethys / df6b162a-6409-4216-a048-95638a1cf8c3

11 Apr 2025 10:17AM UTC coverage: 90.237% (+0.001%) from 90.236%
df6b162a-6409-4216-a048-95638a1cf8c3

Pull #3335

circleci

wangyuan-ky
feat(time-picker): enhance time manipulation functions with timezone support
Pull Request #3335: feat(date-picker): add timezone support to date picker components and utilities #TINFR-1734

5610 of 6877 branches covered (81.58%)

Branch coverage included in aggregate %.

45 of 49 new or added lines in 12 files covered. (91.84%)

46 existing lines in 8 files now uncovered.

13365 of 14151 relevant lines covered (94.45%)

992.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.3
/src/time-picker/inner/inner-time-picker.component.ts
1
import {
2
    ChangeDetectionStrategy,
3
    ChangeDetectorRef,
4
    Component,
5
    EventEmitter,
6
    forwardRef,
7
    inject,
8
    Input,
9
    OnChanges,
10
    OnDestroy,
1✔
11
    Output,
12
    SimpleChanges,
33✔
13
    StaticProvider
14
} from '@angular/core';
15

16
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
17

18
import { getControlsValue } from '../time-picker-controls.util';
1✔
19
import { TimePickerConfig } from './inner-time-picker.config';
20

557✔
21
import { TimeChangeSource, TimePickerComponentState, TimePickerControls } from './inner-time-picker.class';
22

23
import {
33✔
24
    isHourInputValid,
33✔
25
    isInputLimitValid,
33✔
26
    isInputValid,
27
    isMinuteInputValid,
33✔
28
    isSecondInputValid,
33✔
29
    isValidDate,
33✔
30
    padNumber,
31
    parseTime
33✔
32
} from '../time-picker.utils';
33✔
33

33✔
34
import { Subscription } from 'rxjs';
33✔
35

33✔
36
import { ThyTimePickerStore } from './inner-time-picker.store';
33✔
37

33✔
38
export const TIMEPICKER_CONTROL_VALUE_ACCESSOR: StaticProvider = {
33✔
39
    provide: NG_VALUE_ACCESSOR,
210✔
40
    useExisting: forwardRef(() => ThyInnerTimePicker),
41
    multi: true
42
};
81✔
43

81✔
44
/**
81✔
45
 * @internal
46
 */
33✔
47
@Component({
177✔
48
    selector: 'thy-inner-time-picker',
49
    changeDetection: ChangeDetectionStrategy.OnPush,
177✔
50
    providers: [TIMEPICKER_CONTROL_VALUE_ACCESSOR, ThyTimePickerStore],
177✔
51
    templateUrl: './inner-time-picker.component.html',
177✔
52
    imports: []
53
})
54
export class ThyInnerTimePicker implements ControlValueAccessor, TimePickerComponentState, TimePickerControls, OnChanges, OnDestroy {
55
    private _cd = inject(ChangeDetectorRef);
22✔
56
    private _store = inject(ThyTimePickerStore);
22✔
57

22✔
58
    /** hours change step */
59
    @Input() hourStep: number;
60
    /** hours change step */
193✔
61
    @Input() minuteStep: number;
62
    /** seconds change step */
63
    @Input() secondsStep: number;
×
64
    /** if true hours and minutes fields will be readonly */
65
    @Input() readonlyInput: boolean;
66
    /** if true hours and minutes fields will be disabled */
×
67
    @Input() disabled: boolean;
68
    /** if true scroll inside hours and minutes inputs will change time */
69
    @Input() mousewheel: boolean;
48✔
70
    /** if true the values of hours and minutes can be changed using the up/down arrow keys on the keyboard */
71
    @Input() arrowKeys: boolean;
6✔
72
    /** if true spinner arrows above and below the inputs will be shown */
6✔
73
    @Input() showSpinners: boolean;
6✔
74
    /** if true meridian button will be shown */
75
    @Input() showMeridian: boolean;
4✔
76
    /** show minutes in timePicker */
4✔
77
    @Input() showMinutes: boolean;
4✔
78
    /** show seconds in timePicker */
79
    @Input() showSeconds: boolean;
4✔
80
    /** meridian labels based on locale */
4✔
81
    @Input() meridians: string[];
4✔
82
    /** minimum time user can select */
83
    @Input() min: Date;
84
    /** maximum time user can select */
4✔
85
    @Input() max: Date;
4✔
86
    /** placeholder for hours field in timePicker */
4✔
87
    @Input() hoursPlaceholder: string;
4✔
88
    /** placeholder for minutes field in timePicker */
2✔
89
    @Input() minutesPlaceholder: string;
2✔
90
    /** placeholder for seconds field in timePicker */
2✔
91
    @Input() secondsPlaceholder: string;
2✔
92
    /** timezone */
93
    @Input() timeZone: string;
2✔
94

95
    /** emits true if value is a valid date */
96
    @Output() isValid = new EventEmitter<boolean>();
2✔
97

2✔
98
    // ui variables
2✔
99
    hours: string;
2✔
100
    minutes: string;
1✔
101
    seconds: string;
1✔
102
    meridian: string;
1✔
103

1✔
104
    get isEditable(): boolean {
105
        return !(this.readonlyInput || this.disabled);
1✔
106
    }
107

108
    // min/max validation for input fields
2✔
109
    invalidHours = false;
2✔
110
    invalidMinutes = false;
2✔
111
    invalidSeconds = false;
2✔
112

1✔
113
    // time picker controls state
1✔
114
    canIncrementHours: boolean;
1✔
115
    canIncrementMinutes: boolean;
1✔
116
    canIncrementSeconds: boolean;
117

1✔
118
    canDecrementHours: boolean;
119
    canDecrementMinutes: boolean;
120
    canDecrementSeconds: boolean;
5✔
121

122
    canToggleMeridian: boolean;
123

124
    // control value accessor methods
125
    onChange = Function.prototype;
126
    onTouched = Function.prototype;
127

128
    private timerPickerSubscription = new Subscription();
4!
129

4!
130
    constructor() {
4✔
131
        const _config = inject(TimePickerConfig);
1✔
132
        const _cd = this._cd;
1✔
133
        const _store = this._store;
1✔
134

135
        Object.assign(this, _config);
3✔
136

137
        this.timerPickerSubscription.add(
138
            _store
139
                .select(state => state.value)
140
                .subscribe((value: Date) => {
141
                    // update UI values if date changed
142
                    this._renderTime(value);
143
                    this.onChange(value);
1!
NEW
144
                    this._store.updateControls(getControlsValue(this), this.timeZone);
×
145
                })
146
        );
1✔
147

1✔
148
        this.timerPickerSubscription.add(
149
            _store
150
                .select(state => state.controls)
151
                .subscribe((controlsState: TimePickerControls) => {
152
                    this.isValid.emit(isInputValid(this.hours, this.minutes, this.seconds, this.isPM()));
153
                    Object.assign(this, controlsState);
91✔
154
                    _cd.markForCheck();
41✔
155
                })
156
        );
50!
157
    }
50✔
158

159
    resetValidation(): void {
160
        this.invalidHours = false;
161
        this.invalidMinutes = false;
33✔
162
        this.invalidSeconds = false;
163
    }
164

33✔
165
    isPM(): boolean {
166
        return this.showMeridian && this.meridian === this.meridians[1];
167
    }
34✔
168

34✔
169
    prevDef($event: Event) {
170
        $event.preventDefault();
171
    }
33✔
172

173
    wheelSign($event: WheelEventInit): number {
174
        return Math.sign($event.deltaY) * -1;
81✔
175
    }
33✔
176

33✔
177
    ngOnChanges(changes: SimpleChanges): void {
33✔
178
        this._store.updateControls(getControlsValue(this), this.timeZone);
33✔
179
    }
33✔
180

181
    changeHours(step: number, source: TimeChangeSource = ''): void {
48✔
182
        this.resetValidation();
48✔
183
        this._store.changeHours({ step, source }, this.timeZone);
48✔
184
    }
48✔
185

27✔
186
    changeMinutes(step: number, source: TimeChangeSource = ''): void {
27✔
187
        this.resetValidation();
188
        this._store.changeMinutes({ step, source }, this.timeZone);
27✔
189
    }
6✔
190

191
    changeSeconds(step: number, source: TimeChangeSource = ''): void {
192
        this.resetValidation();
48✔
193
        this._store.changeSeconds({ step, source }, this.timeZone);
48✔
194
    }
48✔
195

196
    updateHours(hours: string): void {
1✔
197
        this.resetValidation();
1✔
198
        this.hours = hours;
199

200
        const isValid = isHourInputValid(this.hours, this.isPM()) && this.isValidLimit();
201

202
        if (!isValid) {
203
            this.invalidHours = true;
204
            this.isValid.emit(false);
205
            this.onChange(null);
206

207
            return;
208
        }
209

210
        this._updateTime();
211
    }
212

213
    updateMinutes(minutes: string) {
214
        this.resetValidation();
215
        this.minutes = minutes;
216

217
        const isValid = isMinuteInputValid(this.minutes) && this.isValidLimit();
218

219
        if (!isValid) {
1✔
220
            this.invalidMinutes = true;
221
            this.isValid.emit(false);
222
            this.onChange(null);
223

224
            return;
225
        }
226

227
        this._updateTime();
228
    }
229

230
    updateSeconds(seconds: string) {
231
        this.resetValidation();
232
        this.seconds = seconds;
233

234
        const isValid = isSecondInputValid(this.seconds) && this.isValidLimit();
235

236
        if (!isValid) {
237
            this.invalidSeconds = true;
238
            this.isValid.emit(false);
239
            this.onChange(null);
240

241
            return;
242
        }
243

244
        this._updateTime();
245
    }
246

247
    isValidLimit(): boolean {
248
        return isInputLimitValid(
249
            {
250
                hour: this.hours,
251
                minute: this.minutes,
252
                seconds: this.seconds,
253
                isPM: this.isPM()
254
            },
255
            this.max,
256
            this.min,
257
            this.timeZone
258
        );
259
    }
260

261
    _updateTime() {
262
        const _seconds = this.showSeconds ? this.seconds : void 0;
263
        const _minutes = this.showMinutes ? this.minutes : void 0;
264
        if (!isInputValid(this.hours, _minutes, _seconds, this.isPM())) {
265
            this.isValid.emit(false);
266
            this.onChange(null);
267

268
            return;
269
        }
270

271
        this._store.setTime(
272
            {
273
                hour: this.hours,
274
                minute: this.minutes,
275
                seconds: this.seconds,
276
                isPM: this.isPM()
277
            },
278
            this.timeZone
279
        );
280
    }
281

282
    toggleMeridian(): void {
283
        if (!this.showMeridian || !this.isEditable) {
284
            return;
285
        }
286

287
        const _hoursPerDayHalf = 12;
288
        this._store.changeHours({
289
            step: _hoursPerDayHalf,
290
            source: ''
291
        });
292
    }
293

294
    writeValue(obj: string | null | undefined | Date): void {
295
        if (isValidDate(obj)) {
296
            this._store.writeValue(parseTime(obj, this.timeZone));
297
        } else if (obj == null) {
298
            this._store.writeValue(null);
299
        }
300
    }
301

302
    registerOnChange(fn: (_: any) => {}): void {
303
        this.onChange = fn;
304
    }
305

306
    registerOnTouched(fn: () => {}): void {
307
        this.onTouched = fn;
308
    }
309

310
    setDisabledState(isDisabled: boolean): void {
311
        this.disabled = isDisabled;
312
        this._cd.markForCheck();
313
    }
314

315
    ngOnDestroy(): void {
316
        this.timerPickerSubscription.unsubscribe();
317
    }
318

319
    private _renderTime(value: string | Date): void {
320
        if (!isValidDate(value)) {
321
            this.hours = '';
322
            this.minutes = '';
323
            this.seconds = '';
324
            this.meridian = this.meridians[0];
325

326
            return;
327
        }
328

329
        const _value = parseTime(value, this.timeZone);
330
        const _hoursPerDayHalf = 12;
331
        let _hours = _value.getHours();
332

333
        if (this.showMeridian) {
334
            this.meridian = this.meridians[_hours >= _hoursPerDayHalf ? 1 : 0];
335
            _hours = _hours % _hoursPerDayHalf;
336
            // should be 12 PM, not 00 PM
337
            if (_hours === 0) {
338
                _hours = _hoursPerDayHalf;
339
            }
340
        }
341

342
        this.hours = padNumber(_hours);
343
        this.minutes = padNumber(_value.getMinutes());
344
        this.seconds = padNumber(_value.getUTCSeconds());
345
    }
346
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc