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

atinc / ngx-tethys / 3b4d9699-9d9c-4cc2-ad10-45e76ef5954d

14 Apr 2025 10:17AM UTC coverage: 90.254% (+0.02%) from 90.233%
3b4d9699-9d9c-4cc2-ad10-45e76ef5954d

push

circleci

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

5618 of 6886 branches covered (81.59%)

Branch coverage included in aggregate %.

48 of 52 new or added lines in 12 files covered. (92.31%)

41 existing lines in 8 files now uncovered.

13384 of 14168 relevant lines covered (94.47%)

994.49 hits per line

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

90.3
/src/date-picker/base-picker.component.ts
1
import { ThyClickDispatcher, ThyPlacement } from 'ngx-tethys/core';
2
import { elementMatchClosest, FunctionProp, TinyDate } from 'ngx-tethys/util';
3

4
import {
5
    Component,
6
    ElementRef,
7
    EventEmitter,
8
    inject,
9
    Input,
10
    NgZone,
11
    OnChanges,
12
    OnInit,
13
    Output,
14
    PLATFORM_ID,
1✔
15
    TemplateRef,
16
    ViewChild
156✔
17
} from '@angular/core';
156✔
18

156✔
19
import { coerceBooleanProperty } from '@angular/cdk/coercion';
156✔
20
import { isPlatformBrowser } from '@angular/common';
156✔
21
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
156✔
22
import { AbstractPickerComponent } from './abstract-picker.component';
156✔
23
import { CompatibleValue, RangeAdvancedValue } from './inner-types';
156✔
24
import { ThyPicker } from './picker.component';
156✔
25
import { hasTimeInStringDate, isValidStringDate, parseStringDate, transformDateValue } from './picker.util';
156✔
26
import { CompatibleDate, ThyPanelMode } from './standard-types';
156✔
27

156✔
28
/**
156✔
29
 * @private
30
 */
31
@Component({
158✔
32
    template: ``,
158✔
33
    standalone: true,
8✔
34
    host: {
35
        '[attr.tabindex]': `tabIndex`,
36
        '(focus)': 'onFocus($event)',
37
        '(blur)': 'onBlur($event)'
865✔
38
    }
39
})
40
export class BasePicker extends AbstractPickerComponent implements OnInit, OnChanges {
570✔
41
    protected element = inject(ElementRef);
42

43
    showWeek = false;
68!
44

45
    panelMode: ThyPanelMode | ThyPanelMode[];
46

156✔
47
    initialized: boolean;
156✔
48

156✔
49
    private innerPreviousDate: string;
156!
50

156✔
51
    @ViewChild('thyPicker', { static: true }) thyPicker: ThyPicker;
52

53
    @Input() thyDateRender: FunctionProp<TemplateRef<Date> | string>;
54

226✔
55
    @Input() set thyMode(value: ThyPanelMode) {
56
        this._panelMode = value ?? 'date';
57
        if (this.initialized) {
1✔
58
            this.setDefaultTimePickerState(this._panelMode);
1✔
59
        }
1✔
60
    }
61

62
    get thyMode() {
63
        return this._panelMode;
64
    }
65

66
    /**
49✔
67
     * 是否有幕布
49✔
68
     * @default true
49✔
69
     */
49✔
70
    @Input({ transform: coerceBooleanProperty }) thyHasBackdrop = true;
41✔
71

72
    /**
49✔
73
     * @type EventEmitter<ThyPanelMode | ThyPanelMode[]>
74
     */
75
    @Output() readonly thyOnPanelChange = new EventEmitter<ThyPanelMode | ThyPanelMode[]>();
14✔
76

6✔
77
    /**
6✔
78
     * @type EventEmitter<Date[]>
6✔
79
     */
6✔
80
    @Output() readonly thyOnCalendarChange = new EventEmitter<Date[]>();
81

8✔
82
    private _showTime: object | boolean;
8✔
83

8!
84
    /**
8✔
85
     * 增加时间选择功能
5✔
86
     * @default false
87
     */
88
    @Input() get thyShowTime(): object | boolean {
3✔
89
        return this._showTime;
90
    }
8✔
91
    set thyShowTime(value: object | boolean) {
5✔
92
        this._showTime = typeof value === 'object' ? value : coerceBooleanProperty(value);
93
    }
94

95
    /**
8✔
96
     * 是否展示时间(时、分)
8✔
97
     * @default false
98
     */
99
    @Input({ transform: coerceBooleanProperty }) thyMustShowTime = false;
100

164✔
101
    /**
164✔
102
     * 弹出位置
62✔
103
     * @type top | topLeft | topRight | bottom | bottomLeft | bottomRight | left | leftTop | leftBottom | right | rightTop | rightBottom
104
     */
105
    @Input() thyPlacement: ThyPlacement = 'bottomLeft';
102✔
106

107
    /**
164✔
108
     * @type EventEmitter<CompatibleDate | null>
164✔
109
     */
124✔
110
    @Output() readonly thyOnOk = new EventEmitter<CompatibleDate | null>();
111

112
    takeUntilDestroyed = takeUntilDestroyed();
113

114
    thyClickDispatcher = inject(ThyClickDispatcher);
124✔
115

116
    platformId = inject(PLATFORM_ID);
124✔
117

118
    ngZone = inject(NgZone);
119

120
    ngOnInit(): void {
121
        super.ngOnInit();
63✔
122
        this.setDefaultTimePickerState(this._panelMode);
10✔
123
        this.initialized = true;
124

125
        if (isPlatformBrowser(this.platformId)) {
126
            this.thyClickDispatcher
127
                .clicked(0)
14!
128
                .pipe(this.takeUntilDestroyed)
21✔
129
                .subscribe((event: Event) => {
14✔
130
                    if (
131
                        !this.element.nativeElement.contains(event.target) &&
132
                        !this.thyPicker?.overlayContainer?.nativeElement.contains(event.target as Node) &&
133
                        this.realOpenState
66✔
134
                    ) {
135
                        this.ngZone.run(() => {
136
                            this.closeOverlay();
5!
UNCOV
137
                            this.cdr.markForCheck();
×
138
                        });
×
UNCOV
139
                    }
×
140
                });
141
        }
UNCOV
142
    }
×
143

144
    onValueChange(value: CompatibleValue | RangeAdvancedValue): void {
145
        this.thyPicker.entering = false;
146
        this.restoreTimePickerState(value as CompatibleValue);
5!
147
        super.onValueChange(value);
5✔
148
        if (!this.flexible) {
149
            this.closeOverlay();
UNCOV
150
        }
×
151
        this.innerPreviousDate = this.thyPicker.getReadableValue(this.thyValue);
152
    }
153

5✔
154
    onInputValueChange(formatDate: string | null | Array<null>) {
155
        if (!formatDate || !formatDate.length) {
156
            const compatibleValue = formatDate ? (formatDate as CompatibleValue) : null;
181✔
157
            this.restoreTimePickerState(compatibleValue);
181✔
158
            super.onValueChange(compatibleValue);
57✔
159
            return;
160
        }
161
        let value = formatDate as string;
162
        const valueValid = isValidStringDate(value, this.thyTimeZone);
1✔
163
        const valueLimitValid = valueValid ? this.isValidDateLimit(parseStringDate(value, this.thyTimeZone)) : false;
164
        if (valueValid && valueLimitValid) {
165
            this.innerPreviousDate = value;
166
        } else {
5✔
167
            value = this.innerPreviousDate;
1✔
168
        }
169
        const tinyDate = value
4✔
170
            ? this.thyShowTime
171
                ? parseStringDate(value, this.thyTimeZone)
172
                : parseStringDate(value, this.thyTimeZone).startOfDay()
13!
173
            : null;
13✔
174
        this.restoreTimePickerState(tinyDate);
4✔
175
        super.onValueChange(tinyDate);
176
    }
13✔
177

178
    // Displays the time directly when the time must be displayed by default
179
    setDefaultTimePickerState(value: ThyPanelMode) {
180
        this.withTime = this.thyMustShowTime;
8✔
181
        if (this.isRange) {
8!
182
            this.panelMode = this.flexible ? ['date', 'date'] : [value, value];
×
183
        } else {
184
            this.panelMode = value;
8✔
185
        }
8✔
186
        this.showWeek = value === 'week';
8✔
187
        if (!this.thyFormat) {
188
            const inputFormats: { [key in ThyPanelMode]?: string } = {
189
                year: 'yyyy',
190
                quarter: 'yyyy-qqq',
1✔
191
                month: 'yyyy-MM',
192
                week: this.locale().weekThFormat,
193
                date: this.thyShowTime ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd'
194
            };
195
            this.thyFormat = this.flexible ? inputFormats['date'] : inputFormats[value];
196
        }
197
    }
198

199
    // Restore after clearing time to select whether the original picker time is displayed or not
200
    restoreTimePickerState(value: CompatibleValue | null) {
201
        if (!value) {
202
            this.withTime = this.thyMustShowTime || this.originWithTime;
203
        }
1✔
204
    }
205

206
    // Emit thyOnCalendarChange when select date by thy-range-picker
207
    onCalendarChange(value: TinyDate[]): void {
208
        if (this.isRange) {
209
            const rangeValue = value.map(x => x.nativeDate);
210
            this.thyOnCalendarChange.emit(rangeValue);
211
        }
212
    }
213

214
    onShowTimePickerChange(show: boolean): void {
215
        this.withTime = show;
216
    }
217

218
    onResultOk(): void {
219
        if (this.isRange) {
220
            const value = this.thyValue as TinyDate[];
221
            if (value.length) {
222
                this.thyOnOk.emit([value[0].nativeDate, value[1].nativeDate]);
223
            } else {
224
                this.thyOnOk.emit([]);
225
            }
226
        } else {
227
            if (this.thyValue) {
228
                this.thyOnOk.emit((this.thyValue as TinyDate).nativeDate);
229
            } else {
230
                this.thyOnOk.emit(null);
231
            }
232
        }
233
        this.closeOverlay();
234
    }
235

236
    onOpenChange(open: boolean): void {
237
        this.thyOpenChange.emit(open);
238
        if (!open) {
239
            this.onTouchedFn();
240
        }
241
    }
242

243
    onFocus(event: Event) {
244
        this.picker.focus();
245
    }
246

247
    onBlur(event?: FocusEvent) {
248
        // Tab 聚焦后自动聚焦到 input 输入框,此分支下直接返回,无需触发 onTouchedFn
249
        if (elementMatchClosest(event?.relatedTarget as HTMLElement, ['date-popup', 'thy-picker'])) {
250
            return;
251
        }
252
        this.onTouchedFn();
253
    }
254

255
    onInputDate(value: string) {
256
        if (value && isValidStringDate(value, this.thyTimeZone)) {
257
            if (this.thyShowTime) {
258
                this.withTime = hasTimeInStringDate(value, this.thyTimeZone);
259
            }
260
            this.thyValue = parseStringDate(value, this.thyTimeZone);
261
        }
262
    }
263

264
    private isValidDateLimit(date: TinyDate): boolean {
265
        let disable = false;
266
        if (this.thyDisabledDate !== undefined) {
267
            disable = this.thyDisabledDate(date.nativeDate);
268
        }
269
        const minDate = this.thyMinDate ? new TinyDate(transformDateValue(this.thyMinDate).value as Date, this.thyTimeZone) : null;
270
        const maxDate = this.thyMaxDate ? new TinyDate(transformDateValue(this.thyMaxDate).value as Date, this.thyTimeZone) : null;
271
        return (
272
            (!minDate || date.startOfDay().nativeDate >= minDate.startOfDay().nativeDate) &&
273
            (!maxDate || date.startOfDay().nativeDate <= maxDate.startOfDay().nativeDate) &&
274
            !disable
275
        );
276
    }
277
}
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

© 2026 Coveralls, Inc