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

atinc / ngx-tethys / 0da75401-415d-4187-8c75-efbd749fd7ee

16 Apr 2025 09:09AM UTC coverage: 90.197% (+0.02%) from 90.176%
0da75401-415d-4187-8c75-efbd749fd7ee

push

circleci

xinglu01
feat(date-picker): add timezone support to date picker components and utilities #TINFR-1734 (#3335)

5611 of 6886 branches covered (81.48%)

Branch coverage included in aggregate %.

50 of 53 new or added lines in 11 files covered. (94.34%)

46 existing lines in 9 files now uncovered.

13379 of 14168 relevant lines covered (94.43%)

993.69 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
    standalone: true,
53
    imports: []
54
})
55
export class ThyInnerTimePicker implements ControlValueAccessor, TimePickerComponentState, TimePickerControls, OnChanges, OnDestroy {
22✔
56
    private _cd = inject(ChangeDetectorRef);
22✔
57
    private _store = inject(ThyTimePickerStore);
22✔
58

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

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

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

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

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

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

119
    canDecrementHours: boolean;
120
    canDecrementMinutes: boolean;
5✔
121
    canDecrementSeconds: boolean;
122

123
    canToggleMeridian: boolean;
124

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

208
            return;
209
        }
210

211
        this._updateTime();
212
    }
213

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

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

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

225
            return;
226
        }
227

228
        this._updateTime();
229
    }
230

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

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

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

242
            return;
243
        }
244

245
        this._updateTime();
246
    }
247

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

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

269
            return;
270
        }
271

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

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

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

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

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

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

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

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

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

327
            return;
328
        }
329

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

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

343
        this.hours = padNumber(_hours);
344
        this.minutes = padNumber(_value.getMinutes());
345
        this.seconds = padNumber(_value.getUTCSeconds());
346
    }
347
}
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