@@ -2,6 +2,8 @@ import { format as dfFormat, isValid as dfIsValid, parse as dfParse } from 'date
22import React from 'react' ;
33import styled from 'styled-components' ;
44
5+ import { CalendarDate , fromDate , getLocalTimeZone , type DateValue } from '@internationalized/date' ;
6+
57import type { Matcher , DateRange as RdpRange } from 'react-day-picker' ;
68import { DropdownSelectIcon , DropupSelectIcon } from '../../../icons' ;
79import { CalendarTodayOutlineIcon } from '../../../icons/experimental' ;
@@ -14,6 +16,8 @@ import { Chip, ChipRemoveButton, Chips } from './DatePicker.styled';
1416
1517type DateRange = RdpRange | undefined ;
1618
19+ type Mode = 'single' | 'multiple' | 'range' ;
20+
1721type CommonProps = Pick < FieldProps , 'description' | 'errorMessage' > & {
1822 label ?: string ;
1923 placeholder ?: string ;
@@ -110,6 +114,8 @@ export function DatePicker(props: DatePickerProps): JSX.Element {
110114 const legacyMaxValue = maxValue ;
111115 const legacyIsDisabled = isDisabled ;
112116
117+ const modeLocal : Mode = ( props as { mode ?: Mode } ) . mode ?? 'single' ;
118+
113119 const minDateCompat = toJSDate ( legacyMinValue ) ?? minDate ;
114120 const maxDateCompat = toJSDate ( legacyMaxValue ) ?? maxDate ;
115121
@@ -120,23 +126,23 @@ export function DatePicker(props: DatePickerProps): JSX.Element {
120126 const positionRef = React . useRef < HTMLDivElement | null > ( null ) ;
121127 const triggerRef = React . useRef < HTMLButtonElement | null > ( null ) ;
122128 const contentId = React . useId ( ) ;
123- const inputId = id ?? `dp-${ mode } ` ;
129+ const inputId = id ?? `dp-${ modeLocal } ` ;
124130
125131 // current values by mode
126- const isControlledSingle = mode === 'single' && ( props as SingleProps ) . value instanceof Date ;
132+ const isControlledSingle = modeLocal === 'single' && ( props as SingleProps ) . value instanceof Date ;
127133 const singleSource : Date | null =
128- mode === 'single' ? ( isControlledSingle ? ( props as SingleProps ) . value : internalSingle ) : null ;
129- const singleValue = mode === 'single' ? ( props as SingleProps ) . value : null ;
130- const multipleValue = mode === 'multiple' ? ( props as MultipleProps ) . value : undefined ;
131- const rangeValue = mode === 'range' ? ( props as RangeProps ) . value : undefined ;
134+ modeLocal === 'single' ? ( isControlledSingle ? ( props as SingleProps ) . value : internalSingle ) : null ;
135+ const singleValue = modeLocal === 'single' ? ( props as SingleProps ) . value : null ;
136+ const multipleValue = modeLocal === 'multiple' ? ( props as MultipleProps ) . value : undefined ;
137+ const rangeValue = modeLocal === 'range' ? ( props as RangeProps ) . value : undefined ;
132138
133- const sepForRange = React . useMemo < string > ( ( ) => getSeparator ( props ) , [ mode , ( props as RangeProps ) . separator ] ) ;
139+ const sepForRange = React . useMemo < string > ( ( ) => getSeparator ( props ) , [ modeLocal , ( props as RangeProps ) . separator ] ) ;
134140
135141 const neutralPlaceholder =
136142 placeholder ??
137- ( mode === 'range'
143+ ( modeLocal === 'range'
138144 ? `dd / mm / yyyy${ sepForRange } dd / mm / yyyy`
139- : mode === 'multiple'
145+ : modeLocal === 'multiple'
140146 ? 'Select dates'
141147 : 'dd / mm / yyyy' ) ;
142148
@@ -145,22 +151,22 @@ export function DatePicker(props: DatePickerProps): JSX.Element {
145151
146152 // visible month
147153 const [ month , setMonth ] = React . useState < Date | undefined > (
148- mode === 'single'
154+ modeLocal === 'single'
149155 ? singleValue ?? initialMonth
150- : mode === 'multiple'
156+ : modeLocal === 'multiple'
151157 ? multipleValue ?. [ 0 ] ?? initialMonth
152158 : rangeValue ?. from ?? initialMonth
153159 ) ;
154160
155161 React . useEffect ( ( ) => {
156- if ( mode === 'single' ) {
162+ if ( modeLocal === 'single' ) {
157163 const source = singleSource ;
158164 setText ( source ? dfFormat ( source , displayFormat , { locale } ) : '' ) ;
159165 if ( source ) setMonth ( source ) ;
160166 return ;
161167 }
162168
163- if ( mode === 'range' ) {
169+ if ( modeLocal === 'range' ) {
164170 const a = rangeValue ?. from ? dfFormat ( rangeValue . from , displayFormat , { locale } ) : '' ;
165171 const b = rangeValue ?. to ? dfFormat ( rangeValue . to , displayFormat , { locale } ) : '' ;
166172 setText ( a || b ? `${ a } ${ sepForRange } ${ b } ` : '' ) ;
@@ -172,7 +178,7 @@ export function DatePicker(props: DatePickerProps): JSX.Element {
172178 // multiple
173179 if ( multipleValue ?. [ 0 ] ) setMonth ( multipleValue [ 0 ] ) ;
174180 } , [
175- mode ,
181+ modeLocal ,
176182 displayFormat ,
177183 locale ,
178184 singleSource ?. getTime ?.( ) ,
@@ -304,80 +310,113 @@ export function DatePicker(props: DatePickerProps): JSX.Element {
304310 return (
305311 < div ref = { positionRef } aria-label = { label } >
306312 < div style = { { position : 'relative' } } >
307- < DateField
308- variant = "text"
309- id = { inputId }
310- name = { name }
311- label = { label }
312- description = { description }
313- errorMessage = { errorMessage }
314- isInvalid = { isInvalid }
315- isVisuallyFocused = { open }
316- leadingIcon = { < CalendarTodayOutlineIcon /> }
317- value = { inputValue }
318- placeholder = { neutralPlaceholder }
319- onChange = { ( v : string ) => {
320- if ( readOnly ) return ;
321- setText ( v ) ;
322- // optimistic month update for valid partials
323- const tmp =
324- mode === 'single'
325- ? tryParse ( v , displayFormat , locale )
326- : tryParse ( v . split ( sepForRange ) [ 0 ] ?. trim ( ) , displayFormat , locale ) ;
327- if ( tmp ) setMonth ( tmp ) ;
328- } }
329- inputProps = { {
330- role : 'combobox' ,
331- 'aria-haspopup' : 'dialog' ,
332- 'aria-expanded' : open ,
333- 'aria-controls' : contentId ,
334- 'aria-autocomplete' : 'none' ,
335- readOnly,
336- autoFocus,
337- onBlur : event => {
338- const nextEl = event . relatedTarget as HTMLElement | null ;
339- if ( nextEl && nextEl === triggerRef . current ) return ;
340- if ( mode === 'single' ) commitSingle ( event . currentTarget . value ) ;
341- else if ( mode === 'range' ) commitRange ( event . currentTarget . value , sepForRange ) ;
342- } ,
343- onKeyDown : ( event : React . KeyboardEvent < HTMLInputElement > ) => {
344- switch ( event . key ) {
345- case 'ArrowDown' :
346- event . preventDefault ( ) ;
347- setOpen ( true ) ;
348- break ;
349- case 'Enter' : {
350- const v = ( event . target as HTMLInputElement ) . value ;
351- if ( mode === 'single' ) commitSingle ( v ) ;
352- else if ( mode === 'range' ) commitRange ( v , sepForRange ) ;
353- break ;
313+ { mode === 'single' ? (
314+ < DateField
315+ variant = "segments"
316+ id = { inputId }
317+ name = { name }
318+ label = { label }
319+ description = { description }
320+ errorMessage = { errorMessage }
321+ isInvalid = { isInvalid }
322+ isVisuallyFocused = { open }
323+ leadingIcon = { < CalendarTodayOutlineIcon /> }
324+ value = { singleSource ? dateToCalendarDate ( singleSource ) : undefined }
325+ onChange = { ( dv : DateValue | null | undefined ) => {
326+ const next = dv ? calendarDateToDate ( dv ) : null ;
327+ handleSelectSingle ( next ) ;
328+ } }
329+ autoFocus = { autoFocus }
330+ actionIcon = {
331+ < Button
332+ ref = { triggerRef }
333+ isDisabled = { legacyIsDisabled }
334+ onPress = { ( ) => ! legacyIsDisabled && setOpen ( v => ! v ) }
335+ aria-haspopup = "dialog"
336+ aria-expanded = { open }
337+ aria-controls = { contentId }
338+ aria-label = { label ? `${ label } : open calendar` : 'Open calendar' }
339+ >
340+ { open ? < DropupSelectIcon /> : < DropdownSelectIcon /> }
341+ </ Button >
342+ }
343+ />
344+ ) : (
345+ < DateField
346+ variant = "text"
347+ id = { inputId }
348+ name = { name }
349+ label = { label }
350+ description = { description }
351+ errorMessage = { errorMessage }
352+ isInvalid = { isInvalid }
353+ isVisuallyFocused = { open }
354+ leadingIcon = { < CalendarTodayOutlineIcon /> }
355+ value = { inputValue }
356+ placeholder = { neutralPlaceholder }
357+ onChange = { ( v : string ) => {
358+ if ( readOnly ) return ;
359+ setText ( v ) ;
360+ // optimistic month update for valid partials
361+ const tmp =
362+ modeLocal === 'single'
363+ ? tryParse ( v , displayFormat , locale )
364+ : tryParse ( v . split ( sepForRange ) [ 0 ] ?. trim ( ) , displayFormat , locale ) ;
365+ if ( tmp ) setMonth ( tmp ) ;
366+ } }
367+ inputProps = { {
368+ role : 'combobox' ,
369+ 'aria-haspopup' : 'dialog' ,
370+ 'aria-expanded' : open ,
371+ 'aria-controls' : contentId ,
372+ 'aria-autocomplete' : 'none' ,
373+ readOnly,
374+ autoFocus,
375+ onBlur : event => {
376+ const nextEl = event . relatedTarget as HTMLElement | null ;
377+ if ( nextEl && nextEl === triggerRef . current ) return ;
378+ if ( modeLocal === 'single' ) commitSingle ( event . currentTarget . value ) ;
379+ else if ( modeLocal === 'range' ) commitRange ( event . currentTarget . value , sepForRange ) ;
380+ } ,
381+ onKeyDown : ( event : React . KeyboardEvent < HTMLInputElement > ) => {
382+ switch ( event . key ) {
383+ case 'ArrowDown' :
384+ event . preventDefault ( ) ;
385+ setOpen ( true ) ;
386+ break ;
387+ case 'Enter' : {
388+ const v = ( event . target as HTMLInputElement ) . value ;
389+ if ( modeLocal === 'single' ) commitSingle ( v ) ;
390+ else if ( modeLocal === 'range' ) commitRange ( v , sepForRange ) ;
391+ break ;
392+ }
393+ case 'Escape' :
394+ setOpen ( false ) ;
395+ break ;
396+ default :
397+ break ;
354398 }
355- case 'Escape' :
356- setOpen ( false ) ;
357- break ;
358- default :
359- break ;
360399 }
400+ } }
401+ actionIcon = {
402+ < Button
403+ ref = { triggerRef }
404+ isDisabled = { legacyIsDisabled }
405+ onPress = { ( ) => ! legacyIsDisabled && setOpen ( v => ! v ) }
406+ aria-haspopup = "dialog"
407+ aria-expanded = { open }
408+ aria-controls = { contentId }
409+ aria-label = { label ? `${ label } : open calendar` : 'Open calendar' }
410+ >
411+ { open ? < DropupSelectIcon /> : < DropdownSelectIcon /> }
412+ </ Button >
361413 }
362- } }
363- actionIcon = {
364- < Button
365- ref = { triggerRef }
366- isDisabled = { legacyIsDisabled }
367- onPress = { ( ) => ! legacyIsDisabled && setOpen ( v => ! v ) }
368- aria-haspopup = "dialog"
369- aria-expanded = { open }
370- aria-controls = { contentId }
371- aria-label = { label ? `${ label } : open calendar` : 'Open calendar' }
372- >
373- { open ? < DropupSelectIcon /> : < DropdownSelectIcon /> }
374- </ Button >
375- }
376- />
414+ />
415+ ) }
377416 </ div >
378417
379418 { /* chips for multiple */ }
380- { mode === 'multiple' && ( multipleValue ?. length ?? 0 ) > 0 && (
419+ { modeLocal === 'multiple' && ( multipleValue ?. length ?? 0 ) > 0 && (
381420 < Chips aria-label = "Selected dates" >
382421 { multipleValue . map ( d => {
383422 const key = stripTime ( d ) . getTime ( ) ; // stable per day
@@ -418,7 +457,7 @@ export function DatePicker(props: DatePickerProps): JSX.Element {
418457 < FocusTrap role = "dialog" >
419458 < div id = { contentId } ref = { contentRef } >
420459 { /* eslint-disable react/jsx-no-bind */ }
421- { mode === 'single' && (
460+ { modeLocal === 'single' && (
422461 < Calendar
423462 selectionType = "single"
424463 { ...commonCalProps }
@@ -428,7 +467,7 @@ export function DatePicker(props: DatePickerProps): JSX.Element {
428467 />
429468 ) }
430469
431- { mode === 'multiple' && (
470+ { modeLocal === 'multiple' && (
432471 < Calendar
433472 selectionType = "multiple"
434473 { ...commonCalProps }
@@ -438,7 +477,7 @@ export function DatePicker(props: DatePickerProps): JSX.Element {
438477 />
439478 ) }
440479
441- { mode === 'range' && (
480+ { modeLocal === 'range' && (
442481 < Calendar
443482 selectionType = "range"
444483 { ...commonCalProps }
@@ -498,3 +537,14 @@ function toJSDate(d: any): Date | undefined {
498537 }
499538 return undefined ;
500539}
540+
541+ function dateToCalendarDate ( d : Date ) : CalendarDate {
542+ const zdt = fromDate ( d , getLocalTimeZone ( ) ) ;
543+ return new CalendarDate ( zdt . year , zdt . month , zdt . day ) ;
544+ }
545+
546+ function calendarDateToDate ( dv : DateValue ) : Date {
547+ // DateValue has year/month/day in Gregorian by default
548+ // Construct a JS Date in local time at midnight.
549+ return new Date ( dv . year , dv . month - 1 , dv . day ) ;
550+ }
0 commit comments