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

adobe / spectrum-web-components / 12693741474

09 Jan 2025 04:03PM UTC coverage: 98.259% (+0.05%) from 98.206%
12693741474

Pull #5002

github

web-flow
Merge 3502508d0 into 5bf31e817
Pull Request #5002: feat: Calendar and DateTimePicker components

5677 of 5977 branches covered (94.98%)

Branch coverage included in aggregate %.

2878 of 2891 new or added lines in 26 files covered. (99.55%)

3 existing lines in 1 file now uncovered.

35918 of 36355 relevant lines covered (98.8%)

1123.79 hits per line

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

99.11
/packages/calendar/src/Calendar.ts
1
/*
2✔
2
Copyright 2023 Adobe. All rights reserved.
2✔
3
This file is licensed to you under the Apache License, Version 2.0 (the "License");
2✔
4
you may not use this file except in compliance with the License. You may obtain a copy
2✔
5
of the License at http://www.apache.org/licenses/LICENSE-2.0
2✔
6

2✔
7
Unless required by applicable law or agreed to in writing, software distributed under
2✔
8
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
2✔
9
OF ANY KIND, either express or implied. See the License for the specific language
2✔
10
governing permissions and limitations under the License.
2✔
11
*/
2✔
12
import {
2✔
13
    CalendarDate,
2✔
14
    DateFormatter,
2✔
15
    DateValue,
2✔
16
    endOfMonth,
2✔
17
    getLocalTimeZone,
2✔
18
    getWeeksInMonth,
2✔
19
    isSameDay,
2✔
20
    isSameMonth,
2✔
21
    parseDate,
2✔
22
    startOfMonth,
2✔
23
    startOfWeek,
2✔
24
    toCalendarDate,
2✔
25
    today,
2✔
26
} from '@internationalized/date';
2✔
27
import { NumberFormatter } from '@internationalized/number';
2✔
28

2✔
29
import {
2✔
30
    CSSResultArray,
2✔
31
    html,
2✔
32
    PropertyValues,
2✔
33
    SpectrumElement,
2✔
34
    TemplateResult,
2✔
35
} from '@spectrum-web-components/base';
2✔
36
import {
2✔
37
    property,
2✔
38
    state,
2✔
39
} from '@spectrum-web-components/base/src/decorators.js';
2✔
40
import {
2✔
41
    ClassInfo,
2✔
42
    classMap,
2✔
43
    ifDefined,
2✔
44
} from '@spectrum-web-components/base/src/directives.js';
2✔
45
import {
2✔
46
    LanguageResolutionController,
2✔
47
    languageResolverUpdatedSymbol,
2✔
48
} from '@spectrum-web-components/reactive-controllers/src/LanguageResolution.js';
2✔
49

2✔
50
import styles from './calendar.css.js';
2✔
51

2✔
52
import '@spectrum-web-components/action-button/sp-action-button.js';
2✔
53
import '@spectrum-web-components/icons-workflow/icons/sp-icon-chevron-left.js';
2✔
54
import '@spectrum-web-components/icons-workflow/icons/sp-icon-chevron-right.js';
2✔
55

2✔
56
import {
2✔
57
    CalendarLabels,
2✔
58
    CalendarWeekday,
2✔
59
    DateCellProperties,
2✔
60
} from './types.js';
2✔
61

2✔
62
export const DAYS_PER_WEEK = 7;
2✔
63
/**
2✔
64
 * @element sp-calendar
2✔
65
 *
2✔
66
 * @slot prev-icon - The icon used in the "Previous Month" button
2✔
67
 * @slot next-icon - The icon used in the "Next Month" button
2✔
68
 *
2✔
69
 * @event change - Announces when a day is selected
2✔
70
 */
2✔
71
export class Calendar extends SpectrumElement {
2✔
72
    public static override get styles(): CSSResultArray {
2✔
73
        return [styles];
2✔
74
    }
2✔
75

2✔
76
    /**
2✔
77
     * The selected date in the calendar. If defined, this also indicates where the calendar opens.
2✔
78
     * If not, the calendar opens at the current month.
2✔
79
     */
2✔
80
    @property({ type: Object })
2✔
81
    public value?: DateValue;
2✔
82

2✔
83
    /**
2✔
84
     * The minimum allowed date a user can select
2✔
85
     */
2✔
86
    @property({ type: Object })
2✔
87
    public min?: DateValue;
2✔
88

2✔
89
    /**
2✔
90
     * The maximum allowed date a user can select
2✔
91
     */
2✔
92
    @property({ type: Object })
2✔
93
    public max?: DateValue;
2✔
94

2✔
95
    /**
2✔
96
     * Indicates when the calendar should be disabled entirely
2✔
97
     */
2✔
98
    @property({ type: Boolean, reflect: true })
2✔
99
    public disabled = false;
2✔
100

2✔
101
    /**
2✔
102
     * Labels read by screen readers. The default values are in English
2✔
103
     * and can be overridden to localize the content.
2✔
104
     */
2✔
105
    @property({ type: Object })
2✔
106
    public labels: CalendarLabels = {
2✔
107
        previous: 'Previous',
2✔
108
        next: 'Next',
2✔
109
        today: 'Today',
2✔
110
        selected: 'Selected',
2✔
111
    };
2✔
112

2✔
113
    /**
2✔
114
     * The date that indicates the current position in the calendar.
2✔
115
     */
2✔
116
    @state()
2✔
117
    private currentDate: CalendarDate = this.today;
2✔
118

2✔
119
    /**
2✔
120
     * Adds a padding around the calendar
2✔
121
     */
2✔
122
    @property({ type: Boolean, reflect: true })
2✔
123
    public padded = false;
2✔
124

2✔
125
    private languageResolver = new LanguageResolutionController(this);
2✔
126

2✔
127
    /**
2✔
128
     * The locale used to format the dates and weekdays.
2✔
129
     * The default value is the language of the document or the user's browser.
2✔
130
     */
2✔
131
    public get locale(): string {
2✔
132
        return this.languageResolver.language;
80,657✔
133
    }
80,657✔
134

2✔
135
    private timeZone: string = getLocalTimeZone();
2✔
136
    private get today(): CalendarDate {
2✔
137
        return today(this.timeZone);
37,810✔
138
    }
37,810✔
139

2✔
140
    @state()
2✔
141
    private weekdays: CalendarWeekday[] = [];
2✔
142

2✔
143
    @state()
2✔
144
    private currentMonthDates: CalendarDate[][] = [];
2✔
145

2✔
146
    @state()
2✔
147
    private set isDateFocusIntent(value: boolean) {
2✔
148
        if (this._isDateFocusIntent === value) return;
127✔
149

25✔
150
        this._isDateFocusIntent = value;
25✔
151
        this.requestUpdate('isDateFocusIntent', !value);
25✔
152
    }
127✔
153

2✔
154
    private get isDateFocusIntent(): boolean {
2✔
155
        return this._isDateFocusIntent;
2,634✔
156
    }
2,634✔
157
    private _isDateFocusIntent: boolean = false;
2✔
158

2✔
159
    private setDateFocusIntent(): void {
2✔
160
        this.isDateFocusIntent = true;
106✔
161
    }
106✔
162

2✔
163
    private resetDateFocusIntent(): void {
2✔
164
        this.isDateFocusIntent = false;
44✔
165
    }
44✔
166

2✔
167
    override connectedCallback(): void {
2✔
168
        super.connectedCallback();
580✔
169
        document.addEventListener('mousedown', this.resetDateFocusIntent);
580✔
170
    }
580✔
171

2✔
172
    override disconnectedCallback(): void {
2✔
173
        super.disconnectedCallback();
580✔
174
        document.removeEventListener('mousedown', this.resetDateFocusIntent);
580✔
175
    }
580✔
176

2✔
177
    /**
2✔
178
     * Resets the component's value
2✔
179
     */
2✔
180
    public clear(): void {
2✔
181
        this.value = undefined;
1✔
182
    }
1✔
183

2✔
184
    constructor() {
2✔
185
        super();
580✔
186
        this.setNumberFormatter();
580✔
187
        this.setWeekdays();
580✔
188
        this.setCurrentMonthDates();
580✔
189
    }
580✔
190

2✔
191
    override willUpdate(changedProperties: PropertyValues): void {
2✔
192
        if (changedProperties.has(languageResolverUpdatedSymbol)) {
1,198✔
193
            this.setNumberFormatter();
578✔
194
            this.setWeekdays();
578✔
195
            this.setCurrentMonthDates();
578✔
196
        }
578✔
197

1,198✔
198
        const changesMin = changedProperties.has('min');
1,198✔
199
        const changesMax = changedProperties.has('max');
1,198✔
200
        const changesValue = changedProperties.has('value');
1,198✔
201
        const changesDates = changesMin || changesMax || changesValue;
1,198✔
202

1,198✔
203
        if (changesDates) {
1,198✔
204
            this.convertToCalendarDates();
637✔
205
            this.checkDatePropsCompliance(changesMin || changesMax);
637✔
206
            this.updateCurrentDate();
637✔
207
        }
637✔
208

1,198✔
209
        const previousDate = changedProperties.get('currentDate');
1,198✔
210
        const changesMonth =
1,198✔
211
            changedProperties.has('currentDate') &&
1,198✔
212
            (!previousDate || !isSameMonth(previousDate, this.currentDate));
1,195✔
213

1,198✔
214
        if (changesMonth) {
1,198✔
215
            this.setCurrentMonthDates();
734✔
216
            this.setAttribute('aria-label', this.monthAndYear);
734✔
217
        }
734✔
218
    }
1,198✔
219

2✔
220
    override updated(changedProperties: PropertyValues): void {
2✔
221
        if (changedProperties.has('currentDate') && this.isDateFocusIntent)
1,198✔
222
            this.focusCurrentDate();
1,198✔
223
    }
1,198✔
224

2✔
225
    /**
2✔
226
     * Focuses the tabbable day element in the calendar represented by the current date.
2✔
227
     * Useful while navigating through the calendar as the focus might be lost when the month changes.
2✔
228
     */
2✔
229
    private focusCurrentDate(): void {
2✔
230
        const elementToFocus = this.shadowRoot?.querySelector(
101!
231
            'td span[tabindex="0"]'
101✔
232
        ) as HTMLElement;
101✔
233
        if (elementToFocus) elementToFocus.focus();
101✔
234
    }
101✔
235

2✔
236
    private convertToCalendarDates(): void {
2✔
237
        const era = 'AD'; // Force the era to be AD until we support other eras
637✔
238
        this.min = this.min && toCalendarDate(this.min).set({ era });
637✔
239
        this.max = this.max && toCalendarDate(this.max).set({ era });
637✔
240
        this.value = this.value && toCalendarDate(this.value).set({ era });
637✔
241
    }
637✔
242

2✔
243
    /**
2✔
244
     * Validates the component's date properties (min, max and value) compliance with one another.
2✔
245
     * If the [min, max] constraint interval is invalid, both properties are reset.
2✔
246
     * If the value is not within the [min, max] (valid) interval, it is reset.
2✔
247
     *
2✔
248
     * @param checkInterval - Whether to check the [min, max] interval
2✔
249
     */
2✔
250
    private checkDatePropsCompliance(checkInterval: boolean): void {
2✔
251
        if (checkInterval && this.min && this.max) {
637✔
252
            const isValidInterval = this.min.compare(this.max) < 0;
75✔
253
            if (!isValidInterval) {
75✔
254
                if (window.__swc.DEBUG)
2✔
255
                    window.__swc.warn(
2✔
256
                        this,
2✔
257
                        `<${this.localName}> expects the 'min' to be less than 'max'. Please ensure that 'min' property's date is earlier than 'max' property's date.`,
2✔
258
                        'https://opensource.adobe.com/spectrum-web-components/components/calendar'
2✔
259
                    );
2✔
260
                this.min = undefined;
2✔
261
                this.max = undefined;
2✔
262
            }
2✔
263
        }
75✔
264

637✔
265
        if (this.value && this.isNonCompliantDate(this.value)) {
637✔
266
            if (window.__swc.DEBUG)
47✔
267
                window.__swc.warn(
47✔
268
                    this,
47✔
269
                    `<${this.localName}> expects the preselected value to comply with the min and max constraints. Please ensure that 'value' property's date is in between the dates for the 'min' and 'max' properties.`,
47✔
270
                    'https://opensource.adobe.com/spectrum-web-components/components/calendar'
47✔
271
                );
47✔
272
            this.value = undefined;
47✔
273
        }
47✔
274
    }
637✔
275

2✔
276
    private updateCurrentDate(): void {
2✔
277
        if (this.value) {
637✔
278
            this.currentDate = this.value as CalendarDate;
471✔
279
            return;
471✔
280
        }
471✔
281

166✔
282
        const isTodayNonCompliant = this.isNonCompliantDate(this.today);
166✔
283

166✔
284
        if (isTodayNonCompliant) {
637✔
285
            if (this.min) this.currentDate = this.min as CalendarDate;
17✔
286
            else if (this.max) this.currentDate = this.max as CalendarDate;
1✔
287
        } else this.currentDate = this.today;
637✔
288
    }
637✔
289

2✔
290
    /**
2✔
291
     * Whether the date is non-compliant with the min and max constraints
2✔
292
     */
2✔
293
    private isNonCompliantDate(date: DateValue): boolean {
2✔
294
        return Boolean(
684✔
295
            (this.min && date.compare(this.min) < 0) ||
684✔
296
                (this.max && date.compare(this.max) > 0)
626✔
297
        );
684✔
298
    }
684✔
299

2✔
300
    protected override render(): TemplateResult {
2✔
301
        return html`
1,198✔
302
            ${this.renderCalendarHeader()}${this.renderCalendarGrid()}
1,198✔
303
        `;
1,198✔
304
    }
1,198✔
305

2✔
306
    private get monthAndYear(): string {
2✔
307
        return this.formatDate(this.currentDate, {
1,932✔
308
            month: 'long',
1,932✔
309
            year: 'numeric',
1,932✔
310
        });
1,932✔
311
    }
1,932✔
312

2✔
313
    protected renderCalendarHeader(): TemplateResult {
2✔
314
        return html`
1,198✔
315
            <div class="header" @focusin=${this.resetDateFocusIntent}>
1,198✔
316
                <h2
1,198✔
317
                    class="title"
1,198✔
318
                    aria-live="polite"
1,198✔
319
                    aria-atomic="true"
1,198✔
320
                    data-test-id="calendar-title"
1,198✔
321
                >
1,198✔
322
                    ${this.monthAndYear}
1,198✔
323
                </h2>
1,198✔
324

1,198✔
325
                <sp-action-button
1,198✔
326
                    quiet
1,198✔
327
                    size="s"
1,198✔
328
                    label=${this.labels.previous}
1,198✔
329
                    class="prevMonth"
1,198✔
330
                    data-test-id="prev-btn"
1,198✔
331
                    ?disabled=${this.isPreviousMonthDisabled}
1,198✔
332
                    @click=${this.handlePreviousMonth}
1,198✔
333
                >
1,198✔
334
                    <div slot="icon">
1,198✔
335
                        <slot name="prev-icon">
1,198✔
336
                            <sp-icon-chevron-left></sp-icon-chevron-left>
1,198✔
337
                        </slot>
1,198✔
338
                    </div>
1,198✔
339
                </sp-action-button>
1,198✔
340

1,198✔
341
                <sp-action-button
1,198✔
342
                    quiet
1,198✔
343
                    size="s"
1,198✔
344
                    label=${this.labels.next}
1,198✔
345
                    class="nextMonth"
1,198✔
346
                    data-test-id="next-btn"
1,198✔
347
                    ?disabled=${this.isNextMonthDisabled}
1,198✔
348
                    @click=${this.handleNextMonth}
1,198✔
349
                >
1,198✔
350
                    <div slot="icon">
1,198✔
351
                        <slot name="next-icon">
1,198✔
352
                            <sp-icon-chevron-right></sp-icon-chevron-right>
1,198✔
353
                        </slot>
1,198✔
354
                    </div>
1,198✔
355
                </sp-action-button>
1,198✔
356
            </div>
1,198✔
357
        `;
1,198✔
358
    }
1,198✔
359

2✔
360
    private get isPreviousMonthDisabled(): boolean {
2✔
361
        if (this.disabled) return true;
1,207✔
362

1,204✔
363
        const currentMonthStart = startOfMonth(this.currentDate);
1,204✔
364
        const previousMonthStart = currentMonthStart.subtract({ months: 1 });
1,204✔
365

1,204✔
366
        return (
1,204✔
367
            currentMonthStart.era !== previousMonthStart.era ||
1,204✔
368
            isSameDay(currentMonthStart, previousMonthStart)
1,189✔
369
        );
1,207✔
370
    }
1,207✔
371

2✔
372
    private get isNextMonthDisabled(): boolean {
2✔
373
        if (this.disabled) return true;
1,200✔
374

1,197✔
375
        const currentMonthEnd = endOfMonth(this.currentDate);
1,197✔
376
        const nextMonthEnd = currentMonthEnd.add({ months: 1 });
1,197✔
377

1,197✔
378
        return (
1,197✔
379
            currentMonthEnd.era !== nextMonthEnd.era ||
1,197✔
380
            isSameDay(currentMonthEnd, nextMonthEnd)
1,197✔
381
        );
1,200✔
382
    }
1,200✔
383

2✔
384
    private handlePreviousMonth(): void {
2✔
385
        let newCurrentDate = startOfMonth(this.currentDate).subtract({
8✔
386
            months: 1,
8✔
387
        });
8✔
388

8✔
389
        if (this.value && isSameMonth(newCurrentDate, this.value))
8✔
390
            newCurrentDate = this.value as CalendarDate;
8✔
391
        else if (isSameMonth(newCurrentDate, this.today))
7✔
392
            newCurrentDate = this.today;
7✔
393

8✔
394
        this.currentDate = newCurrentDate;
8✔
395
    }
8✔
396

2✔
397
    private handleNextMonth(): void {
2✔
398
        let newCurrentDate = startOfMonth(this.currentDate).add({
10✔
399
            months: 1,
10✔
400
        });
10✔
401

10✔
402
        if (this.value && isSameMonth(newCurrentDate, this.value))
10✔
403
            newCurrentDate = this.value as CalendarDate;
10!
404
        else if (isSameMonth(newCurrentDate, this.today))
10✔
405
            newCurrentDate = this.today;
10!
406

10✔
407
        this.currentDate = newCurrentDate;
10✔
408
    }
10✔
409

2✔
410
    protected renderCalendarGrid(): TemplateResult {
2✔
411
        return html`
1,198✔
412
            <table
1,198✔
413
                role="grid"
1,198✔
414
                aria-readonly="true"
1,198✔
415
                aria-disabled=${this.disabled}
1,198✔
416
                role="presentation"
1,198✔
417
                class="table body"
1,198✔
418
                @keydown=${this.handleKeydown}
1,198✔
419
            >
1,198✔
420
                ${this.renderCalendarTableHead()}
1,198✔
421
                ${this.renderCalendarTableBody()}
1,198✔
422
            </table>
1,198✔
423
        `;
1,198✔
424
    }
1,198✔
425

2✔
426
    protected renderCalendarTableHead(): TemplateResult {
2✔
427
        return html`
1,198✔
428
            <thead role="presentation">
1,198✔
429
                <tr role="row">
1,198✔
430
                    ${this.weekdays.map(this.renderWeekdayColumn)}
1,198✔
431
                </tr>
1,198✔
432
            </thead>
1,198✔
433
        `;
1,198✔
434
    }
1,198✔
435

2✔
436
    protected renderWeekdayColumn(weekday: CalendarWeekday): TemplateResult {
2✔
437
        return html`
8,386✔
438
            <th role="columnheader" scope="col" class="table-cell">
8,386✔
439
                <abbr class="dayOfWeek" title=${weekday.long}>
8,386✔
440
                    ${weekday.narrow}
8,386✔
441
                </abbr>
8,386✔
442
            </th>
8,386✔
443
        `;
8,386✔
444
    }
8,386✔
445

2✔
446
    protected renderCalendarTableBody(): TemplateResult {
2✔
447
        return html`
1,198✔
448
            <tbody role="presentation">
1,198✔
449
                ${this.currentMonthDates.map(
1,198✔
450
                    (week) => html`
1,198✔
451
                        <tr role="row">
6,688✔
452
                            ${week.map((date) =>
6,688✔
453
                                this.renderCalendarTableCell(date)
46,797✔
454
                            )}
6,688✔
455
                        </tr>
1,198✔
456
                    `
1,198✔
457
                )}
1,198✔
458
            </tbody>
1,198✔
459
        `;
1,198✔
460
    }
1,198✔
461

2✔
462
    private parseDateCellProperties(
2✔
463
        calendarDate: CalendarDate
46,797✔
464
    ): DateCellProperties {
46,797✔
465
        const props = {
46,797✔
466
            isOutsideMonth: false,
46,797✔
467
            isSelected: false,
46,797✔
468
            isToday: false,
46,797✔
469
            isDisabled: false,
46,797✔
470
            isTabbable: false,
46,797✔
471
        };
46,797✔
472
        props.isOutsideMonth = calendarDate.month !== this.currentDate.month;
46,797✔
473
        if (props.isOutsideMonth) return props;
46,797✔
474

36,897✔
475
        props.isDisabled =
36,897✔
476
            this.disabled ||
36,897✔
477
            this.isMinLimitReached(calendarDate) ||
36,804✔
478
            this.isMaxLimitReached(calendarDate);
34,678✔
479

46,797✔
480
        props.isToday = isSameDay(calendarDate, this.today);
46,797✔
481

46,797✔
482
        if (props.isDisabled) return props;
46,797✔
483
        props.isTabbable = isSameDay(calendarDate, this.currentDate);
33,618✔
484

33,618✔
485
        props.isSelected = Boolean(
33,618✔
486
            this.value && isSameDay(this.value, calendarDate)
46,797✔
487
        );
46,797✔
488

46,797✔
489
        return props;
46,797✔
490
    }
46,797✔
491

2✔
492
    protected renderCalendarTableCell(
2✔
493
        calendarDate: CalendarDate
46,797✔
494
    ): TemplateResult {
46,797✔
495
        const { isOutsideMonth, isSelected, isToday, isDisabled, isTabbable } =
46,797✔
496
            this.parseDateCellProperties(calendarDate);
46,797✔
497

46,797✔
498
        const dayClasses: ClassInfo = {
46,797✔
499
            date: true,
46,797✔
500
            'is-outsideMonth': isOutsideMonth,
46,797✔
501
            'is-selected': isSelected,
46,797✔
502
            'is-today': isToday,
46,797✔
503
            'is-disabled': isDisabled,
46,797✔
504
        };
46,797✔
505

46,797✔
506
        let currentDayLabelPrefix = '';
46,797✔
507
        if (isToday) currentDayLabelPrefix = `${this.labels.today}, `;
46,797✔
508
        else if (isSelected)
45,970✔
509
            currentDayLabelPrefix = `${this.labels.selected}, `;
45,970✔
510

46,797✔
511
        const currentDayLabel =
46,797✔
512
            currentDayLabelPrefix +
46,797✔
513
            this.formatDate(calendarDate, {
46,797✔
514
                weekday: 'long',
46,797✔
515
                year: 'numeric',
46,797✔
516
                month: 'long',
46,797✔
517
                day: 'numeric',
46,797✔
518
            });
46,797✔
519

46,797✔
520
        return html`
46,797✔
521
            <td role="gridcell" class="table-cell">
46,797✔
522
                <span
46,797✔
523
                    role="button"
46,797✔
524
                    tabindex=${ifDefined(
46,797✔
525
                        !isOutsideMonth ? (isTabbable ? '0' : '-1') : undefined
46,797✔
526
                    )}
46,797✔
527
                    aria-label=${currentDayLabel}
46,797✔
528
                    aria-disabled=${isOutsideMonth || isDisabled}
46,797✔
529
                    data-value=${calendarDate.toString()}
46,797✔
530
                    @mousedown=${this.handleDaySelect}
46,797✔
531
                >
46,797✔
532
                    <span role="presentation" class=${classMap(dayClasses)}>
46,797✔
533
                        ${this.formatNumber(calendarDate.day)}
46,797✔
534
                    </span>
46,797✔
535
                </span>
46,797✔
536
            </td>
46,797✔
537
        `;
46,797✔
538
    }
46,797✔
539

2✔
540
    private handleKeydown(event: KeyboardEvent): void {
2✔
541
        this.setDateFocusIntent();
106✔
542

106✔
543
        switch (event.code) {
106✔
544
            case 'ArrowLeft': {
106✔
545
                this.moveToPreviousDay();
39✔
546
                break;
39✔
547
            }
39✔
548
            case 'ArrowDown': {
106✔
549
                this.moveToNextWeek();
7✔
550
                break;
7✔
551
            }
7✔
552
            case 'ArrowRight': {
106✔
553
                this.moveToNextDay();
45✔
554
                break;
45✔
555
            }
45✔
556
            case 'ArrowUp': {
106✔
557
                this.moveToPreviousWeek();
6✔
558
                break;
6✔
559
            }
6✔
560
            case 'Space':
106✔
561
            case 'Enter': {
106✔
562
                this.handleDaySelect(event);
9✔
563
                break;
9✔
564
            }
9✔
565
        }
106✔
566
    }
106✔
567

2✔
568
    private handleDaySelect(event: MouseEvent | KeyboardEvent): void {
2✔
569
        if (this.disabled) {
13!
NEW
570
            event.preventDefault();
×
NEW
571
            return;
×
NEW
572
        }
×
573

13✔
574
        const cellButton = (event.target as HTMLElement).closest(
13✔
575
            'span[role="button"]'
13✔
576
        ) as HTMLSpanElement;
13✔
577

13✔
578
        const dateString = cellButton && cellButton.dataset.value;
13✔
579
        if (!dateString) return;
13!
580

13✔
581
        const calendarDateEngaged = parseDate(dateString);
13✔
582
        const isAlreadySelected =
13✔
583
            this.value && isSameDay(this.value, calendarDateEngaged);
13✔
584

13✔
585
        if (
13✔
586
            isAlreadySelected ||
13✔
587
            this.isMinLimitReached(calendarDateEngaged) ||
10✔
588
            this.isMaxLimitReached(calendarDateEngaged)
10✔
589
        ) {
13✔
590
            event.preventDefault();
4✔
591
            return;
4✔
592
        }
4✔
593

9✔
594
        this.value = calendarDateEngaged;
9✔
595

9✔
596
        this.dispatchEvent(
9✔
597
            new CustomEvent('change', {
9✔
598
                bubbles: true,
9✔
599
                composed: true,
9✔
600
            })
9✔
601
        );
9✔
602
    }
13✔
603

2✔
604
    private moveToPreviousDay(): void {
2✔
605
        const previousDay = this.currentDate.subtract({ days: 1 });
39✔
606

39✔
607
        if (this.canMoveBackToDate(previousDay)) this.currentDate = previousDay;
39✔
608
    }
39✔
609

2✔
610
    private moveToNextDay(): void {
2✔
611
        const nextDay = this.currentDate.add({ days: 1 });
45✔
612

45✔
613
        if (this.canMoveForwardToDate(nextDay)) this.currentDate = nextDay;
45✔
614
    }
45✔
615

2✔
616
    private moveToPreviousWeek(): void {
2✔
617
        const previousWeek = this.currentDate.subtract({ weeks: 1 });
6✔
618

6✔
619
        if (this.canMoveBackToDate(previousWeek)) {
6✔
620
            this.currentDate = previousWeek;
4✔
621
            return;
4✔
622
        }
4✔
623

2✔
624
        let dayToFocus = previousWeek.add({ days: 1 });
2✔
625
        while (!this.canMoveBackToDate(dayToFocus)) {
6✔
626
            dayToFocus = dayToFocus.add({ days: 1 });
3✔
627
        }
3✔
628
        this.currentDate = dayToFocus;
2✔
629
    }
6✔
630

2✔
631
    private moveToNextWeek(): void {
2✔
632
        const nextWeek = this.currentDate.add({ weeks: 1 });
7✔
633

7✔
634
        if (this.canMoveForwardToDate(nextWeek)) {
7✔
635
            this.currentDate = nextWeek;
6✔
636
            return;
6✔
637
        }
6✔
638

1✔
639
        let dayToFocus = nextWeek.subtract({ days: 1 });
1✔
640
        while (!this.canMoveForwardToDate(dayToFocus)) {
1✔
641
            dayToFocus = dayToFocus.subtract({ days: 1 });
1✔
642
        }
1✔
643
        this.currentDate = dayToFocus;
1✔
644
    }
7✔
645

2✔
646
    private canMoveBackToDate(previousDate: CalendarDate): boolean {
2✔
647
        if (this.isMinLimitReached(previousDate)) return false;
50✔
648

45✔
649
        return (
45✔
650
            isSameMonth(this.currentDate, previousDate) ||
45✔
651
            !this.isPreviousMonthDisabled
9✔
652
        );
50✔
653
    }
50✔
654

2✔
655
    private canMoveForwardToDate(nextDate: CalendarDate): boolean {
2✔
656
        if (this.isMaxLimitReached(nextDate)) return false;
54✔
657

49✔
658
        return (
49✔
659
            isSameMonth(this.currentDate, nextDate) || !this.isNextMonthDisabled
54✔
660
        );
54✔
661
    }
54✔
662

2✔
663
    /**
2✔
664
     * Defines the array with data for the days of the week, starting on the first day of the week according to the
2✔
665
     * defined location (Sunday, Monday, etc.)
2✔
666
     */
2✔
667
    private setWeekdays(): void {
2✔
668
        const weekStart = startOfWeek(this.currentDate, this.locale);
1,158✔
669

1,158✔
670
        this.weekdays = [...new Array(DAYS_PER_WEEK).keys()].map((dayIndex) => {
1,158✔
671
            const date = weekStart.add({ days: dayIndex });
8,106✔
672

8,106✔
673
            return {
8,106✔
674
                narrow: this.formatDate(date, { weekday: 'narrow' }),
8,106✔
675
                long: this.formatDate(date, { weekday: 'long' }),
8,106✔
676
            };
8,106✔
677
        });
1,158✔
678
    }
1,158✔
679

2✔
680
    /**
2✔
681
     * Defines the 2D-array with the dates of the current month
2✔
682
     */
2✔
683
    private setCurrentMonthDates(): void {
2✔
684
        const numberOfWeeks = getWeeksInMonth(this.currentDate, this.locale);
1,892✔
685
        const newCurrentMonthDates = new Array(numberOfWeeks);
1,892✔
686
        for (const weekIndex of new Array(numberOfWeeks).keys())
1,892✔
687
            newCurrentMonthDates[weekIndex] = this.getDatesInWeek(
1,892✔
688
                this.currentDate,
10,928✔
689
                weekIndex
10,928✔
690
            );
10,928✔
691
        this.currentMonthDates = newCurrentMonthDates;
1,892✔
692
    }
1,892✔
693

2✔
694
    /**
2✔
695
     * Returns an array with all days of the week in a specific month, corresponding to the given index,
2✔
696
     * starting with the first day of the week according to the locale
2✔
697
     *
2✔
698
     * @param weekIndex - The index of the week
2✔
699
     */
2✔
700
    private getDatesInWeek(
2✔
701
        monthDate: CalendarDate,
10,928✔
702
        weekIndex: number
10,928✔
703
    ): CalendarDate[] {
10,928✔
704
        const dates: CalendarDate[] = [];
10,928✔
705

10,928✔
706
        let date = startOfWeek(
10,928✔
707
            startOfMonth(monthDate).add({
10,928✔
708
                weeks: weekIndex,
10,928✔
709
            }),
10,928✔
710
            this.locale
10,928✔
711
        );
10,928✔
712

10,928✔
713
        while (dates.length < DAYS_PER_WEEK) {
10,928✔
714
            dates.push(date);
76,493✔
715
            const nextDate = date.add({ days: 1 });
76,493✔
716

76,493✔
717
            // If the next day is the same, we have hit the end of the calendar system
76,493✔
718
            if (isSameDay(date, nextDate)) break;
76,493✔
719
            date = nextDate;
76,490✔
720
        }
76,490✔
721

10,928✔
722
        return dates;
10,928✔
723
    }
10,928✔
724

2✔
725
    private isMinLimitReached(calendarDate: CalendarDate): boolean {
2✔
726
        return Boolean(this.min && calendarDate.compare(this.min) < 0);
36,864✔
727
    }
36,864✔
728

2✔
729
    private isMaxLimitReached(calendarDate: CalendarDate): boolean {
2✔
730
        return Boolean(this.max && calendarDate.compare(this.max) > 0);
34,742✔
731
    }
34,742✔
732

2✔
733
    /**
2✔
734
     * Formats a `CalendarDate` object using the current locale and the provided date format options
2✔
735
     *
2✔
736
     * @param calendarDate - The `CalendarDate` object that will be formatted
2✔
737
     * @param options - All date format options that will be used by the formatter
2✔
738
     */
2✔
739
    private formatDate(
2✔
740
        calendarDate: CalendarDate,
64,941✔
741
        options: Intl.DateTimeFormatOptions
64,941✔
742
    ): string {
64,941✔
743
        return new DateFormatter(this.locale, options).format(
64,941✔
744
            calendarDate.toDate(this.timeZone)
64,941✔
745
        );
64,941✔
746
    }
64,941✔
747

2✔
748
    private numberFormatter = new NumberFormatter(this.locale);
2✔
749
    private setNumberFormatter(): void {
2✔
750
        this.numberFormatter = new NumberFormatter(this.locale);
1,158✔
751
    }
1,158✔
752

2✔
753
    private formatNumber(number: number): string {
2✔
754
        return this.numberFormatter.format(number);
46,797✔
755
    }
46,797✔
756
}
2✔
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