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

atinc / ngx-tethys / 18d04333-bffc-4369-a760-736c9b3f2b43

22 Dec 2023 09:34AM UTC coverage: 90.34% (+0.008%) from 90.332%
18d04333-bffc-4369-a760-736c9b3f2b43

push

circleci

web-flow
fix(date-picker): when the date picker directive's and component's thyHasBackdrop value is false, clicking outside should close #INFR-11053 (#2985)

5338 of 6569 branches covered (0.0%)

Branch coverage included in aggregate %.

11 of 16 new or added lines in 3 files covered. (68.75%)

4 existing lines in 3 files now uncovered.

13272 of 14031 relevant lines covered (94.59%)

976.69 hits per line

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

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

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

21
import { AbstractPickerComponent } from './abstract-picker.component';
736✔
22
import { CompatibleValue, RangeAdvancedValue } from './inner-types';
23
import { CompatibleDate, ThyPanelMode } from './standard-types';
24
import { ThyPickerComponent } from './picker.component';
481✔
25
import { hasTimeInStringDate, isValidStringDate, parseStringDate, transformDateValue } from './picker.util';
26
import { isPlatformBrowser } from '@angular/common';
27
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
65!
28

29
/**
30
 * @private
142✔
31
 */
142✔
32
@Component({
142✔
33
    template: ``,
142✔
34
    standalone: true,
142✔
35
    host: {
142✔
36
        '[attr.tabindex]': `tabIndex`,
142✔
37
        '(focus)': 'onFocus($event)',
142✔
38
        '(blur)': 'onBlur($event)'
142✔
39
    }
142✔
40
})
142✔
41
export class BasePickerComponent extends AbstractPickerComponent implements OnInit, OnChanges {
142✔
42
    showWeek = false;
142✔
43

44
    panelMode: ThyPanelMode | ThyPanelMode[];
45

142✔
46
    initialized: boolean;
142✔
47

142✔
48
    private innerPreviousDate: string;
142!
49

142✔
50
    @ViewChild('thyPicker', { static: true }) thyPicker: ThyPickerComponent;
51

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

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

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

65
    /**
47✔
66
     * 是否有幕布
47✔
67
     * @default true
47✔
68
     */
47✔
69
    @Input() @InputBoolean() thyHasBackdrop = true;
39✔
70

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

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

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

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

94
    /**
95
     * 是否展示时间(时、分)
147✔
96
     * @default false
147✔
97
     */
58✔
98
    @Input() @InputBoolean() thyMustShowTime = false;
99

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

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

120✔
111
    takeUntilDestroyed = takeUntilDestroyed();
112

113
    thyClickDispatcher = inject(ThyClickDispatcher);
114

115
    platformId = inject(PLATFORM_ID);
60✔
116

9✔
117
    ngZone = inject(NgZone);
118

119
    constructor(cdr: ChangeDetectorRef, protected element: ElementRef) {
120
        super(cdr);
121
    }
10!
122

15✔
123
    ngOnInit(): void {
10✔
124
        super.ngOnInit();
125
        this.setDefaultTimePickerState(this._panelMode);
126
        this.initialized = true;
127

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

147
    onValueChange(value: CompatibleValue | RangeAdvancedValue): void {
5✔
148
        this.thyPicker.entering = false;
149
        this.restoreTimePickerState(value as CompatibleValue);
150
        super.onValueChange(value);
168✔
151
        if (!this.flexible) {
168✔
152
            this.closeOverlay();
54✔
153
        }
154
        this.innerPreviousDate = this.thyPicker.getReadableValue(this.thyValue);
155
    }
156

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

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

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

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

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

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

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

241
    onFocus(event: Event) {
242
        this.picker.focus();
243
    }
244

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

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

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