From 12a5a28fa006460a52a09381b0d435089c467962 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Wed, 4 Feb 2026 09:21:58 +1100 Subject: [PATCH 01/10] chore: calendar new today indicator --- packages/@react-spectrum/s2/src/Calendar.tsx | 27 +++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 57736226afd..d267da9bbaf 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -156,7 +156,6 @@ const cellInnerStyles = style({ insetStart: 'calc(-1 * var(--selection-span) * (var(--cell-width) + var(--cell-gap) + var(--cell-gap)))', insetEnd: 0, bottom: 0, - borderWidth: 2, - borderStyle: 'dashed', + borderWidth: 1, + borderStyle: 'solid', borderColor: { default: 'blue-800', // focus-indicator-color isInvalid: 'negative-900', @@ -592,6 +600,7 @@ const CalendarCellInner = (props: Omit & {isRange ref={ref} style={pressScale(ref, {})(renderProps!)} className={cellInnerStyles({...renderProps!, isSelectionStart, isSelectionEnd, selectionMode: isRangeSelection ? 'range' : 'single'})}> +
{formattedDate}
From ca3775fc0290ba50de91b49d59d685e6f00ab1b7 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Wed, 4 Feb 2026 09:27:39 +1100 Subject: [PATCH 02/10] fix lint --- packages/@react-spectrum/s2/src/Calendar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index d267da9bbaf..2ed083b03df 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -36,7 +36,6 @@ import { useSlottedContext } from 'react-aria-components'; import {AriaCalendarGridProps} from '@react-aria/calendar'; -import {baseColor, focusRing, lightDark, style} from '../style' with {type: 'macro'}; import { CalendarDate, getDayOfWeek, @@ -44,6 +43,7 @@ import { } from '@internationalized/date'; import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; +import {focusRing, lightDark, style} from '../style' with {type: 'macro'}; import {forwardRefType, GlobalDOMAttributes} from '@react-types/shared'; import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {helpTextStyles} from './Field'; From c0062b2482582de9df47339662e676dcb745ba9a Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 17 Feb 2026 10:50:44 +1100 Subject: [PATCH 03/10] simplify css and put selection hover between background and border --- packages/@react-spectrum/s2/src/Calendar.tsx | 104 +++++++++++++------ 1 file changed, 72 insertions(+), 32 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 2ed083b03df..afa1719039c 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -49,7 +49,7 @@ import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro import {helpTextStyles} from './Field'; // @ts-ignore import intlMessages from '../intl/*.json'; -import React, {createContext, CSSProperties, ForwardedRef, forwardRef, Fragment, PropsWithChildren, ReactElement, ReactNode, useContext, useMemo, useRef} from 'react'; +import React, {createContext, ForwardedRef, forwardRef, Fragment, PropsWithChildren, ReactElement, ReactNode, useContext, useMemo, useRef} from 'react'; import {useDateFormatter, useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -310,24 +310,31 @@ const unavailableStyles = style({ backgroundColor: '[currentColor]' }); -const selectionSpanStyles = style<{isInvalid?: boolean}>({ +const selectionBackgroundStyles = style<{isInvalid?: boolean, isFirstDayInWeek?: boolean, isLastDayInWeek?: boolean, isSelectionStart?: boolean, isSelectionEnd?: boolean}>({ position: 'absolute', zIndex: -1, top: 0, - insetStart: 'calc(-1 * var(--selection-span) * (var(--cell-width) + var(--cell-gap) + var(--cell-gap)))', - insetEnd: 0, + insetStart: { + default: -4, + isFirstDayInWeek: 0, + isSelectionStart: 0 + }, + insetEnd: { + default: -4, + isLastDayInWeek: 0, + isSelectionEnd: 0 + }, bottom: 0, - borderWidth: 1, - borderStyle: 'solid', - borderColor: { - default: 'blue-800', // focus-indicator-color - isInvalid: 'negative-900', - forcedColors: { - default: 'ButtonText' - } + borderStartRadius: { + default: 'none', + isFirstDayInWeek: 'full', + isSelectionStart: 'full' + }, + borderEndRadius: { + default: 'none', + isLastDayInWeek: 'full', + isSelectionEnd: 'full' }, - borderStartRadius: 'full', - borderEndRadius: 'full', backgroundColor: { default: 'blue-subtle', isInvalid: 'negative-100', @@ -338,6 +345,52 @@ const selectionSpanStyles = style<{isInvalid?: boolean}>({ forcedColorAdjust: 'none' }); +const selectionBorderStyles = style<{isInvalid?: boolean, isFirstDayInWeek?: boolean, isLastDayInWeek?: boolean, isSelectionStart?: boolean, isSelectionEnd?: boolean}>({ + position: 'absolute', + zIndex: 1, + top: 0, + insetStart: { + default: -4, + isFirstDayInWeek: 0, + isSelectionStart: 0 + }, + insetEnd: { + default: -4, + isLastDayInWeek: 0, + isSelectionEnd: 0 + }, + bottom: 0, + borderStartWidth: { + default: 0, + isFirstDayInWeek: 1, + isSelectionStart: 1 + }, + borderTopWidth: 1, + borderEndWidth: { + default: 0, + isLastDayInWeek: 1, + isSelectionEnd: 1 + }, + borderBottomWidth: 1, + borderStyle: 'solid', + borderColor: { + default: 'blue-800', // focus-indicator-color + isInvalid: 'negative-900', + forcedColors: { + default: 'ButtonText' + } + }, + borderStartRadius: { + default: 'none', + isFirstDayInWeek: 'full', + isSelectionStart: 'full' + }, + borderEndRadius: { + default: 'none', + isLastDayInWeek: 'full', + isSelectionEnd: 'full' + } +}); /** * Calendars display a grid of days in one or more months and allow users to select a single date. */ @@ -536,8 +589,7 @@ const CalendarCell = (props: Omit & {firstDayOfWe }; const CalendarCellInner = (props: Omit & {isRangeSelection: boolean, state: CalendarState | RangeCalendarState, weekIndex: number, dayIndex: number, renderProps?: CalendarCellRenderProps, date: DateValue}): ReactElement => { - let {weekIndex, dayIndex, date, renderProps, state, isRangeSelection} = props; - let {getDatesInWeek} = state; + let {dayIndex, date, renderProps, state, isRangeSelection} = props; let ref = useRef(null); let {isUnavailable, formattedDate, isSelected, isSelectionStart, isSelectionEnd, isInvalid} = renderProps!; // only apply the selection start/end styles if the start/end date is actually selectable (aka not unavailable) @@ -545,9 +597,6 @@ const CalendarCellInner = (props: Omit & {isRange isSelectionStart = isSelectionStart && (!isUnavailable || isInvalid); isSelectionEnd = isSelectionEnd && (!isUnavailable || isInvalid); - let startDate = startOfMonth(date); - let datesInWeek = getDatesInWeek(weekIndex, startDate); - let isDateInRange = (checkDate: CalendarDate) => { if (!('highlightedRange' in state) || !state.highlightedRange) { return state.isSelected(checkDate); @@ -561,20 +610,10 @@ const CalendarCellInner = (props: Omit & {isRange return state.isSelected(checkDate); }; - // Starting from the current day, find the first day before it in the current week that is not selected. - // Then, the span of selected days is the current day minus the first unselected day. - let firstUnselectedInRangeInWeek = datesInWeek.slice(0, dayIndex + 1).reverse().findIndex((date, i) => { - return date && i > 0 && (!isDateInRange(date) || date.month !== props.date.month); - }); - - let selectionSpan = -1; - if (firstUnselectedInRangeInWeek > -1 && isSelected) { - selectionSpan = firstUnselectedInRangeInWeek - 1; - } else if (isSelected) { - selectionSpan = dayIndex; - } let prevDay = date.subtract({days: 1}); let nextDay = date.add({days: 1}); + let isFirstDayInWeek = dayIndex === 0; + let isLastDayInWeek = dayIndex === 6; // when invalid, show background for all selected dates (including unavailable) to make continuous range appearance // when valid, only show background for available selected dates @@ -606,7 +645,8 @@ const CalendarCellInner = (props: Omit & {isRange
{isUnavailable &&
}
- {isBackgroundStyleApplied &&
} + {isBackgroundStyleApplied &&
} + {isBackgroundStyleApplied &&
}
); }; From 9e8bf07d646bd9d7172a32a8d4fad535d4c8a7c9 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 17 Feb 2026 12:31:31 +1100 Subject: [PATCH 04/10] fix unavailable dates --- packages/@react-spectrum/s2/src/Calendar.tsx | 43 +++++++++++++------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index afa1719039c..69912b7b409 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -310,30 +310,34 @@ const unavailableStyles = style({ backgroundColor: '[currentColor]' }); -const selectionBackgroundStyles = style<{isInvalid?: boolean, isFirstDayInWeek?: boolean, isLastDayInWeek?: boolean, isSelectionStart?: boolean, isSelectionEnd?: boolean}>({ +const selectionBackgroundStyles = style<{isInvalid?: boolean, isFirstDayInWeek?: boolean, isLastDayInWeek?: boolean, isSelectionStart?: boolean, isSelectionEnd?: boolean, isPreviousDayNotSelected?: boolean, isNextDayNotSelected?: boolean}>({ position: 'absolute', zIndex: -1, top: 0, insetStart: { default: -4, isFirstDayInWeek: 0, - isSelectionStart: 0 + isSelectionStart: 0, + isPreviousDayNotSelected: 0 }, insetEnd: { default: -4, isLastDayInWeek: 0, - isSelectionEnd: 0 + isSelectionEnd: 0, + isNextDayNotSelected: 0 }, bottom: 0, borderStartRadius: { default: 'none', isFirstDayInWeek: 'full', - isSelectionStart: 'full' + isSelectionStart: 'full', + isPreviousDayNotSelected: 'full' }, borderEndRadius: { default: 'none', isLastDayInWeek: 'full', - isSelectionEnd: 'full' + isSelectionEnd: 'full', + isNextDayNotSelected: 'full' }, backgroundColor: { default: 'blue-subtle', @@ -345,31 +349,35 @@ const selectionBackgroundStyles = style<{isInvalid?: boolean, isFirstDayInWeek?: forcedColorAdjust: 'none' }); -const selectionBorderStyles = style<{isInvalid?: boolean, isFirstDayInWeek?: boolean, isLastDayInWeek?: boolean, isSelectionStart?: boolean, isSelectionEnd?: boolean}>({ +const selectionBorderStyles = style<{isInvalid?: boolean, isFirstDayInWeek?: boolean, isLastDayInWeek?: boolean, isSelectionStart?: boolean, isSelectionEnd?: boolean, isPreviousDayNotSelected?: boolean, isNextDayNotSelected?: boolean}>({ position: 'absolute', zIndex: 1, top: 0, insetStart: { default: -4, isFirstDayInWeek: 0, - isSelectionStart: 0 + isSelectionStart: 0, + isPreviousDayNotSelected: 0 }, insetEnd: { default: -4, isLastDayInWeek: 0, - isSelectionEnd: 0 + isSelectionEnd: 0, + isNextDayNotSelected: 0 }, bottom: 0, borderStartWidth: { default: 0, isFirstDayInWeek: 1, - isSelectionStart: 1 + isSelectionStart: 1, + isPreviousDayNotSelected: 1 }, borderTopWidth: 1, borderEndWidth: { default: 0, isLastDayInWeek: 1, - isSelectionEnd: 1 + isSelectionEnd: 1, + isNextDayNotSelected: 1 }, borderBottomWidth: 1, borderStyle: 'solid', @@ -383,12 +391,14 @@ const selectionBorderStyles = style<{isInvalid?: boolean, isFirstDayInWeek?: boo borderStartRadius: { default: 'none', isFirstDayInWeek: 'full', - isSelectionStart: 'full' + isSelectionStart: 'full', + isPreviousDayNotSelected: 'full' }, borderEndRadius: { default: 'none', isLastDayInWeek: 'full', - isSelectionEnd: 'full' + isSelectionEnd: 'full', + isNextDayNotSelected: 'full' } }); /** @@ -592,6 +602,9 @@ const CalendarCellInner = (props: Omit & {isRange let {dayIndex, date, renderProps, state, isRangeSelection} = props; let ref = useRef(null); let {isUnavailable, formattedDate, isSelected, isSelectionStart, isSelectionEnd, isInvalid} = renderProps!; + let calendarStateContext = useContext(CalendarStateContext); + let rangeCalendarStateContext = useContext(RangeCalendarStateContext); + let {isCellUnavailable} = calendarStateContext ?? rangeCalendarStateContext ?? {isCellUnavailable: () => false}; // only apply the selection start/end styles if the start/end date is actually selectable (aka not unavailable) // 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 isSelectionStart = isSelectionStart && (!isUnavailable || isInvalid); @@ -614,6 +627,8 @@ const CalendarCellInner = (props: Omit & {isRange let nextDay = date.add({days: 1}); let isFirstDayInWeek = dayIndex === 0; let isLastDayInWeek = dayIndex === 6; + let isPreviousDayNotSelected = !prevDay || isCellUnavailable(prevDay) || (!isDateInRange(prevDay) || prevDay.month !== props.date.month); + let isNextDayNotSelected = !nextDay || isCellUnavailable(nextDay) || (!isDateInRange(nextDay) || nextDay.month !== props.date.month); // when invalid, show background for all selected dates (including unavailable) to make continuous range appearance // when valid, only show background for available selected dates @@ -645,8 +660,8 @@ const CalendarCellInner = (props: Omit & {isRange
{isUnavailable &&
}
- {isBackgroundStyleApplied &&
} - {isBackgroundStyleApplied &&
} + {isBackgroundStyleApplied &&
} + {isBackgroundStyleApplied &&
}
); }; From df034d543faeb85adbcab9a262ec02cd94c2e6df Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 17 Feb 2026 12:45:26 +1100 Subject: [PATCH 05/10] fix to match expectations in chromatic --- packages/@react-spectrum/s2/src/Calendar.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 69912b7b409..d6e525121f0 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -602,9 +602,6 @@ const CalendarCellInner = (props: Omit & {isRange let {dayIndex, date, renderProps, state, isRangeSelection} = props; let ref = useRef(null); let {isUnavailable, formattedDate, isSelected, isSelectionStart, isSelectionEnd, isInvalid} = renderProps!; - let calendarStateContext = useContext(CalendarStateContext); - let rangeCalendarStateContext = useContext(RangeCalendarStateContext); - let {isCellUnavailable} = calendarStateContext ?? rangeCalendarStateContext ?? {isCellUnavailable: () => false}; // only apply the selection start/end styles if the start/end date is actually selectable (aka not unavailable) // 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 isSelectionStart = isSelectionStart && (!isUnavailable || isInvalid); @@ -627,8 +624,8 @@ const CalendarCellInner = (props: Omit & {isRange let nextDay = date.add({days: 1}); let isFirstDayInWeek = dayIndex === 0; let isLastDayInWeek = dayIndex === 6; - let isPreviousDayNotSelected = !prevDay || isCellUnavailable(prevDay) || (!isDateInRange(prevDay) || prevDay.month !== props.date.month); - let isNextDayNotSelected = !nextDay || isCellUnavailable(nextDay) || (!isDateInRange(nextDay) || nextDay.month !== props.date.month); + let isPreviousDayNotSelected = !prevDay || (!isDateInRange(prevDay) || prevDay.month !== props.date.month); + let isNextDayNotSelected = !nextDay || (!isDateInRange(nextDay) || nextDay.month !== props.date.month); // when invalid, show background for all selected dates (including unavailable) to make continuous range appearance // when valid, only show background for available selected dates From bd3501773a29a3630248985adf8cf1252ec81087 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 3 Mar 2026 10:07:28 +1100 Subject: [PATCH 06/10] adjust padding --- packages/@react-spectrum/s2/src/DatePicker.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index c5e1ee8ca40..cae7afb6c07 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -248,8 +248,7 @@ export function CalendarPopover(props: Omit & {childre padding="none">
Date: Tue, 17 Mar 2026 16:48:01 +1100 Subject: [PATCH 07/10] update remaining tokens and setup error message --- packages/@react-spectrum/s2/src/Calendar.tsx | 18 +++++++++++++----- packages/@react-spectrum/s2/src/DatePicker.tsx | 12 +++++++++--- .../@react-spectrum/s2/src/DateRangePicker.tsx | 12 +++++++++--- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index d6e525121f0..7da325534d5 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -135,7 +135,10 @@ const cellStyles = style({ default: 2, isFirstWeek: 0 }, - paddingBottom: 2, + paddingBottom: { + default: 2, + isLastWeek: 0 + }, position: 'relative', width: 32, height: 32, @@ -579,20 +582,22 @@ const CalendarCell = (props: Omit & {firstDayOfWe let {locale} = useLocale(); let firstDayOfWeek = props.firstDayOfWeek; // Calculate the day and week index based on the date. - let {dayIndex, weekIndex} = useWeekAndDayIndices(props.date, locale, firstDayOfWeek); + let {dayIndex, weekIndex, lastWeekIndex} = useWeekAndDayIndices(props.date, locale, firstDayOfWeek); let calendarStateContext = useContext(CalendarStateContext); let rangeCalendarStateContext = useContext(RangeCalendarStateContext); let state = (calendarStateContext ?? rangeCalendarStateContext)!; + let isFirstWeek = weekIndex === 0; + let isLastWeek = weekIndex === lastWeekIndex; let isFirstChild = dayIndex === 0; let isLastChild = dayIndex === 6; return ( cellStyles({...renderProps, isFirstChild, isLastChild, isFirstWeek})}> + className={(renderProps) => cellStyles({...renderProps, isFirstChild, isLastChild, isFirstWeek, isLastWeek})}> {(renderProps) => } ); @@ -677,7 +682,7 @@ function useWeekAndDayIndices( locale: string, firstDayOfWeek?: DayOfWeek ) { - let {dayIndex, weekIndex} = useMemo(() => { + let result = useMemo(() => { // Get the day index within the week (0-6) const dayIndex = getDayOfWeek(date, locale, firstDayOfWeek); @@ -689,12 +694,15 @@ function useWeekAndDayIndices( const dayOfMonth = date.day; const weekIndex = Math.floor((dayOfMonth + monthStartDayOfWeek - 1) / 7); + const lastDayOfMonth = startOfMonth(date).add({months: 1}).subtract({days: 1}); + const lastWeekIndex = Math.floor((lastDayOfMonth.day + monthStartDayOfWeek - 1) / 7); return { weekIndex, + lastWeekIndex, dayIndex }; }, [date, locale, firstDayOfWeek]); - return {dayIndex, weekIndex}; + return result; } diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index cae7afb6c07..260473b1b4d 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -57,7 +57,11 @@ export interface DatePickerProps extends * The maximum number of months to display at once in the calendar popover, if screen space permits. * @default 1 */ - maxVisibleMonths?: number + maxVisibleMonths?: number, + /** + * The error message to display when the calendar is invalid. + */ + errorMessage?: ReactNode } export const DatePickerContext = createContext>, HTMLDivElement>>(null); @@ -208,7 +212,8 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function + createCalendar={createCalendar} + errorMessage={errorMessage} /> {showTimeField && (
& {childre padding="none">
extends * The maximum number of months to display at once in the calendar popover, if screen space permits. * @default 1 */ - maxVisibleMonths?: number + maxVisibleMonths?: number, + /** + * The error message to display when the calendar is invalid. + */ + errorMessage?: ReactNode } export const DateRangePickerContext = createContext>, HTMLDivElement>>(null); @@ -83,6 +87,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func } = props; let formContext = useContext(FormContext); let [buttonHasFocus, setButtonHasFocus] = useState(false); + console.log('errorMessage', errorMessage); return ( + createCalendar={createCalendar} + errorMessage={errorMessage} /> {showTimeField && (
Date: Tue, 17 Mar 2026 17:46:43 +1100 Subject: [PATCH 08/10] responsive calendar --- packages/@react-spectrum/s2/src/Calendar.tsx | 53 ++++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 7da325534d5..54f86c652be 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -75,7 +75,15 @@ const calendarStyles = style({ flexDirection: 'column', gap: 24, width: 'fit', - disableTapHighlight: true + disableTapHighlight: true, + '--cell-gap': { + type: 'paddingStart', + value: 4 + }, + '--cell-min-width': { + type: 'width', + value: 32 + } }, getAllowedOverrides()); const headerStyles = style({ @@ -119,10 +127,7 @@ const headerCellStyles = style({ const cellStyles = style({ outlineStyle: 'none', - '--cell-gap': { - type: 'paddingStart', - value: 4 - }, + boxSizing: 'content-box', paddingStart: { default: 4, isFirstChild: 0 @@ -140,15 +145,20 @@ const cellStyles = style({ isLastWeek: 0 }, position: 'relative', - width: 32, - height: 32, display: { default: 'flex', isOutsideMonth: 'none' }, alignItems: 'center', justifyContent: 'center', - disableTapHighlight: true + disableTapHighlight: true, + '--cell-min-width': { + type: 'width', + value: 32 + }, + width: '[min(var(--cell-min-width), calc((100cqw / 7) - var(--cell-gap)))]', + aspectRatio: 'square', + height: 'auto' }); const cellInnerStyles = style({ @@ -172,7 +182,7 @@ const cellInnerStyles = style {Array.from({length: visibleMonths}).map((_, i) => ( - +
+ +
))}
{isInvalid && ( @@ -474,7 +497,8 @@ export const CalendarGrid = (props: Omit & Pr className={style({ borderCollapse: 'collapse', borderSpacing: 0, - isolation: 'isolate' + isolation: 'isolate', + width: 'full' })} offset={{months: props.months}}> @@ -646,11 +670,8 @@ const CalendarCellInner = (props: Omit & {isRange
Date: Thu, 26 Mar 2026 12:47:48 +1100 Subject: [PATCH 09/10] support range calendar --- packages/@react-spectrum/s2/src/Calendar.tsx | 5 +++- .../s2/src/DateRangePicker.tsx | 1 - .../@react-spectrum/s2/src/RangeCalendar.tsx | 30 +++++++++++++++++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 3188b425985..611d9791fc8 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -470,7 +470,10 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca flexBasis: '0%', minWidth: 0, width: 'calc(7 * var(--cell-min-width) + var(--cell-gap) * 12)', - maxWidth: 'calc(100% / var(--visible-months))' + maxWidth: { + default: 'calc(100vw / var(--visible-months))', + '@media (max-width: 375px)': '100%' + } })}>
diff --git a/packages/@react-spectrum/s2/src/DateRangePicker.tsx b/packages/@react-spectrum/s2/src/DateRangePicker.tsx index 8e906a46002..2f5074e5ea8 100644 --- a/packages/@react-spectrum/s2/src/DateRangePicker.tsx +++ b/packages/@react-spectrum/s2/src/DateRangePicker.tsx @@ -88,7 +88,6 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func } = props; let formContext = useContext(FormContext); let [buttonHasFocus, setButtonHasFocus] = useState(false); - console.log('errorMessage', errorMessage); return ( {Array.from({length: visibleMonths}).map((_, i) => ( - +
+ +
))}
{isInvalid && ( From 418983dd1aa485253a1923de2dcad24c24238497 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Sat, 28 Mar 2026 14:00:52 +1100 Subject: [PATCH 10/10] Fix responsive behaviour for single calendar --- packages/@react-spectrum/s2/src/Calendar.tsx | 83 ++++++++++--------- .../@react-spectrum/s2/src/RangeCalendar.tsx | 70 +++++++--------- 2 files changed, 71 insertions(+), 82 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 611d9791fc8..24a9587215a 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -72,27 +72,44 @@ export interface CalendarProps export const CalendarContext = createContext>, HTMLDivElement>>(null); -const calendarStyles = style({ +const calendarStyles = style<{isMultiMonth?: boolean}>({ display: 'flex', + containerType: { + default: 'inline-size', + isMultiMonth: 'unset' + }, flexDirection: 'column', gap: 24, - width: 'fit', disableTapHighlight: true, '--cell-gap': { type: 'paddingStart', value: 4 }, - '--cell-min-width': { + '--cell-max-width': { type: 'width', value: 32 + }, + '--cell-responsive-size': { + type: 'width', + value: { + default: '[min(var(--cell-max-width), (100cqw - (var(--cell-gap) * 12)) / 7)]', + isMultiMonth: '--cell-max-width' + } + }, + width: { + default: 'calc(7 * var(--cell-max-width) + var(--cell-gap) * 12)', + isMultiMonth: 'fit' + }, + maxWidth: { + default: 'full', + isMultiMonth: 'unset' } }, getAllowedOverrides()); const headerStyles = style({ display: 'flex', alignItems: 'center', - justifyContent: 'space-between', - width: 'full' + justifyContent: 'space-between' }); const headingStyles = style({ @@ -100,16 +117,14 @@ const headingStyles = style({ alignItems: 'center', justifyContent: 'space-between', margin: 0, - width: 'full' + flexGrow: 1 }); const titleStyles = style({ font: 'title-lg', textAlign: 'center', flexGrow: 1, - flexShrink: 0, - flexBasis: '0%', - minWidth: 0 + flexShrink: 0 }); const headerCellStyles = style({ @@ -154,11 +169,7 @@ const cellStyles = style({ alignItems: 'center', justifyContent: 'center', disableTapHighlight: true, - '--cell-min-width': { - type: 'width', - value: 32 - }, - width: '[min(var(--cell-min-width), calc((100cqw / 7) - var(--cell-gap)))]', + width: '--cell-responsive-size', aspectRatio: 'square', height: 'auto' }); @@ -301,7 +312,7 @@ const cellInnerStyles = style 1; return ( + className={(UNSAFE_className || '') + calendarStyles({isMultiMonth}, styles)}> {({isInvalid, isDisabled}) => { return ( <> @@ -445,11 +457,7 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca [HeaderContext, null], [HeadingContext, null] ]}> -
- - - -
+
{Array.from({length: visibleMonths}).map((_, i) => ( -
- -
+ ))}
{isInvalid && ( @@ -491,6 +483,16 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca ); }); +export const CalendarHeader = (): ReactElement => { + return ( +
+ + + +
+ ); +}; + export const CalendarGrid = (props: Omit & PropsWithChildren & {months: number}): ReactElement => { let rangeCalendarProps = useSlottedContext(RangeCalendarContext); let calendarProps = useSlottedContext(AriaCalendarContext); @@ -502,8 +504,7 @@ export const CalendarGrid = (props: Omit & Pr className={style({ borderCollapse: 'collapse', borderSpacing: 0, - isolation: 'isolate', - width: 'full' + isolation: 'isolate' })} offset={{months: props.months}}> @@ -524,7 +525,7 @@ export const CalendarGrid = (props: Omit & Pr // Ordinarily the heading is a formatted date range, ie January 2025 - February 2025. // However, we want to show each month individually. -export const CalendarHeading = (): ReactElement => { +const CalendarHeading = (): ReactElement => { let calendarStateContext = useContext(CalendarStateContext); let rangeCalendarStateContext = useContext(RangeCalendarStateContext); let {visibleRange, timeZone} = calendarStateContext ?? rangeCalendarStateContext ?? {}; diff --git a/packages/@react-spectrum/s2/src/RangeCalendar.tsx b/packages/@react-spectrum/s2/src/RangeCalendar.tsx index a63b8e9bb2e..8ab990b22e2 100644 --- a/packages/@react-spectrum/s2/src/RangeCalendar.tsx +++ b/packages/@react-spectrum/s2/src/RangeCalendar.tsx @@ -15,14 +15,12 @@ import { RangeCalendarProps as AriaRangeCalendarProps, DateValue } from 'react-aria-components/RangeCalendar'; -import {CalendarButton, CalendarGrid, CalendarHeading} from './Calendar'; -import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg'; -import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg'; +import {CalendarGrid, CalendarHeader} from './Calendar'; import {ContextValue, Provider} from 'react-aria-components/utils'; import {createContext, CSSProperties, ForwardedRef, forwardRef, ReactNode} from 'react'; import {forwardRefType, GlobalDOMAttributes} from '@react-types/shared'; import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; -import {Header, HeaderContext, HeadingContext} from './Content'; +import {HeaderContext, HeadingContext} from './Content'; import {helpTextStyles} from './Field'; // @ts-ignore import intlMessages from '../intl/*.json'; @@ -31,7 +29,6 @@ import {Text} from 'react-aria-components/Text'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; import {useSpectrumContextProps} from './useSpectrumContextProps'; - export interface RangeCalendarProps extends Omit, 'visibleDuration' | 'style' | 'className' | 'render' | 'children' | 'styles' | keyof GlobalDOMAttributes>, StyleProps { @@ -48,29 +45,40 @@ export interface RangeCalendarProps export const RangeCalendarContext = createContext>, HTMLDivElement>>(null); - -const calendarStyles = style({ +const calendarStyles = style<{isMultiMonth?: boolean}>({ display: 'flex', + containerType: { + default: 'inline-size', + isMultiMonth: 'unset' + }, flexDirection: 'column', gap: 24, - width: 'fit', + disableTapHighlight: true, '--cell-gap': { type: 'paddingStart', value: 4 }, - '--cell-min-width': { + '--cell-max-width': { type: 'width', value: 32 + }, + '--cell-responsive-size': { + type: 'width', + value: { + default: '[min(var(--cell-max-width), (100cqw - (var(--cell-gap) * 12)) / 7)]', + isMultiMonth: '--cell-max-width' + } + }, + width: { + default: 'calc(7 * var(--cell-max-width) + var(--cell-gap) * 12)', + isMultiMonth: 'fit' + }, + maxWidth: { + default: 'full', + isMultiMonth: 'unset' } }, getAllowedOverrides()); -const headerStyles = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - width: 'full' -}); - /** * RangeCalendars display a grid of days in one or more months and allow users to select a contiguous range of dates. */ @@ -86,13 +94,14 @@ export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(functi } = props; let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); + let isMultiMonth = visibleMonths > 1; return ( + style={{...UNSAFE_style, '--num-calendars': visibleMonths} as CSSProperties} + className={(UNSAFE_className || '') + calendarStyles({isMultiMonth}, styles)}> {({isInvalid, isDisabled}) => { return ( <> @@ -101,38 +110,17 @@ export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(functi [HeaderContext, null], [HeadingContext, null] ]}> -
- - - -
+
{Array.from({length: visibleMonths}).map((_, i) => ( -
- -
+ ))}
{isInvalid && (