@@ -36,20 +36,20 @@ import {
3636 useSlottedContext
3737} from 'react-aria-components' ;
3838import { AriaCalendarGridProps } from '@react-aria/calendar' ;
39- import { baseColor , focusRing , lightDark , style } from '../style' with { type : 'macro' } ;
4039import {
4140 CalendarDate ,
4241 getDayOfWeek ,
4342 startOfMonth
4443} from '@internationalized/date' ;
4544import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg' ;
4645import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg' ;
46+ import { focusRing , lightDark , style } from '../style' with { type : 'macro' } ;
4747import { forwardRefType , GlobalDOMAttributes } from '@react-types/shared' ;
4848import { getAllowedOverrides , StyleProps } from './style-utils' with { type : 'macro' } ;
4949import { helpTextStyles } from './Field' ;
5050// @ts -ignore
5151import intlMessages from '../intl/*.json' ;
52- import React , { createContext , CSSProperties , ForwardedRef , forwardRef , Fragment , PropsWithChildren , ReactElement , ReactNode , useContext , useMemo , useRef } from 'react' ;
52+ import React , { createContext , ForwardedRef , forwardRef , Fragment , PropsWithChildren , ReactElement , ReactNode , useContext , useMemo , useRef } from 'react' ;
5353import { useDateFormatter , useLocale , useLocalizedStringFormatter } from '@react-aria/i18n' ;
5454import { useSpectrumContextProps } from './useSpectrumContextProps' ;
5555
@@ -135,7 +135,10 @@ const cellStyles = style({
135135 default : 2 ,
136136 isFirstWeek : 0
137137 } ,
138- paddingBottom : 2 ,
138+ paddingBottom : {
139+ default : 2 ,
140+ isLastWeek : 0
141+ } ,
139142 position : 'relative' ,
140143 width : 32 ,
141144 height : 32 ,
@@ -156,7 +159,6 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
156159 } ,
157160 outlineOffset : {
158161 default : - 2 ,
159- isToday : 2 ,
160162 isSelected : {
161163 selectionMode : {
162164 single : 2 ,
@@ -184,10 +186,6 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
184186 } ,
185187 isPressed : 'gray-100' ,
186188 isDisabled : 'transparent' ,
187- isToday : {
188- default : baseColor ( 'gray-300' ) ,
189- isDisabled : 'disabled'
190- } ,
191189 isSelected : {
192190 selectionMode : {
193191 single : {
@@ -254,7 +252,6 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
254252 } ,
255253 forcedColors : {
256254 default : 'transparent' ,
257- isToday : 'ButtonFace' ,
258255 isHovered : 'Highlight' ,
259256 isSelected : {
260257 selectionMode : {
@@ -282,7 +279,6 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
282279 isDisabled : 'disabled' ,
283280 forcedColors : {
284281 default : 'ButtonText' ,
285- isToday : 'ButtonFace' ,
286282 isSelected : 'HighlightText' ,
287283 isSelectionStart : 'HighlightText' ,
288284 isSelectionEnd : 'HighlightText' ,
@@ -291,6 +287,21 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
291287 }
292288} ) ;
293289
290+ const todayStyles = style ( {
291+ position : 'absolute' ,
292+ bottom : 4 ,
293+ left : '50%' ,
294+ transform : 'translateX(-50%)' ,
295+ width : 4 ,
296+ height : 4 ,
297+ borderRadius : 'full' ,
298+ backgroundColor : '[currentColor]' ,
299+ display : {
300+ default : 'none' ,
301+ isToday : 'block'
302+ }
303+ } ) ;
304+
294305const unavailableStyles = style ( {
295306 position : 'absolute' ,
296307 top : 'calc(50% - 1px)' ,
@@ -302,24 +313,35 @@ const unavailableStyles = style({
302313 backgroundColor : '[currentColor]'
303314} ) ;
304315
305- const selectionSpanStyles = style < { isInvalid ?: boolean } > ( {
316+ const selectionBackgroundStyles = style < { isInvalid ?: boolean , isFirstDayInWeek ?: boolean , isLastDayInWeek ?: boolean , isSelectionStart ?: boolean , isSelectionEnd ?: boolean , isPreviousDayNotSelected ?: boolean , isNextDayNotSelected ?: boolean } > ( {
306317 position : 'absolute' ,
307318 zIndex : - 1 ,
308319 top : 0 ,
309- insetStart : 'calc(-1 * var(--selection-span) * (var(--cell-width) + var(--cell-gap) + var(--cell-gap)))' ,
310- insetEnd : 0 ,
320+ insetStart : {
321+ default : - 4 ,
322+ isFirstDayInWeek : 0 ,
323+ isSelectionStart : 0 ,
324+ isPreviousDayNotSelected : 0
325+ } ,
326+ insetEnd : {
327+ default : - 4 ,
328+ isLastDayInWeek : 0 ,
329+ isSelectionEnd : 0 ,
330+ isNextDayNotSelected : 0
331+ } ,
311332 bottom : 0 ,
312- borderWidth : 2 ,
313- borderStyle : 'dashed' ,
314- borderColor : {
315- default : 'blue-800' , // focus-indicator-color
316- isInvalid : 'negative-900' ,
317- forcedColors : {
318- default : 'ButtonText'
319- }
333+ borderStartRadius : {
334+ default : 'none' ,
335+ isFirstDayInWeek : 'full' ,
336+ isSelectionStart : 'full' ,
337+ isPreviousDayNotSelected : 'full'
338+ } ,
339+ borderEndRadius : {
340+ default : 'none' ,
341+ isLastDayInWeek : 'full' ,
342+ isSelectionEnd : 'full' ,
343+ isNextDayNotSelected : 'full'
320344 } ,
321- borderStartRadius : 'full' ,
322- borderEndRadius : 'full' ,
323345 backgroundColor : {
324346 default : 'blue-subtle' ,
325347 isInvalid : 'negative-100' ,
@@ -330,6 +352,58 @@ const selectionSpanStyles = style<{isInvalid?: boolean}>({
330352 forcedColorAdjust : 'none'
331353} ) ;
332354
355+ const selectionBorderStyles = style < { isInvalid ?: boolean , isFirstDayInWeek ?: boolean , isLastDayInWeek ?: boolean , isSelectionStart ?: boolean , isSelectionEnd ?: boolean , isPreviousDayNotSelected ?: boolean , isNextDayNotSelected ?: boolean } > ( {
356+ position : 'absolute' ,
357+ zIndex : 1 ,
358+ top : 0 ,
359+ insetStart : {
360+ default : - 4 ,
361+ isFirstDayInWeek : 0 ,
362+ isSelectionStart : 0 ,
363+ isPreviousDayNotSelected : 0
364+ } ,
365+ insetEnd : {
366+ default : - 4 ,
367+ isLastDayInWeek : 0 ,
368+ isSelectionEnd : 0 ,
369+ isNextDayNotSelected : 0
370+ } ,
371+ bottom : 0 ,
372+ borderStartWidth : {
373+ default : 0 ,
374+ isFirstDayInWeek : 1 ,
375+ isSelectionStart : 1 ,
376+ isPreviousDayNotSelected : 1
377+ } ,
378+ borderTopWidth : 1 ,
379+ borderEndWidth : {
380+ default : 0 ,
381+ isLastDayInWeek : 1 ,
382+ isSelectionEnd : 1 ,
383+ isNextDayNotSelected : 1
384+ } ,
385+ borderBottomWidth : 1 ,
386+ borderStyle : 'solid' ,
387+ borderColor : {
388+ default : 'blue-800' , // focus-indicator-color
389+ isInvalid : 'negative-900' ,
390+ forcedColors : {
391+ default : 'ButtonText'
392+ }
393+ } ,
394+ borderStartRadius : {
395+ default : 'none' ,
396+ isFirstDayInWeek : 'full' ,
397+ isSelectionStart : 'full' ,
398+ isPreviousDayNotSelected : 'full'
399+ } ,
400+ borderEndRadius : {
401+ default : 'none' ,
402+ isLastDayInWeek : 'full' ,
403+ isSelectionEnd : 'full' ,
404+ isNextDayNotSelected : 'full'
405+ }
406+ } ) ;
333407/**
334408 * Calendars display a grid of days in one or more months and allow users to select a single date.
335409 */
@@ -508,38 +582,36 @@ const CalendarCell = (props: Omit<CalendarCellProps, 'children'> & {firstDayOfWe
508582 let { locale} = useLocale ( ) ;
509583 let firstDayOfWeek = props . firstDayOfWeek ;
510584 // Calculate the day and week index based on the date.
511- let { dayIndex, weekIndex} = useWeekAndDayIndices ( props . date , locale , firstDayOfWeek ) ;
585+ let { dayIndex, weekIndex, lastWeekIndex } = useWeekAndDayIndices ( props . date , locale , firstDayOfWeek ) ;
512586
513587 let calendarStateContext = useContext ( CalendarStateContext ) ;
514588 let rangeCalendarStateContext = useContext ( RangeCalendarStateContext ) ;
515589 let state = ( calendarStateContext ?? rangeCalendarStateContext ) ! ;
516590
591+
517592 let isFirstWeek = weekIndex === 0 ;
593+ let isLastWeek = weekIndex === lastWeekIndex ;
518594 let isFirstChild = dayIndex === 0 ;
519595 let isLastChild = dayIndex === 6 ;
520596
521597 return (
522598 < AriaCalendarCell
523599 date = { props . date }
524- className = { ( renderProps ) => cellStyles ( { ...renderProps , isFirstChild, isLastChild, isFirstWeek} ) } >
600+ className = { ( renderProps ) => cellStyles ( { ...renderProps , isFirstChild, isLastChild, isFirstWeek, isLastWeek } ) } >
525601 { ( renderProps ) => < CalendarCellInner { ...props } weekIndex = { weekIndex } dayIndex = { dayIndex } state = { state } isRangeSelection = { ! ! rangeCalendarStateContext } renderProps = { renderProps } /> }
526602 </ AriaCalendarCell >
527603 ) ;
528604} ;
529605
530606const CalendarCellInner = ( props : Omit < CalendarCellProps , 'children' > & { isRangeSelection : boolean , state : CalendarState | RangeCalendarState , weekIndex : number , dayIndex : number , renderProps ?: CalendarCellRenderProps , date : DateValue } ) : ReactElement => {
531- let { weekIndex, dayIndex, date, renderProps, state, isRangeSelection} = props ;
532- let { getDatesInWeek} = state ;
607+ let { dayIndex, date, renderProps, state, isRangeSelection} = props ;
533608 let ref = useRef < HTMLDivElement > ( null ) ;
534609 let { isUnavailable, formattedDate, isSelected, isSelectionStart, isSelectionEnd, isInvalid} = renderProps ! ;
535610 // only apply the selection start/end styles if the start/end date is actually selectable (aka not unavailable)
536611 // or if the range is invalid and thus we still want to show the styles even if the start/end date is an unavailable one
537612 isSelectionStart = isSelectionStart && ( ! isUnavailable || isInvalid ) ;
538613 isSelectionEnd = isSelectionEnd && ( ! isUnavailable || isInvalid ) ;
539614
540- let startDate = startOfMonth ( date ) ;
541- let datesInWeek = getDatesInWeek ( weekIndex , startDate ) ;
542-
543615 let isDateInRange = ( checkDate : CalendarDate ) => {
544616 if ( ! ( 'highlightedRange' in state ) || ! state . highlightedRange ) {
545617 return state . isSelected ( checkDate ) ;
@@ -553,20 +625,12 @@ const CalendarCellInner = (props: Omit<CalendarCellProps, 'children'> & {isRange
553625 return state . isSelected ( checkDate ) ;
554626 } ;
555627
556- // Starting from the current day, find the first day before it in the current week that is not selected.
557- // Then, the span of selected days is the current day minus the first unselected day.
558- let firstUnselectedInRangeInWeek = datesInWeek . slice ( 0 , dayIndex + 1 ) . reverse ( ) . findIndex ( ( date , i ) => {
559- return date && i > 0 && ( ! isDateInRange ( date ) || date . month !== props . date . month ) ;
560- } ) ;
561-
562- let selectionSpan = - 1 ;
563- if ( firstUnselectedInRangeInWeek > - 1 && isSelected ) {
564- selectionSpan = firstUnselectedInRangeInWeek - 1 ;
565- } else if ( isSelected ) {
566- selectionSpan = dayIndex ;
567- }
568628 let prevDay = date . subtract ( { days : 1 } ) ;
569629 let nextDay = date . add ( { days : 1 } ) ;
630+ let isFirstDayInWeek = dayIndex === 0 ;
631+ let isLastDayInWeek = dayIndex === 6 ;
632+ let isPreviousDayNotSelected = ! prevDay || ( ! isDateInRange ( prevDay ) || prevDay . month !== props . date . month ) ;
633+ let isNextDayNotSelected = ! nextDay || ( ! isDateInRange ( nextDay ) || nextDay . month !== props . date . month ) ;
570634
571635 // when invalid, show background for all selected dates (including unavailable) to make continuous range appearance
572636 // when valid, only show background for available selected dates
@@ -592,12 +656,14 @@ const CalendarCellInner = (props: Omit<CalendarCellProps, 'children'> & {isRange
592656 ref = { ref }
593657 style = { pressScale ( ref , { } ) ( renderProps ! ) }
594658 className = { cellInnerStyles ( { ...renderProps ! , isSelectionStart, isSelectionEnd, selectionMode : isRangeSelection ? 'range' : 'single' } ) } >
659+ < div className = { todayStyles ( renderProps ! ) } role = "presentation" />
595660 < div >
596661 { formattedDate }
597662 </ div >
598663 { isUnavailable && < div className = { unavailableStyles } role = "presentation" /> }
599664 </ div >
600- { isBackgroundStyleApplied && < div style = { { '--selection-span' : selectionSpan } as CSSProperties } className = { selectionSpanStyles ( { isInvalid} ) } role = "presentation" /> }
665+ { isBackgroundStyleApplied && < div className = { selectionBackgroundStyles ( { isInvalid, isFirstDayInWeek, isLastDayInWeek, isSelectionStart, isSelectionEnd, isPreviousDayNotSelected, isNextDayNotSelected} ) } role = "presentation" /> }
666+ { isBackgroundStyleApplied && < div className = { selectionBorderStyles ( { isInvalid, isFirstDayInWeek, isLastDayInWeek, isSelectionStart, isSelectionEnd, isPreviousDayNotSelected, isNextDayNotSelected} ) } role = "presentation" /> }
601667 </ div >
602668 ) ;
603669} ;
@@ -616,7 +682,7 @@ function useWeekAndDayIndices(
616682 locale : string ,
617683 firstDayOfWeek ?: DayOfWeek
618684) {
619- let { dayIndex , weekIndex } = useMemo ( ( ) => {
685+ let result = useMemo ( ( ) => {
620686 // Get the day index within the week (0-6)
621687 const dayIndex = getDayOfWeek ( date , locale , firstDayOfWeek ) ;
622688
@@ -628,12 +694,15 @@ function useWeekAndDayIndices(
628694 const dayOfMonth = date . day ;
629695
630696 const weekIndex = Math . floor ( ( dayOfMonth + monthStartDayOfWeek - 1 ) / 7 ) ;
697+ const lastDayOfMonth = startOfMonth ( date ) . add ( { months : 1 } ) . subtract ( { days : 1 } ) ;
698+ const lastWeekIndex = Math . floor ( ( lastDayOfMonth . day + monthStartDayOfWeek - 1 ) / 7 ) ;
631699
632700 return {
633701 weekIndex,
702+ lastWeekIndex,
634703 dayIndex
635704 } ;
636705 } , [ date , locale , firstDayOfWeek ] ) ;
637706
638- return { dayIndex , weekIndex } ;
707+ return result ;
639708}
0 commit comments