Skip to content

Commit 144befe

Browse files
authored
fix: Responsive calendar (#9847)
* chore: calendar new today indicator * fix lint * simplify css and put selection hover between background and border * fix unavailable dates * fix to match expectations in chromatic * adjust padding * update remaining tokens and setup error message * responsive calendar * support range calendar * Fix responsive behaviour for single calendar
1 parent e5a0928 commit 144befe

File tree

2 files changed

+93
-56
lines changed

2 files changed

+93
-56
lines changed

packages/@react-spectrum/s2/src/Calendar.tsx

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -72,36 +72,59 @@ export interface CalendarProps<T extends DateValue>
7272

7373
export const CalendarContext = createContext<ContextValue<Partial<CalendarProps<any>>, HTMLDivElement>>(null);
7474

75-
const calendarStyles = style({
75+
const calendarStyles = style<{isMultiMonth?: boolean}>({
7676
display: 'flex',
77+
containerType: {
78+
default: 'inline-size',
79+
isMultiMonth: 'unset'
80+
},
7781
flexDirection: 'column',
7882
gap: 24,
79-
width: 'fit',
80-
disableTapHighlight: true
83+
disableTapHighlight: true,
84+
'--cell-gap': {
85+
type: 'paddingStart',
86+
value: 4
87+
},
88+
'--cell-max-width': {
89+
type: 'width',
90+
value: 32
91+
},
92+
'--cell-responsive-size': {
93+
type: 'width',
94+
value: {
95+
default: '[min(var(--cell-max-width), (100cqw - (var(--cell-gap) * 12)) / 7)]',
96+
isMultiMonth: '--cell-max-width'
97+
}
98+
},
99+
width: {
100+
default: 'calc(7 * var(--cell-max-width) + var(--cell-gap) * 12)',
101+
isMultiMonth: 'fit'
102+
},
103+
maxWidth: {
104+
default: 'full',
105+
isMultiMonth: 'unset'
106+
}
81107
}, getAllowedOverrides());
82108

83109
const headerStyles = style({
84110
display: 'flex',
85111
alignItems: 'center',
86-
justifyContent: 'space-between',
87-
width: 'full'
112+
justifyContent: 'space-between'
88113
});
89114

90115
const headingStyles = style({
91116
display: 'flex',
92117
alignItems: 'center',
93118
justifyContent: 'space-between',
94119
margin: 0,
95-
width: 'full'
120+
flexGrow: 1
96121
});
97122

98123
const titleStyles = style({
99124
font: 'title-lg',
100125
textAlign: 'center',
101126
flexGrow: 1,
102-
flexShrink: 0,
103-
flexBasis: '0%',
104-
minWidth: 0
127+
flexShrink: 0
105128
});
106129

107130
const headerCellStyles = style({
@@ -121,10 +144,7 @@ const headerCellStyles = style({
121144

122145
const cellStyles = style({
123146
outlineStyle: 'none',
124-
'--cell-gap': {
125-
type: 'paddingStart',
126-
value: 4
127-
},
147+
boxSizing: 'content-box',
128148
paddingStart: {
129149
default: 4,
130150
isFirstChild: 0
@@ -142,15 +162,16 @@ const cellStyles = style({
142162
isLastWeek: 0
143163
},
144164
position: 'relative',
145-
width: 32,
146-
height: 32,
147165
display: {
148166
default: 'flex',
149167
isOutsideMonth: 'none'
150168
},
151169
alignItems: 'center',
152170
justifyContent: 'center',
153-
disableTapHighlight: true
171+
disableTapHighlight: true,
172+
width: '--cell-responsive-size',
173+
aspectRatio: 'square',
174+
height: 'auto'
154175
});
155176

156177
const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single' | 'range'}>({
@@ -174,7 +195,7 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
174195
font: 'body-sm',
175196
cursor: 'default',
176197
width: 'full',
177-
height: 32,
198+
height: 'full',
178199
borderRadius: 'full',
179200
display: 'flex',
180201
alignItems: 'center',
@@ -291,7 +312,7 @@ const cellInnerStyles = style<CalendarCellRenderProps & {selectionMode: 'single'
291312

292313
const todayStyles = style({
293314
position: 'absolute',
294-
bottom: 4,
315+
bottom: '12.5%',
295316
left: '50%',
296317
transform: 'translateX(-50%)',
297318
width: 4,
@@ -420,13 +441,14 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca
420441
...otherProps
421442
} = props;
422443
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');
444+
let isMultiMonth = visibleMonths > 1;
423445
return (
424446
<AriaCalendar
425447
{...otherProps}
426448
ref={ref}
427449
visibleDuration={{months: visibleMonths}}
428450
style={UNSAFE_style}
429-
className={(UNSAFE_className || '') + calendarStyles(null, styles)}>
451+
className={(UNSAFE_className || '') + calendarStyles({isMultiMonth}, styles)}>
430452
{({isInvalid, isDisabled}) => {
431453
return (
432454
<>
@@ -435,11 +457,7 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca
435457
[HeaderContext, null],
436458
[HeadingContext, null]
437459
]}>
438-
<Header styles={headerStyles}>
439-
<CalendarButton slot="previous"><ChevronLeftIcon /></CalendarButton>
440-
<CalendarHeading />
441-
<CalendarButton slot="next"><ChevronRightIcon /></CalendarButton>
442-
</Header>
460+
<CalendarHeader />
443461
</Provider>
444462
<div
445463
className={style({
@@ -450,7 +468,7 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca
450468
alignItems: 'start'
451469
})}>
452470
{Array.from({length: visibleMonths}).map((_, i) => (
453-
<CalendarGrid months={i} key={i} />
471+
<CalendarGrid key={i} months={i} />
454472
))}
455473
</div>
456474
{isInvalid && (
@@ -465,6 +483,16 @@ export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ca
465483
);
466484
});
467485

486+
export const CalendarHeader = (): ReactElement => {
487+
return (
488+
<Header styles={headerStyles}>
489+
<CalendarButton slot="previous"><ChevronLeftIcon /></CalendarButton>
490+
<CalendarHeading />
491+
<CalendarButton slot="next"><ChevronRightIcon /></CalendarButton>
492+
</Header>
493+
);
494+
};
495+
468496
export const CalendarGrid = (props: Omit<AriaCalendarGridProps, 'children'> & PropsWithChildren & {months: number}): ReactElement => {
469497
let rangeCalendarProps = useSlottedContext(RangeCalendarContext);
470498
let calendarProps = useSlottedContext(AriaCalendarContext);
@@ -497,7 +525,7 @@ export const CalendarGrid = (props: Omit<AriaCalendarGridProps, 'children'> & Pr
497525

498526
// Ordinarily the heading is a formatted date range, ie January 2025 - February 2025.
499527
// However, we want to show each month individually.
500-
export const CalendarHeading = (): ReactElement => {
528+
const CalendarHeading = (): ReactElement => {
501529
let calendarStateContext = useContext(CalendarStateContext);
502530
let rangeCalendarStateContext = useContext(RangeCalendarStateContext);
503531
let {visibleRange, timeZone} = calendarStateContext ?? rangeCalendarStateContext ?? {};
@@ -648,11 +676,8 @@ const CalendarCellInner = (props: Omit<CalendarCellProps, 'children'> & {isRange
648676
<div
649677
className={style({
650678
position: 'relative',
651-
width: 32,
652-
'--cell-width': {
653-
type: 'width',
654-
value: '[self(width)]'
655-
}
679+
width: 'full',
680+
height: 'full'
656681
})}>
657682
<div
658683
ref={ref}

packages/@react-spectrum/s2/src/RangeCalendar.tsx

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,12 @@ import {
1515
RangeCalendarProps as AriaRangeCalendarProps,
1616
DateValue
1717
} from 'react-aria-components/RangeCalendar';
18-
import {CalendarButton, CalendarGrid, CalendarHeading} from './Calendar';
19-
import ChevronLeftIcon from '../s2wf-icons/S2_Icon_ChevronLeft_20_N.svg';
20-
import ChevronRightIcon from '../s2wf-icons/S2_Icon_ChevronRight_20_N.svg';
18+
import {CalendarGrid, CalendarHeader} from './Calendar';
2119
import {ContextValue, Provider} from 'react-aria-components/slots';
22-
import {createContext, ForwardedRef, forwardRef, ReactNode} from 'react';
20+
import {createContext, CSSProperties, ForwardedRef, forwardRef, ReactNode} from 'react';
2321
import {forwardRefType, GlobalDOMAttributes} from '@react-types/shared';
2422
import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'};
25-
import {Header, HeaderContext, HeadingContext} from './Content';
23+
import {HeaderContext, HeadingContext} from './Content';
2624
import {helpTextStyles} from './Field';
2725
// @ts-ignore
2826
import intlMessages from '../intl/*.json';
@@ -31,7 +29,6 @@ import {Text} from 'react-aria-components/Text';
3129
import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter';
3230
import {useSpectrumContextProps} from './useSpectrumContextProps';
3331

34-
3532
export interface RangeCalendarProps<T extends DateValue>
3633
extends Omit<AriaRangeCalendarProps<T>, 'visibleDuration' | 'style' | 'className' | 'render' | 'children' | 'styles' | keyof GlobalDOMAttributes>,
3734
StyleProps {
@@ -48,21 +45,40 @@ export interface RangeCalendarProps<T extends DateValue>
4845

4946
export const RangeCalendarContext = createContext<ContextValue<Partial<RangeCalendarProps<any>>, HTMLDivElement>>(null);
5047

51-
52-
const calendarStyles = style({
48+
const calendarStyles = style<{isMultiMonth?: boolean}>({
5349
display: 'flex',
50+
containerType: {
51+
default: 'inline-size',
52+
isMultiMonth: 'unset'
53+
},
5454
flexDirection: 'column',
5555
gap: 24,
56-
width: 'fit'
56+
disableTapHighlight: true,
57+
'--cell-gap': {
58+
type: 'paddingStart',
59+
value: 4
60+
},
61+
'--cell-max-width': {
62+
type: 'width',
63+
value: 32
64+
},
65+
'--cell-responsive-size': {
66+
type: 'width',
67+
value: {
68+
default: '[min(var(--cell-max-width), (100cqw - (var(--cell-gap) * 12)) / 7)]',
69+
isMultiMonth: '--cell-max-width'
70+
}
71+
},
72+
width: {
73+
default: 'calc(7 * var(--cell-max-width) + var(--cell-gap) * 12)',
74+
isMultiMonth: 'fit'
75+
},
76+
maxWidth: {
77+
default: 'full',
78+
isMultiMonth: 'unset'
79+
}
5780
}, getAllowedOverrides());
5881

59-
const headerStyles = style({
60-
display: 'flex',
61-
alignItems: 'center',
62-
justifyContent: 'space-between',
63-
width: 'full'
64-
});
65-
6682
/**
6783
* RangeCalendars display a grid of days in one or more months and allow users to select a contiguous range of dates.
6884
*/
@@ -78,13 +94,14 @@ export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(functi
7894
} = props;
7995
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');
8096

97+
let isMultiMonth = visibleMonths > 1;
8198
return (
8299
<AriaRangeCalendar
83100
{...otherProps}
84101
ref={ref}
85102
visibleDuration={{months: visibleMonths}}
86-
style={UNSAFE_style}
87-
className={(UNSAFE_className || '') + calendarStyles(null, styles)}>
103+
style={{...UNSAFE_style, '--num-calendars': visibleMonths} as CSSProperties}
104+
className={(UNSAFE_className || '') + calendarStyles({isMultiMonth}, styles)}>
88105
{({isInvalid, isDisabled}) => {
89106
return (
90107
<>
@@ -93,22 +110,17 @@ export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(functi
93110
[HeaderContext, null],
94111
[HeadingContext, null]
95112
]}>
96-
<Header styles={headerStyles}>
97-
<CalendarButton slot="previous"><ChevronLeftIcon /></CalendarButton>
98-
<CalendarHeading />
99-
<CalendarButton slot="next"><ChevronRightIcon /></CalendarButton>
100-
</Header>
113+
<CalendarHeader />
101114
</Provider>
102115
<div
103116
className={style({
104117
display: 'flex',
105118
flexDirection: 'row',
106119
gap: 24,
107-
width: 'full',
108120
alignItems: 'start'
109121
})}>
110122
{Array.from({length: visibleMonths}).map((_, i) => (
111-
<CalendarGrid months={i} key={i} />
123+
<CalendarGrid key={i} months={i} />
112124
))}
113125
</div>
114126
{isInvalid && (

0 commit comments

Comments
 (0)