Skip to content
Draft
Show file tree
Hide file tree
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
6 changes: 4 additions & 2 deletions packages/@react-stately/calendar/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import {CalendarDate} from '@internationalized/date';
import {CalendarDate, DateDuration} from '@internationalized/date';
import {DateValue} from '@react-types/calendar';
import {RangeValue, ValidationState} from '@react-types/shared';

Expand All @@ -19,6 +19,8 @@ interface CalendarStateBase {
readonly isDisabled: boolean,
/** Whether the calendar is in a read only state. */
readonly isReadOnly: boolean,
/** The duration of visible dates. */
readonly visibleDuration: DateDuration,
/** The date range that is currently visible in the calendar. */
readonly visibleRange: RangeValue<CalendarDate>,
/** The minimum allowed date that a user may select. */
Expand All @@ -37,7 +39,7 @@ interface CalendarStateBase {
/** The currently focused date. */
readonly focusedDate: CalendarDate,
/** Sets the focused date. */
setFocusedDate(value: CalendarDate): void,
setFocusedDate(value: CalendarDate, align?: 'start' | 'center' | 'end'): void,
/** Moves focus to the next calendar date. */
focusNextDay(): void,
/** Moves focus to the previous calendar date. */
Expand Down
24 changes: 12 additions & 12 deletions packages/@react-stately/calendar/src/useCalendarState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import {alignCenter, alignEnd, alignStart, constrainStart, constrainValue, isInvalid, previousAvailableDate} from './utils';
import {alignCenter, alignDate, alignEnd, alignStart, constrainStart, constrainValue, isInvalid, previousAvailableDate} from './utils';
import {
Calendar,
CalendarDate,
Expand Down Expand Up @@ -56,6 +56,9 @@ export interface CalendarStateOptions<T extends DateValue = DateValue> extends C
*/
selectionAlignment?: 'start' | 'center' | 'end'
}

const DEFAULT_VISIBLE_DURATION: DateDuration = {months: 1};

/**
* Provides state management for a calendar component.
* A calendar displays one or more date grids and allows users to select a single date.
Expand All @@ -66,7 +69,7 @@ export function useCalendarState<T extends DateValue = DateValue>(props: Calenda
let {
locale,
createCalendar,
visibleDuration = {months: 1},
visibleDuration = DEFAULT_VISIBLE_DURATION,
minValue,
maxValue,
selectionAlignment,
Expand Down Expand Up @@ -95,15 +98,7 @@ export function useCalendarState<T extends DateValue = DateValue>(props: Calenda
), [props.defaultFocusedValue, calendarDateValue, timeZone, calendar, minValue, maxValue]);
let [focusedDate, setFocusedDate] = useControlledState(focusedCalendarDate, defaultFocusedCalendarDate, props.onFocusChange);
let [startDate, setStartDate] = useState(() => {
switch (selectionAlignment) {
case 'start':
return alignStart(focusedDate, visibleDuration, locale, minValue, maxValue);
case 'end':
return alignEnd(focusedDate, visibleDuration, locale, minValue, maxValue);
case 'center':
default:
return alignCenter(focusedDate, visibleDuration, locale, minValue, maxValue);
}
return alignDate(focusedDate, selectionAlignment || 'center', visibleDuration, locale, minValue, maxValue);
});
let [isFocused, setFocused] = useState(props.autoFocus || false);

Expand Down Expand Up @@ -194,6 +189,7 @@ export function useCalendarState<T extends DateValue = DateValue>(props: Calenda
isReadOnly: props.isReadOnly ?? false,
value: calendarDateValue,
setValue,
visibleDuration,
visibleRange: {
start: startDate,
end: endDate
Expand All @@ -204,9 +200,13 @@ export function useCalendarState<T extends DateValue = DateValue>(props: Calenda
timeZone,
validationState,
isValueInvalid,
setFocusedDate(date) {
setFocusedDate(date, align) {
focusCell(date);
setFocused(true);

if (align && (date.compare(startDate) < 0 || date.compare(endDate) > 0)) {
setStartDate(alignDate(date, align, visibleDuration, locale, minValue, maxValue));
}
},
focusNextDay() {
focusCell(focusedDate.add({days: 1}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export interface RangeCalendarStateOptions<T extends DateValue = DateValue> exte
selectionAlignment?: 'start' | 'center' | 'end'
}

const DEFAULT_VISIBLE_DURATION: DateDuration = {months: 1};

/**
* Provides state management for a range calendar component.
* A range calendar displays one or more date grids and allows users to select a contiguous range of dates.
Expand All @@ -52,7 +54,7 @@ export function useRangeCalendarState<T extends DateValue = DateValue>(props: Ra
onChange,
createCalendar,
locale,
visibleDuration = {months: 1},
visibleDuration = DEFAULT_VISIBLE_DURATION,
minValue,
maxValue,
...calendarProps} = props;
Expand Down
12 changes: 12 additions & 0 deletions packages/@react-stately/calendar/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ export function alignEnd(date: CalendarDate, duration: DateDuration, locale: str
return constrainStart(date, aligned, duration, locale, minValue, maxValue);
}

export function alignDate(date: CalendarDate, selectionAlignment: 'start' | 'center' | 'end', duration: DateDuration, locale: string, minValue?: DateValue | null, maxValue?: DateValue | null): CalendarDate {
switch (selectionAlignment) {
case 'start':
return alignStart(date, duration, locale, minValue, maxValue);
case 'end':
return alignEnd(date, duration, locale, minValue, maxValue);
case 'center':
default:
return alignCenter(date, duration, locale, minValue, maxValue);
}
}

export function constrainStart(
date: CalendarDate,
aligned: CalendarDate,
Expand Down
75 changes: 45 additions & 30 deletions packages/dev/s2-docs/pages/react-aria/Calendar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ Set the `visibleDuration` prop and render multiple `CalendarGrid` elements to di

```tsx render docs={docs.exports.Calendar} links={docs.links} props={['visibleDuration', 'pageBehavior', 'firstDayOfWeek']} initialProps={{visibleDuration: {months: 2}}} wide
"use client";
import {Calendar, Heading} from 'react-aria-components';
import {Calendar, Heading, CalendarCarousel} from 'react-aria-components';
import {CalendarGrid, CalendarCell} from 'vanilla-starter/Calendar';
import {Button} from 'vanilla-starter/Button';
import {useDateFormatter} from 'react-aria';
Expand All @@ -204,29 +204,38 @@ function Example(props) {
///- begin highlight -///
/* PROPS */
///- end highlight -///
style={{display: 'flex', gap: 12, overflow: 'auto'}}
>
{({state}) => (
[...Array(props.visibleDuration.months).keys()].map(i => (
<div key={i} style={{flex: 1}}>
<header style={{minHeight: 32}}>
{i === 0 &&
<Button slot="previous" variant="quiet">
<ChevronLeft />
</Button>
}
<Heading>{monthFormatter.format(state.visibleRange.start.add({months: i}).toDate(state.timeZone))}</Heading>
{i === props.visibleDuration.months - 1 &&
<Button slot="next" variant="quiet">
<ChevronRight />
</Button>
}
</header>
<CalendarGrid offset={{months: i}}>
{date => <CalendarCell date={date} />}
</CalendarGrid>
<>
<div style={{display: 'flex', gap: 12, width: '100%'}}>
{[...Array(props.visibleDuration.months).keys()].map(i => (
<header key={i} style={{flex: 1, minHeight: 32}}>
{i === 0 &&
<Button slot="previous" variant="quiet">
<ChevronLeft />
</Button>
}
<Heading>{monthFormatter.format(state.visibleRange.start.add({months: i}).toDate(state.timeZone))}</Heading>
{i === props.visibleDuration.months - 1 &&
<Button slot="next" variant="quiet">
<ChevronRight />
</Button>
}
</header>
))}
</div>
))
<CalendarCarousel>
<div style={{display: 'flex', gap: 12}}>
{[...Array(props.visibleDuration.months).keys()].map(i => (
<div key={i} style={{flex: 1}}>
<CalendarGrid offset={{months: i}}>
{date => <CalendarCell date={date} />}
</CalendarGrid>
</div>
))}
</div>
</CalendarCarousel>
</>
)}
</Calendar>
);
Expand Down Expand Up @@ -304,19 +313,21 @@ import {ChevronLeft, ChevronRight} from 'lucide-react';
role="img"
aria-label="Anatomy diagram of a calendar component, which consists of a heading, grid of cells, previous, and next buttons." />

```tsx links={{Calendar: '#calendar', Button: 'Button.html', CalendarGrid: '#calendargrid', CalendarGridHeader: '#calendargridheader', CalendarHeaderCell: '#calendarheadercell', CalendarGridBody: '#calendargridbody', CalendarCell: '#calendarcell'}}
```tsx links={{Calendar: '#calendar', Button: 'Button.html', CalendarCarousel: '#calendarcarousel', CalendarGrid: '#calendargrid', CalendarGridHeader: '#calendargridheader', CalendarHeaderCell: '#calendarheadercell', CalendarGridBody: '#calendargridbody', CalendarCell: '#calendarcell'}}
<Calendar>
<Button slot="previous" />
<Heading />
<Button slot="next" />
<CalendarGrid>
<CalendarGridHeader>
{day => <CalendarHeaderCell />}
</CalendarGridHeader>
<CalendarGridBody>
{date => <CalendarCell date={date} />}
</CalendarGridBody>
</CalendarGrid>
<CalendarCarousel> (optional)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we show that this component is optional? I guess we don't do this anywhere else, but I'm not sure we have other optional components that accept children?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could make sense for Virtualizer in some examples, e.g. Virtualized Select, to illustrate where the Virtualizer goes.

<CalendarGrid>
<CalendarGridHeader>
{day => <CalendarHeaderCell />}
</CalendarGridHeader>
<CalendarGridBody>
{date => <CalendarCell date={date} />}
</CalendarGridBody>
</CalendarGrid>
</CalendarCarousel>
<Text slot="errorMessage" />
</Calendar>
```
Expand All @@ -325,6 +336,10 @@ import {ChevronLeft, ChevronRight} from 'lucide-react';

<PropTable component={docs.exports.Calendar} links={docs.links} showDescription />

### CalendarCarousel

<PropTable component={docs.exports.CalendarCarousel} links={docs.links} showDescription />

### CalendarGrid

<PropTable component={docs.exports.CalendarGrid} links={docs.links} showDescription />
Expand Down
75 changes: 45 additions & 30 deletions packages/dev/s2-docs/pages/react-aria/RangeCalendar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ Set the `visibleDuration` prop and render multiple `CalendarGrid` elements to di

```tsx render docs={docs.exports.RangeCalendar} links={docs.links} props={['visibleDuration', 'pageBehavior', 'firstDayOfWeek']} initialProps={{visibleDuration: {months: 2}}} wide
"use client";
import {RangeCalendar, Heading} from 'react-aria-components';
import {RangeCalendar, Heading, CalendarCarousel} from 'react-aria-components';
import {CalendarGrid, CalendarCell} from 'vanilla-starter/RangeCalendar';
import {Button} from 'vanilla-starter/Button';
import {useDateFormatter} from 'react-aria';
Expand All @@ -222,29 +222,38 @@ function Example(props) {
///- begin highlight -///
/* PROPS */
///- end highlight -///
style={{display: 'flex', gap: 12, overflow: 'auto'}}
>
{({state}) => (
[...Array(props.visibleDuration.months).keys()].map(i => (
<div key={i} style={{flex: 1}}>
<header style={{minHeight: 32}}>
{i === 0 &&
<Button slot="previous" variant="quiet">
<ChevronLeft />
</Button>
}
<Heading>{monthFormatter.format(state.visibleRange.start.add({months: i}).toDate(state.timeZone))}</Heading>
{i === props.visibleDuration.months - 1 &&
<Button slot="next" variant="quiet">
<ChevronRight />
</Button>
}
</header>
<CalendarGrid offset={{months: i}}>
{date => <CalendarCell date={date} />}
</CalendarGrid>
<>
<div style={{display: 'flex', gap: 12, width: '100%'}}>
{[...Array(props.visibleDuration.months).keys()].map(i => (
<header key={i} style={{flex: 1, minHeight: 32}}>
{i === 0 &&
<Button slot="previous" variant="quiet">
<ChevronLeft />
</Button>
}
<Heading>{monthFormatter.format(state.visibleRange.start.add({months: i}).toDate(state.timeZone))}</Heading>
{i === props.visibleDuration.months - 1 &&
<Button slot="next" variant="quiet">
<ChevronRight />
</Button>
}
</header>
))}
</div>
))
<CalendarCarousel>
<div style={{display: 'flex', gap: 12}}>
{[...Array(props.visibleDuration.months).keys()].map(i => (
<div key={i} style={{flex: 1}}>
<CalendarGrid offset={{months: i}}>
{date => <CalendarCell date={date} />}
</CalendarGrid>
</div>
))}
</div>
</CalendarCarousel>
</>
)}
</RangeCalendar>
);
Expand Down Expand Up @@ -322,19 +331,21 @@ import {ChevronLeft, ChevronRight} from 'lucide-react';
role="img"
aria-label="Anatomy diagram of a range calendar component, which consists of a heading, grid of cells, previous, and next buttons." />

```tsx links={{RangeCalendar: '#rangecalendar', Button: 'Button.html', CalendarGrid: '#calendargrid', CalendarGridHeader: '#calendargridheader', CalendarHeaderCell: '#calendarheadercell', CalendarGridBody: '#calendargridbody', CalendarCell: '#calendarcell'}}
```tsx links={{RangeCalendar: '#rangecalendar', Button: 'Button.html', CalendarCarousel: '#calendarcarousel', CalendarGrid: '#calendargrid', CalendarGridHeader: '#calendargridheader', CalendarHeaderCell: '#calendarheadercell', CalendarGridBody: '#calendargridbody', CalendarCell: '#calendarcell'}}
<RangeCalendar>
<Button slot="previous" />
<Heading />
<Button slot="next" />
<CalendarGrid>
<CalendarGridHeader>
{day => <CalendarHeaderCell />}
</CalendarGridHeader>
<CalendarGridBody>
{date => <CalendarCell date={date} />}
</CalendarGridBody>
</CalendarGrid>
<CalendarCarousel> (optional)
<CalendarGrid>
<CalendarGridHeader>
{day => <CalendarHeaderCell />}
</CalendarGridHeader>
<CalendarGridBody>
{date => <CalendarCell date={date} />}
</CalendarGridBody>
</CalendarGrid>
</CalendarCarousel>
<Text slot="errorMessage" />
</RangeCalendar>
```
Expand All @@ -343,6 +354,10 @@ import {ChevronLeft, ChevronRight} from 'lucide-react';

<PropTable component={docs.exports.RangeCalendar} links={docs.links} showDescription />

### CalendarCarousel

<PropTable component={docs.exports.CalendarCarousel} links={docs.links} showDescription />

### CalendarGrid

<PropTable component={docs.exports.CalendarGrid} links={docs.links} showDescription />
Expand Down
5 changes: 4 additions & 1 deletion packages/dev/s2-docs/src/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ const example = style({
padding: {
default: 12,
lg: 24
}
},
display: 'flex',
flexDirection: 'column',
gap: 24
});

const standaloneCode = style({
Expand Down
14 changes: 11 additions & 3 deletions packages/dev/s2-docs/src/ExampleOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,19 @@ export function ExampleOutput({component, props = {}, align = 'center', orientat
borderRadius: 'lg',
font: 'ui',
padding: {
default: 12,
lg: 24
default: 4,
isOverBackground: {
default: 12,
lg: 24
}
},
margin: {
// Undo effect of padding, but keep so focus rings extend outside.
default: -4,
isOverBackground: 0
},
boxSizing: 'border-box'
})({align, orientation})}
})({align, orientation, isOverBackground: Boolean(props.staticColor || props.isOverBackground)})}
style={{background: getBackgroundColor(props.staticColor || (props.isOverBackground ? 'white' : undefined))}}>
{isValidElement(component) ? cloneElement(component, props) : createElement(component, props)}
</div>
Expand Down
Loading