Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 101 additions & 40 deletions packages/@react-spectrum/s2/src/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,20 @@ 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,
startOfMonth
} 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';

Expand Down Expand Up @@ -156,7 +156,6 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
},
outlineOffset: {
default: -2,
isToday: 2,
isSelected: {
selectionMode: {
single: 2,
Expand Down Expand Up @@ -184,10 +183,6 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
},
isPressed: 'gray-100',
isDisabled: 'transparent',
isToday: {
default: baseColor('gray-300'),
isDisabled: 'disabled'
},
isSelected: {
selectionMode: {
single: {
Expand Down Expand Up @@ -254,7 +249,6 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
},
forcedColors: {
default: 'transparent',
isToday: 'ButtonFace',
isHovered: 'Highlight',
isSelected: {
selectionMode: {
Expand Down Expand Up @@ -282,7 +276,6 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
isDisabled: 'disabled',
forcedColors: {
default: 'ButtonText',
isToday: 'ButtonFace',
isSelected: 'HighlightText',
isSelectionStart: 'HighlightText',
isSelectionEnd: 'HighlightText',
Expand All @@ -291,6 +284,21 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
}
});

const todayStyles = style({
position: 'absolute',
bottom: 4,
left: '50%',
transform: 'translateX(-50%)',
width: 4,
height: 4,
borderRadius: 'full',
backgroundColor: '[currentColor]',
display: {
default: 'none',
isToday: 'block'
}
});

const unavailableStyles = style({
position: 'absolute',
top: 'calc(50% - 1px)',
Expand All @@ -302,24 +310,35 @@ const unavailableStyles = style({
backgroundColor: '[currentColor]'
});

const selectionSpanStyles = style<{isInvalid?: 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: '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',
Expand All @@ -330,6 +349,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.
*/
Expand Down Expand Up @@ -528,18 +599,14 @@ const CalendarCell = (props: Omit<CalendarCellProps, 'children'> & {firstDayOfWe
};

const CalendarCellInner = (props: Omit<CalendarCellProps, 'children'> & {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<HTMLDivElement>(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)
// 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);
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);
Expand All @@ -553,20 +620,12 @@ const CalendarCellInner = (props: Omit<CalendarCellProps, 'children'> & {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
Expand All @@ -592,12 +651,14 @@ const CalendarCellInner = (props: Omit<CalendarCellProps, 'children'> & {isRange
ref={ref}
style={pressScale(ref, {})(renderProps!)}
className={cellInnerStyles({...renderProps!, isSelectionStart, isSelectionEnd, selectionMode: isRangeSelection ? 'range' : 'single'})}>
<div className={todayStyles(renderProps!)} role="presentation" />
<div>
{formattedDate}
</div>
{isUnavailable && <div className={unavailableStyles} role="presentation" />}
</div>
{isBackgroundStyleApplied && <div style={{'--selection-span': selectionSpan} as CSSProperties} className={selectionSpanStyles({isInvalid})} role="presentation" />}
{isBackgroundStyleApplied && <div className={selectionBackgroundStyles({isInvalid, isFirstDayInWeek, isLastDayInWeek, isSelectionStart, isSelectionEnd, isPreviousDayNotSelected, isNextDayNotSelected})} role="presentation" />}
{isBackgroundStyleApplied && <div className={selectionBorderStyles({isInvalid, isFirstDayInWeek, isLastDayInWeek, isSelectionStart, isSelectionEnd, isPreviousDayNotSelected, isNextDayNotSelected})} role="presentation" />}
</div>
);
};
Expand Down