diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 57736226afd..7da325534d5 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,12 +43,13 @@ 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'; // @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'; @@ -135,7 +135,10 @@ const cellStyles = style({ default: 2, isFirstWeek: 0 }, - paddingBottom: 2, + paddingBottom: { + default: 2, + isLastWeek: 0 + }, position: 'relative', width: 32, height: 32, @@ -156,7 +159,6 @@ const cellInnerStyles = style({ +const selectionBackgroundStyles = style<{isInvalid?: boolean, isFirstDayInWeek?: boolean, isLastDayInWeek?: boolean, isSelectionStart?: boolean, isSelectionEnd?: boolean, isPreviousDayNotSelected?: boolean, isNextDayNotSelected?: 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, + isPreviousDayNotSelected: 0 + }, + insetEnd: { + default: -4, + isLastDayInWeek: 0, + isSelectionEnd: 0, + isNextDayNotSelected: 0 + }, bottom: 0, - borderWidth: 2, - borderStyle: 'dashed', - borderColor: { - default: 'blue-800', // focus-indicator-color - isInvalid: 'negative-900', - forcedColors: { - default: 'ButtonText' - } + borderStartRadius: { + default: 'none', + isFirstDayInWeek: 'full', + isSelectionStart: 'full', + isPreviousDayNotSelected: 'full' + }, + borderEndRadius: { + default: 'none', + isLastDayInWeek: 'full', + isSelectionEnd: 'full', + isNextDayNotSelected: 'full' }, - borderStartRadius: 'full', - borderEndRadius: 'full', backgroundColor: { default: 'blue-subtle', isInvalid: 'negative-100', @@ -330,6 +352,58 @@ const selectionSpanStyles = style<{isInvalid?: boolean}>({ forcedColorAdjust: 'none' }); +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, + isPreviousDayNotSelected: 0 + }, + insetEnd: { + default: -4, + isLastDayInWeek: 0, + isSelectionEnd: 0, + isNextDayNotSelected: 0 + }, + bottom: 0, + borderStartWidth: { + default: 0, + isFirstDayInWeek: 1, + isSelectionStart: 1, + isPreviousDayNotSelected: 1 + }, + borderTopWidth: 1, + borderEndWidth: { + default: 0, + isLastDayInWeek: 1, + isSelectionEnd: 1, + isNextDayNotSelected: 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', + isPreviousDayNotSelected: 'full' + }, + borderEndRadius: { + default: 'none', + isLastDayInWeek: 'full', + isSelectionEnd: 'full', + isNextDayNotSelected: 'full' + } +}); /** * Calendars display a grid of days in one or more months and allow users to select a single date. */ @@ -508,28 +582,29 @@ 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) => } ); }; 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) @@ -537,9 +612,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); @@ -553,20 +625,12 @@ 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; + 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 @@ -592,12 +656,14 @@ const CalendarCellInner = (props: Omit & {isRange ref={ref} style={pressScale(ref, {})(renderProps!)} className={cellInnerStyles({...renderProps!, isSelectionStart, isSelectionEnd, selectionMode: isRangeSelection ? 'range' : 'single'})}> +
{formattedDate}
{isUnavailable &&
}
- {isBackgroundStyleApplied &&
} + {isBackgroundStyleApplied &&
} + {isBackgroundStyleApplied &&
}
); }; @@ -616,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); @@ -628,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 c5e1ee8ca40..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
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); @@ -148,7 +152,8 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func + createCalendar={createCalendar} + errorMessage={errorMessage} /> {showTimeField && (