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

atinc / ngx-tethys / d2403336-af28-467c-abd6-c9f215016198

14 Apr 2025 10:11AM UTC coverage: 90.254% (+0.02%) from 90.233%
d2403336-af28-467c-abd6-c9f215016198

Pull #3335

circleci

wangyuan-ky
fix: fix test
Pull Request #3335: feat(date-picker): add timezone support to date picker components and utilities #TINFR-1734

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.5 hits per line

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

95.45
/src/date-picker/picker.component.ts
1
import { getFlexiblePositions, ThyPlacement } from 'ngx-tethys/core';
2
import { coerceBooleanProperty, TinyDate } from 'ngx-tethys/util';
3

4
import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedOverlayPositionChange, ConnectionPositionPair } from '@angular/cdk/overlay';
5
import {
6
    AfterViewInit,
7
    ChangeDetectionStrategy,
8
    ChangeDetectorRef,
9
    Component,
10
    ElementRef,
11
    EventEmitter,
12
    inject,
13
    Input,
14
    OnChanges,
15
    Output,
16
    SimpleChanges,
17
    ViewChild
18
} from '@angular/core';
1✔
19

20
import { NgClass, NgTemplateOutlet } from '@angular/common';
160✔
21
import { scaleMotion, scaleXMotion, scaleYMotion } from 'ngx-tethys/core';
160✔
22
import { ThyI18nService } from 'ngx-tethys/i18n';
160✔
23
import { ThyIcon } from 'ngx-tethys/icon';
160✔
24
import { ThyInputDirective } from 'ngx-tethys/input';
160✔
25
import { ThyEnterDirective } from 'ngx-tethys/shared';
160✔
26
import { DateHelperService } from './date-helper.service';
160✔
27
import { CompatibleValue, RangePartType } from './inner-types';
160✔
28
import { getFlexibleAdvancedReadableValue } from './picker.util';
160✔
29
import { ThyDateGranularity } from './standard-types';
160✔
30

160✔
31
/**
160✔
32
 * @private
160✔
33
 */
160✔
34
@Component({
160✔
35
    selector: 'thy-picker',
160✔
36
    exportAs: 'thyPicker',
37
    templateUrl: './picker.component.html',
38
    changeDetection: ChangeDetectionStrategy.OnPush,
×
39
    imports: [CdkOverlayOrigin, ThyInputDirective, ThyEnterDirective, NgTemplateOutlet, ThyIcon, NgClass, CdkConnectedOverlay],
40
    animations: [scaleXMotion, scaleYMotion, scaleMotion]
41
})
162✔
42
export class ThyPicker implements OnChanges, AfterViewInit {
162✔
43
    private changeDetector = inject(ChangeDetectorRef);
44
    private dateHelper = inject(DateHelperService);
45
    private i18n = inject(ThyI18nService);
×
46

47
    @Input() isRange = false;
48
    @Input() open: boolean | undefined = undefined;
166✔
49
    @Input() disabled: boolean;
166✔
50
    @Input() placeholder: string | string[];
51
    @Input() readonly: boolean;
52
    @Input() allowClear: boolean;
660✔
53
    @Input() autoFocus: boolean;
54
    @Input() className: string;
55
    @Input() size: 'sm' | 'xs' | 'lg' | 'md' | 'default';
324✔
56
    @Input() suffixIcon: string;
324✔
57
    @Input() placement: ThyPlacement = 'bottomLeft';
316✔
58
    @Input() flexible: boolean = false;
59
    @Input() mode: string;
60
    @Input({ transform: coerceBooleanProperty }) hasBackdrop: boolean;
61
    @Input() separator: string;
62
    @Input() timeZone: string;
1,569✔
63
    @Output() blur = new EventEmitter<Event>();
64
    @Output() readonly valueChange = new EventEmitter<TinyDate | TinyDate[] | null>();
65
    @Output() readonly openChange = new EventEmitter<boolean>(); // Emitted when overlay's open state change
669✔
66
    @Output() readonly inputChange = new EventEmitter<string>();
67

68
    @ViewChild('origin', { static: true }) origin: CdkOverlayOrigin;
69
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
341✔
70
    @ViewChild('pickerInput', { static: true }) pickerInput: ElementRef;
8✔
71
    @ViewChild('overlayContainer', { static: false }) overlayContainer: ElementRef<HTMLElement>;
5✔
72

73
    @Input()
74
    get format() {
3✔
75
        return this.innerFormat;
76
    }
77

341✔
78
    set format(value: string) {
3✔
79
        this.innerFormat = value;
80
        this.updateReadableDate(this.innerValue);
81
    }
82

160✔
83
    @Input()
160✔
84
    get flexibleDateGranularity() {
1✔
85
        return this.innerflexibleDateGranularity;
86
    }
87

88
    set flexibleDateGranularity(granularity: ThyDateGranularity) {
59✔
89
        this.innerflexibleDateGranularity = granularity;
90
        this.updateReadableDate(this.innerValue);
91
    }
5✔
92

5!
93
    @Input()
5✔
94
    get value() {
95
        return this.innerValue;
5✔
96
    }
97

98
    set value(value: TinyDate | TinyDate[] | null) {
17✔
99
        this.innerValue = value;
17✔
100
        if (!this.entering) {
17✔
101
            this.updateReadableDate(this.innerValue);
102
        }
103
    }
8!
UNCOV
104

×
105
    private innerflexibleDateGranularity: ThyDateGranularity;
106

8!
107
    private innerFormat: string;
8✔
108

109
    private innerValue: TinyDate | TinyDate[] | null;
110

125✔
111
    entering = false;
124✔
112

124✔
113
    prefixCls = 'thy-calendar';
124✔
114

124✔
115
    isShowDatePopup = false;
124!
116

124✔
117
    overlayOpen = false; // Available when "open"=undefined
118

119
    overlayPositions: ConnectionPositionPair[] = getFlexiblePositions(this.placement, 4);
120

121
    get realOpenState(): boolean {
122
        // The value that really decide the open state of overlay
111✔
123
        return this.isOpenHandledByUser() ? !!this.open : this.overlayOpen;
57✔
124
    }
57✔
125

57✔
126
    get readonlyState(): boolean {
57✔
127
        return this.isRange || this.readonly || this.mode !== 'date';
128
    }
129

130
    ngOnChanges(changes: SimpleChanges): void {
129✔
131
        // open by user
129✔
132
        if (changes.open && changes.open.currentValue !== undefined) {
133
            if (changes.open.currentValue) {
134
                this.showDatePopup();
135
            } else {
60✔
136
                this.closeDatePopup();
60✔
137
            }
60✔
138
        }
139
        if (changes.timeZone && changes.timeZone.currentValue) {
140
            this.formatDate(this.innerValue as TinyDate);
141
        }
128✔
142
    }
125✔
143

144
    ngAfterViewInit(): void {
145
        this.overlayPositions = getFlexiblePositions(this.placement, 4);
146
        if (this.autoFocus) {
12✔
147
            this.focus();
148
        }
149
    }
52✔
150

151
    focus(): void {
152
        this.pickerInput.nativeElement.focus();
128✔
153
    }
154

155
    onBlur(event: FocusEvent) {
6✔
156
        this.blur.emit(event);
6✔
157
        if (this.entering) {
6✔
158
            this.valueChange.emit(this.pickerInput.nativeElement.value);
6✔
159
        }
160
        this.entering = false;
UNCOV
161
    }
×
162

163
    onInput(event: InputEvent) {
164
        this.entering = true;
660✔
165
        const inputValue = (event.target as HTMLElement)['value'];
260✔
166
        this.inputChange.emit(inputValue);
167
    }
400✔
168

249✔
169
    onEnter() {
170
        if (this.readonlyState) {
171
            return;
151✔
172
        }
173
        this.valueChange.emit(this.pickerInput.nativeElement.value || this.getReadableValue(new TinyDate(undefined, this.timeZone)));
174
        this.entering = false;
175
    }
176

1,697✔
177
    showOverlay(): void {
178
        if (!this.realOpenState) {
179
            this.overlayOpen = true;
180
            this.showDatePopup();
693✔
181

264✔
182
            this.openChange.emit(this.overlayOpen);
46✔
183
            setTimeout(() => {
184
                if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) {
185
                    this.cdkConnectedOverlay.overlayRef.updatePosition();
218✔
186
                }
218✔
187
            });
218✔
188
        }
189
    }
190

191
    hideOverlay(): void {
429✔
192
        if (this.realOpenState) {
429✔
193
            this.overlayOpen = false;
194
            this.closeDatePopup();
195

196
            this.openChange.emit(this.overlayOpen);
197
            this.focus();
198
        }
211✔
199
    }
13✔
200

201
    showDatePopup() {
202
        this.isShowDatePopup = true;
198✔
203
        this.changeDetector.markForCheck();
204
    }
205

206
    closeDatePopup() {
660✔
207
        // Delay 200ms before destroying the date-popup, otherwise you will not see the closing animation.
208
        setTimeout(() => {
209
            this.isShowDatePopup = false;
210
            this.changeDetector.markForCheck();
211
        }, 200);
644✔
212
    }
644✔
213

40✔
214
    onClickInputBox(): void {
215
        if (!this.disabled && !this.readonly && !this.isOpenHandledByUser()) {
604✔
216
            this.showOverlay();
217
        }
1✔
218
    }
219

220
    onClickBackdrop(): void {
221
        this.hideOverlay();
222
    }
223

224
    onOverlayDetach(): void {
225
        this.hideOverlay();
226
    }
227

228
    onPositionChange(position: ConnectedOverlayPositionChange): void {
229
        this.changeDetector.detectChanges();
230
    }
231

232
    onClickClear(event: MouseEvent): void {
233
        event.preventDefault();
234
        event.stopPropagation();
235

236
        this.innerValue = this.isRange ? [] : null;
237
        this.valueChange.emit(this.innerValue);
238
    }
239

240
    getPartTypeIndex(partType: RangePartType): number {
241
        return { left: 0, right: 1 }[partType];
242
    }
243

244
    isEmptyValue(value: CompatibleValue | null): boolean {
245
        if (value === null) {
246
            return true;
247
        } else if (this.isRange) {
1✔
248
            return !value || !Array.isArray(value) || value.every(val => !val);
249
        } else {
250
            return !value;
251
        }
252
    }
253

254
    // Whether open state is permanently controlled by user himself
255
    isOpenHandledByUser(): boolean {
256
        return this.open !== undefined;
257
    }
258

259
    getReadableValue(tinyDate: TinyDate | TinyDate[]): string | null {
260
        let value: TinyDate;
261
        if (this.isRange) {
262
            if (this.flexible && this.innerflexibleDateGranularity !== 'day') {
263
                return getFlexibleAdvancedReadableValue(
264
                    tinyDate as TinyDate[],
265
                    this.innerflexibleDateGranularity,
266
                    this.separator,
267
                    this.i18n.getLocale()
268
                );
269
            } else {
270
                const start = tinyDate[0] ? this.formatDate(tinyDate[0]) : '';
271
                const end = tinyDate[1] ? this.formatDate(tinyDate[1]) : '';
272
                return start && end ? `${start}${this.separator}${end}` : null;
273
            }
274
        } else {
275
            value = tinyDate as TinyDate;
276
            return value ? this.formatDate(value) : null;
277
        }
278
    }
279

280
    formatDate(value: TinyDate) {
281
        // dateHelper.format() 使用的是 angular 的 format,不支持季度,修改的话,改动比较大。
282
        // 此处通过对 innerFormat 做下判断,如果是季度的 format,使用 date-fns 的 format()
283
        if (this.innerFormat && (this.innerFormat.includes('q') || this.innerFormat.includes('Q'))) {
284
            return value.format(this.innerFormat);
285
        } else {
286
            return this.dateHelper.format(value?.nativeDate, this.innerFormat);
287
        }
288
    }
289

290
    getPlaceholder(): string {
291
        return this.isRange && this.placeholder && Array.isArray(this.placeholder)
292
            ? (this.placeholder as string[]).join(this.separator)
293
            : (this.placeholder as string);
294
    }
295

296
    private updateReadableDate(setValue: TinyDate | TinyDate[] | null) {
297
        const readableValue = this.getReadableValue(setValue);
298
        if (readableValue === this.pickerInput.nativeElement['value']) {
299
            return;
300
        }
301

302
        this.pickerInput.nativeElement.value = readableValue;
303
    }
304
}
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