Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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