@@ -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 - Z a - z 0 - 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 - Z a - z 0 - 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