Skip to content

Commit 2085079

Browse files
committed
feat(date-picker): integrate DateUtils for date validation and formatting; enhance error handling for invalid and disabled dates
1 parent 317624c commit 2085079

6 files changed

Lines changed: 444 additions & 192 deletions

File tree

src/components/date-picker/components/calendar-view.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { IDateFormatter } from '../services';
2+
import { DateUtils } from '../../../utilities/date-utils';
23

34
export interface CalendarViewConfig {
45
formatter: IDateFormatter;
@@ -132,12 +133,7 @@ export class CalendarView {
132133

133134
private isSameDate(date1: Date | null, date2: Date | null): boolean {
134135
if (!date1 || !date2) return false;
135-
136-
return (
137-
date1.getFullYear() === date2.getFullYear() &&
138-
date1.getMonth() === date2.getMonth() &&
139-
date1.getDate() === date2.getDate()
140-
);
136+
return DateUtils.isSameDay(date1, date2);
141137
}
142138

143139
private hasEventsOnDate(date: Date): boolean {
@@ -157,14 +153,6 @@ export class CalendarView {
157153
}
158154

159155
private isDateDisabled(date: Date): boolean {
160-
if (this.config.minDate && date < this.config.minDate) {
161-
return true;
162-
}
163-
164-
if (this.config.maxDate && date > this.config.maxDate) {
165-
return true;
166-
}
167-
168-
return false;
156+
return DateUtils.isDateDisabled(date, this.config.minDate, this.config.maxDate);
169157
}
170158
}

src/components/date-picker/date-picker.scss

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,52 @@
8383
}
8484
}
8585

86+
/* Error message for invalid/disabled date input */
87+
.date-picker-input-error {
88+
position: absolute;
89+
top: calc(100% + 4px);
90+
left: 0;
91+
right: 0;
92+
padding: 0.5rem;
93+
background-color: #fff1f1;
94+
border: 1px solid #ff8a8a;
95+
border-radius: 0.25rem;
96+
color: #d42626;
97+
font-size: 0.75rem;
98+
line-height: 1.4;
99+
z-index: 10;
100+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
101+
animation: slideIn 0.2s ease-out;
102+
}
103+
104+
.date-picker-input-invalid {
105+
border-color: #ff4d4d !important;
106+
box-shadow: 0 0 0 1px rgba(255, 77, 77, 0.5) !important;
107+
}
108+
109+
/* Dark theme styles for error messages */
110+
&[data-theme="dark"] {
111+
.date-picker-input-error {
112+
background-color: #3b1a1a;
113+
border-color: #6e2a2a;
114+
color: #ff8a8a;
115+
}
116+
}
117+
118+
/* High contrast theme styles for error messages */
119+
&[data-theme="high-contrast"] {
120+
.date-picker-input-error {
121+
background-color: #000;
122+
border: 2px solid #f00;
123+
color: #f00;
124+
font-weight: bold;
125+
}
126+
127+
.date-picker-input-invalid {
128+
border: 2px solid #f00 !important;
129+
}
130+
}
131+
86132
.date-picker-dialog {
87133
position: absolute;
88134
z-index: 100;

src/components/date-picker/date-picker.ts

Lines changed: 199 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -422,21 +422,145 @@ export class DatePicker extends HTMLElement implements EventListenerObject {
422422
this.setRangeFromString(inputValue);
423423
} else {
424424
const date = this.formatter.parse(inputValue);
425+
425426
if (!isNaN(date.getTime())) {
426-
this.stateService.selectedDate = date;
427-
this.stateService.viewDate = new Date(date);
427+
// Check if the date is disabled before setting it
428+
if (this.stateService.isDateDisabled(date)) {
429+
this.showDisabledDateFeedback(date);
430+
} else {
431+
// Valid date, update state
432+
this.stateService.selectedDate = date;
433+
this.stateService.viewDate = new Date(date);
434+
this.clearDateInputError();
435+
}
428436
} else {
429437
// Invalid date format, restore previous value
438+
this.showInvalidDateFormatFeedback();
430439
this.updateInputDisplay();
431440
}
432441
}
433442
} catch (e) {
434443
console.error("Error parsing input date:", e);
435444
// Invalid format, restore previous value
445+
this.showInvalidDateFormatFeedback();
436446
this.updateInputDisplay();
437447
}
438448
}
439449

450+
/**
451+
* Display feedback when user tries to select a disabled date
452+
*/
453+
private showDisabledDateFeedback(date: Date): void {
454+
// Get the reason why this date is disabled
455+
const reason = this.stateService.getDisabledDateReason(date);
456+
457+
// Create or update the error message element
458+
let errorElement = this.querySelector('.date-picker-input-error');
459+
if (!errorElement) {
460+
errorElement = document.createElement('div');
461+
errorElement.className = 'date-picker-input-error';
462+
this.inputWrapperElement.insertAdjacentElement('afterend', errorElement);
463+
}
464+
465+
// Add error class to the input
466+
this.inputElement.classList.add('date-picker-input-invalid');
467+
468+
// Find the nearest available date as a suggestion
469+
const formattedDate = this.formatter.format(date, this.stateService.format);
470+
let suggestMessage = '';
471+
472+
// Try to find a valid date within one week in either direction
473+
const today = new Date();
474+
const oneWeekForward = new Date(today);
475+
oneWeekForward.setDate(today.getDate() + 7);
476+
477+
const nearestAvailableDate = this.findNearestAvailableDate(date);
478+
if (nearestAvailableDate) {
479+
const formattedNearestDate = this.formatter.format(nearestAvailableDate, this.stateService.format);
480+
suggestMessage = ` Try ${formattedNearestDate} instead.`;
481+
}
482+
483+
// Set the error message with the reason and suggestion
484+
if (reason) {
485+
errorElement.textContent = `${formattedDate} can't be selected: ${reason}.${suggestMessage}`;
486+
} else {
487+
errorElement.textContent = `${formattedDate} is not available.${suggestMessage}`;
488+
}
489+
490+
// Restore the previous value
491+
this.updateInputDisplay();
492+
493+
// Auto-hide the error after 5 seconds
494+
setTimeout(() => {
495+
this.clearDateInputError();
496+
}, 5000);
497+
}
498+
499+
/**
500+
* Display feedback for invalid date format
501+
*/
502+
private showInvalidDateFormatFeedback(): void {
503+
// Create or update the error message element
504+
let errorElement = this.querySelector('.date-picker-input-error');
505+
if (!errorElement) {
506+
errorElement = document.createElement('div');
507+
errorElement.className = 'date-picker-input-error';
508+
this.inputWrapperElement.insertAdjacentElement('afterend', errorElement);
509+
}
510+
511+
// Add error class to the input
512+
this.inputElement.classList.add('date-picker-input-invalid');
513+
514+
// Show a format hint
515+
errorElement.textContent = `Invalid date. Please use format: ${this.stateService.format}`;
516+
517+
// Auto-hide the error after 5 seconds
518+
setTimeout(() => {
519+
this.clearDateInputError();
520+
}, 5000);
521+
}
522+
523+
/**
524+
* Clear any input error state
525+
*/
526+
private clearDateInputError(): void {
527+
const errorElement = this.querySelector('.date-picker-input-error');
528+
if (errorElement) {
529+
errorElement.remove();
530+
}
531+
532+
this.inputElement.classList.remove('date-picker-input-invalid');
533+
}
534+
535+
/**
536+
* Find the nearest available date to a given date
537+
* @param date The reference date
538+
* @returns The nearest available date or null if none found within reasonable range
539+
*/
540+
private findNearestAvailableDate(date: Date): Date | null {
541+
const maxDays = 14; // Look up to 2 weeks in either direction
542+
const dateToCheck = new Date(date);
543+
544+
// Check forward
545+
for (let i = 1; i <= maxDays; i++) {
546+
dateToCheck.setDate(date.getDate() + i);
547+
if (!this.stateService.isDateDisabled(dateToCheck)) {
548+
return new Date(dateToCheck);
549+
}
550+
551+
// Also check backward on the same iteration
552+
dateToCheck.setDate(date.getDate() - i);
553+
if (!this.stateService.isDateDisabled(dateToCheck)) {
554+
return new Date(dateToCheck);
555+
}
556+
557+
// Reset for next iteration
558+
dateToCheck.setTime(date.getTime());
559+
}
560+
561+
return null; // No available date found within the range
562+
}
563+
440564
/**
441565
* Update the input display to match the current state
442566
*/
@@ -551,14 +675,6 @@ export class DatePicker extends HTMLElement implements EventListenerObject {
551675
public toggleCalendar(): void {
552676
const wasOpen = this.stateService.isOpen;
553677
this.stateService.isOpen = !wasOpen;
554-
555-
// If opening the calendar, maintain focus on the input element
556-
if (!wasOpen) {
557-
// Use setTimeout to ensure the focus happens after rendering
558-
setTimeout(() => {
559-
this.inputElement.focus();
560-
}, 0);
561-
}
562678
}
563679

564680
/**
@@ -595,12 +711,77 @@ export class DatePicker extends HTMLElement implements EventListenerObject {
595711
}
596712

597713
try {
598-
const rangeParts = value.split('-').map(part => part.trim());
714+
// Look for a separator between dates (dash, "to", or other common separators)
715+
let rangeParts: string[] = [];
716+
717+
// Try standard dash separator first (most common)
718+
if (value.includes('-')) {
719+
rangeParts = value.split('-').map(part => part.trim());
720+
}
721+
// Try "to" as separator
722+
else if (value.toLowerCase().includes('to')) {
723+
rangeParts = value.toLowerCase().split('to').map(part => part.trim());
724+
}
725+
// Try slash as separator (less common, but possible user input)
726+
else if (value.split('/').length > 2) {
727+
// This might be a complex case like "04/15/2025 / 04/26/2025"
728+
// Try to intelligently split this based on the format
729+
const formatParts = this.stateService.format.split('-');
730+
if (formatParts.length === 3) {
731+
// Count separators expected in a single date based on the format
732+
const expectedSeparators = formatParts.join('').split('').filter(c => /[^A-Za-z0-9]/.test(c)).length;
733+
734+
// Find the position where the second date starts
735+
const separatorCount = [...value].reduce((count, char, index) => {
736+
if (/[^A-Za-z0-9]/.test(char)) count++;
737+
if (count === expectedSeparators + 1) return index;
738+
return count;
739+
}, 0);
740+
741+
if (typeof separatorCount === 'number' && separatorCount > 0) {
742+
rangeParts = [
743+
value.substring(0, separatorCount).trim(),
744+
value.substring(separatorCount + 1).trim()
745+
];
746+
}
747+
}
748+
}
749+
750+
// If we couldn't parse it using known separators, try to infer based on the format pattern
751+
if (rangeParts.length !== 2) {
752+
// Get the expected length of a single date based on the format
753+
const sampleDate = new Date();
754+
const formattedSample = this.formatter.format(sampleDate, this.stateService.format);
755+
const expectedLength = formattedSample.length;
756+
757+
// If the input is roughly twice the expected length, try to split in the middle
758+
if (value.length >= expectedLength * 1.8) {
759+
// Find a natural break point (space, comma, etc.) near the middle
760+
const midPoint = Math.floor(value.length / 2);
761+
let splitIndex = value.indexOf(' ', midPoint - 3);
762+
763+
if (splitIndex === -1) {
764+
splitIndex = value.indexOf(',', midPoint - 3);
765+
}
766+
767+
if (splitIndex === -1) {
768+
// No natural break point, just split in the middle
769+
splitIndex = midPoint;
770+
}
771+
772+
rangeParts = [
773+
value.substring(0, splitIndex).trim(),
774+
value.substring(splitIndex).trim()
775+
];
776+
}
777+
}
778+
779+
// Try to parse both parts as dates
599780
if (rangeParts.length === 2) {
600781
const start = this.formatter.parse(rangeParts[0]);
601782
const end = this.formatter.parse(rangeParts[1]);
602783

603-
if (start && end && !isNaN(start.getTime()) && !isNaN(end.getTime())) {
784+
if (!isNaN(start.getTime()) && !isNaN(end.getTime())) {
604785
if (start > end) {
605786
this.stateService.rangeStart = end;
606787
this.stateService.rangeEnd = start;
@@ -609,10 +790,16 @@ export class DatePicker extends HTMLElement implements EventListenerObject {
609790
this.stateService.rangeEnd = end;
610791
}
611792
this.stateService.viewDate = new Date(this.stateService.rangeStart);
793+
return;
612794
}
613795
}
796+
797+
// If we get here, we couldn't parse the input as a valid range
798+
console.warn("Could not parse input as date range:", value);
799+
this.updateInputDisplay(); // Restore previous valid value
614800
} catch (e) {
615801
console.error("Error parsing date range:", e);
802+
this.updateInputDisplay(); // Restore previous valid value
616803
}
617804
}
618805

0 commit comments

Comments
 (0)